174 lines
8.5 KiB
JavaScript
174 lines
8.5 KiB
JavaScript
/* ═══════════════════════════════════════════════════════════════
|
|
MAP GRID
|
|
|
|
Pure layout. Where each tile sits, what it's called, and how to
|
|
work out which tile a chapter belongs to. None of this is in the
|
|
database because none of it changes — Rhode Island will not be
|
|
moving, and no admin form should offer to move it.
|
|
|
|
What IS in the database is which regions cover which areas, and
|
|
how much of each. That arrives as region_areas rows whose
|
|
area_code matches a key in AREAS below. A code with no match
|
|
here simply doesn't paint, which is how Africa and the UK exist
|
|
as regions with no tile.
|
|
═══════════════════════════════════════════════════════════════ */
|
|
|
|
export const GRID_COLS = 13;
|
|
export const GRID_ROWS = 7;
|
|
|
|
/* ── States ────────────────────────────────────────────────────
|
|
[column, row], both 1-based. A tile grid rather than true
|
|
geography: every state reads at the same size, it stays
|
|
legible on a phone, and there's no map library to load.
|
|
───────────────────────────────────────────────────────────── */
|
|
const STATE_GRID = {
|
|
AK: [1, 1], ME: [13, 1],
|
|
WA: [2, 2], ID: [3, 2], MT: [4, 2], ND: [5, 2], MN: [6, 2], WI: [7, 2],
|
|
MI: [8, 3], NY: [10, 2], VT: [11, 2], NH: [12, 2],
|
|
OR: [2, 3], NV: [3, 4], WY: [4, 3], SD: [5, 3], IA: [6, 3], IL: [7, 3],
|
|
IN: [7, 4], OH: [8, 4], PA: [9, 2], NJ: [10, 3], MA: [11, 3],
|
|
CA: [2, 4], UT: [3, 3], CO: [4, 4], NE: [5, 4], MO: [6, 4], KY: [7, 5],
|
|
WV: [9, 3], VA: [9, 4], MD: [10, 5], DE: [10, 4], CT: [11, 4],
|
|
AZ: [3, 5], NM: [4, 5], KS: [5, 5], AR: [6, 5], TN: [8, 5], NC: [10, 6],
|
|
DC: [9, 5], RI: [12, 3],
|
|
OK: [5, 6], LA: [6, 6], MS: [7, 6], AL: [8, 6], SC: [9, 6],
|
|
HI: [1, 7], TX: [5, 7], GA: [9, 7], FL: [10, 7],
|
|
};
|
|
|
|
/* ── Bands ─────────────────────────────────────────────────────
|
|
Wide areas that aren't states. A band is just a tile with a
|
|
span, which keeps the renderer from needing a second code path.
|
|
───────────────────────────────────────────────────────────── */
|
|
const BAND_GRID = {
|
|
CANADA: [3, 1, 8], // col, row, span
|
|
};
|
|
|
|
export const AREA_NAMES = {
|
|
AL: "Alabama", AK: "Alaska", AZ: "Arizona", AR: "Arkansas",
|
|
CA: "California", CO: "Colorado", CT: "Connecticut", DE: "Delaware",
|
|
DC: "District of Columbia", FL: "Florida", GA: "Georgia", HI: "Hawai'i",
|
|
ID: "Idaho", IL: "Illinois", IN: "Indiana", IA: "Iowa", KS: "Kansas",
|
|
KY: "Kentucky", LA: "Louisiana", ME: "Maine", MD: "Maryland",
|
|
MA: "Massachusetts", MI: "Michigan", MN: "Minnesota", MS: "Mississippi",
|
|
MO: "Missouri", MT: "Montana", NE: "Nebraska", NV: "Nevada",
|
|
NH: "New Hampshire", NJ: "New Jersey", NM: "New Mexico", NY: "New York",
|
|
NC: "North Carolina", ND: "North Dakota", OH: "Ohio", OK: "Oklahoma",
|
|
OR: "Oregon", PA: "Pennsylvania", RI: "Rhode Island",
|
|
SC: "South Carolina", SD: "South Dakota", TN: "Tennessee", TX: "Texas",
|
|
UT: "Utah", VT: "Vermont", VA: "Virginia", WA: "Washington",
|
|
WV: "West Virginia", WI: "Wisconsin", WY: "Wyoming",
|
|
CANADA: "Canada",
|
|
};
|
|
|
|
/* ── One list the renderer walks ───────────────────────────────
|
|
States and bands unified, so drawing the map is a single map()
|
|
over AREAS rather than two loops with different shapes.
|
|
───────────────────────────────────────────────────────────── */
|
|
export const AREAS = Object.freeze([
|
|
...Object.entries(STATE_GRID).map(([code, [col, row]]) => ({
|
|
code,
|
|
name: AREA_NAMES[code] ?? code,
|
|
col,
|
|
row,
|
|
span: 1,
|
|
isState: true,
|
|
})),
|
|
...Object.entries(BAND_GRID).map(([code, [col, row, span]]) => ({
|
|
code,
|
|
name: AREA_NAMES[code] ?? code,
|
|
col,
|
|
row,
|
|
span,
|
|
isState: false,
|
|
})),
|
|
]);
|
|
|
|
export const AREA_BY_CODE = Object.fromEntries(AREAS.map((a) => [a.code, a]));
|
|
|
|
/* ── Which tile a chapter sits on ──────────────────────────────
|
|
The database stores a real address — state_code for US, and a
|
|
country code otherwise — rather than a tile name. This is the
|
|
one place that translates between the two, so adding a Mexico
|
|
band later means a line here and nothing in SQL.
|
|
|
|
Returns null for anything with no tile, which covers virtual
|
|
chapters and any country not drawn.
|
|
───────────────────────────────────────────────────────────── */
|
|
const COUNTRY_AREA = {
|
|
CA: "CANADA", // ISO country code, not California
|
|
};
|
|
|
|
export function areaForChapter(chapter) {
|
|
if (!chapter) return null;
|
|
if (chapter.is_online) return null;
|
|
|
|
if (chapter.country === "US") {
|
|
return AREA_BY_CODE[chapter.state_code] ? chapter.state_code : null;
|
|
}
|
|
return COUNTRY_AREA[chapter.country] ?? null;
|
|
}
|
|
|
|
/* ── Slices per tile ───────────────────────────────────────────
|
|
Turns region_areas rows into what the SVG needs: for each
|
|
tile, the regions painting it and the fraction each takes.
|
|
|
|
A region with share 1 and no edge fills the tile. A shared
|
|
tile has one row per region, each declaring its own slice, so
|
|
California is two entries of 0.5 rather than a primary plus a
|
|
remainder — no arithmetic, and the renderer doesn't need to
|
|
know which region "really" owns it.
|
|
|
|
regionAreas [{ region_id, area_code, share, edge, note }]
|
|
regions [{ id, name, color, ... }]
|
|
───────────────────────────────────────────────────────────── */
|
|
export function buildAreaSlices(regionAreas = [], regions = []) {
|
|
const regionById = Object.fromEntries(regions.map((r) => [r.id, r]));
|
|
const byArea = {};
|
|
|
|
for (const row of regionAreas) {
|
|
const area = AREA_BY_CODE[row.area_code];
|
|
const region = regionById[row.region_id];
|
|
if (!area || !region) continue; // untiled region, or unknown code
|
|
|
|
(byArea[row.area_code] ??= []).push({
|
|
regionId: region.id,
|
|
name: region.name,
|
|
color: region.color,
|
|
share: row.share ?? 1,
|
|
edge: row.edge ?? null,
|
|
note: row.note ?? null,
|
|
});
|
|
}
|
|
|
|
// Full-tile slice first, so a partial slice paints over it.
|
|
for (const slices of Object.values(byArea)) {
|
|
slices.sort((a, b) => b.share - a.share);
|
|
}
|
|
|
|
return byArea;
|
|
}
|
|
|
|
/* ── Chapter counts per tile ───────────────────────────────────
|
|
{ WA: 2, MO: 1, CANADA: 1 }
|
|
───────────────────────────────────────────────────────────── */
|
|
export function countChaptersByArea(chapters = []) {
|
|
const counts = {};
|
|
for (const chapter of chapters) {
|
|
const code = areaForChapter(chapter);
|
|
if (code) counts[code] = (counts[code] ?? 0) + 1;
|
|
}
|
|
return counts;
|
|
}
|
|
|
|
/* ── "AK, ID, MT, OR, UT (Salt Lake City area), WA" ────────────
|
|
Replaces statesLabel. Note the annotation now comes straight
|
|
from the row rather than being looked up in a splits table and
|
|
branched on whether this region is the primary.
|
|
───────────────────────────────────────────────────────────── */
|
|
export function areasLabel(regionId, regionAreas = []) {
|
|
return regionAreas
|
|
.filter((row) => row.region_id === regionId && row.area_code !== "CANADA")
|
|
.map((row) => (row.note ? `${row.area_code} (${row.note})` : row.area_code))
|
|
.sort()
|
|
.join(", ");
|
|
}
|