From 84e796ab49136e67c874e036083bd932932f0ad1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Ign=C3=A1cio=20Fontoura=20Solla?= Date: Sun, 26 Jul 2026 23:08:02 -0300 Subject: [PATCH] =?UTF-8?q?B8=20F2:=20pol=C3=ADgonos=20das=20ZE=20de=20Sal?= =?UTF-8?q?vador=20e=20re-keying=20do=20coropl=C3=A9tico=20por=20map=20key?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Salvador era um polígono só carregando os votos da cidade inteira, então o coroplético não conseguia responder "qual pedaço de Salvador está fraco / é meu". As 19 ZE ganham polígono próprio, dissolvido da malha de bairros do IBGE (Censo 2022) pela lista de circunscrição da RA 02/2017 do TRE-BA, e o mapa passa a pintar por map key (slug para as zonas, ibgeCode para o resto) em vez de codarea. O polígono municipal de Salvador continua desenhado por baixo, sem dado e sem interação, para não abrir buraco no contorno do estado. Aproximação declarada na copy e no cabeçalho do script: o TRE publica a circunscrição como lista de bairros, então o bairro é o átomo geométrico — Periperi, que a resolução corta na Rua das Pedrinhas, vai inteiro para a ZE 17. competitiveRank segue por cidade (o artefato TSE só tem federalRankByIbgeCode). Sem migration, sem collection, sem action, sem Consent. --- package.json | 3 + pnpm-lock.yaml | 58 +++ .../build-municipality-zone-geometries.mjs | 407 ++++++++++++++++++ src/components/campaign/map/BahiaMap.tsx | 115 ++--- .../campaign/map/MapFeatureReadout.tsx | 5 - .../campaign/map/MunicipalityMapPanel.tsx | 67 ++- src/lib/bahiaGeometries.ts | 15 +- src/lib/bahiaGeometriesTypes.ts | 28 +- src/lib/bahiaMapStyle.ts | 26 +- src/lib/bahiaMunicipalityZoneGeometries.ts | 23 + .../bahia-municipality-zones.topo.json | 1 + src/lib/municipalityProximity.ts | 7 +- src/lib/municipalityZoneNeighborhoods.ts | 85 ++-- src/utilities/municipalityMapContract.ts | 34 +- src/utilities/municipalityMapData.ts | 52 +-- src/utilities/municipalityMapNavigation.ts | 53 ++- tests/e2e/campaignZoneMap.e2e.spec.ts | 63 +++ tests/int/bahiaGeometries.int.spec.ts | 110 ++++- tests/int/municipalityMapData.int.spec.ts | 31 +- .../municipalityZoneNeighborhoods.int.spec.ts | 9 +- tests/unit/bahiaMapStyle.unit.spec.ts | 29 ++ .../municipalityMapNavigation.unit.spec.ts | 73 ++-- 22 files changed, 1051 insertions(+), 243 deletions(-) create mode 100644 scripts/build-municipality-zone-geometries.mjs create mode 100644 src/lib/bahiaMunicipalityZoneGeometries.ts create mode 100644 src/lib/geometries/bahia-municipality-zones.topo.json create mode 100644 tests/e2e/campaignZoneMap.e2e.spec.ts diff --git a/package.json b/package.json index f5f84470..e5ded53c 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "build:demographics": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-demographics.mjs", "build:election-aggregates": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm --import=./scripts/seed-loader.mjs\" node scripts/build-election-aggregates.mjs", "build:geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-geometries.mjs", + "build:municipality-zone-geometries": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-municipality-zone-geometries.mjs", "build:tse-city-codes": "cross-env NODE_OPTIONS=\"--no-deprecation --import=tsx/esm\" node scripts/build-bahia-tse-city-codes.mjs", "dev": "node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", "devsafe": "rm -rf .next && node scripts/guard-dev-db.mjs && cross-env NODE_OPTIONS=--no-deprecation next dev", @@ -108,6 +109,7 @@ "@types/pg": "^8.10.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/shpjs": "^3.4.7", "@types/topojson-client": "^3.1.5", "@types/topojson-server": "^3.0.4", "@types/topojson-simplify": "^3.0.3", @@ -129,6 +131,7 @@ "prettier": "^3.4.2", "prettier-plugin-organize-imports": "^4.3.0", "schema-dts": "^2.0.0", + "shpjs": "^6.2.0", "tailwindcss": "^4.1.18", "topojson-server": "^3.0.1", "topojson-simplify": "^3.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2912c656..f1ae38ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -147,6 +147,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) + '@types/shpjs': + specifier: ^3.4.7 + version: 3.4.7 '@types/topojson-client': specifier: ^3.1.5 version: 3.1.5 @@ -210,6 +213,9 @@ importers: schema-dts: specifier: ^2.0.0 version: 2.0.0(@typescript/typescript6@6.0.2) + shpjs: + specifier: ^6.2.0 + version: 6.2.0 tailwindcss: specifier: ^4.1.18 version: 4.1.18 @@ -3148,6 +3154,9 @@ packages: '@types/react@19.2.14': resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + '@types/shpjs@3.4.7': + resolution: {integrity: sha512-/6PjggpFsq9NFxar6ZpXsnYZ+nQJR8Cv03Gne1enIJuMZ/eFVOpu0orHxL9D7RT3ciJElzF2H6l+49US23ydUw==} + '@types/statuses@2.0.6': resolution: {integrity: sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==} @@ -3834,6 +3843,9 @@ packages: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} + but-unzip@0.1.10: + resolution: {integrity: sha512-hLfQ9WlUimmv/okzsRl6AYG3Ew5HNWhWgUslSR93FsDdeL0MAoQvmC/BJfs35lqEAO5t/QD7Y4vCFcPJtijt3A==} + bytes@3.1.2: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} @@ -5627,6 +5639,9 @@ packages: merge@2.1.1: resolution: {integrity: sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w==} + mgrs@1.0.0: + resolution: {integrity: sha512-awNbTOqCxK1DBGjalK3xqWIstBZgN6fxsMSiXLs9/spqWkF2pAhb2rrYCFSsr1/tT7PhcDGjZndG8SWYn0byYA==} + micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -5974,6 +5989,9 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parsedbf@2.0.0: + resolution: {integrity: sha512-WNjKn/cwgGBkXqQLif+2VMEahcRHkBRU0/RfBWZ7Vj7snRNNW63yW1mVuuHRDyXTRxuGCzAHHBcr/Fn+U/bXjQ==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -6239,6 +6257,14 @@ packages: process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + proj4@2.20.9: + resolution: {integrity: sha512-GLBGqXaTcdWnppre3o1sMmy4DcMGSGq/ng+9k2MTNddarRK6SveINqlqYzi3xEXuy06ljY1TTrC6H9C4f360IQ==} + peerDependencies: + geotiff: '*' + peerDependenciesMeta: + geotiff: + optional: true + prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -6607,6 +6633,9 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shpjs@6.2.0: + resolution: {integrity: sha512-8cR/RKYHQepmVyBMtzZQ+1bnSbWrtLXS6aoEJmpUlOSHtSUddterebVxYmIWq2g9kOEX9jm2kjHiikyPX7cNQA==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -7318,6 +7347,9 @@ packages: engines: {node: '>=8'} hasBin: true + wkt-parser@1.5.6: + resolution: {integrity: sha512-cqHU3lzGt/gt2OqIORP0uVy5yeOX43WABmgFkmJsmcqH3HhQo9PiJG6ftytwGKmpQUbegeX7+pQc2BuNCRfcrw==} + wmf@1.0.2: resolution: {integrity: sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==} engines: {node: '>=0.8'} @@ -10260,6 +10292,11 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/shpjs@3.4.7': + dependencies: + '@types/geojson': 7946.0.16 + '@types/node': 22.19.9 + '@types/statuses@2.0.6': {} '@types/topojson-client@3.1.5': @@ -10912,6 +10949,8 @@ snapshots: dependencies: streamsearch: 1.1.0 + but-unzip@0.1.10: {} + bytes@3.1.2: {} call-bind-apply-helpers@1.0.2: @@ -12826,6 +12865,8 @@ snapshots: merge@2.1.1: {} + mgrs@1.0.0: {} + micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.3.0 @@ -13344,6 +13385,8 @@ snapshots: dependencies: entities: 6.0.1 + parsedbf@2.0.0: {} + parseurl@1.3.3: {} path-browserify@1.0.1: {} @@ -13633,6 +13676,11 @@ snapshots: process-warning@5.0.0: {} + proj4@2.20.9: + dependencies: + mgrs: 1.0.0 + wkt-parser: 1.5.6 + prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -14209,6 +14257,14 @@ snapshots: shebang-regex@3.0.0: {} + shpjs@6.2.0: + dependencies: + but-unzip: 0.1.10 + parsedbf: 2.0.0 + proj4: 2.20.9 + transitivePeerDependencies: + - geotiff + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -14940,6 +14996,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wkt-parser@1.5.6: {} + wmf@1.0.2: {} word-wrap@1.2.5: {} diff --git a/scripts/build-municipality-zone-geometries.mjs b/scripts/build-municipality-zone-geometries.mjs new file mode 100644 index 00000000..c32a1a1c --- /dev/null +++ b/scripts/build-municipality-zone-geometries.mjs @@ -0,0 +1,407 @@ +/** + * Builds the static TopoJSON of the zone municipalities — Salvador's 19 ZE + * (roadmap B8 F2). One feature per catalog `kind: 'zona'` entry, dissolved from + * the IBGE neighborhood mesh using the official TRE circunscrição list. + * + * Provenance (do not strip): + * - IBGE Malhas de bairros, Censo 2022 (shapefile, UF BA): + * https://geoftp.ibge.gov.br/organizacao_do_territorio/malhas_territoriais/malhas_de_setores_censitarios__divisoes_intramunicipais/censo_2022/bairros/shp/UF/BA_bairros_CD2022.zip + * - Zone → neighborhood assignment: src/lib/municipalityZoneNeighborhoods.ts + * (TRE-BA Resolução Administrativa nº 2/2017, Anexo I). + * + * APPROXIMATION — the result is NOT an official TSE boundary. The TRE publishes + * the circunscrição as a list of neighborhoods, so the polygon is only as exact + * as the neighborhood mesh: where the resolution splits one neighborhood + * between two zones by a street, the polygon is atomic and goes whole to one of + * them (see SPLIT_NEIGHBORHOOD_ZONES). Every surface that draws it says so. + * + * Safety: does not touch any database — only downloads public IBGE data and + * writes a versioned artifact under src/lib/geometries/. + * + * Usage: + * pnpm build:municipality-zone-geometries + * pnpm build:municipality-zone-geometries --report # reconciliation only + * GEOMETRIES_CACHE_DIR=./data/geometries pnpm build:municipality-zone-geometries + */ + +import { createHash } from 'node:crypto' +import { access, mkdir, readFile, writeFile } from 'node:fs/promises' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +import shp from 'shpjs' +import { merge, quantize } from 'topojson-client' +import { topology } from 'topojson-server' +import { presimplify, quantile, simplify } from 'topojson-simplify' + +const { municipalityCatalog } = await import('../src/lib/municipalityCatalog.ts') +const { municipalityZoneNeighborhoods } = + await import('../src/lib/municipalityZoneNeighborhoods.ts') +const { downloadToBuffer } = await import('../src/lib/electionResultsZip.ts') + +const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..') + +const SOURCE = { + key: 'bahia-bairros-cd2022', + url: 'https://geoftp.ibge.gov.br/organizacao_do_territorio/malhas_territoriais/malhas_de_setores_censitarios__divisoes_intramunicipais/censo_2022/bairros/shp/UF/BA_bairros_CD2022.zip', + ext: 'zip', +} + +const OUTPUT_PATH = 'src/lib/geometries/bahia-municipality-zones.topo.json' + +/** Quantile of triangle area kept after presimplify (higher = more detail). */ +const SIMPLIFY_QUANTILE = 0.35 +/** Quantization precision for the final topology. */ +const QUANTIZE_DIGITS = 1e4 + +/** + * Same neighborhood, different name in the two sources — IBGE composites + * ("Beiru / Tancredo Neves") and spellings the accent folding cannot reach. + * Identity beats the adjacency rule below: the resolution naming a + * neighborhood is the fact, adjacency is only the fallback for the ones it + * never mentions. Both sides are matched normalized and the target must exist + * in the TRE catalog, or the build fails. + */ +const IBGE_TO_TRE_NEIGHBORHOOD = { + 'BEIRU TANCREDO NEVES': 'Tancredo Neves', + 'CENTRO ADMINISTRATIVO DA BAHIA CAB': 'Centro Administrativo da Bahia', + 'NORDESTE DO AMARALINA': 'Nordeste de Amaralina', + // Islands have no shared boundary, so the adjacency rule cannot place them — + // and it does not need to: RA 02/2017 names both in ZE 4. + 'ILHA BOM JESUS DOS PASSOS': 'Ilha de Bom Jesus dos Passos', + 'ILHA DOS FRADES ILHA DE SANTO ANTONIO': 'Ilha dos Frades', + 'VILA RUY BARBOSA JARDIM CRUZEIRO': 'Vila Ruy Barbosa', +} + +/** + * TRE neighborhoods with no polygon of their own because the IBGE mesh draws + * them inside a neighbor. Only legal within one zone — the build refuses a + * merge that would move territory across zones. + */ +const TRE_NEIGHBORHOODS_MERGED_INTO = { + 'Jardim Cruzeiro': 'Vila Ruy Barbosa', +} + +/** + * Neighborhoods the resolution splits between two zones by a street. The + * polygon is atomic without street-level geodata, so it goes whole to the zone + * named here, and every build reports it. + */ +const SPLIT_NEIGHBORHOOD_ZONES = { + // RA 02/2017 cuts Periperi at Rua das Pedrinhas (north = ZE 4, south = ZE 17). + // It goes to ZE 17, whose other neighborhoods (Alto do Cabrito, Itacaranha, + // Plataforma, Praia Grande, Rio Sena, São João do Cabrito) wrap it; ZE 4 + // keeps Paripe, Coutos and the islands. + PERIPERI: 'salvador-ze-17', +} + +const REPORT_ONLY = process.argv.includes('--report') + +const die = (message) => { + console.error(`\n[build:municipality-zone-geometries] ${message}\n`) + process.exit(1) +} + +const log = (message) => console.log(`[build:municipality-zone-geometries] ${message}`) + +const sha256 = (buffer) => createHash('sha256').update(buffer).digest('hex') + +const cacheDir = () => process.env.GEOMETRIES_CACHE_DIR || join(ROOT, 'data', 'geometries') + +const ensureCachedBinary = async ({ key, url, ext }) => { + const dir = cacheDir() + await mkdir(dir, { recursive: true }) + const path = join(dir, `${key}.${ext}`) + try { + await access(path) + log(`cache hit ${path}`) + const buffer = await readFile(path) + return { url, hash: sha256(buffer), buffer } + } catch (error) { + if (error?.code !== 'ENOENT') throw error + } + log(`downloading ${url}`) + const buffer = await downloadToBuffer(url) + const hash = sha256(buffer) + await writeFile(path, buffer) + log(`saved ${path} (${buffer.length} bytes, sha256=${hash})`) + return { url, hash, buffer } +} + +const writeJson = async (relativePath, value) => { + const path = join(ROOT, relativePath) + await mkdir(dirname(path), { recursive: true }) + const body = `${JSON.stringify(value)}\n` + await writeFile(path, body) + log(`wrote ${relativePath} (${Buffer.byteLength(body)} bytes)`) +} + +const simplifyTopology = (topo) => { + let next = presimplify(topo) + const minWeight = quantile(next, SIMPLIFY_QUANTILE) + next = simplify(next, minWeight) + return quantize(next, QUANTIZE_DIGITS) +} + +/** Accent-folded, punctuation-free key; parentheses (the TRE's split notes) drop. */ +const normalizeNeighborhood = (name) => + name + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .toUpperCase() + .replace(/\(.*?\)/g, ' ') + .replace(/[^A-Z0-9]+/g, ' ') + .trim() + +const polygonCount = (geometry) => + geometry?.type === 'MultiPolygon' ? geometry.coordinates.length : geometry ? 1 : 0 + +const arcIndexes = (arcs, into = new Set()) => { + for (const item of arcs) { + if (Array.isArray(item)) arcIndexes(item, into) + else into.add(item < 0 ? -1 - item : item) + } + return into +} + +const arcLength = (arc) => { + let total = 0 + for (let index = 1; index < arc.length; index += 1) { + const [previousX, previousY] = arc[index - 1] + const [x, y] = arc[index] + total += Math.hypot(x - previousX, y - previousY) + } + return total +} + +/** + * Places a polygon RA 02/2017 never names in the zone it shares the most + * boundary with. Validated against the neighborhoods the resolution DOES name + * under a different label (Beiru/Tancredo Neves, CAB, Nordeste de Amaralina, + * Vila Ruy Barbosa): the rule reproduces the official zone in all four. + */ +const rankZonesBySharedBoundary = (topo, geometry, zoneSlugByGeometry) => { + const ownArcs = arcIndexes(geometry.arcs) + const lengthByZone = new Map() + + for (const other of topo.objects.neighborhoods.geometries) { + const zone = zoneSlugByGeometry.get(other) + if (!zone || other === geometry) continue + const otherArcs = arcIndexes(other.arcs) + let shared = 0 + for (const arc of ownArcs) if (otherArcs.has(arc)) shared += arcLength(topo.arcs[arc]) + if (shared > 0) lengthByZone.set(zone, (lengthByZone.get(zone) ?? 0) + shared) + } + + return [...lengthByZone.entries()].sort((left, right) => right[1] - left[1]) +} + +const main = async () => { + const zoneEntries = municipalityCatalog.filter((entry) => entry.kind === 'zona') + if (zoneEntries.length === 0) die('No zone municipalities in the catalog.') + const zoneIbgeCodes = new Set(zoneEntries.map((entry) => entry.ibgeCode)) + + // TRE neighborhood → zone slug. A name in two zones is a documented split. + const zoneSlugByNeighborhood = new Map() + const splitNeighborhoods = new Set() + for (const record of municipalityZoneNeighborhoods) { + for (const neighborhood of record.neighborhoods) { + const key = normalizeNeighborhood(neighborhood) + const previous = zoneSlugByNeighborhood.get(key) + if (previous && previous !== record.municipalitySlug) { + const resolved = SPLIT_NEIGHBORHOOD_ZONES[key] + if (!resolved) { + die( + `"${neighborhood}" is listed in ${previous} and ${record.municipalitySlug}; add it to SPLIT_NEIGHBORHOOD_ZONES.`, + ) + } + splitNeighborhoods.add(key) + zoneSlugByNeighborhood.set(key, resolved) + continue + } + if (!previous) zoneSlugByNeighborhood.set(key, record.municipalitySlug) + } + } + + const aliasTargets = new Map() + for (const [ibgeName, treName] of Object.entries(IBGE_TO_TRE_NEIGHBORHOOD)) { + const treKey = normalizeNeighborhood(treName) + if (!zoneSlugByNeighborhood.has(treKey)) { + die(`IBGE_TO_TRE_NEIGHBORHOOD maps "${ibgeName}" to unknown TRE neighborhood "${treName}".`) + } + aliasTargets.set(normalizeNeighborhood(ibgeName), treKey) + } + + for (const [treName, hostName] of Object.entries(TRE_NEIGHBORHOODS_MERGED_INTO)) { + const treZone = zoneSlugByNeighborhood.get(normalizeNeighborhood(treName)) + const hostZone = zoneSlugByNeighborhood.get(normalizeNeighborhood(hostName)) + if (!treZone) die(`TRE_NEIGHBORHOODS_MERGED_INTO has unknown neighborhood "${treName}".`) + if (!hostZone) die(`TRE_NEIGHBORHOODS_MERGED_INTO has unknown host "${hostName}".`) + if (treZone !== hostZone) { + die( + `Merging "${treName}" (${treZone}) into "${hostName}" (${hostZone}) would move territory across zones.`, + ) + } + } + + const { hash, buffer } = await ensureCachedBinary(SOURCE) + const features = [await shp(buffer)] + .flat() + .flatMap((collection) => collection.features) + .filter((feature) => zoneIbgeCodes.has(feature.properties?.CD_MUN)) + if (features.length === 0) die('No neighborhood polygons found for the zone municipalities.') + log(`${features.length} neighborhood polygons in the zone municipalities (sha256=${hash})`) + + // Topology before anything else: the dissolve needs shared arcs to drop + // interior boundaries instead of leaving slivers, adjacency is read off those + // same arcs, and simplification then moves both sides of an arc identically. + const neighborhoodTopology = topology({ + neighborhoods: { + type: 'FeatureCollection', + features: features.map((feature) => ({ + type: 'Feature', + properties: { name: feature.properties?.NM_BAIRRO ?? '' }, + geometry: feature.geometry, + })), + }, + }) + const geometries = neighborhoodTopology.objects.neighborhoods.geometries + + // Pass 1 — the resolution's own names. + const zoneSlugByGeometry = new Map() + const unnamed = [] + for (const geometry of geometries) { + const key = normalizeNeighborhood(geometry.properties.name) + const slug = zoneSlugByNeighborhood.get(aliasTargets.get(key) ?? key) + if (slug) zoneSlugByGeometry.set(geometry, slug) + else unnamed.push(geometry) + } + + // Pass 2 — polygons RA 02/2017 never names, by shared boundary. + const orphans = [] + for (const geometry of unnamed) { + const ranked = rankZonesBySharedBoundary(neighborhoodTopology, geometry, zoneSlugByGeometry) + const [winner, runnerUp] = ranked + if (!winner) { + orphans.push(geometry.properties.name) + continue + } + zoneSlugByGeometry.set(geometry, winner[0]) + const margin = runnerUp + ? `+${(((winner[1] - runnerUp[1]) / runnerUp[1]) * 100).toFixed(0)}%` + : 'only neighbour' + log( + `not in RA 02/2017 — "${geometry.properties.name}" → ${winner[0]} by shared boundary (${margin}${ + runnerUp ? ` over ${runnerUp[0]}` : '' + })`, + ) + } + + const coveredKeys = new Set( + geometries.map((geometry) => { + const key = normalizeNeighborhood(geometry.properties.name) + return aliasTargets.get(key) ?? key + }), + ) + for (const hostName of Object.values(TRE_NEIGHBORHOODS_MERGED_INTO)) { + if (!coveredKeys.has(normalizeNeighborhood(hostName))) continue + for (const [treName, host] of Object.entries(TRE_NEIGHBORHOODS_MERGED_INTO)) { + if (host === hostName) coveredKeys.add(normalizeNeighborhood(treName)) + } + } + const neighborhoodsWithoutPolygon = municipalityZoneNeighborhoods.flatMap((record) => + record.neighborhoods + .filter((neighborhood) => !coveredKeys.has(normalizeNeighborhood(neighborhood))) + .map((neighborhood) => `${record.municipalitySlug}: ${neighborhood}`), + ) + + const geometriesByZone = new Map() + for (const [geometry, slug] of zoneSlugByGeometry) { + geometriesByZone.set(slug, [...(geometriesByZone.get(slug) ?? []), geometry]) + } + log( + `assigned ${zoneSlugByGeometry.size}/${geometries.length} polygons to ${geometriesByZone.size}/${zoneEntries.length} zones`, + ) + if (splitNeighborhoods.size > 0) { + log( + `split by RA 02/2017, kept whole: ${[...splitNeighborhoods] + .map((key) => `${key} → ${SPLIT_NEIGHBORHOOD_ZONES[key]}`) + .join(', ')}`, + ) + } + if (orphans.length > 0) { + log(`polygons with no zone (${orphans.length}):`) + for (const name of [...orphans].sort()) console.log(` - ${name}`) + } + if (neighborhoodsWithoutPolygon.length > 0) { + log(`TRE neighborhoods with no polygon (${neighborhoodsWithoutPolygon.length}):`) + for (const entry of neighborhoodsWithoutPolygon.sort()) console.log(` - ${entry}`) + } + if (orphans.length > 0 || neighborhoodsWithoutPolygon.length > 0) { + die( + 'Reconciliation is incomplete — extend IBGE_TO_TRE_NEIGHBORHOOD / TRE_NEIGHBORHOODS_MERGED_INTO until every polygon has a zone and every neighborhood has a polygon.', + ) + } + + if (REPORT_ONLY) { + log('--report: reconciliation complete, artifact not written.') + return + } + + // Simplification keeps geometry order and properties, so the zone buckets + // above still address the right shapes. + const simplified = simplifyTopology(neighborhoodTopology) + const simplifiedByName = new Map( + simplified.objects.neighborhoods.geometries.map((geometry) => [ + geometry.properties.name, + geometry, + ]), + ) + + const zoneFeatures = zoneEntries.map((entry) => { + const members = (geometriesByZone.get(entry.slug) ?? []).map((geometry) => + simplifiedByName.get(geometry.properties.name), + ) + if (members.length === 0 || members.some((member) => !member)) { + die(`Missing neighborhood geometry for ${entry.slug}.`) + } + + const geometry = merge(simplified, members) + if (!geometry || geometry.coordinates.length === 0) { + die(`Dissolve produced an empty polygon for ${entry.slug}.`) + } + + return { + type: 'Feature', + properties: { + municipalitySlug: entry.slug, + name: entry.name, + zoneNumber: entry.zoneNumber, + ibgeCode: entry.ibgeCode, + }, + geometry, + } + }) + + // Already simplified above — the final pass only quantizes, so the artifact + // stores short integers instead of the floats `merge` hands back. + await writeJson( + OUTPUT_PATH, + quantize( + topology({ municipalityZones: { type: 'FeatureCollection', features: zoneFeatures } }), + QUANTIZE_DIGITS, + ), + ) + + for (const feature of zoneFeatures) { + const pieces = polygonCount(feature.geometry) + log( + `${feature.properties.municipalitySlug}: ${ + geometriesByZone.get(feature.properties.municipalitySlug).length + } neighborhoods → ${pieces} ${pieces === 1 ? 'piece' : 'pieces'}`, + ) + } + log(`done — ${zoneFeatures.length} zone polygons.`) +} + +await main() diff --git a/src/components/campaign/map/BahiaMap.tsx b/src/components/campaign/map/BahiaMap.tsx index 22f880d3..80e77c8e 100644 --- a/src/components/campaign/map/BahiaMap.tsx +++ b/src/components/campaign/map/BahiaMap.tsx @@ -9,17 +9,16 @@ import 'leaflet/dist/leaflet.css' import { Spinner } from '@/components/ui/Spinner' import { loadMunicipalityGeometryModule, + loadMunicipalityZoneGeometryModule, loadTerritoryGeometryModule, - type MunicipalityGeometryModule, - type TerritoryGeometryModule, } from '@/lib/bahiaGeometries' +import type { BahiaGeometryFeature } from '@/lib/bahiaGeometriesTypes' import { bubbleRadius, buildHighlightSet, buildLayerStyleContext, canonicalMapKeysKey, - featureKeyFromProperties, - keyPropertyForMode, + featureMapKey, resolvePathStyle, type BahiaMapFillMode, type ChoroplethFills, @@ -64,6 +63,32 @@ const applyBahiaMinZoom = (map: L.Map) => { export type BahiaMapMode = 'municipality' | 'territory' +/** + * The municipality layer is two meshes: the 417 municípios plus the 19 Salvador + * zone polygons on top (insertion order is z-order in the SVG). The zone city's + * own polygon stays underneath as a BASE — no value, no pointer — so a sliver of + * disagreement between two independently simplified meshes cannot punch a hole + * in the state's outline. + */ +const loadLayerFeatures = async ( + mode: BahiaMapMode, +): Promise<{ features: BahiaGeometryFeature[]; baseKeys: Set }> => { + if (mode === 'territory') { + const { features } = await loadTerritoryGeometryModule() + return { features: [...features], baseKeys: new Set() } + } + + const [municipalities, zones] = await Promise.all([ + loadMunicipalityGeometryModule(), + loadMunicipalityZoneGeometryModule(), + ]) + + return { + features: [...municipalities.features, ...zones.features], + baseKeys: new Set(zones.features.map((zone) => zone.properties.ibgeCode)), + } +} + export type BahiaMapFeatureInfo = { key: string name: string @@ -106,37 +131,28 @@ type BahiaMapProps = { const getPointerType = (event: L.LeafletMouseEvent, fallback = 'mouse') => event.originalEvent instanceof PointerEvent ? event.originalEvent.pointerType : fallback +/** + * Fits from the drawn paths rather than the geometry modules: since B8 F2 a + * municipality layer is assembled from two meshes, and the paths are the one + * index that already knows which key came from which. + */ const fitMapToHighlights = ( map: L.Map, - mode: BahiaMapMode, highlightSet: Set, - geometryModule: MunicipalityGeometryModule | TerritoryGeometryModule, + pathByKey: Map, ) => { - if (highlightSet.size === 0) { + const highlightBounds = L.latLngBounds([]) + for (const key of highlightSet) { + const path = pathByKey.get(key) + if (path instanceof L.Polygon) highlightBounds.extend(path.getBounds()) + } + + if (!highlightBounds.isValid()) { map.fitBounds(BAHIA_BOUNDS, { padding: BAHIA_FIT_PADDING }) return } - const highlightedFeatures = - mode === 'municipality' - ? [...highlightSet].flatMap((key) => { - const feature = (geometryModule as MunicipalityGeometryModule).getMunicipalityFeature(key) - return feature ? [feature] : [] - }) - : [...highlightSet].flatMap((key) => { - const feature = (geometryModule as TerritoryGeometryModule).getTerritoryFeature(key) - return feature ? [feature] : [] - }) - - if (highlightedFeatures.length === 0) { - map.fitBounds(BAHIA_BOUNDS, { padding: BAHIA_FIT_PADDING }) - return - } - - const highlightBounds = L.geoJSON(highlightedFeatures).getBounds() - if (highlightBounds.isValid()) { - map.fitBounds(highlightBounds, { padding: [24, 24], maxZoom: 10 }) - } + map.fitBounds(highlightBounds, { padding: [24, 24], maxZoom: 10 }) } export const BahiaMap = ({ @@ -160,9 +176,6 @@ export const BahiaMap = ({ const containerRef = useRef(null) const mapRef = useRef(null) const layerRef = useRef(null) - const geometryModuleRef = useRef( - null, - ) const pathByKeyRef = useRef>(new Map()) const styleContextRef = useRef(null) const geometryReadyRef = useRef(false) @@ -299,7 +312,6 @@ export const BahiaMap = ({ map.remove() mapRef.current = null layerRef.current = null - geometryModuleRef.current = null pathByKeyRef.current = new Map() styleContextRef.current = null geometryReadyRef.current = false @@ -317,19 +329,13 @@ export const BahiaMap = ({ const renderLayer = async () => { try { - const geometryModule = - mode === 'municipality' - ? await loadMunicipalityGeometryModule() - : await loadTerritoryGeometryModule() + const { features, baseKeys } = await loadLayerFeatures(mode) if (cancelled) return layerRef.current?.remove() pathByKeyRef.current = new Map() - geometryModuleRef.current = geometryModule - - const keyProperty = keyPropertyForMode(mode) const syncMountStyleContext = () => { styleContextRef.current = buildLayerStyleContext({ ...stylePropsRef.current, @@ -340,21 +346,32 @@ export const BahiaMap = ({ syncMountStyleContext() const mountContext = styleContextRef.current! - const layer = L.geoJSON([...geometryModule.features], { + /** + * The key this feature answers for, or `undefined` when it is a base + * polygon: those carry no value, take no pointer, and must not answer + * for a key the data has moved off of. + */ + const paintedKey = (properties: Feature['properties'] | undefined) => { + const key = featureMapKey(properties) + return key && !baseKeys.has(key) ? key : undefined + } + + const layer = L.geoJSON(features, { style: (feature?: Feature) => { - const properties = feature?.properties as Record | undefined - const key = featureKeyFromProperties(properties, keyProperty) const context = styleContextRef.current ?? mountContext - return resolvePathStyleForFeature(context, key ?? '') + const key = paintedKey(feature?.properties) + return key + ? resolvePathStyleForFeature(context, key) + : { ...resolvePathStyle(context, ''), interactive: false } }, onEachFeature: (feature, featureLayer) => { if (!(featureLayer instanceof L.Path)) return - const properties = feature.properties as Record | undefined - const key = featureKeyFromProperties(properties, keyProperty) + const key = paintedKey(feature.properties) if (!key) return - const name = properties?.name ?? key + const name = + typeof feature.properties?.name === 'string' ? feature.properties.name : key pathByKeyRef.current.set(key, featureLayer) featureLayer.on('mouseover', (event) => { @@ -403,7 +420,7 @@ export const BahiaMap = ({ restyleAllPaths() const fitIdentity = `${mode}:${viewportKey}` - fitMapToHighlights(map, mode, buildHighlightSet(viewportKey), geometryModule) + fitMapToHighlights(map, buildHighlightSet(viewportKey), pathByKeyRef.current) lastFittedViewportKeyRef.current = fitIdentity geometryReadyRef.current = true @@ -420,7 +437,6 @@ export const BahiaMap = ({ layerRef.current?.remove() layerRef.current = null pathByKeyRef.current = new Map() - geometryModuleRef.current = null geometryReadyRef.current = false } // Geometry layer rebuilds only when `mode` changes; metric/hover updates use separate effects. @@ -434,10 +450,9 @@ export const BahiaMap = ({ if (lastFittedViewportKeyRef.current === fitIdentity) return const map = mapRef.current - const geometryModule = geometryModuleRef.current - if (!map || !geometryModule || !layerRef.current) return + if (!map || !layerRef.current) return - fitMapToHighlights(map, mode, buildHighlightSet(viewportKey), geometryModule) + fitMapToHighlights(map, buildHighlightSet(viewportKey), pathByKeyRef.current) lastFittedViewportKeyRef.current = fitIdentity }, [viewportKey, mode]) diff --git a/src/components/campaign/map/MapFeatureReadout.tsx b/src/components/campaign/map/MapFeatureReadout.tsx index dea9f8ca..ef8309ab 100644 --- a/src/components/campaign/map/MapFeatureReadout.tsx +++ b/src/components/campaign/map/MapFeatureReadout.tsx @@ -103,11 +103,6 @@ export const MapFeatureReadout = ({ {/* The bubble's own line: mixing "votes he got" and "votes at stake" into one sentence is how a reader ends up quoting the wrong one. */} {bubbleReading ?

{bubbleReading}

: null} - {navigation?.kind === 'zones' ? ( -

- Toque de novo no mapa ou role até os Municípios por zona abaixo. -

- ) : null} {navigation?.kind === 'navigate' ? ( diff --git a/src/components/campaign/map/MunicipalityMapPanel.tsx b/src/components/campaign/map/MunicipalityMapPanel.tsx index ffeb2d51..04e2cf04 100644 --- a/src/components/campaign/map/MunicipalityMapPanel.tsx +++ b/src/components/campaign/map/MunicipalityMapPanel.tsx @@ -202,20 +202,20 @@ export const MunicipalityMapPanel = ({ }, [comparisonActive, displayValues, percentScaleActive]) const scopedKeys = useMemo( - () => Object.keys(bundle.municipalitiesByIbgeCode), - [bundle.municipalitiesByIbgeCode], + () => Object.keys(bundle.municipalitiesByMapKey), + [bundle.municipalitiesByMapKey], ) /** LQ per município against his statewide standard — the same ratio E10 classes on. */ - const lqByCode = useMemo(() => { + const lqByMapKey = useMemo(() => { const statewideShare = bundle.statewideShareByYear[String(year)] ?? 0 if (statewideShare <= 0) return {} const lq: Record = {} - for (const [code, votes] of Object.entries(rawValues)) { - const validVotes = validVotesForYear[code] ?? 0 + for (const [mapKey, votes] of Object.entries(rawValues)) { + const validVotes = validVotesForYear[mapKey] ?? 0 if (votes <= 0 || validVotes <= 0) continue - lq[code] = votes / validVotes / statewideShare + lq[mapKey] = votes / validVotes / statewideShare } return lq }, [bundle.statewideShareByYear, rawValues, validVotesForYear, year]) @@ -230,12 +230,12 @@ export const MunicipalityMapPanel = ({ case 'quantile': return buildQuantileClassing(rawValues) case 'lq': - return buildLqClassing(lqByCode) + return buildLqClassing(lqByMapKey) case 'competitiveRank': return buildCompetitiveRankClassing( Object.fromEntries( - Object.entries(competitiveRankForYear).map(([code, placement]) => [ - code, + Object.entries(competitiveRankForYear).map(([mapKey, placement]) => [ + mapKey, placement.rank, ]), ), @@ -243,7 +243,7 @@ export const MunicipalityMapPanel = ({ default: return null } - }, [competitiveRankForYear, effectiveScaleMode, lqByCode, rawValues]) + }, [competitiveRankForYear, effectiveScaleMode, lqByMapKey, rawValues]) // A classing with no classes paints nothing, so the legend must fall through // to the continuous ramp — passing `{}` here would tell `BahiaMap` the map is @@ -320,16 +320,16 @@ export const MunicipalityMapPanel = ({ * * Both sides are already stable references, so there is nothing to memoize. */ - const bubbleValues = showBubbles ? bundle.projectedValidVotesByCode : undefined + const bubbleValues = showBubbles ? bundle.projectedValidVotesByMapKey : undefined const bubbleFillByKey = useMemo(() => { if (!showBubbles) return undefined const fills: ChoroplethFills = {} - for (const [code, territorialClass] of Object.entries(bundle.territorialClassByCode)) { - fills[code] = territorialClassMapFill[territorialClass] + for (const [mapKey, territorialClass] of Object.entries(bundle.territorialClassByMapKey)) { + fills[mapKey] = territorialClassMapFill[territorialClass] } return fills - }, [bundle.territorialClassByCode, showBubbles]) + }, [bundle.territorialClassByMapKey, showBubbles]) // The key's reference circles and the map's radius denominator have to be // the same number, or the key labels a size the map never draws. @@ -339,14 +339,14 @@ export const MunicipalityMapPanel = ({ (key: string): string | null => { if (!showBubbles) return null - const projected = bundle.projectedValidVotesByCode[key] ?? 0 + const projected = bundle.projectedValidVotesByMapKey[key] ?? 0 if (projected <= 0) return null - const territorialClass = bundle.territorialClassByCode[key] + const territorialClass = bundle.territorialClassByMapKey[key] const classLabel = territorialClass ? territorialClassLabels[territorialClass] : null return `${formatElectionNumber(projected)} válidos projetados em jogo${classLabel ? ` · ${classLabel}` : ''}` }, - [bundle.projectedValidVotesByCode, bundle.territorialClassByCode, showBubbles], + [bundle.projectedValidVotesByMapKey, bundle.territorialClassByMapKey, showBubbles], ) /** @@ -366,7 +366,7 @@ export const MunicipalityMapPanel = ({ return `${classIndex + 1}ª de ${classing.classes.length} faixas${range ? ` (${range} votos)` : ''}` } case 'lq': { - const lq = lqByCode[key] + const lq = lqByMapKey[key] return lq === undefined ? null : formatDominanceAgainstOwnStandard(lq) } case 'competitiveRank': { @@ -378,7 +378,7 @@ export const MunicipalityMapPanel = ({ return null } }, - [classing, competitiveRankForYear, effectiveScaleMode, lqByCode], + [classing, competitiveRankForYear, effectiveScaleMode, lqByMapKey], ) // Optimistic on the control, honest pending on the map: the comparison data @@ -582,7 +582,7 @@ export const MunicipalityMapPanel = ({ relativeReadingFor={relativeReadingFor} bubbleReadingFor={bubbleReadingFor} comparisonActive={comparisonActive} - municipalitiesByIbgeCode={bundle.municipalitiesByIbgeCode} + municipalitiesByMapKey={bundle.municipalitiesByMapKey} ariaLabel={ comparisonActive && comparison ? `Mapa comparativo entre ${bundle.candidateName} e ${comparison.candidateName} por município` @@ -592,11 +592,12 @@ export const MunicipalityMapPanel = ({ {/* zone breakdown below */} {bundle.zoneBreakdown.length > 0 && !comparisonActive ? ( -
+

Municípios por zona eleitoral

- Zonas não têm polígono oficial — Salvador e Camaçari aparecem agregadas no mapa e - detalhadas aqui. + Salvador é pintada zona por zona. O desenho vem dos bairros de cada circunscrição + (TRE-BA, RA nº 2/2017) sobre a malha do IBGE — é aproximado, não é o limite oficial do + TSE.

    {bundle.zoneBreakdown.map((zone) => ( @@ -645,7 +646,7 @@ const MunicipalityMapSelection = ({ relativeReadingFor, bubbleReadingFor, comparisonActive, - municipalitiesByIbgeCode, + municipalitiesByMapKey, ariaLabel, }: { displayValues: ChoroplethValues @@ -660,7 +661,7 @@ const MunicipalityMapSelection = ({ relativeReadingFor: (key: string) => string | null bubbleReadingFor: (key: string) => string | null comparisonActive: boolean - municipalitiesByIbgeCode: MunicipalityMapBundle['municipalitiesByIbgeCode'] + municipalitiesByMapKey: MunicipalityMapBundle['municipalitiesByMapKey'] ariaLabel: string }) => { const router = useRouter() @@ -676,20 +677,12 @@ const MunicipalityMapSelection = ({ (key: string) => { if (selectedKeyRef.current !== key) return - const navigation = resolveMunicipalityMapNavigation(key, municipalitiesByIbgeCode) + const navigation = resolveMunicipalityMapNavigation(key, municipalitiesByMapKey) if (navigation.kind === 'navigate') { router.push(`/campanha/municipios/${navigation.slug}`) - return - } - - if (navigation.kind === 'zones') { - document.getElementById('municipality-zone-breakdown')?.scrollIntoView({ - behavior: 'smooth', - block: 'nearest', - }) } }, - [municipalitiesByIbgeCode, router], + [municipalitiesByMapKey, router], ) const selectedMetricValue = @@ -703,9 +696,9 @@ const MunicipalityMapSelection = ({ const selectedNavigation = useMemo( () => selectedFeature - ? resolveMunicipalityMapNavigation(selectedFeature.key, municipalitiesByIbgeCode) + ? resolveMunicipalityMapNavigation(selectedFeature.key, municipalitiesByMapKey) : null, - [municipalitiesByIbgeCode, selectedFeature], + [municipalitiesByMapKey, selectedFeature], ) return ( diff --git a/src/lib/bahiaGeometries.ts b/src/lib/bahiaGeometries.ts index 4aa2ecc4..6fbf5e81 100644 --- a/src/lib/bahiaGeometries.ts +++ b/src/lib/bahiaGeometries.ts @@ -1,21 +1,25 @@ /** - * Lazy loaders for Bahia map geometries (B3 / B5 F1). + * Lazy loaders for Bahia map geometries (B3 / B5 F1 / B8 F2). * * TopoJSON decode runs only when a map surface mounts — not on the default `/campanha` graph. - * Submodules: `bahiaMunicipalityGeometries.ts`, `bahiaTerritoryGeometries.ts`. + * Submodules: `bahiaMunicipalityGeometries.ts`, `bahiaTerritoryGeometries.ts`, + * `bahiaMunicipalityZoneGeometries.ts`. */ export type { MunicipalityGeometryModule, + MunicipalityZoneGeometryModule, TerritoryGeometryModule, } from '@/lib/bahiaGeometriesTypes' import type { MunicipalityGeometryModule, + MunicipalityZoneGeometryModule, TerritoryGeometryModule, } from '@/lib/bahiaGeometriesTypes' let municipalityModulePromise: Promise | null = null +let municipalityZoneModulePromise: Promise | null = null let territoryModulePromise: Promise | null = null export const loadMunicipalityGeometryModule = (): Promise => { @@ -25,6 +29,13 @@ export const loadMunicipalityGeometryModule = (): Promise => { + municipalityZoneModulePromise ??= import('@/lib/bahiaMunicipalityZoneGeometries').then( + (module) => module.municipalityZoneGeometryModule, + ) + return municipalityZoneModulePromise +} + export const loadTerritoryGeometryModule = (): Promise => { territoryModulePromise ??= import('@/lib/bahiaTerritoryGeometries').then( (module) => module.territoryGeometryModule, diff --git a/src/lib/bahiaGeometriesTypes.ts b/src/lib/bahiaGeometriesTypes.ts index ec2855e5..363d877d 100644 --- a/src/lib/bahiaGeometriesTypes.ts +++ b/src/lib/bahiaGeometriesTypes.ts @@ -1,4 +1,4 @@ -import type { Feature, MultiPolygon, Polygon } from 'geojson' +import type { Feature, GeoJsonProperties, MultiPolygon, Polygon } from 'geojson' import type { GeometryCollection, Topology } from 'topojson-specification' type BahiaMunicipalityProperties = { @@ -11,10 +11,26 @@ type BahiaTerritoryProperties = { name: string } +/** + * Zone municipalities (Salvador ZE 1–19): keyed by the catalog slug, because a + * zone has no IBGE code of its own — the whole city shares `ibgeCode`. + */ +type MunicipalityZoneProperties = { + municipalitySlug: string + name: string + zoneNumber: number + ibgeCode: string +} + export type BahiaMunicipalityFeature = Feature export type BahiaTerritoryFeature = Feature +export type MunicipalityZoneFeature = Feature + +/** Any feature of the committed meshes — for helpers that only read coordinates. */ +export type BahiaGeometryFeature = Feature + export type MunicipalityTopology = Topology<{ municipalities: GeometryCollection }> @@ -23,6 +39,10 @@ export type TerritoryTopology = Topology<{ territories: GeometryCollection }> +export type MunicipalityZoneTopology = Topology<{ + municipalityZones: GeometryCollection +}> + export type MunicipalityGeometryModule = { topology: MunicipalityTopology features: readonly BahiaMunicipalityFeature[] @@ -34,3 +54,9 @@ export type TerritoryGeometryModule = { features: readonly BahiaTerritoryFeature[] getTerritoryFeature: (code: string) => BahiaTerritoryFeature | undefined } + +export type MunicipalityZoneGeometryModule = { + topology: MunicipalityZoneTopology + features: readonly MunicipalityZoneFeature[] + getMunicipalityZoneFeature: (municipalitySlug: string) => MunicipalityZoneFeature | undefined +} diff --git a/src/lib/bahiaMapStyle.ts b/src/lib/bahiaMapStyle.ts index 6a5f111a..9f72b66b 100644 --- a/src/lib/bahiaMapStyle.ts +++ b/src/lib/bahiaMapStyle.ts @@ -12,7 +12,8 @@ export type ChoroplethValues = Record export type BahiaMapFillMode = 'sequential' | 'diverging' -export type FeatureKeyProperty = 'codarea' | 'code' +/** Properties that carry a map key, most specific first (see `featureMapKey`). */ +const MAP_KEY_PROPERTIES = ['municipalitySlug', 'codarea', 'code'] as const /** * Feature key → fill, for the discrete relative scales (B13). When present it @@ -91,13 +92,22 @@ export const buildHighlightSet = (highlightKey: string): Set => export const canonicalMapKeysKey = (keys: string[]): string => keys.length > 0 ? [...keys].sort().join(',') : '' -export const keyPropertyForMode = (mode: 'municipality' | 'territory'): FeatureKeyProperty => - mode === 'municipality' ? 'codarea' : 'code' - -export const featureKeyFromProperties = ( - properties: Record | undefined, - keyProperty: FeatureKeyProperty, -): string | undefined => properties?.[keyProperty] +/** + * The key a feature is painted and addressed by, whichever mesh it came from: + * a zone municipality carries `municipalitySlug` (B8 F2 — the whole city shares + * one codarea, so the code cannot identify it), a município `codarea`, an + * identity territory `code`. The three never coexist on one feature, so no mode + * argument is needed. + */ +export const featureMapKey = ( + properties: Record | undefined | null, +): string | undefined => { + for (const property of MAP_KEY_PROPERTIES) { + const value = properties?.[property] + if (typeof value === 'string' && value.length > 0) return value + } + return undefined +} export const resolvePathStyle = (context: LayerStyleContext, key: string): PathOptions => { const classed = context.fillByKey !== undefined diff --git a/src/lib/bahiaMunicipalityZoneGeometries.ts b/src/lib/bahiaMunicipalityZoneGeometries.ts new file mode 100644 index 00000000..dc236004 --- /dev/null +++ b/src/lib/bahiaMunicipalityZoneGeometries.ts @@ -0,0 +1,23 @@ +import { feature } from 'topojson-client' + +import type { + MunicipalityZoneFeature, + MunicipalityZoneGeometryModule, + MunicipalityZoneTopology, +} from '@/lib/bahiaGeometriesTypes' +import municipalityZoneTopologyJson from '@/lib/geometries/bahia-municipality-zones.topo.json' + +const topology = municipalityZoneTopologyJson as unknown as MunicipalityZoneTopology + +const municipalityZoneFeatures = feature(topology, topology.objects.municipalityZones) + .features as MunicipalityZoneFeature[] + +const zoneBySlug = new Map( + municipalityZoneFeatures.map((entry) => [entry.properties.municipalitySlug, entry]), +) + +export const municipalityZoneGeometryModule: MunicipalityZoneGeometryModule = { + topology, + features: municipalityZoneFeatures, + getMunicipalityZoneFeature: (municipalitySlug: string) => zoneBySlug.get(municipalitySlug), +} diff --git a/src/lib/geometries/bahia-municipality-zones.topo.json b/src/lib/geometries/bahia-municipality-zones.topo.json new file mode 100644 index 00000000..de33847a --- /dev/null +++ b/src/lib/geometries/bahia-municipality-zones.topo.json @@ -0,0 +1 @@ +{"type":"Topology","bbox":[-38.65748489999993,-13.017326199999957,-38.304332699999975,-12.742395399999964],"transform":{"scale":[0.000035318751875182926,0.000027495829582957576],"translate":[-38.65748489999993,-13.017326199999957]},"objects":{"municipalityZones":{"type":"GeometryCollection","geometries":[{"type":"MultiPolygon","arcs":[[[0,1,2,3]]],"properties":{"municipalitySlug":"salvador-ze-1","name":"Salvador — ZE 1","zoneNumber":1,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[4,5,6,-3]]],"properties":{"municipalitySlug":"salvador-ze-2","name":"Salvador — ZE 2","zoneNumber":2,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[7,8,9,10]]],"properties":{"municipalitySlug":"salvador-ze-3","name":"Salvador — ZE 3","zoneNumber":3,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[11,12,13]],[[14]],[[15]],[[16]],[[17]],[[18]],[[19]]],"properties":{"municipalitySlug":"salvador-ze-4","name":"Salvador — ZE 4","zoneNumber":4,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[20,21,22,23,24,25,26,-10]]],"properties":{"municipalitySlug":"salvador-ze-5","name":"Salvador — ZE 5","zoneNumber":5,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-2,27,28,29,-5]]],"properties":{"municipalitySlug":"salvador-ze-6","name":"Salvador — ZE 6","zoneNumber":6,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-27,-28,-1,30,31,-11]]],"properties":{"municipalitySlug":"salvador-ze-7","name":"Salvador — ZE 7","zoneNumber":7,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[32,33,34,35,-13,36]],[[37,38]]],"properties":{"municipalitySlug":"salvador-ze-8","name":"Salvador — ZE 8","zoneNumber":8,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[39,40,41]]],"properties":{"municipalitySlug":"salvador-ze-9","name":"Salvador — ZE 9","zoneNumber":9,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[42,43,-25,44,45,46,47]]],"properties":{"municipalitySlug":"salvador-ze-10","name":"Salvador — ZE 10","zoneNumber":10,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[48,-38,49,50,51,-22,52,-34]]],"properties":{"municipalitySlug":"salvador-ze-11","name":"Salvador — ZE 11","zoneNumber":11,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-47,53,54]]],"properties":{"municipalitySlug":"salvador-ze-12","name":"Salvador — ZE 12","zoneNumber":12,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[55,-6,-30,56,-43]]],"properties":{"municipalitySlug":"salvador-ze-13","name":"Salvador — ZE 13","zoneNumber":13,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-23,-52,57]]],"properties":{"municipalitySlug":"salvador-ze-14","name":"Salvador — ZE 14","zoneNumber":14,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[58,-40,59,60,-35,-53,-21,-9]]],"properties":{"municipalitySlug":"salvador-ze-15","name":"Salvador — ZE 15","zoneNumber":15,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-44,-57,-29,-26]],[[-58,-51,61,-45,-24]]],"properties":{"municipalitySlug":"salvador-ze-16","name":"Salvador — ZE 16","zoneNumber":16,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-14,-36,-61,62]]],"properties":{"municipalitySlug":"salvador-ze-17","name":"Salvador — ZE 17","zoneNumber":17,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-59,-8,-32,63,-41]]],"properties":{"municipalitySlug":"salvador-ze-18","name":"Salvador — ZE 18","zoneNumber":18,"ibgeCode":"2927408"}},{"type":"MultiPolygon","arcs":[[[-46,-62,-50,-39,-49,-33,64,-54]]],"properties":{"municipalitySlug":"salvador-ze-19","name":"Salvador — ZE 19","zoneNumber":19,"ibgeCode":"2927408"}}]}},"arcs":[[[3883,1288],[4,-5],[6,-9],[4,-3],[3,19],[35,57],[30,40],[10,16],[16,44],[5,16],[4,8],[8,-4],[0,-4],[3,1],[7,-11],[3,1],[5,-3],[4,2],[3,7],[4,3],[-4,11],[1,3],[17,21],[10,14],[9,24],[12,21],[13,20],[16,29],[4,5],[8,8],[3,-1],[9,13],[7,11],[5,6],[10,9],[9,7],[6,7],[3,0],[9,14],[8,5],[2,12],[5,11],[3,13],[2,1],[11,10],[-1,2],[6,8],[2,-1],[3,-5],[5,-5],[4,4],[6,10],[2,6],[4,-3],[-1,-9],[2,-4],[3,-2],[-1,-3],[6,-3],[3,4],[8,-1],[0,-3],[-7,-8],[-6,-9],[0,-8],[-6,-36],[-4,-38],[8,-51],[2,-15],[-2,-18],[-2,-7],[-4,-6],[-9,-9],[-47,-20],[-37,-15],[-9,-7],[-39,-44],[2,-17],[4,2],[1,-8],[4,0],[2,-30],[-22,-4],[3,-19],[-28,0],[0,-45],[1,-9],[8,-20],[-11,-11],[-11,-9],[-1,-11],[-3,-14],[-14,-17],[13,-20],[-11,-15],[-20,-27],[-5,-8],[-18,31],[0,-1],[-12,-27],[7,-7],[6,-9],[3,-9],[3,-15],[3,-17],[1,-22],[3,-10],[7,-10],[-13,-18],[-3,-6],[0,-9],[1,-4],[5,-6],[7,-2],[6,-6],[3,-9],[-2,-9],[-20,-24],[-8,-7],[-11,-3],[4,-17],[4,-9],[8,2],[5,3],[5,1],[3,4],[4,10],[12,16],[13,18],[4,14],[4,12],[3,5],[5,4],[8,8],[3,8],[0,10],[17,0],[16,0],[2,10],[3,6],[4,3],[6,2],[7,-1],[5,-2],[5,-4],[5,-6],[11,-25],[1,-8],[1,-13],[5,3],[7,4],[12,1],[17,-1],[14,1],[24,11],[7,5],[2,4],[3,10],[-1,21]],[[4242,1070],[6,-25],[4,-9],[9,-12],[6,-10],[6,-8],[7,-7],[4,-1]],[[4284,998],[-9,-18],[-11,-35],[-5,-19],[-2,-11],[0,-11],[2,-29],[-5,5],[-9,3],[-6,-1],[-6,-3],[-4,-4],[-6,-9],[-2,-9],[-10,-1],[-9,-2],[-10,-4],[-8,-5],[-8,-8],[-3,-5],[-12,-22],[-7,-10],[-5,-5],[-6,-4],[-10,-1],[-7,1],[-18,8],[-6,1],[-14,-1],[-22,-7],[-11,-1],[-9,1],[-6,1],[-9,3],[-7,2],[-12,5],[-10,9],[-11,9],[-3,-3],[14,-14],[4,-9],[1,-9],[-1,-9],[-5,-18],[-2,-3],[-5,-8],[-2,-6],[-20,-64],[-2,-11],[-1,-9],[3,-22],[-1,-4],[17,-4],[-2,-8],[-4,-17],[2,-6],[6,-5],[7,-1],[7,7],[3,9],[4,4],[-5,8],[8,8],[3,4],[3,14],[0,8],[3,10],[10,-3],[4,-6],[2,-6],[-1,-13],[1,0],[3,7],[3,4],[5,14],[6,3],[10,1],[5,4],[0,5],[4,23],[0,8],[9,3],[4,0],[-2,-16],[11,-2],[-3,-7],[1,-9],[5,-13],[7,-22],[0,-10],[1,-29],[1,-6],[3,-13],[4,-12],[-3,-12],[-21,-62],[1,-4],[5,-7],[-6,-7],[-13,-13],[-9,-3],[-7,0],[-7,-4],[-24,-23],[-8,-2],[-4,-6],[-2,1],[-1,-6],[0,-13],[-3,-25],[0,-3],[-7,4],[-1,2],[2,5],[-12,9],[2,9],[0,9],[-3,7],[2,5],[1,4],[-4,6],[7,1],[3,8],[4,7],[6,11],[1,6],[0,4],[-5,16],[-3,15],[-1,16],[-2,5],[-9,23],[-14,25],[-11,13],[-2,2],[1,9],[-5,6],[-5,-2],[-24,3],[-8,1],[-11,-2],[-12,-5],[-9,-4],[-8,-7],[-6,-6],[-9,-13],[-14,-17],[-29,-47],[-20,-29],[-7,-16],[-1,-4],[-1,-5],[0,-26],[5,-32],[2,0],[24,18],[10,0],[8,-1],[9,-5],[5,-5],[6,-9],[7,-15],[12,-18],[14,-24],[9,6],[6,2],[7,0],[0,-5],[2,-2],[0,-5],[-5,-8],[-3,-13],[0,-6],[3,-4],[15,-11],[3,-7],[3,-8],[0,-6],[4,-23]],[[3946,246],[-4,-1],[-3,2],[-7,-1],[-2,5],[-2,0],[-9,6],[-4,10],[-14,-2],[-6,-1],[-10,-6],[-8,-5],[-5,-1],[-6,-2],[-7,-5],[-12,-11],[-21,-8],[-14,-3],[-6,-2],[0,-3],[-5,-1],[-3,-5],[-3,0],[-8,-9],[-5,0],[1,10],[-6,-3],[-6,5],[0,3],[-5,-1],[0,9],[1,3],[-2,2],[2,12],[-1,10],[-15,8],[-20,4],[-6,3],[-12,-1],[-11,-1],[-12,2],[-19,0],[-16,-4],[-13,0],[-16,-3],[-18,-4],[-12,-1],[-9,-3],[-22,-1],[-10,-5],[-7,-9],[7,-4],[1,-4],[-5,-7],[-3,-2],[-3,-9],[-3,-4],[-5,-2],[-11,3],[-4,10],[3,4],[-3,0],[-3,-6],[-5,1],[-7,4],[6,6],[6,1],[4,3],[-1,3],[-11,-2],[-1,2],[-3,-1],[-6,3],[-4,-1],[-1,7],[7,1],[-2,5],[1,10],[8,11],[12,8],[-2,4],[4,1],[9,-1],[4,3],[0,3],[3,8],[1,6],[-1,4],[3,2],[0,6],[-2,8],[-10,1],[-5,5],[-2,5],[0,7],[-4,9],[-11,14],[-4,-1],[-3,1],[-1,2],[5,7],[2,4],[-2,11],[-4,1],[3,6],[2,1],[5,7],[7,4],[1,3],[-1,9],[-2,4],[-3,0],[-5,2],[3,13],[-3,3],[-1,-1],[-12,0],[0,4],[3,5],[5,2],[-1,6],[-8,-3],[-1,3],[-3,-1],[0,4],[-5,-1],[5,7],[0,7],[-4,2],[-3,0],[0,-6],[-3,-1],[-1,4],[3,8],[6,-1],[2,4],[-1,3],[5,1],[3,-4],[4,4],[-1,-8],[6,0],[5,5],[4,5],[4,10],[2,19],[1,14],[0,15],[-2,7],[-3,2],[0,6],[3,5],[0,4],[3,3],[-3,2],[0,3],[4,6],[-2,10],[4,8],[-3,2],[6,4],[0,6],[9,8],[5,-4],[0,-3],[4,-2],[7,1],[4,4],[8,3],[3,11],[3,3],[7,6],[6,1],[6,3],[15,15],[2,3],[7,16],[1,24],[0,11],[-3,8],[-2,4],[0,12],[0,7],[5,9],[-1,6],[-3,-1],[-1,5],[3,8],[2,12],[5,10],[9,12],[8,7],[4,7],[0,16],[1,4],[7,12],[-1,10],[1,4],[5,5],[12,12],[7,10],[5,10],[3,8],[-1,8],[1,3],[7,1],[4,3],[-3,3],[-5,1],[0,8],[5,8],[11,20],[7,10],[5,9],[8,9],[2,4],[0,13],[8,18],[4,7],[7,0],[2,6],[3,3],[2,5],[0,6],[3,9],[8,10],[2,2],[2,-4],[3,1],[0,3],[-3,5],[0,4],[3,5],[7,7],[2,11],[3,3],[5,5],[4,8],[10,10],[4,7],[4,17],[3,10],[5,11],[5,7],[5,9],[16,15],[4,3],[9,1],[4,3],[1,6],[-7,0],[0,4],[3,4],[2,7],[3,16],[6,13],[11,13],[6,7]],[[4284,998],[10,-4],[14,-4],[59,-16],[7,-2],[6,-4],[11,-9],[10,-13],[5,-11],[6,-19],[4,-11],[7,-7],[8,-5],[7,-1],[27,-1],[10,-2],[13,-9],[13,-12],[28,-30],[6,-9],[22,-26],[6,-9],[11,-11],[29,-18],[11,-5],[32,-20],[10,-7],[9,-8],[3,-4],[3,-6],[13,11],[25,11],[19,25],[22,-16],[10,-1],[1,28],[2,6],[4,2],[6,-1],[17,-7],[3,1],[4,7],[16,38],[7,-13],[1,-5],[-1,-4],[-5,-25],[-2,-3],[18,-1],[1,2],[4,2],[21,8],[8,-4],[3,-19],[21,-30],[5,-10],[1,11],[9,-7],[7,-2],[16,-11],[11,-12],[6,1],[5,-2],[4,-4],[3,2],[9,-19],[5,-14],[6,-32]],[[4976,628],[-11,-5],[-9,-6],[-6,-6],[-4,-6],[-5,-14],[-1,-9],[0,-9],[3,-37],[1,-24],[-1,-8],[-3,-7],[19,3],[2,3],[4,20],[2,7],[3,4],[10,7],[3,9],[6,3],[11,7],[19,6],[4,3],[29,28],[11,11],[11,8],[9,11],[7,4],[5,4],[0,-28],[-21,-38],[-1,-2],[-2,-56],[1,-3],[3,3],[-6,-9],[6,-16],[-1,-37],[13,-12],[5,-4],[3,-13],[3,-10],[5,-18],[1,-11],[6,-35],[6,-14],[5,-7],[9,-6],[2,-14],[-22,-2],[-7,-2],[-8,-4],[-34,-18],[-2,10],[-12,-1],[-8,3],[-9,7],[-7,9],[-13,11],[-4,5],[-9,15],[-10,10],[-8,4],[-14,0],[-6,2],[-9,5],[-3,3],[-6,7],[-4,3],[-12,45],[-9,-2],[2,-5],[-7,-4],[-5,3],[-4,0],[-27,-18],[-5,-4],[-28,3],[-4,-7],[1,-4],[15,-6],[8,-6],[3,8],[3,2],[7,-3],[13,-12],[13,-5],[8,-4],[2,-14],[-2,-13],[2,1],[2,-8],[2,-11],[4,-3],[10,-7],[6,1],[9,-9],[-2,10],[9,2],[7,7],[6,-3],[-1,-11],[4,-2],[4,-4],[3,4],[7,6],[3,-1],[0,-4],[4,-5],[3,-5],[4,-2],[4,-9],[6,-19],[0,-6],[-1,-12],[2,-3],[4,5],[7,-2],[1,-7],[-1,-5],[-19,-30],[-12,-9],[8,-3],[5,-4],[3,3],[13,4],[3,-6],[3,-1],[7,-13],[2,-8],[-2,-8],[11,-6],[6,-2],[6,0],[27,3],[24,4],[-5,40],[12,-1],[10,-7],[22,2],[8,-2],[5,7],[41,4],[5,16],[5,7],[7,3],[-1,10],[0,9],[3,3],[7,-22],[8,-10],[2,-13],[41,5],[0,16],[9,3],[26,23],[30,-35],[17,4],[-11,15],[-9,4],[-7,7],[3,4],[18,14],[6,5],[6,-10],[33,-54],[5,-2]],[[5398,193],[-10,-9],[-7,-10],[-5,-12],[-8,-12],[-5,-15],[-6,-22],[-7,-19],[-6,-5],[-24,10],[-17,3],[-8,-2],[-17,-11],[-6,4],[-10,8],[-33,12],[-14,3],[-13,-4],[-13,-3],[-12,-1],[-13,-2],[-12,0],[-9,-3],[-40,-14],[-18,-7],[-18,-8],[-12,-9],[0,-6],[-7,-7],[-18,-8],[-5,1],[-1,4],[-3,5],[-5,4],[1,4],[-3,4],[-5,4],[-10,2],[-3,-3],[-6,3],[-2,5],[-3,13],[-8,1],[-6,2],[-11,-2],[-6,-7],[-8,0],[-5,-4],[-14,-9],[-8,-7],[-5,3],[-7,-1],[-4,-3],[-3,-11],[-5,0],[-2,-2],[-1,-7],[-4,-3],[-9,4],[-2,4],[-8,2],[-4,-9],[-2,2],[-3,-11],[-2,-3],[-2,2],[-2,-1],[-2,-11],[-4,-4],[-4,1],[-4,-4],[-12,-11],[-4,-1],[-4,2],[-2,5],[-17,2],[-5,-1],[-3,-6],[-6,0],[-5,15],[-1,7],[3,5],[-3,5],[-4,1],[-1,5],[-2,19],[-1,18],[-3,4],[-4,-1],[-3,-4],[-6,1],[-2,4],[-5,5],[0,11],[0,19],[-5,3],[-10,10],[-9,8],[-6,4],[-5,-3],[-6,0],[-5,-3],[-6,-3],[-7,0],[-5,2],[-2,4],[3,8],[-7,3],[-4,-1],[-4,2],[1,11],[0,8],[6,10],[4,4],[-4,8],[-4,8],[-11,7],[-7,-1],[0,-3],[-9,0],[-4,-3],[-12,-1],[-3,-4],[-1,4],[-3,0],[-6,6],[-6,2],[-5,-4],[-4,0],[0,6],[-6,-5],[-4,0],[-6,-5],[-5,1],[-2,4],[-17,0],[-3,4],[-3,20],[-3,4],[-5,1],[-17,-8],[-1,-4],[0,-6],[2,-5],[-1,-5],[-12,-3],[-5,-1],[-13,-4],[-4,-2],[-5,0],[-11,-8],[-9,2],[-4,-2],[-1,-5],[-2,-2],[-7,-2],[-6,3],[-3,-5],[-7,-3],[-3,3],[-4,-1],[-9,8],[-1,5],[2,5],[-3,3],[-1,7],[-9,4],[-7,3],[-5,6],[-2,11],[-9,4],[-9,-1],[-11,-2],[-10,-4],[-14,-6],[-8,-5],[-14,-12],[-6,-3],[-3,2],[-5,-6],[0,-8],[-4,-6],[-6,-2],[-5,1],[-5,3],[0,7],[4,10],[-5,5],[-8,3],[-7,0],[-4,-2],[-2,-3],[-6,-7],[-11,-4],[-6,1],[-10,-7],[-8,6],[-6,8],[-9,5],[-3,6],[-7,4],[-3,6],[1,4],[-4,5],[-6,2],[-18,-1],[-14,-12],[0,-5],[-4,-8],[-4,-3],[0,-15],[1,-13],[-7,-1],[-2,-4],[-3,-1],[-1,6],[-2,4],[2,3],[-7,4],[0,4],[5,3],[1,9],[1,1],[-2,6],[-10,9],[-5,2],[-4,-2],[-4,-11],[-5,0],[-3,3],[-4,6],[0,3],[-9,-1],[-7,-4],[-3,2],[-2,8],[-4,0],[-1,-5],[-7,-5],[-3,-3],[-6,-2],[-16,10],[-3,13],[2,5],[-1,3],[-5,-3],[-5,1],[0,5],[3,6],[3,2],[-5,1],[-4,5],[-9,6],[-6,2],[-6,-1],[-4,-5],[0,-5],[-5,-3],[-4,0],[-4,4],[-1,-4],[-12,-3],[-4,3],[-9,-1],[-4,4],[-1,-1],[-2,-5],[-4,-3],[-4,0],[-6,3]],[[4583,1987],[1,4],[4,-1],[8,0],[9,5],[9,11],[2,5],[5,15],[3,2],[1,6],[5,-2],[0,-9],[-1,-14],[1,-7],[4,-10],[3,-3],[7,-4],[2,0],[17,7],[2,13],[15,-3],[5,31],[10,2],[7,19],[15,-3],[4,18],[9,-3],[15,-5],[-1,-4],[-5,-12],[-2,-3],[4,1],[1,-6],[11,12],[2,6],[7,25],[5,-3],[0,-11],[-2,-13],[-5,-7],[-16,-18],[2,-7],[19,26],[5,9],[1,7],[4,29],[1,8],[2,21],[-16,4],[5,4],[7,3],[3,-1],[1,8],[5,14],[10,20],[2,5],[-16,22],[2,2],[-16,19],[-10,7],[-4,2],[8,21],[5,-3],[14,-1],[3,-1],[7,-8],[0,-1],[14,18],[-15,10],[-9,8],[-3,4],[-2,7],[-2,3],[-1,6],[-7,1],[-9,3],[-12,-3],[-16,-3],[-13,-2],[-9,-1],[-16,1],[-24,2],[1,3],[-5,9],[-4,8],[0,13],[2,6],[1,8],[-1,15],[3,13],[19,32],[8,16],[1,4],[1,19],[-4,21],[-2,6],[23,8],[-7,28],[-5,25],[2,0],[12,7],[2,-6],[2,-22],[3,-19],[6,3],[15,-8],[11,-5],[14,-1],[22,3],[16,-3],[5,-3],[5,-1],[13,-8],[2,-1],[31,-4],[3,9],[-6,4],[-7,15],[8,7],[3,6],[2,7],[8,1],[0,8],[1,5],[0,6],[3,11],[2,-1],[4,12],[16,28],[8,10],[7,3],[4,0],[5,5],[7,4],[7,2],[-2,5],[9,4],[-7,23],[-12,44],[-11,24],[-13,28],[-4,5],[-6,5],[-11,8],[-16,6],[-7,4],[-4,5],[-5,8],[-5,8],[-4,11],[-5,19],[-5,10],[-5,3],[-7,2],[-11,-1],[-28,-5],[-13,-1],[-8,0]],[[4751,2814],[1,13],[3,11],[6,0],[10,5],[10,8],[8,7],[9,10],[4,2],[36,18],[5,3],[6,1],[24,9],[14,9],[4,5],[10,13],[18,15],[16,10],[13,14],[8,-14],[4,-10],[8,-14],[3,-4],[7,8],[2,4],[9,11],[4,-5],[10,-6],[10,-14],[11,-1],[9,3],[1,-9],[8,-19],[5,-5],[7,-2],[10,2],[7,-5],[7,-8],[3,-1],[7,1],[10,2],[22,-11],[-2,-8],[-7,-11],[0,-3],[4,-2],[8,-9],[9,-14],[3,-5],[-3,-3],[2,-5],[3,1],[5,-10],[8,3],[4,0],[12,-6],[5,2],[19,-15],[12,-6],[9,-1],[9,1],[1,-18],[5,1],[6,0],[8,-4],[8,-4],[7,-5],[-1,-1],[-11,-44],[10,-3],[-11,-9],[-7,-6],[-5,-10],[8,-5],[-2,-6],[2,-6],[-10,-3],[-5,13],[-9,-7],[-4,5],[-6,-2],[-5,3],[-14,-9],[-4,-6],[-1,-5],[3,-14],[-2,-13],[1,-1],[-2,-9],[-2,2],[-3,-5],[-8,-6],[-6,-6],[2,-10],[7,-3],[7,-11],[9,6],[0,-21],[4,-24],[0,-5],[-2,-8],[-3,1],[-2,-3],[-9,0],[-16,2],[-2,-14],[8,-6],[5,-3],[-3,-10],[5,-6],[11,-18],[-10,-9],[-16,-10],[-4,-2],[-8,-1],[-3,-2],[-6,-16],[3,-8],[-1,-8],[1,-6]],[[5138,2401],[-8,-2],[0,-3],[-1,-8],[11,-5],[11,-7],[11,-9],[0,-16],[1,-32],[0,-14],[-1,-13],[-1,-11],[-2,-8],[-15,-46],[-11,-33],[-30,-92],[-31,-96],[-3,-10],[-10,-18],[-9,-26],[-3,-14],[-5,-22],[-3,-15],[-13,-94],[-2,-19],[4,-29]],[[5028,1759],[-21,-1],[-10,1],[-33,7],[-16,3],[-45,10],[-87,19],[-16,4],[-13,6],[-54,31],[-29,17],[-8,2],[-10,2],[-18,0],[-2,14],[-4,12],[-6,-3],[-14,0],[-11,28],[-4,15],[7,5],[-3,9],[-4,0],[-4,19],[0,11],[7,-2],[4,-2],[6,-6],[-1,17],[-9,3],[-19,1],[-15,2],[-13,4]],[[5120,5863],[5,20],[5,10],[3,11],[3,4],[2,10],[4,8],[1,12],[5,19],[1,18],[0,8],[-3,9],[-2,7],[-2,21],[-2,8],[-4,6],[1,5],[-3,4],[0,16],[2,7],[2,6],[2,16],[3,12],[4,20],[4,25],[1,2],[18,-3],[15,-3],[11,-2],[2,2],[2,10],[1,6],[8,14],[5,13],[6,12],[1,6],[9,21],[2,2],[3,8],[8,14],[11,16],[-1,13],[0,10],[-1,2],[0,7],[-3,-5],[0,9],[3,7],[-3,2],[-6,-6],[-1,8],[2,2],[8,5],[-2,3],[-31,4],[-3,3],[12,-1],[3,6],[3,-2],[2,-5],[5,-1],[6,9],[2,10],[1,12],[-1,13],[-1,11],[-4,18],[-2,13],[-5,14],[-9,14],[-22,30],[-11,12],[-2,0],[-15,20],[-11,17],[-10,15],[-7,9],[-2,6],[0,3],[-6,2],[-4,3],[-10,14],[-3,7],[-8,16],[-7,10],[-6,7],[-9,12],[-7,7],[-8,12],[-13,13],[-6,7],[-16,15],[-12,10],[-9,9],[-11,9],[-26,18],[-18,12],[-9,5],[-8,4],[-11,8],[-4,1],[-6,-2],[-3,-2],[-2,-11],[-5,-6],[-6,-6],[-7,-8],[-5,-8],[-1,-3],[-3,-2],[-13,-1],[-1,-6],[-3,0],[-4,4],[-10,6],[-4,0],[-7,-7],[-8,-1],[-4,-4],[-4,0],[-6,6],[-12,1],[-6,6],[-2,6],[-5,2],[-3,5],[-1,14],[-3,5],[-3,-1],[-9,-5],[-1,4],[1,4],[-1,6],[-7,11],[-4,6],[-9,10],[-6,9],[-6,7],[-6,11],[1,7],[5,8],[-3,9],[1,3],[4,5],[7,3],[6,12],[1,4],[1,16],[-1,6],[1,9],[-1,11],[-4,13],[-1,11],[-1,2],[-7,26],[-2,16],[0,11],[1,33],[3,7],[5,-1],[6,1],[4,4],[2,7],[1,9],[4,25],[2,10],[1,10],[2,7],[5,0],[3,-2],[6,-1],[2,5],[0,5],[-2,5],[-6,4],[-7,10],[0,7],[1,7],[7,21],[6,11],[3,9],[3,12],[2,12],[3,25],[-1,15],[4,5],[-1,5],[-2,5],[1,7],[-1,3],[-5,7],[-1,9],[-5,12],[1,8],[1,7],[-1,12],[-3,10],[-6,15],[-1,5],[-6,14],[-3,12],[-10,21],[-9,15],[-9,15],[-10,14],[-5,6],[-13,13],[-8,6],[-12,6],[-11,5],[-6,6],[-6,7],[-8,7],[-16,12],[-8,5],[-20,6],[-9,2],[-11,0],[-9,-1],[-4,-2],[-6,-4],[-7,-7],[-8,-4],[-9,-6],[-8,-4],[-5,-4],[-2,5],[-7,2],[-12,0],[-4,-1],[-3,-3],[-1,1],[0,7],[-3,3],[-4,2],[-9,2],[-6,4],[-10,10],[-10,7],[-5,0],[-8,4],[-3,-2],[1,13],[-3,1],[0,-10],[-2,-1],[-4,4],[-3,13],[-3,5],[1,4],[4,0],[1,3],[-1,8],[-2,13],[0,6],[5,9],[-1,6],[-1,7],[2,9],[0,8],[-4,4],[-1,14],[-3,4],[0,7],[4,14],[-1,37],[1,21],[3,10],[2,11],[2,6],[6,27],[4,10],[5,20],[0,6],[-9,5],[-7,7],[-3,5],[-4,13],[0,8],[7,2],[5,3],[1,3],[-1,11],[0,11],[2,10],[-1,16],[0,12],[-2,9],[-4,21],[-1,13],[4,25],[-1,14],[2,9],[4,8],[2,6],[0,20],[0,17],[1,11],[0,10],[-3,13],[-4,10],[-12,25],[-2,6],[-1,18],[1,4],[7,11],[3,5],[2,9],[4,14],[-1,6],[-12,28],[-4,4],[-2,5],[-1,12],[4,16],[3,10],[10,24],[10,6],[8,1],[6,-1],[16,-7],[6,-4],[8,-7],[16,-20],[10,-6],[11,-9],[18,-8],[2,1],[11,-3],[6,-4],[2,-2],[1,-8],[1,-6],[0,-9],[7,-1],[-2,-20],[1,-2],[-7,-54],[-6,1],[1,-17],[-1,-20],[2,-3],[5,0],[7,13],[3,0],[1,-7],[2,0],[14,108],[4,0],[-14,-114],[1,-7],[39,-66],[1,-14],[-7,-5],[-27,-25],[-10,-10],[-4,-6],[3,-5],[6,3],[37,34],[6,6],[15,-8],[47,44],[46,43],[3,1],[0,2],[3,3],[-2,4],[-6,0],[-2,3],[-5,11],[1,9],[4,7],[3,2],[12,5],[9,6],[11,4],[15,1],[15,1],[5,2],[3,3],[5,9],[4,10],[3,11],[0,12],[4,10],[1,6],[-2,11],[-8,5],[4,5],[1,7],[2,2],[-1,3],[4,2],[2,5],[7,9],[8,7],[9,2],[3,2],[11,-2],[10,-3],[8,-1],[5,0],[15,6],[4,4],[9,5],[0,1],[6,4],[11,4],[12,2],[15,4],[4,5],[8,1],[13,8],[7,5],[3,0],[5,-3],[5,-1],[2,-3],[9,-5],[7,-1],[8,1],[14,6],[1,-3],[-7,-7],[-6,-4],[-4,-1],[0,-5],[-6,-8],[11,-4],[4,-1],[15,2],[6,3],[6,8],[8,29],[2,7],[1,10],[-5,15],[-3,4],[-6,3],[-5,10],[3,9],[1,6],[6,4],[10,1],[6,-2],[4,-5],[13,-32],[10,-2],[13,7],[1,2],[0,7],[1,4],[6,3],[9,-6],[3,-1],[3,-7],[4,-15],[2,-5],[4,-5],[3,-5],[5,-2],[9,5],[5,7],[7,3],[7,-2],[6,-3],[5,-10],[6,-7],[5,-18],[1,-5],[4,-7],[6,-6],[3,-7],[3,-5],[1,-11],[9,-21],[4,-5],[8,-8],[5,-2],[7,4],[9,9],[6,3],[4,0],[16,-9],[2,-9],[-3,-4],[-10,-7],[-6,-7],[-9,-11],[-6,-12],[-3,-9],[0,-11],[0,-13],[3,-8],[1,-4],[0,-5],[-1,-2],[-15,7],[-9,2],[-18,-1],[-4,1],[-6,7],[-5,4],[-2,3],[-5,12],[-2,7],[1,4],[-2,1],[0,6],[6,1],[7,3],[3,4],[0,10],[-2,3],[-7,7],[-5,4],[-10,0],[-2,1],[-7,0],[-2,-2],[-3,1],[-11,-2],[-9,6],[-3,1],[-10,1],[-2,-3],[-7,-3],[-4,2],[-7,-5],[-5,0],[-8,-4],[-6,-4],[-3,-4],[-12,-21],[-2,-7],[0,-5],[2,-5],[8,-9],[5,-9],[1,-4],[2,-11],[2,-4],[1,-8],[8,-19],[1,-10],[-2,-3],[-10,-5],[-7,-1],[-14,2],[-5,-1],[-12,-6],[-5,0],[1,-8],[2,-6],[5,-8],[11,-9],[17,-18],[9,-7],[13,-3],[14,2],[14,-1],[4,-2],[2,-3],[0,-5],[-3,-7],[1,-9],[-6,-10],[-4,-4],[-9,-5],[-2,-2],[-7,-12],[-6,-5],[-13,-9],[-7,-6],[-3,-4],[-4,-8],[-3,-10],[-6,-29],[-6,-17],[-4,-15],[-2,-18],[0,-22],[-2,-6],[1,-13],[0,-10],[6,-6],[2,-6],[5,-1],[5,-4],[3,3],[8,-1],[3,-6],[-1,-14],[-3,-12],[-1,-10],[0,-21],[-2,-11],[-3,-15],[-10,-25],[-7,-15],[-8,-14],[-3,-10],[-4,-5],[-6,-1],[-4,2],[-4,6],[-5,3],[-15,0],[-3,-2],[-1,-6],[-4,-9],[-4,-6],[-5,-1],[-3,-4],[-3,-6],[-3,-12],[-4,-10],[0,-7],[-2,-7],[-6,-10],[-9,-16],[-3,-10],[-4,-9],[-7,-8],[-8,-17],[-1,-12],[-5,-10],[-6,-4],[-6,-20],[-5,-15],[0,-10],[4,-4],[0,-10],[2,-9],[1,-12],[2,-8],[2,-13],[1,-10],[-1,-7],[-4,-9],[-5,-19],[-6,-4],[-3,-11],[2,-9],[5,-5],[5,-9],[1,-8],[2,-4],[14,-8],[6,-5],[5,4],[9,-2],[6,1],[7,8],[1,11],[3,4],[-2,4],[0,5],[4,6],[5,2],[1,4],[3,2],[7,0],[4,1],[4,5],[0,3],[3,4],[1,4],[3,2],[-1,7],[2,3],[4,15],[3,3],[6,10],[3,10],[0,10],[-2,2],[1,7],[-2,6],[-6,11],[-4,22],[1,14],[2,5],[-1,2],[2,12],[3,5],[3,1],[2,3],[4,3],[-3,8],[0,13],[3,5],[2,-1],[7,5],[8,0],[5,4],[2,5],[1,6],[2,4],[-1,3],[1,6],[4,5],[4,-2],[0,7],[1,4],[5,4],[-2,4],[3,8],[8,7],[1,5],[9,7],[3,4],[6,13],[7,9],[6,13],[8,9],[8,3],[4,-2],[5,-5],[9,-3],[12,-9],[9,-11],[4,-20],[1,-10],[0,-21],[3,-31],[0,-20],[3,-6],[-1,-4],[1,-8],[2,-3],[3,-6],[2,-10],[2,-8],[3,-15],[2,-6],[-1,-3],[6,-7],[6,-2],[5,-3],[5,-6],[6,-3],[4,1],[7,-5],[2,-3],[4,0],[8,-4],[5,1],[6,4],[5,8],[1,5],[5,8],[7,7],[10,1],[4,-2],[2,-6],[6,-9],[5,-12],[0,-9],[-2,-11],[-3,-5],[-4,-2],[-3,-6],[-5,-6],[-5,-2],[-6,0],[-14,0],[-3,-6],[8,-1],[10,-3],[2,-3],[10,-7],[12,-2],[14,0],[6,3],[4,5],[2,5],[6,0],[-1,5],[1,8],[3,7],[0,5],[1,6],[6,9],[2,7],[3,2],[5,8],[1,5],[10,13],[10,17],[2,1],[1,-5],[5,-10],[2,-12],[0,-7],[-3,-5],[-7,-8],[-7,-9],[-3,-8],[-2,-12],[1,-9],[2,-5],[4,-8],[7,-4],[1,-8],[-7,-9],[-3,-7],[2,-5],[9,-2],[8,-5],[0,-4],[-2,-4],[-10,-1],[-2,-1],[0,-8],[4,-3],[4,-5],[3,-9],[2,-5],[4,-5],[2,-4],[1,-12],[8,-14],[2,-14],[3,-6],[-1,-4],[-2,-5],[0,-3],[6,-4],[1,-4],[0,-26],[-3,-11],[0,-4],[4,-12],[-2,-9],[-2,-19],[2,-8],[4,-10],[3,-4],[11,-4],[3,-2],[11,-11],[1,-8],[-5,-21],[-1,-16],[-1,-17],[-3,-15],[-5,-15],[-5,-41],[-5,-19],[-1,-9],[2,-10],[6,-12],[1,-6],[-2,-13],[1,-5],[6,-10],[5,-1],[22,-4],[6,-3],[6,-5],[5,-9],[10,-10],[22,-9],[7,-5],[16,-7],[29,-10],[20,-6],[7,-2],[23,-13],[9,-9],[10,-13],[0,-12],[-2,-8],[-12,-30],[-10,-31],[-7,-48],[-2,-29],[1,-10],[2,-9],[6,-18],[3,-11],[11,-6],[12,-4],[15,2],[13,0],[10,-7],[5,-1],[7,1],[4,-1],[5,-6],[3,-5],[4,-8],[3,-15],[-1,-6],[-8,-11],[-11,-12],[-8,-11],[-1,-6],[0,-6],[3,-14],[5,-13],[3,-13],[5,-11],[3,-7],[6,-9],[9,-9],[7,-4],[10,-4],[3,-5],[15,-37],[4,-8],[14,-13],[12,-5],[16,-5],[9,-7],[5,-5],[5,-7],[14,-9],[7,11]],[[5872,6248],[3,-4],[9,-1],[3,-27],[-12,0],[-16,1],[-16,2],[-11,3],[-18,7],[-17,7],[-23,7],[-17,2],[-17,-1],[-23,-3],[-12,-1],[-7,2],[-5,-4],[3,-17],[2,0],[7,-33],[2,-14],[0,-13],[-1,-17],[-4,-28],[-2,-5],[-5,-33],[-5,-44],[-1,-8],[-2,-10],[-1,-6],[1,-21],[3,-21],[2,-12],[3,-9],[4,-10],[1,-6],[3,-6]],[[5703,5925],[-8,-2],[-8,-6],[-5,-11],[0,-7],[1,-4],[11,-16],[2,-7],[1,-11],[-3,-8],[0,-2],[-4,-9],[-10,-14],[-12,-13],[-5,-8],[2,-9],[2,-6],[2,-11],[-4,-12],[-1,-10],[2,-8],[4,-12],[0,-11],[-8,-27],[-2,-3],[-8,-12],[-14,-27],[-4,-10],[-14,-46],[-1,-17],[-1,-17],[-2,-12],[-7,-2],[-10,0],[-8,-5],[-5,-2],[-4,-3],[-9,0],[-8,-3],[-4,0],[-5,2],[-3,-4],[2,-2],[-1,-4],[-4,-2],[0,-10],[-8,-8],[-1,-2],[-6,-3],[-3,-4],[-10,-4],[-4,-1],[-13,-6],[-16,1],[-1,-1],[3,-6],[2,-7],[-4,-14],[-1,-1],[-3,-15],[-2,-2],[-3,-8],[-15,-14],[-3,-5],[-3,1],[-4,-4],[-6,0],[-5,5],[-3,5],[-1,4],[-10,10],[-5,2],[-6,-1],[-6,2],[-1,4],[-4,1],[-5,5],[0,5],[-4,0],[-2,14],[1,2],[-2,15],[-3,5],[2,9],[-3,8],[0,10],[-6,20],[-5,6],[-9,6],[-2,4],[0,5],[4,9],[0,4],[-2,9],[0,6],[1,4],[-14,3],[-2,10],[-6,3],[-10,1],[-7,16],[-5,5],[-12,8],[1,3],[9,14],[18,19],[6,9],[23,43],[4,6],[9,7],[12,2],[12,4],[10,25],[-34,9],[-44,8],[-19,3],[-32,2],[-27,4],[-1,15],[1,5],[-27,5],[-13,3],[-4,-10],[-3,-14],[-3,-2],[-6,1],[-19,0],[-2,-1],[-9,7],[1,18],[5,24],[2,4],[-16,-4],[-4,0],[-16,5],[-14,-1],[-9,3],[-8,4]],[[3539,8631],[4,11],[0,13],[-2,18],[-3,8],[-6,26],[-2,4],[-5,3],[1,3],[4,3],[-3,6],[0,3],[-4,-1],[-2,-4],[-6,1],[-6,4],[-2,7],[2,6],[5,3],[-2,6],[11,13],[6,12],[7,19],[1,14],[-2,17],[-2,14],[-3,10],[-4,15],[-2,5],[-17,25],[-7,12],[-12,21],[-8,10],[-5,6],[-14,13],[-13,9],[-28,18],[-16,10],[-33,22],[-43,22],[-22,13],[-24,19],[-23,20],[-18,21],[-12,18],[-5,12],[-8,26],[-14,40],[-20,35],[-25,30],[-4,8],[-2,10],[0,7],[3,12],[3,7],[10,14],[18,22],[15,18],[4,6],[4,8],[9,27],[1,8],[2,18],[1,35],[1,25],[1,9],[2,11],[5,19],[7,19],[7,12],[10,24],[6,16],[7,15],[8,15],[13,28],[4,10],[5,12],[11,19],[5,12],[4,16],[2,18],[1,13],[-2,7],[-7,9],[-1,7],[5,8],[11,4],[7,15],[3,3],[4,0],[4,-5],[5,-3],[10,-3],[7,2],[-10,0],[-8,2],[-5,5],[-1,2],[3,5],[11,14],[10,15],[5,10],[3,14],[2,23],[3,20],[5,21],[7,21],[6,10],[3,1],[4,-1],[8,-11],[7,-13],[5,-6],[8,1],[9,2],[20,12],[10,8],[23,15],[10,9],[7,6],[5,3],[9,2],[7,-1],[11,-9],[5,-3],[7,-6],[5,-6],[8,-3],[4,-13],[2,-2],[10,-8],[4,-2],[16,-6],[6,-4],[2,-9],[4,-10],[9,-18],[5,-9],[8,-6],[7,-5],[9,-4],[10,-2],[9,-5],[12,-11],[14,-14],[3,-4],[4,-15],[3,-7],[7,-8],[16,-23],[4,-8],[8,-8],[10,-9],[10,-5],[6,2],[10,-5],[9,-7],[4,-1],[6,-7],[6,-2],[4,0],[3,2],[10,10],[10,8],[1,3],[3,14],[3,-5],[0,-8],[-1,-7],[4,-5],[1,-8],[-1,-5],[7,7],[1,-1],[-1,-7],[1,-3],[8,-2],[21,22],[2,9],[7,4],[0,4],[-5,3],[-7,2],[-4,-2],[-10,-1],[-4,2],[0,4],[6,3],[6,2],[3,4],[3,2],[4,4],[6,1],[10,3],[4,0],[8,-4],[5,-6],[7,-5],[2,-4],[0,-4],[-3,-13],[-2,-10],[4,-13],[3,-1],[8,-12],[9,-15],[18,-15],[18,-19],[3,-6],[3,-11],[7,-9],[4,-9],[1,-5],[-2,-6],[-4,-9],[-1,-4],[0,-18],[0,-23],[1,-11],[0,-11],[-1,-6],[-5,-11],[-4,-2],[0,-7],[-2,-7],[1,-14],[-3,-13],[0,-6],[-2,-4],[-10,8],[-1,-2],[-5,2],[-3,-1],[-3,-4],[-1,-4],[-3,-4],[-16,-30],[-2,-8],[1,-12],[-3,-7],[2,-12],[5,-14],[1,-8],[-2,-4],[8,-13],[3,-10],[3,-9],[7,-44],[3,-10],[1,-8],[4,-9],[-3,-14],[-6,0],[-1,-6],[-4,-6],[7,-19],[0,-5],[-1,-5],[2,-11],[-3,-4],[1,-4],[-7,-10],[-2,-1],[-9,2],[-1,-1],[-2,-11],[0,-14],[1,-4],[6,-6],[4,-8],[1,-6],[1,-18],[-1,-10],[-5,-9],[1,-10],[-2,-9],[-6,-10],[-9,-9],[-3,-5],[-4,-14],[-1,-13],[1,-5],[0,-7],[11,-7],[0,-5],[-3,-2],[-1,-5],[3,-4],[2,-7],[7,-3],[1,-12],[-3,-12],[6,-10],[5,-9],[4,-15],[2,-9],[0,-8],[0,-12],[0,-19],[0,-6],[1,-3],[3,-4],[1,-20],[1,-5],[4,-12],[1,-7],[7,-14],[8,-21],[7,-13],[3,-3],[0,-3],[10,-16],[4,-9],[1,-7],[-1,-4],[3,-11],[2,-5],[-5,-14],[1,-6],[7,-7],[3,-10],[2,-1],[2,-5],[-4,-14],[1,-5],[4,-4],[3,-9],[3,-7],[0,-4],[-3,-9],[2,-5],[3,-4],[0,-2],[3,-4],[1,-10],[4,-1],[0,-6],[2,-5],[4,-15],[1,-5],[4,-3],[4,-19],[2,-5],[5,-8],[1,-14],[3,-1],[1,-6],[4,-24],[0,-5],[-2,-2],[-9,22],[-2,1],[-4,-2],[-11,-10],[-5,-5],[-8,-10],[-4,-3],[-4,-6],[-2,-7],[-6,-5],[0,-10],[4,-4],[4,-13],[3,-8],[-1,-10],[0,-24],[1,-22],[5,-14],[6,-2],[3,-5],[2,0],[2,4],[7,0],[-1,-8],[-5,-1],[-6,1],[0,3],[-3,-1],[0,5],[-4,1],[0,-2],[0,-11],[3,-5],[3,0],[2,-6],[-7,-7],[-5,-12],[0,-5],[2,-7],[0,-4],[3,-4],[1,-7],[3,-3],[1,-2],[-8,-9],[-7,-1],[1,-4],[-1,-6],[7,-6],[1,-3],[-8,-8],[-4,-1],[-3,8],[-5,5],[-1,-5],[1,-3],[-2,-1],[-3,0],[-6,5],[-4,-8],[0,3],[-5,-3],[-3,-9],[2,-3],[11,-11],[0,-3],[-3,-3],[-3,4],[-1,-2],[-3,0],[-3,-6],[-1,-8],[7,1],[4,-2],[2,-7],[-2,-6],[-3,0],[-6,11],[-4,2],[-4,-3],[2,-7],[-3,6],[-3,2],[-6,-2],[0,-6],[5,-6],[-4,-16],[7,-3],[3,-6],[3,0],[6,5],[2,-4],[-1,-7],[-4,-6],[-9,-14],[-10,-9],[-9,-4],[-12,-8],[-7,-11],[-8,-7],[-6,-9],[-13,-7],[-2,-2],[-13,-10],[-9,-13],[-4,-8],[-8,-1],[-3,-1],[-8,-9],[-1,-9],[2,-4],[-3,-8],[-6,-2],[-3,-6],[-3,-3],[-4,-8],[-7,0],[-3,-2],[-2,-8],[-4,-9],[0,-6],[3,-6],[1,-5],[-1,-5],[1,-8],[0,-4],[-5,0],[-5,11],[-2,11],[-8,2],[-1,5],[1,11],[0,3],[-4,1],[-6,-4],[-9,-3],[-5,-7],[-12,2],[-1,-1],[1,-5],[10,-4],[4,-6],[3,1],[3,4],[3,-2],[1,-5],[-3,-6],[-1,-9],[-4,-3],[-5,2],[1,8],[-2,5],[-7,4],[-2,-1],[0,-6],[-2,-6],[-2,-2],[-5,1],[-3,3],[-9,14],[-3,10],[-4,3],[-4,0],[-2,-4],[0,-10],[-3,-5],[-5,-3],[-3,-5],[-3,-1],[-2,4],[0,10],[-2,9],[-6,3],[-8,1],[-4,1],[-6,4],[-17,7],[-9,4],[-10,3],[-3,2],[-5,-3],[-4,3],[-2,-2],[-5,1],[-8,4],[-12,1],[-7,2],[-5,4],[-5,3],[-9,3],[-5,0],[-16,-2],[-3,-4],[-2,1],[1,5],[0,9],[-6,5],[-8,4],[-4,-1],[-5,-9],[-10,-7],[-8,1],[-5,-1],[-4,-5],[-6,-2],[-5,2],[-10,0],[-4,2],[-6,11],[-2,2],[-5,0],[-3,-5],[-7,-1],[-8,0],[-7,-1],[-7,1],[-11,7],[-3,4],[0,7],[0,7],[3,5],[7,10],[1,8],[-3,7],[2,9],[4,8],[5,3],[12,1],[3,4],[-2,9],[-5,3],[-2,12],[-1,2],[-5,-4],[-2,0],[-3,5],[1,6],[-3,0],[-3,-2],[-6,0],[-2,4],[0,9],[2,13],[2,7],[7,5],[7,0],[4,-2],[3,-9],[2,-1],[5,9],[1,6],[-1,7],[-3,2],[-6,2],[-10,1],[-7,3],[3,12],[2,1],[5,0],[1,2],[3,12],[6,5],[3,-2],[3,-6],[6,-1],[5,-4],[7,-1],[6,6],[5,8],[4,1],[2,5],[-2,0],[1,6],[5,4],[3,6],[10,8],[5,2],[-1,8],[-8,0],[-9,8],[-4,0],[0,6],[4,1],[1,2],[-4,4],[-2,0],[2,8],[4,2],[4,3],[1,4],[2,-1],[5,11],[-1,3],[-3,-1],[0,6],[2,2],[4,-4],[-3,5],[0,7],[-1,9],[4,0],[1,10],[-2,4],[-6,-2],[-2,5],[2,3],[-2,8],[-2,-5],[-2,7],[1,3],[-1,10],[0,9],[-8,20],[-3,4],[-4,8],[-2,-2],[-1,8],[-5,6],[0,4],[7,6],[3,-1],[3,4],[-1,7],[-9,6],[-3,7],[-2,-2],[-3,4],[3,7],[-1,4],[4,2],[3,-4],[0,5],[-3,1],[0,7],[-2,1],[-1,-7],[-3,5],[1,2],[-2,3],[1,3],[-1,3],[-3,0],[-2,14],[2,10],[4,2],[-1,4],[-1,1],[-3,10],[1,3],[-3,2],[-2,4],[-1,6],[-2,3],[2,7],[2,2],[4,6],[4,3],[1,5],[4,8],[0,9],[2,1],[-1,8],[3,5],[8,3],[5,-1],[4,0],[6,2],[5,-2],[7,6],[1,4],[6,1],[1,9],[3,7]],[[242,9213],[5,-10],[-1,-8],[3,-9],[0,-7],[1,-4],[5,-10],[2,-6],[-2,-9],[-3,-6],[-25,-22],[-7,-2],[-7,-6],[-5,1],[-2,6],[0,9],[5,9],[3,6],[0,31],[-7,18],[4,6],[-1,1],[3,8],[-5,2],[1,2],[4,-2],[8,7],[9,1],[5,0],[7,-6]],[[191,9241],[-1,-3],[-4,-2],[-8,-1],[-4,-1],[-3,-5],[-1,-4],[-6,-12],[-3,-1],[-3,-6],[0,-15],[-1,-11],[4,-5],[6,-11],[0,-15],[3,-6],[5,-3],[7,-7],[6,-2],[3,-4],[7,-3],[0,-5],[-5,3],[-5,-1],[-6,6],[-3,-1],[-5,-5],[-4,0],[-5,-3],[-9,3],[-5,0],[-1,4],[-7,0],[-6,4],[-6,3],[-8,0],[0,7],[-2,-4],[0,-4],[-7,-2],[-3,-2],[-10,0],[-2,-1],[-7,-8],[-6,-2],[-1,-9],[-9,-2],[-4,2],[3,5],[-2,4],[-4,-3],[-8,5],[3,8],[-2,6],[2,7],[4,0],[3,8],[-4,3],[-2,0],[-2,3],[-1,10],[1,5],[4,6],[7,4],[4,-1],[5,-9],[5,2],[-1,4],[3,5],[0,7],[2,12],[-4,7],[-5,3],[-4,5],[-4,9],[0,9],[7,18],[3,0],[4,-9],[6,13],[4,5],[3,5],[6,4],[3,0],[5,3],[4,12],[3,2],[4,6],[6,5],[4,19],[0,4],[-3,8],[-9,5],[0,8],[6,2],[11,1],[3,2],[13,2],[10,-2],[6,-6],[3,-5],[2,-8],[4,-6],[3,-9],[1,-6],[2,-5],[0,-17],[2,-3],[-3,-4],[0,-14],[2,-6],[6,-5],[-1,-3],[-4,-6]],[[342,8087],[-2,6],[-2,3],[-7,15],[-8,9],[-3,11],[2,7],[4,5],[6,5],[4,6],[0,8],[2,9],[10,14],[0,5],[4,5],[1,4],[-6,6],[0,22],[-2,16],[0,12],[3,2],[1,5],[-5,10],[-11,-4],[-6,3],[-4,11],[-4,15],[-3,23],[-3,10],[3,15],[-3,9],[-1,12],[-5,13],[-3,3],[-2,-2],[-4,3],[-1,4],[-10,17],[-5,5],[-3,8],[-6,6],[-2,11],[-6,7],[-3,8],[-6,7],[-5,2],[-3,-3],[-4,1],[-2,8],[1,5],[-1,12],[-2,10],[0,5],[-2,5],[-5,10],[-7,7],[-4,8],[-17,21],[-6,2],[-11,8],[-6,2],[-5,4],[-10,6],[-7,4],[-6,6],[-4,8],[-5,5],[-10,5],[-8,5],[-14,3],[-13,1],[-13,3],[-6,-2],[-4,2],[-4,-7],[-10,-7],[-8,1],[-4,5],[-2,10],[0,5],[-5,5],[-5,4],[-6,13],[-5,14],[-3,8],[-10,2],[-1,6],[6,1],[3,4],[-1,11],[-5,8],[-5,5],[-2,7],[-2,8],[2,12],[0,9],[-5,4],[-5,2],[3,7],[0,5],[2,8],[10,7],[3,5],[0,5],[-7,5],[3,3],[5,-1],[6,2],[-2,5],[1,2],[5,6],[2,-1],[1,4],[-5,5],[2,4],[5,-1],[5,2],[5,12],[9,3],[3,9],[6,5],[1,5],[6,5],[7,8],[5,12],[2,4],[1,9],[5,0],[-4,8],[8,6],[3,0],[2,4],[5,2],[7,6],[3,7],[2,-7],[-2,-14],[1,-6],[2,-5],[5,4],[-1,8],[3,16],[0,8],[9,0],[2,-4],[-3,-4],[-4,0],[0,-2],[4,-4],[-1,-7],[-3,-4],[1,-3],[3,5],[5,0],[0,3],[3,3],[4,0],[0,-4],[-3,-1],[0,-4],[7,-17],[0,-2],[5,-2],[1,4],[-4,7],[-2,7],[1,7],[7,-10],[2,2],[0,10],[1,3],[0,10],[-2,8],[1,4],[-5,17],[-6,4],[-1,3],[5,12],[1,5],[-2,13],[0,9],[4,5],[2,7],[1,9],[-2,6],[3,8],[4,3],[5,0],[1,2],[2,-4],[5,3],[1,6],[8,9],[6,2],[2,5],[5,-3],[3,-1],[5,1],[7,-4],[2,3],[2,-4],[6,3],[2,5],[5,8],[-2,20],[1,0],[3,-26],[2,-7],[1,4],[4,1],[7,17],[1,2],[3,2],[4,-3],[0,-6],[-4,2],[-4,-4],[0,-10],[-4,-11],[-9,-5],[-7,-2],[-18,-9],[-7,-13],[-5,-8],[2,-7],[2,-13],[0,-4],[2,-11],[-1,-12],[-1,-4],[3,-6],[-1,-3],[5,0],[3,3],[6,1],[3,-4],[0,-3],[9,-7],[1,-7],[-2,-2],[2,-3],[1,-4],[4,-6],[-2,-15],[0,-2],[7,8],[2,1],[5,-3],[1,6],[3,4],[6,-3],[4,5],[2,1],[2,-5],[5,2],[4,-4],[1,-3],[2,8],[0,9],[2,5],[4,4],[0,6],[4,7],[-4,14],[-3,13],[-6,6],[-3,0],[-7,5],[-2,5],[-2,6],[2,14],[4,3],[1,5],[10,7],[2,4],[6,1],[6,-1],[9,-5],[8,-1],[6,-5],[2,-3],[4,-2],[4,1],[8,-3],[7,-6],[3,-5],[-1,-9],[-2,-4],[-3,-2],[-2,-4],[3,-5],[6,0],[1,-4],[4,1],[5,-2],[1,-13],[4,4],[9,-6],[1,3],[6,-8],[6,9],[-1,5],[3,1],[1,10],[-3,5],[-1,5],[-1,12],[-1,7],[1,7],[3,2],[2,8],[-1,5],[7,4],[6,-1],[3,3],[9,-4],[5,1],[10,-1],[3,-3],[16,-4],[8,-5],[3,3],[0,6],[-3,5],[-4,0],[-6,3],[-7,9],[-2,0],[-4,6],[4,10],[3,10],[-2,3],[1,8],[-9,4],[-6,8],[0,5],[4,6],[5,13],[4,4],[-1,12],[1,7],[3,2],[9,0],[3,1],[2,-2],[6,2],[4,-5],[4,-1],[1,2],[7,2],[6,4],[6,-1],[2,4],[5,3],[1,3],[7,4],[8,2],[6,7],[2,0],[5,3],[4,-2],[1,-5],[-3,-4],[3,-17],[4,1],[1,-5],[-4,-1],[0,-9],[5,1],[3,-6],[3,-20],[-2,-11],[4,-19],[0,-7],[2,-3],[2,-8],[-3,-8],[6,-8],[3,-13],[0,-7],[4,-6],[3,-11],[4,-9],[0,-9],[2,3],[6,-10],[-4,-14],[2,-3],[3,1],[3,-5],[7,5],[2,-6],[2,-2],[-3,10],[0,3],[4,-3],[3,2],[10,-1],[-1,14],[-2,4],[2,3],[9,7],[6,4],[3,-1],[2,-3],[2,-6],[8,3],[-2,7],[0,8],[12,10],[4,-2],[1,6],[13,11],[6,10],[2,8],[4,3],[8,3],[5,6],[8,5],[4,1],[4,3],[5,5],[6,9],[3,2],[7,12],[2,8],[10,12],[5,2],[12,-3],[-1,4],[1,7],[3,5],[2,-2],[4,1],[0,5],[7,9],[2,9],[7,11],[2,6],[0,7],[2,3],[-2,8],[3,6],[2,12],[2,3],[4,1],[2,4],[-1,4],[-4,6],[-2,6],[6,10],[9,8],[4,5],[7,3],[3,10],[3,6],[6,8],[9,17],[8,13],[6,14],[5,4],[5,4],[4,9],[6,11],[4,9],[5,7],[8,22],[4,6],[5,10],[4,4],[1,3],[6,7],[3,12],[9,8],[6,-1],[0,-4],[4,3],[2,4],[6,2],[7,8],[9,3],[10,0],[2,5],[4,-1],[3,-20],[5,-3],[9,-11],[13,-9],[-1,-1],[7,-3],[18,-1],[5,2],[2,4],[-3,4],[6,1],[0,6],[1,10],[6,1],[5,-3],[3,2],[8,-3],[10,-1],[10,-5],[4,-2],[7,-5],[2,-4],[4,-11],[0,-10],[5,-9],[2,-5],[8,-8],[2,-4],[7,-6],[8,-16],[3,-12],[0,-6],[7,-13],[-1,-6],[-3,-5],[0,-8],[3,-5],[2,-10],[-5,-16],[0,-9],[2,-11],[-3,-5],[2,-5],[-4,-8],[2,-10],[5,-12],[0,-7],[3,-4],[0,-11],[-3,-1],[-1,-7],[0,-5],[-3,-10],[-1,0],[-2,-6],[0,-10],[-3,-9],[-6,-1],[-7,-12],[-5,-13],[-8,-14],[-4,-9],[-13,-22],[-4,-11],[-7,-15],[-7,-11],[-6,-17],[-1,-9],[1,-6],[-5,-18],[1,-9],[0,-8],[2,-7],[-2,-11],[2,-4],[57,31],[1,0],[-60,-34],[5,-13],[2,-1],[6,1],[1,-4],[7,-8],[5,-3],[1,1],[2,-7],[3,-4],[3,1],[-6,-14],[-3,-2],[-2,-8],[-1,-6],[-3,-14],[-5,-10],[-5,-15],[-1,-8],[-1,-2],[-5,-19],[-4,-16],[-2,-6],[0,-8],[-1,-10],[3,-12],[0,-18],[-2,-4],[1,-3],[-1,-11],[-4,-8],[-1,-16],[-8,-13],[-4,-13],[-3,-3],[-4,-5],[-1,-8],[-3,-4],[-4,-1],[0,-3],[-2,-11],[-2,-5],[-5,-7],[-5,-17],[-6,-16],[-4,-3],[0,-6],[-2,-4],[-1,-9],[-2,-7],[-1,-9],[-3,-22],[-1,-21],[-2,-7],[-1,-19],[-4,-9],[0,-6],[-3,-14],[1,-15],[4,-7],[10,-14],[3,-6],[0,-6],[3,-12],[-2,-10],[0,-7],[-11,-22],[-27,-16],[-5,-10],[-10,-42],[0,-5],[-4,-3],[-2,-8],[0,-5],[-1,-7],[-4,-3],[0,-5],[-3,-7],[-9,-7],[0,-3],[-5,-13],[-1,-6],[-3,-9],[-5,-7],[-11,-10],[-11,-12],[-4,-6],[-5,-13],[-2,-2],[-11,-7],[-4,-1],[0,-5],[-2,-13],[-5,-12],[-3,-14],[0,-10],[-3,-17],[0,-8],[2,-10],[3,-14],[4,-5],[5,6],[2,1],[2,-4],[0,-5],[-2,-4],[1,-3],[-2,-6],[2,-6],[0,-19],[-3,-14],[1,-7],[0,-8],[-3,-5],[0,-9],[-2,-13],[-5,-5],[0,-14],[-1,-5],[-3,-4],[-6,-13],[-10,-14],[-4,-7],[-3,-7],[-6,-8],[-8,-6],[-3,-6],[-4,-6],[-3,-1],[-3,-6],[-2,-8],[-10,-23],[-4,-21],[-1,-7],[-4,-8],[0,-13],[10,-14],[9,-8],[14,-4],[10,1],[5,-5],[2,-11],[-2,-1],[0,-9],[-1,-6],[-1,-12],[-4,-9],[-5,-8],[-2,-9],[-5,-4],[-2,-7],[-1,-15],[-2,-13],[-8,-13],[-7,-7],[-14,-12],[-13,-6],[-7,-7],[-18,-8],[-12,-6],[-10,-8],[-3,-5],[-6,-5],[-8,-3],[-8,-11],[-2,-4],[-5,-1],[-2,-4],[-5,-3],[-7,-9],[-5,-12],[-3,-10],[-3,-5],[-2,-8],[0,-10],[1,-3],[-2,-9],[-6,-10],[-7,-7],[-7,-3],[-2,-4],[-6,0],[-2,6],[-3,2],[-5,-3],[-4,4],[-9,-7],[1,-3],[1,-15],[0,-16],[-3,-6],[-7,-6],[-12,-2],[-2,2],[1,6],[-2,7],[-6,3],[-1,4],[-4,2],[-11,3],[-11,0],[-22,-4],[-5,-4],[-6,-1],[-9,-5],[-10,-9],[-10,-12],[-3,-6],[-20,-27],[-21,-23],[-4,-7],[-5,-4],[-3,-5],[-8,-2],[0,-4],[-3,0],[-1,4],[-13,20],[-3,4],[1,1],[15,-23],[4,2],[11,14],[3,2],[10,13],[3,1],[6,8],[5,4],[5,9],[7,8],[-7,36],[-8,40],[-5,32],[-12,23],[-6,12],[-4,9],[-3,17],[0,17],[3,16],[-3,16],[-4,11],[-10,15],[-17,16],[-24,24],[-12,9],[-30,11],[-10,1],[-6,5],[-5,7],[2,5],[0,14],[1,12],[-5,24],[-1,29],[-2,15],[-5,13],[-8,15],[0,5],[-4,-5],[-5,5],[8,10],[6,3],[3,6],[4,14],[9,23],[2,7],[0,20],[-3,10],[-1,8],[6,13],[1,9],[15,13],[10,4],[8,5],[1,9],[5,15],[12,15],[3,11],[-2,7],[0,10],[-1,6],[3,18],[-2,3],[-6,6],[4,10],[-2,7],[2,12],[-2,7]],[[344,9306],[-4,-12],[-2,-7],[-3,-5],[-4,-2],[-1,-3],[-6,-7],[3,-2],[1,-8],[0,-4],[-3,-9],[-1,-6],[-4,0],[-2,-4],[-4,0],[-1,3],[-5,2],[0,9],[3,10],[-4,10],[-11,19],[4,6],[6,-9],[3,0],[3,5],[3,0],[7,8],[2,4],[3,2],[9,0],[9,6],[-1,-6]],[[360,9359],[-4,5],[0,-4],[-4,-7],[-6,2],[-5,12],[1,-7],[0,-3],[-10,-8],[-2,-5],[-4,0],[-4,4],[-1,5],[0,7],[-1,8],[1,3],[6,-3],[6,4],[1,2],[4,1],[8,-2],[4,-2],[3,3],[5,1],[0,2],[6,5],[3,-2],[3,2],[11,1],[20,1],[9,0],[1,17],[1,3],[11,4],[0,-9],[-8,1],[-2,-17],[4,-2],[7,4],[-2,4],[4,4],[2,-3],[5,8],[3,1],[1,8],[3,5],[2,-1],[7,5],[1,3],[6,1],[0,6],[7,0],[1,-1],[6,-1],[0,-2],[6,0],[7,-7],[6,8],[5,8],[2,-1],[-1,6],[3,2],[1,5],[3,8],[-1,3],[2,11],[-6,24],[7,6],[12,-1],[0,18],[1,3],[0,-22],[7,-10],[2,-4],[2,0],[3,-9],[-1,-2],[2,-14],[0,-4],[2,-6],[-1,-2],[4,-10],[-3,-2],[5,-22],[-1,-4],[3,-4],[-1,-5],[3,0],[1,-5],[-4,0],[0,-11],[1,-1],[-2,-5],[-2,-10],[0,-8],[-4,-2],[0,-8],[-5,-2],[3,-5],[0,-3],[14,-15],[6,-4],[4,3],[4,-2],[7,4],[6,-1],[1,-2],[6,-3],[2,7],[2,3],[6,1],[3,-2],[3,-7],[2,-6],[-2,-20],[-2,-9],[-6,-11],[-1,-8],[3,-9],[-2,-5],[1,-7],[3,-5],[1,-6],[5,-16],[0,-10],[-1,-5],[-6,-16],[-4,-3],[-4,0],[-13,1],[-6,3],[-4,4],[2,5],[-7,7],[-4,0],[-10,7],[-13,6],[-7,0],[-6,4],[-2,-3],[-4,0],[-3,8],[-3,3],[3,19],[-6,-14],[-5,-1],[-3,3],[-1,8],[-3,-2],[-3,1],[-1,-3],[-3,2],[-1,5],[-5,5],[-2,-1],[3,-18],[2,-3],[-5,-3],[-1,-10],[-3,-3],[-2,6],[-4,10],[1,2],[-6,0],[-1,2],[3,6],[-3,3],[0,4],[-4,1],[-5,-2],[-3,-9],[-7,-2],[-3,-3],[-5,2],[-13,-9],[-7,-6],[-25,18],[8,14],[2,19],[11,1],[-1,20],[8,3],[-2,2],[-9,-4],[-2,5],[7,6],[-7,12],[-16,-10],[-5,-4],[-1,2],[5,3],[-1,3],[-4,-4],[-4,4],[5,4],[-1,3],[13,11],[-1,1],[-13,-11],[0,1],[13,11],[-4,10],[-7,8],[-4,-2],[-1,3],[-2,-3],[-3,3],[-3,6],[-3,0],[-4,4],[2,4]],[[5138,2401],[10,3],[8,1],[7,-3],[2,9],[3,7],[9,13],[7,11],[12,13],[3,5],[7,20],[8,31],[2,5],[11,32],[0,7],[6,8],[13,12],[9,9],[5,2],[9,9],[1,4],[3,22],[3,24],[6,-1],[5,3],[4,-5],[10,18],[1,-5],[8,2],[3,-20],[4,-12],[2,1],[6,-3],[10,0],[7,-2],[1,-2],[-7,-16],[0,-6],[18,-9],[6,-1],[11,13],[-1,5],[3,6],[4,10],[19,-11],[15,-9],[-7,-17],[-3,0],[-5,-12],[-2,-6],[-2,-11],[0,-15],[4,-6],[7,-2],[4,5],[3,7],[3,11],[4,4],[7,13],[5,-11],[5,6],[1,-4],[5,-9],[6,-5],[2,1],[9,-7],[6,-6],[2,-5],[0,-7],[5,-3],[2,-3],[2,-9],[3,-6],[3,-3],[3,-6],[0,-5],[3,-6],[0,-4],[4,-7],[2,-6],[2,-2],[22,0],[7,4],[7,8],[2,6],[2,15],[6,10],[-11,3],[-7,4],[-4,6],[-3,7],[-1,7],[2,7],[-4,19],[-3,8],[-22,23],[-6,7],[-6,13],[31,33],[-3,7],[-3,9],[0,9],[-3,15],[-1,4],[-4,21],[0,6],[-2,7],[-14,4],[-3,5],[-6,4],[-4,4],[-7,14],[-7,10],[-5,0],[-9,0],[-8,3],[-8,8],[-9,11],[-4,4],[-10,4],[-3,2],[-10,10],[-10,9],[-14,4],[-21,-1],[-4,-2],[-5,0],[-3,-4],[-3,-2],[0,-3],[-11,-6],[-2,5],[-1,6],[-8,5],[-8,9],[-4,11],[-4,6],[-1,10],[1,4],[0,8],[-2,14],[-2,9],[0,8],[3,9],[3,2],[7,2]],[[5294,2909],[6,-1],[-1,2],[16,14],[6,3],[13,1],[7,1],[7,7],[4,9],[3,14],[1,20],[3,20],[2,8],[1,4],[-4,4],[6,8],[7,5],[3,5],[2,10],[7,9],[4,3],[11,3],[4,3],[9,1],[-1,13],[3,8],[6,5],[8,6],[15,1],[9,5],[6,4],[4,-3],[2,-1],[5,6],[8,1],[15,-13],[3,-7],[3,-16],[-1,-13],[2,-9],[6,-1],[1,4],[4,0],[3,-2],[12,1],[4,-3],[4,0],[4,2],[5,5],[20,11],[6,8],[9,14],[5,13],[-1,1],[2,7],[5,11],[1,16],[-1,12],[-1,12],[8,21],[7,1],[2,8],[1,7],[3,11],[1,8],[5,19],[7,22],[-3,11],[9,28],[3,5],[6,4],[-7,3],[-7,6],[-3,4],[-6,12],[-14,24],[-7,18],[-1,6],[18,16],[5,3],[12,6],[6,2],[7,1],[18,0],[27,-3],[24,-3],[15,-2],[14,-4],[11,-12],[3,0],[25,-39],[6,5],[12,13],[10,8],[3,8],[4,4],[9,12],[7,10],[8,13],[9,19],[6,15],[7,7],[0,10],[1,2],[26,-13],[31,-17],[11,-12],[8,-13],[4,-12],[2,-11],[0,-6],[-3,-13],[-8,-21],[-4,-16],[-1,-12],[0,-7],[4,-16],[3,-10],[10,-13],[7,-6],[7,-4],[5,-1]],[[5959,3266],[4,-26],[1,-19],[-2,-8],[-4,-6],[-14,-9],[-4,-6],[-16,-23],[-2,-6],[2,-13],[3,-12],[0,-4],[-5,-13],[-7,-11],[-5,-5],[-7,-4],[-14,-14],[-14,5],[-5,0],[-16,-6],[-7,2],[-1,7],[-3,-1],[-4,-5],[-4,-3],[-10,-6],[-13,0],[-6,-7],[-2,-4],[-1,-11],[-7,-13],[-2,-9],[-5,-11],[-3,-8],[-7,-13],[-2,-1],[-7,-12],[-1,-4],[-2,-24],[1,-18],[2,-4],[3,-11],[-1,-5],[2,-5],[-1,-15],[-12,6],[-5,-9],[7,-10],[8,-19],[4,-5],[11,-22],[13,-17],[4,-7],[2,-13],[2,-14],[4,-5],[1,-4],[-13,-10],[3,-7],[7,-26],[-7,-6],[3,-10],[2,-8],[1,1],[2,-15],[-11,1],[-14,-2],[-11,-4],[-9,-7],[-5,-3],[-5,-5],[-2,-7],[-6,-14],[1,-8],[5,-10],[3,-4],[5,-4],[3,-5],[4,-9],[5,-5],[5,0],[5,-5],[14,-9],[-2,-11],[-3,-20],[3,-13],[3,-19],[-4,-3],[-2,-8],[-6,-6],[-12,-10],[-2,-6],[1,-7],[-1,-9],[-5,-8],[1,-12],[1,-7],[0,-15],[-2,-17],[-2,0],[2,-11],[-1,-8],[-2,-12],[0,-8],[-4,-10],[-5,-7],[1,-2],[0,-7],[1,-2],[9,-9],[8,-3],[5,0],[4,-1],[4,1],[10,-1],[-1,-12],[3,-9],[7,4],[0,-1],[8,-4],[2,2],[6,-1]],[[5822,2333],[0,-4],[3,-10],[-3,-1],[2,-13],[-4,-1],[-4,-8],[-1,-6],[-1,-10],[-4,0],[2,-9],[3,-5],[-2,-5],[6,-7],[6,-4],[18,21],[13,13],[1,4],[0,13],[-1,4],[0,7],[12,-2],[16,-10],[4,-5],[-1,-6],[-3,-22],[-1,-10],[1,-18],[-3,-12],[-8,-8],[-23,-19],[-4,-18],[-7,-21],[-21,-30],[-1,-1],[-10,13],[-6,-5],[0,-5],[15,-16],[12,-9],[11,-7],[7,-2],[-8,-26],[-5,-7],[0,-39],[-18,-29],[1,-2],[3,-10],[-1,-14],[2,-8],[3,-8],[6,-7],[5,-5],[10,-7],[15,-9],[10,-5],[21,-10],[13,-2],[8,0],[5,-3],[5,-4],[2,-10],[0,-16],[2,-17],[2,-7],[2,-14],[6,-14],[6,-10],[9,-8],[7,-5],[5,-1],[14,-8],[14,-5],[13,-7],[7,-1],[12,-8],[7,-6],[6,-8],[5,-11],[8,-14],[5,-6],[8,-19]],[[6061,1709],[-19,-14],[-96,-65],[-64,-42]],[[5882,1588],[-41,44],[-10,10],[-14,16],[-7,10],[-7,14],[-4,8],[-3,11],[-3,12],[-7,36],[-3,16],[-8,16],[-9,16],[-13,14],[-9,8],[-8,6],[-45,35],[-14,11],[-5,5],[-18,20],[-4,6],[-7,17],[-12,-3],[-2,8],[-12,-8],[-7,2],[0,-3],[-7,0],[-11,3],[-4,1],[-11,6],[-7,1],[-2,-1],[-7,2],[-22,1],[-5,24],[-26,15],[0,5],[-12,4],[-11,-17],[-17,-1],[-13,11],[4,-10],[-8,-6],[-15,-7],[-7,0],[-20,5],[-19,2],[-2,-2],[-22,-10],[-13,5],[-2,0],[-12,0],[-12,4],[-24,6],[-6,0],[-7,2],[-2,0],[-16,-10],[-3,-3],[-2,-6],[-3,-6],[-8,-17],[1,-18],[-2,0],[-1,-5],[-9,1],[-5,-4],[1,-7],[-3,-1],[-8,2],[-2,-2],[-13,-4],[-6,-4],[-5,-4],[-22,-23],[-11,-11],[-18,-14],[-7,-5],[-27,-16],[9,-23],[8,-21],[5,-17],[4,-16],[3,-19],[2,-13],[0,-22],[0,-11],[-2,-19],[-2,-14],[-5,-20],[-4,-12],[-8,-18],[-9,-16],[-5,-9]],[[5115,1551],[-2,8],[-2,20],[-4,26],[-7,23],[-6,16],[-10,22],[-15,25],[-7,10],[-9,14],[-20,32],[-5,12]],[[4242,1070],[-2,9],[-1,8],[0,7],[2,8],[4,9],[18,34],[9,12],[7,7],[6,4],[6,3],[5,-1],[18,-9],[9,-1],[7,1],[4,2],[9,8],[6,11],[2,15],[3,82],[0,6],[-1,16],[2,19],[3,17],[1,6],[15,27],[25,43],[5,7],[3,8],[5,13],[29,-19],[7,-6],[9,-11],[7,-11],[11,-17],[14,-10],[41,-8],[5,-2],[10,-4],[10,-7],[5,-5],[6,-8],[49,-71],[8,-7],[10,-4],[16,-5],[36,-9],[37,-20],[23,-12],[18,-2],[28,3],[8,2],[18,0],[16,-4],[21,-9],[11,-2],[13,0],[26,11],[34,28],[6,6],[8,11],[5,10],[3,10],[3,12],[8,36],[5,19],[4,12],[25,43],[10,15],[22,32],[11,15],[18,25],[38,56],[11,17]],[[5115,1551],[2,-13],[4,-16],[14,-32],[10,-12],[7,-9],[8,-6],[8,-4],[20,-8],[13,-5],[17,-5],[35,-9],[7,-1],[22,-9],[40,-18],[37,-16],[18,-10],[10,-5],[23,-16]],[[5410,1357],[10,-14],[3,-10],[1,-17],[0,-10],[-1,-10],[-8,-37],[-8,-30],[-3,-12],[-2,-13],[-3,-21],[-2,-32],[-2,-44],[-2,-10],[-6,-13],[-8,-9],[-3,-2],[-24,-14],[-38,-25],[-14,-6],[-19,-7],[-23,-5],[-13,-4],[-13,-7],[-5,-5],[-9,-10],[-53,-68],[-7,-10],[-4,-8],[-2,-11],[-1,-12],[0,-34],[1,-7],[0,-6],[-3,-15],[-6,-16],[-6,-8],[-14,-14],[-36,-33],[-21,-21],[-9,-15],[-18,-40],[-5,-11],[-7,-10],[-3,-4],[-12,-7],[-36,-12]],[[3883,1288],[10,11],[3,6],[0,5],[-2,7],[-7,1],[-79,77],[-1,7],[53,84],[2,-3],[-49,-80],[1,-5],[65,-67],[2,2],[7,12],[1,6],[13,21],[3,1],[5,0],[6,-6],[1,0],[19,30],[3,-1],[5,2],[11,6],[5,5],[5,6],[13,28],[5,13],[1,5],[-24,14],[3,9],[2,1],[6,-2],[23,35],[5,9],[-8,6],[-20,37],[-31,57],[-25,46],[-9,19],[-8,23],[-9,31],[-4,27],[-1,25],[1,-2],[1,-11],[3,-24],[2,-17],[8,-27],[11,-29],[8,-17],[39,-70],[35,-64],[47,60],[1,-5],[-3,-4],[6,-7],[6,7],[3,-1],[4,8],[-7,8],[-3,-3],[0,6],[5,7],[-2,6],[-28,35],[0,2],[79,98],[-1,3],[55,69],[8,17],[2,-1],[40,86],[53,117],[0,3],[-38,59],[-1,2],[33,116],[45,3],[2,2],[3,10],[7,9],[3,3],[6,-1],[4,9],[5,13],[1,11],[-1,1],[2,5],[1,-1],[6,9],[3,8],[4,8],[0,3],[-2,9],[-9,26],[-3,13],[-4,7],[1,6],[3,-1],[2,-2],[6,-20],[2,-3],[4,2],[-4,4],[9,2],[1,3],[-5,11],[3,3],[4,-14],[4,2],[0,20],[4,0],[2,-14],[2,-2],[6,16],[4,7],[8,20],[2,7],[2,21],[2,-1],[1,-12],[2,-5],[12,0],[4,2],[8,5],[2,10],[-2,7]],[[4413,2446],[13,0],[8,1],[-4,15],[-12,44],[1,3],[15,7],[23,11],[-2,4],[29,17],[11,9],[3,3],[5,13],[13,-10],[-4,-27],[-4,-4],[-14,-3],[7,-16],[7,-7],[3,-6],[-3,-10],[3,-2],[-7,-14],[-3,-9],[-7,-11],[-6,-10],[-5,-4],[-3,-1],[-9,-7],[-6,0],[-12,-11],[2,-11],[-2,-17],[-1,-7],[-6,-24],[-5,-27],[-3,-8],[-4,-6],[-3,-3],[-2,-4],[-1,-15],[3,-11],[0,-4],[-9,-42],[-2,-9],[-2,-9],[1,-4],[2,-2],[-1,-10],[14,-5],[9,0],[3,-3],[2,-5],[-6,-9],[-3,-7],[-4,-13],[-6,-12],[-1,-6],[-1,-7],[7,-18],[8,-5],[7,-3],[-2,-7],[17,-9],[15,-12],[6,-5],[19,-21],[7,-6],[13,-9],[14,-12],[7,-6],[9,-10],[10,-14],[12,-12],[7,-4],[0,-1]],[[7099,5230],[-13,-11],[-15,2],[-2,-2],[-5,-1],[-14,-13],[-12,-33],[-2,-18],[-4,-6],[-9,-4],[-4,-23],[1,-5],[-3,-7],[-2,-2],[-9,-3],[-13,-7],[-10,-6],[-7,-9],[-9,-6],[-4,-7],[-9,2],[-21,0],[-20,-3],[-8,-4],[-10,-10],[-5,-7],[-3,-8],[-3,-8],[0,-22],[-1,-11],[0,-4],[0,-19],[-2,-9],[-6,-16],[-13,-18],[-6,3],[-6,1],[-5,-1],[-51,-14]],[[6794,4921],[-18,8],[-25,13],[-19,16],[-22,21],[-22,17],[-18,8],[-16,3],[-23,2],[-22,0],[-52,-2],[-15,0],[-43,-8],[-22,-6],[-29,-5],[-4,2],[-14,1],[-3,-6],[-16,-15],[4,-7],[-2,-6],[-3,-18],[-5,-10],[-3,-5],[-5,10],[-4,3],[-6,2],[-5,9],[-3,2],[-14,-8],[-9,-6],[-6,4],[-1,-2],[-12,-22],[-10,-18],[-7,-12],[-18,-27],[-11,-14],[-14,-16],[-15,-15],[-26,-22],[-11,-9],[-28,-23],[-88,-70],[-40,-34],[-25,-25],[-11,-12],[-12,-14],[-16,-21],[-14,-21],[-20,-35],[-9,-17],[-14,-31],[-7,-15],[-16,-34],[-20,-39],[-20,-42],[-7,-14],[-14,-25],[-15,-25],[-28,-43],[-46,-70],[-19,-29],[-26,-41],[-28,-47],[-16,-32],[-12,-26],[-8,-22],[-22,-58],[-27,-75],[-19,-51],[-14,-30],[-9,-17],[-6,-10],[-15,-22],[-18,-25],[-34,-45],[-104,-139],[-7,-11],[-9,-17],[-4,-9],[-9,-22],[-3,-12],[-3,-14],[-2,-11],[-2,-19],[-1,-18],[0,-29],[0,-53],[0,-12],[-2,-15],[-3,-14],[-5,-15],[-6,-14],[-11,-17],[-19,-25],[-10,-14],[-6,-11],[-6,-13],[-4,2],[-4,-1],[-1,4],[2,5],[-4,-3],[-2,1]],[[5277,3147],[7,14],[5,4],[6,8],[2,3],[6,7],[-1,8],[-5,12],[-6,14],[-7,4],[-7,1],[-6,4],[-4,6],[-8,25],[-6,8],[-20,17],[-10,12],[-6,13],[-7,5],[-5,9],[-5,2],[-2,5],[-2,9],[-3,8],[2,4],[-1,3],[2,4],[0,10],[2,9],[7,12],[2,11],[7,19],[-1,2],[-4,3],[-4,5],[-2,5],[-1,7],[1,7],[-4,7],[2,9],[7,9],[4,3],[5,11],[0,7],[3,7],[2,7],[1,12],[1,1],[3,19],[14,-2],[11,5],[2,6],[-1,2],[6,22],[-4,10],[3,16],[4,8],[5,5],[5,9],[2,2],[-2,2],[-2,-3],[-8,-3],[-2,1],[-9,17],[-7,6],[-10,3]],[[5234,3644],[4,7],[-7,24],[1,17],[4,26],[0,19],[-1,23],[-3,13],[-9,22],[-5,8],[-3,10],[1,11],[7,23],[24,39],[6,11],[10,17],[-3,23],[6,16],[6,11],[4,2],[7,11],[5,5],[5,7],[6,23],[7,14],[0,3],[-2,14],[-6,18],[-4,19],[0,4],[4,11],[3,13],[5,18],[0,6],[0,15],[0,7],[-1,7],[1,4],[-6,0],[-8,5],[-11,1],[-1,-4],[-8,-4],[-11,-1],[-11,-1],[-10,2],[-7,-1],[-8,2],[6,21],[5,15],[4,14],[-4,20],[-3,1],[-1,7],[4,12],[16,18],[-1,9],[11,7],[9,8],[6,11],[5,8],[4,10],[9,1],[2,1],[12,-2],[3,1],[3,4],[0,4],[6,12],[2,8],[0,4],[4,16],[16,29],[4,6],[5,13],[6,9],[6,7],[9,7],[7,9],[0,5],[4,8],[5,5],[7,4],[11,-1],[5,-2],[7,2],[6,-1],[5,-3],[12,-14],[6,-5],[11,-7],[26,-13],[9,-2],[9,4],[2,5],[-1,14],[-6,8],[-2,5],[0,6],[-4,-8],[-5,-8],[-6,-4],[-5,0],[-13,2],[-4,3],[-4,7],[-1,11],[1,16],[-3,13],[0,11],[-2,6],[-4,5],[-7,4],[-3,3],[-8,1],[-10,0],[-4,3],[-2,6],[-1,14],[1,8],[6,24],[2,11],[0,6],[-4,7],[-21,21],[-5,9],[-4,10],[-12,38],[-4,10],[-6,7],[-8,3],[-18,6],[7,9],[3,18],[1,15],[3,24],[7,2],[4,7],[2,7],[2,3],[6,0],[4,3],[1,4],[-1,10],[1,6],[6,3],[3,3],[7,13],[8,27],[5,25],[4,15],[11,27],[6,27],[3,14],[11,45],[9,31],[7,22],[1,7],[-2,2],[3,19],[3,7],[5,17],[8,7],[18,11],[25,18],[19,12],[13,9],[2,-4],[6,2],[3,6],[10,15],[-3,5],[0,6],[12,0],[2,19],[-10,5],[3,11],[11,-2],[2,18],[-9,30],[8,34],[-2,2],[3,14],[-1,60],[16,6],[3,-2],[-4,-3],[-2,-5],[2,-6],[8,-1],[5,1],[4,-4],[6,3],[3,-2],[1,-9],[2,-12],[18,-14],[4,-8],[20,4],[7,22],[-2,44],[-3,18],[58,0],[0,4],[1,7],[5,9],[9,19],[5,17],[9,11],[1,3],[0,9],[6,15],[4,7],[5,15],[2,13],[1,19],[-1,15],[-4,15],[-4,10],[-15,19],[0,3],[5,7],[15,13],[4,10],[0,13],[-1,7],[-9,19],[-8,30],[-2,13],[0,13],[-3,19],[4,3],[1,1],[-2,8],[-2,2],[-8,18],[-8,20],[-6,12],[-14,26],[-16,31],[-5,11]],[[5872,6248],[73,106],[4,11],[0,7],[0,12],[3,6],[7,9],[9,10],[8,10],[6,3],[9,1],[11,8],[17,5],[17,0],[6,3],[9,2],[13,4],[7,4],[23,6],[14,3],[12,3],[8,2],[9,-5],[9,-12],[18,-32],[8,-9],[15,-19],[7,-5],[45,-42],[4,-3],[8,0],[7,-1],[6,-5],[11,-18],[4,-16],[4,-12],[6,-6],[3,-6],[4,-10],[2,-16],[2,-5],[7,-4],[15,2],[17,-3],[17,-11],[5,-7],[7,-14],[5,-4],[22,-11],[19,-4],[14,-1],[15,0],[7,-2],[15,-11],[3,-3],[17,-29],[5,-8],[3,-11],[2,-11],[-2,-39],[-2,-18],[-5,-19],[-9,-19],[-4,-11],[1,-17],[3,-15],[1,-18],[0,-15],[2,-9],[-1,-20],[-4,-15],[-4,-13],[-6,-15],[-2,-8],[-1,-8],[-1,-19],[-2,-20],[1,-10],[4,-12],[12,-10],[4,-1],[12,3],[29,18],[17,12],[26,11],[12,9],[13,5],[13,7],[15,12],[6,3],[13,4],[9,1],[4,-2],[7,-13],[5,-6],[18,-19],[9,-3],[11,-3],[3,-3],[2,-6],[0,-12],[-2,-20],[1,-8],[6,-16],[4,-8],[12,-20],[3,-8],[2,-7],[4,-6],[7,-2],[11,-1],[17,-11],[6,-2],[6,0],[11,-7],[6,-8],[12,-20],[5,-9],[-1,-18],[0,-11],[-6,-24],[-4,-16],[-2,-24],[5,-11],[3,-14],[2,-10],[3,-4],[9,-19],[13,-24],[6,-6],[14,-12],[7,-8],[9,-11],[12,-14],[4,-3],[12,-2],[9,1],[5,-1],[8,-4],[16,-9],[11,-5],[5,-4],[8,-8],[8,-10],[6,-6],[7,-4],[2,0],[10,-5],[14,-6],[10,-2],[4,-3],[9,-9],[7,-9],[7,-10],[1,-10],[2,-5],[6,-2],[6,-8],[8,-4],[8,-9],[8,-10],[4,-10],[4,-20],[2,-7],[3,-3]],[[6680,4159],[-5,15],[-4,18],[-4,31],[-6,21],[-5,8],[-10,13],[-6,5],[-9,4],[-14,2],[-27,0],[-19,2],[-30,4],[-12,1],[-16,4],[-27,11],[-11,7],[-26,15],[-8,3],[-7,-1],[-8,-2],[-5,-6],[-16,13],[0,2],[-7,1],[-2,3],[-4,8],[-6,10],[-1,3],[-2,10],[-2,6],[-2,19],[1,15],[10,32],[7,24],[4,8],[3,13],[2,15],[-1,4],[-5,17],[10,9],[3,9],[4,-4],[6,3],[-1,3],[-10,15],[-18,25],[-10,17],[-4,10],[6,8],[6,7],[11,5],[8,0],[26,-8],[8,0],[8,2],[4,3],[7,5],[-3,1],[-5,7],[-14,15],[-3,9],[0,14],[-1,8],[-4,13],[0,9],[-8,0],[6,3],[11,14],[6,8],[12,22],[2,7],[-4,4],[3,8],[0,8],[26,-8],[3,-3],[3,-9],[8,4],[9,3],[10,2],[11,1],[5,1],[5,6],[12,5],[1,5],[33,4],[8,6],[4,7],[7,8],[8,6],[12,2],[10,-3],[27,-14],[10,-3],[9,0],[4,1],[10,5],[5,5],[7,4],[6,2],[6,0],[7,-4],[11,-9],[26,-23],[8,-6],[8,-2],[9,1],[5,3]],[[6795,4773],[3,-4],[9,-37],[6,-10],[3,-9],[0,-18],[2,-5],[9,-12],[5,-13],[2,-11],[0,-12],[-6,-28],[-3,-5],[-5,-4],[-4,-6],[0,-3],[2,-6],[8,-2],[2,-3],[2,-11],[-2,-6],[-9,-11],[-4,-10],[-1,-6],[0,-6],[3,-8],[2,0],[8,5],[3,0],[4,-3],[5,2],[1,-3],[4,-1],[1,-3],[7,-5],[1,-4],[-1,-4],[1,-6],[-1,-3],[1,-11],[3,-6],[0,-3],[2,-5],[0,-5],[-5,-1],[-1,-6],[1,-7],[1,-8],[-1,-9],[-5,-24],[0,-3],[-2,-5],[11,-7],[6,-2],[6,0],[21,6],[9,3],[11,-1],[7,-3],[6,-5],[7,-9],[7,-11],[4,-12],[3,-11],[16,1],[4,-5],[10,-1],[10,1],[17,-3],[13,-6],[5,-5],[8,-4],[13,-3],[6,1],[10,3],[0,-9],[2,-7],[11,-24],[-2,-3],[3,-12],[-12,-4],[-8,2],[-2,-2],[-22,-8],[0,-7],[0,-5],[-4,-1],[-1,-10],[-6,-5],[-9,-6],[0,-4],[2,-12],[3,-21],[0,-9],[-2,-9],[-8,-10],[-7,-13],[-1,-8],[1,-5],[3,-7],[15,-13],[3,-7],[-1,-9],[-1,-4],[-4,-5],[0,-7],[1,-7],[4,-5],[0,-9],[-13,-7],[-3,-10],[-2,-5],[-4,-4],[-4,-2],[-1,-3],[0,-6],[-1,-3],[-7,-1],[-1,-3],[-12,1],[-8,-4],[-2,-3],[-4,-11],[-1,-9],[-2,-2],[-8,1],[-13,3],[-5,-3],[-2,-5],[-7,-8],[-16,-12],[-4,0],[-8,-8],[-6,9],[-9,9],[-7,4],[-10,9],[-26,17],[-15,8],[-17,16],[-8,7],[-13,16],[-18,21],[-18,17],[-6,7],[-14,11],[-13,13],[-14,20],[-12,8]],[[4814,3231],[10,-3],[11,-7],[1,0],[5,11],[10,24],[25,60],[28,67],[6,11],[8,8],[8,6],[23,15],[-5,-24],[-4,-11],[-7,-14],[-18,-28],[-1,-1],[-12,-23],[-7,-17],[-8,-23],[-15,-38],[-7,-15],[-9,-15],[-10,-16],[3,-3],[-2,-4],[6,-6],[3,2],[6,-4],[8,-6],[-1,-9],[-8,-18],[-9,-7],[-5,1],[-10,7],[-10,1],[-3,-3],[-9,-26],[-1,-5],[-6,-14],[-5,-13],[1,-10],[4,-8],[3,-4],[6,-2],[6,-5],[-3,-8],[7,-2],[7,1],[18,0],[0,-12],[1,-4],[-1,0],[-15,4],[-4,-3],[-2,-6],[-4,-4],[-6,-2],[-5,1],[-23,11],[-4,-1],[-4,3],[-2,-1],[-6,-16],[1,-3],[-3,-12],[-7,4],[-2,-2],[-6,-2],[-9,-16],[-11,-23],[-3,-6],[-1,1],[-8,-10],[-1,1],[-8,-11],[9,-9],[-3,-6],[-7,-4],[-13,-14],[-9,-18],[-8,-18],[-5,-14],[0,-11],[-5,-10],[12,-9],[11,-7],[7,-3],[8,-8]],[[4716,2813],[-1,-6],[-12,6],[-10,6],[-5,2],[-6,0],[-20,-13],[-21,-4],[-2,-2],[-26,-5],[9,7],[7,13],[4,12],[6,27],[5,13],[14,37],[9,18],[-2,2],[-8,4],[-10,11],[-9,8],[-11,10],[-62,-124],[-19,-34],[0,-4],[-35,19],[-8,-32],[-1,-13],[-12,-5],[11,-21],[-18,-11],[-10,12],[-4,-2],[-8,11],[-14,3],[1,7],[-30,49],[-6,10],[7,8],[-8,15],[-7,15],[-8,14],[-9,-9],[-22,36],[7,7],[4,-6],[9,3],[19,10],[3,-7],[12,9],[-8,21],[6,5],[2,-8],[6,-10],[11,-15],[10,13],[16,-17],[12,16],[19,23],[17,14],[10,9],[-15,18],[-12,-11],[-17,-18],[-8,11],[-4,15],[-7,20],[-12,22],[-9,18],[-13,25],[-7,9],[-15,21],[-9,11],[-19,27],[-19,26],[-10,13],[2,14],[8,37],[3,15],[8,44],[10,51],[4,23],[2,12],[-22,7],[-26,8],[-3,-5],[-5,-3],[-10,-3],[-6,-1],[-14,3],[0,-46],[0,-17],[2,-55],[0,-49],[0,-32],[2,-35],[1,-68],[3,-87],[3,-20],[-3,-16],[-5,15],[-3,4],[-4,2],[-6,1],[-7,-2],[1,-2],[-5,-5],[-4,-11],[0,-8],[-9,7],[-14,14],[-1,0],[-31,-30]],[[4215,2939],[-13,17],[-10,17],[-8,9],[-7,6],[-18,22],[-12,15],[-14,12],[-26,24],[-13,9],[-14,9],[-13,10],[-9,10],[-11,7],[-40,29],[-15,12],[-3,3],[-9,5],[-12,5],[-13,0],[-10,8],[-4,9],[-2,17],[-16,13],[-6,-1],[-3,-3],[-3,0],[-4,4],[-3,2],[-3,9],[1,10],[2,2],[0,6],[-4,4],[4,6],[4,-2],[3,-13],[5,-1],[3,0],[2,8],[-6,12],[1,6],[0,-5],[7,-13],[4,-2],[0,-2],[21,-9],[9,13],[1,5],[5,4],[15,23],[1,0],[12,-12],[6,-4],[1,-4],[4,0],[4,-4],[14,1],[5,1],[8,9],[8,12],[5,9],[4,11],[6,35],[-1,10],[4,25],[0,13],[3,14],[3,14],[2,12],[5,3],[2,10],[-1,6],[-2,4],[1,3],[11,0],[5,1],[5,4],[6,17],[4,6],[11,12],[9,7],[6,0],[12,-4],[7,2],[8,0],[10,-5],[17,-1],[24,-3],[7,-3],[5,0],[9,2],[31,11],[4,-4],[7,-3],[7,3],[4,0],[22,-3],[7,-3],[6,1],[6,2],[7,5],[18,15],[6,6],[11,13],[13,17],[26,40],[7,10],[3,9],[3,5],[8,7],[5,5],[13,21],[17,27],[8,19],[9,16],[3,-1],[8,16],[8,23],[4,13],[8,24],[4,13],[1,7],[3,16],[2,14],[5,22],[0,20],[0,15],[0,23],[1,12],[1,3],[4,5],[5,2],[4,0],[6,-2],[7,5],[21,7],[2,0],[6,-6],[10,6],[5,1],[5,-2],[7,0],[6,-13],[-3,-3],[0,-7],[-4,-9],[1,-16],[2,-18],[5,-3],[1,-3],[0,-10],[-3,-2],[-1,-5],[-2,-2],[9,-11],[2,3],[3,-2],[-1,-3],[5,-4],[8,-2],[11,1],[4,-3],[26,-1],[6,-3],[6,-7],[4,0],[3,-9],[-1,-8],[-2,0],[0,-4],[-4,-11],[3,2],[2,-4],[-6,-12],[-2,1],[-1,-5],[-8,-14],[-5,-6],[-6,-13],[-4,-1],[-4,-5],[-8,-15],[-6,-9],[-6,-19],[-7,-8],[6,-5],[-3,-7],[-1,-5],[7,-3],[0,-17],[3,-13],[0,-9],[0,-18],[-2,-10],[-2,-8],[-1,-11],[-3,-7],[-5,-5],[-5,-3],[-8,0],[-4,-4],[-7,-2],[-2,2],[-6,-2],[-1,-1],[-7,-3],[-5,7],[-8,-5],[-4,6],[-4,-2],[2,-5],[-2,-6],[4,-11],[-3,-2],[0,4],[-1,6],[-2,0],[-6,-6],[1,-12],[5,-5],[2,-5],[16,-24],[5,-9],[0,-7],[-9,-29],[-7,-4],[-9,-4],[-5,-4],[-2,-14],[2,-8],[-1,-10],[2,-18],[17,19],[14,10],[11,14],[8,12],[9,9],[8,0],[3,1],[10,1],[4,-4],[5,-12],[2,-7],[4,-8],[2,-10],[3,-12],[1,-7],[5,-17],[9,-14],[5,-16],[2,-11],[-3,-8],[1,-6],[1,-7],[2,-4],[6,-6],[11,-1],[22,6],[11,5],[2,2],[3,-2],[4,-5],[12,-9],[1,-6],[0,-9],[3,-3],[5,0],[2,-5],[4,0],[4,10],[5,5],[7,-3],[3,-6],[2,-14],[0,-11],[-3,-7],[-7,-8],[-5,-2],[-5,0],[-9,5],[-2,0],[-1,-4],[2,-16],[0,-8],[1,-12],[0,-6],[3,-13],[-5,-14],[-3,-11],[-5,-3],[-3,-6],[-2,-8],[-4,-6],[2,-3],[2,6],[9,11],[6,6],[8,6],[7,8],[4,11],[8,11],[1,5],[2,2],[1,4],[8,10],[3,10],[3,12],[5,5],[0,8],[4,0]],[[5979,650],[8,25],[1,13],[-1,6],[-3,5],[-4,3],[-9,-1],[-11,-6],[-3,-2],[-8,-10],[-7,-10],[-7,-5],[-13,-4],[-4,-1],[-5,1],[-6,5],[-7,9],[-16,30],[-5,10],[-9,20],[-5,13],[-9,29],[-7,16],[-1,8],[0,9],[1,4],[7,16],[10,19],[14,28],[11,24],[1,6],[0,12],[-2,17],[-2,8],[-3,15],[-1,13],[2,13],[7,27],[3,11],[5,14],[1,14],[0,16],[-1,18],[-1,5],[-8,18],[-5,8],[-4,5],[-9,9],[-4,3],[-9,9],[-7,12],[-4,22],[-1,12],[-2,10],[-4,9],[-7,11],[7,6],[4,7],[4,11],[5,14],[10,33],[4,14],[4,21],[0,14],[-3,14],[-4,7],[-5,7],[-16,15],[-10,7],[-21,17],[-33,26],[-14,11],[-20,16],[-6,8],[-6,17]],[[5736,1486],[25,18],[27,20],[38,25],[19,13],[37,26]],[[6061,1709],[63,45],[8,6],[47,37],[12,16],[35,48],[61,91],[56,84],[54,78],[14,22],[110,192],[13,21],[32,55],[37,64],[46,81],[48,82],[15,26],[23,39],[17,32],[5,9],[13,21],[10,14],[15,16],[15,13],[19,14],[21,10],[33,10],[58,12],[9,2],[23,4],[86,17],[25,5]],[[7084,2875],[138,27],[81,17],[6,1],[26,7],[18,9],[27,15],[23,16],[22,17],[51,55],[87,90],[111,118],[31,27],[14,12],[20,13],[27,13],[29,12],[21,6],[31,8],[15,4],[57,13],[76,18],[91,22],[79,18],[12,3]],[[8177,3416],[1,-20],[3,-41],[1,-14],[1,-7],[4,-7],[3,-2],[11,-7],[7,-8],[5,-18],[-2,-13],[-10,-28],[-4,-2],[-40,-39],[-5,-5],[-7,-15],[-1,-12],[2,-16],[0,-12],[-3,-11],[-4,-7],[-5,-6],[-21,-13],[-4,-4],[-1,-4],[0,-5],[1,-7],[6,-13],[7,-10],[0,-5],[-1,-11],[-3,-4],[-14,-15],[-11,-9],[-7,-2],[-18,1],[-24,21],[-6,-5],[-10,-19],[-9,-23],[-1,-6],[4,-14],[4,-5],[-2,-11],[-5,-23],[-13,-15],[-1,-6],[-5,-12],[-7,-14],[-6,-8],[-4,-8],[1,-9],[-5,-2],[-5,-9],[-7,-26],[-5,-19],[4,-10],[5,-3],[-1,-5],[4,-3],[-10,-35],[-9,-41],[-4,-15],[-4,-9],[-1,-7],[-6,-17],[-5,-15],[1,-3],[-5,-17],[0,-5],[6,-8],[8,-36],[0,-5],[19,3],[8,-4],[12,-9],[25,-20],[-8,-15],[3,-1],[10,-10],[6,-6],[3,-5],[10,-11],[8,-7],[8,-5],[19,7],[4,1],[7,-29],[-82,-30],[4,-18],[9,-39],[6,-15]],[[8016,2365],[-16,-9],[-15,-6],[-22,-12],[-12,-8],[-10,-11],[-4,-7],[-9,-7],[-7,-14],[-4,-10],[0,-9],[-2,-24],[-10,-7],[-9,-1],[-2,-7],[2,-9],[0,-11],[-7,-4],[-5,5],[-5,6],[-15,12],[-15,5],[-19,10],[-21,4],[-15,-3],[-39,-11],[-14,-6],[-10,1],[-17,-2],[-13,-7],[-15,-4],[-35,-19],[-33,-25],[-15,-10],[-10,-7],[-20,-12],[-33,-30],[-12,-14],[-15,-13],[-25,-18],[-37,-31],[-80,-54],[-30,-19],[-37,-35],[-18,-18],[-16,-16],[-17,-17],[-19,-19],[-13,-14],[-11,-16],[-8,-10],[-42,-29],[-13,-13],[-25,-21],[-15,-17],[-20,-14],[-34,-32],[-29,-21],[-31,-29],[-44,-37],[-42,-46],[-41,-31],[-22,-19],[-27,-18],[-9,-8],[-2,-2],[-42,-33],[-14,-12],[-8,-10],[-4,-1],[-2,-7],[-6,-7],[-3,2],[-5,0],[-2,-2],[-6,-1],[-4,-2],[-9,-2],[-2,2],[-9,3],[2,3],[-2,3],[-8,0],[-10,-4],[-17,-13],[-10,-10],[-16,-13],[-39,-37],[-16,-12],[-34,-31],[-40,-41],[-39,-46],[-43,-49],[-40,-41],[-31,-46],[-12,-18],[-13,-19],[-29,-34],[-15,-19],[-13,-18],[-18,-27],[-10,-17],[-23,-35],[-31,-52],[-16,-36],[-2,-4],[-18,-30],[2,-3],[0,-3],[-4,-7],[1,-8],[-1,-1],[1,-9],[-7,-2],[-6,4],[-3,-2],[5,-10],[-1,-6],[4,-6],[-5,-13],[0,-5],[5,-5],[-4,-5],[-5,0],[-7,-5],[-6,2],[-1,5],[-5,2],[-5,-1],[-4,-3],[-6,-1],[-7,-3],[-10,-7],[-5,2],[-7,-4],[-2,-5],[-4,-3],[-9,5],[-4,3],[-2,7],[-9,1],[-6,-2],[-7,-5],[-6,-6],[-7,-6],[-4,-6],[-6,-5],[-9,0],[-9,-4],[-3,1]],[[6794,4921],[4,-15],[4,-20],[1,-9],[1,-24],[0,-11],[0,-22],[2,-25],[-1,-7],[-5,-12],[-5,-3]],[[6680,4159],[8,-18],[4,-8],[7,-5],[6,-13],[7,-22],[2,-18],[0,-10],[-6,-35],[-13,-63],[-2,-10],[-6,-15],[-6,-11],[-28,-48],[-7,-18],[-12,-37],[-5,-14],[-7,-13],[-12,-16],[-9,-7],[-7,-5],[-13,-6],[-13,-4],[-9,-4],[-13,-9],[-7,-6],[-3,-3],[-15,-16],[-9,-7],[-9,-5],[-17,-7],[-18,-8],[-9,-7],[-5,-5],[-8,-10],[-8,-13]],[[6438,3663],[-4,-9],[-8,-21],[-6,-15],[-11,-29],[-9,-24],[-2,-7],[-5,-24],[-3,-12]],[[6390,3522],[-9,4],[-18,-2],[4,-14],[0,-3],[-5,-12],[-3,-5],[-4,-17],[-3,-6],[-8,-8],[-7,-4],[-9,-8],[-9,-4],[-8,16],[-7,10],[-5,2],[-10,-4],[-14,0],[-8,-3],[-7,1],[-5,3],[1,6],[-8,5],[-2,3],[-13,7],[-8,-2],[-5,-6],[-9,-8],[-11,-1],[-6,-2],[-8,-7],[-4,5],[-13,5],[-5,5],[-7,15],[-7,24],[-1,0],[-11,4],[-8,8],[-6,12],[-5,6],[-9,13],[-5,5],[-10,6],[-8,13],[-9,18],[-6,-33],[0,-13],[4,-27],[5,-7],[5,-2],[0,-9],[-1,-1],[5,-10],[-2,-10],[-9,-11],[-1,-11],[-7,-17],[-6,-9],[-10,-13],[-1,-7],[2,-8],[3,-11],[7,-17],[7,-22],[9,-25],[6,-27],[1,-15],[-8,-1],[-12,-4],[-12,-6],[-7,-5],[-17,-6],[-31,-6],[-11,-3],[-15,-2],[-9,0],[-8,2]],[[5294,2909],[3,7],[-4,7],[-11,1],[-10,3],[-10,11],[-2,11],[0,18],[2,10],[3,8],[13,32],[1,4],[-1,12],[-2,18],[-2,3],[2,10],[3,5],[-10,4],[-7,2],[-1,4],[1,7],[-1,0],[2,7],[3,15],[7,21],[1,5],[2,5],[1,8]],[[8177,3416],[16,4],[60,15],[95,21],[89,22],[46,11],[15,4],[-5,29],[8,42],[12,47],[3,15],[5,13],[7,6],[-3,5],[-5,-2],[-4,5],[-9,-4],[-8,-3],[-5,-3],[-2,-8],[-7,-5],[-3,12],[-18,20],[-16,13],[-2,8],[-5,3],[4,7],[-5,10],[-12,9],[-13,11],[-8,11],[-9,1],[-14,-2],[-8,-4],[-1,12],[7,25],[6,-2],[7,1],[5,3],[5,9],[2,8],[0,7],[-2,6],[-4,8],[-21,32],[-16,26],[-8,0],[-20,9],[-32,14],[-16,4],[-9,3],[-11,4],[-18,10],[-8,1],[-7,-1],[-9,-7],[-14,-19],[-3,-4],[-9,-6],[-19,-4],[-11,-2],[-8,-1],[-6,1],[-13,6],[-10,7],[-9,4],[-10,-1],[-11,-5],[-7,-1],[-9,1],[-7,3],[-24,11],[-14,5],[-10,6],[-21,9],[-9,5],[-9,7],[-5,6],[-8,16],[-1,11],[0,7],[3,13],[0,9],[0,5],[-6,11],[0,14],[-3,7],[-5,6],[-9,9],[-5,4],[-22,12],[-7,7],[-12,28],[-4,6],[-4,2],[-4,-1],[-2,-4],[0,-6],[3,-8],[1,-8],[-1,-5],[-3,-5],[-17,-15],[-6,-1],[-3,3],[-5,8],[-5,5],[-4,2],[-6,-1],[-6,-5],[-8,-13],[-16,-18],[-33,-35],[-9,-7],[-32,-16],[-7,-4],[-7,-9],[-5,-9],[-6,-14],[-4,-5],[-5,-1],[-8,2],[-6,4],[-7,7],[0,15],[-2,11],[0,7],[2,6],[0,1],[5,1],[5,10],[3,4],[7,4],[2,8],[6,7],[7,16],[0,3],[4,2],[3,17],[3,3],[0,2],[4,3],[2,6],[3,0],[8,7],[11,5],[11,4],[27,14],[17,6],[15,9],[12,8],[18,18],[3,5],[-1,11],[0,9],[1,0],[4,11],[11,9],[4,8],[0,11],[1,4],[6,-3],[13,-4],[4,0],[5,-2],[2,-11],[3,-5],[11,-1],[5,-4],[10,-3],[10,1],[7,0],[5,-3],[5,-7],[6,-3],[15,-4],[32,-15],[7,-2],[7,-1],[7,1],[30,6],[14,5],[6,3],[8,6],[5,1],[7,0],[8,6],[4,0],[12,-2],[18,-8],[9,-6],[9,0],[10,-8],[7,-2],[14,-5],[14,-8],[15,-4],[25,-11],[20,-7],[14,-8],[37,-15],[9,-3],[7,0],[40,-9],[22,2],[8,-1],[5,0],[6,-3],[2,0],[18,1],[48,4],[28,2],[0,-42],[9,3],[41,13],[4,3],[9,11],[-42,34],[-35,29],[-7,5],[-22,12],[-36,14],[-40,9],[-17,5],[-14,6],[-9,6],[-10,8],[-8,9],[-13,16],[-6,10],[-5,9],[-9,25],[-5,25],[-2,47],[0,19],[-2,114],[-1,63],[-1,62],[-2,93],[-1,63],[0,30],[-1,88],[-1,40],[-1,69],[4,176],[3,116],[0,3],[1,48],[1,5],[1,38],[-1,6],[3,92],[2,52],[2,77],[1,43],[2,71],[2,72],[1,12],[2,19],[3,15],[3,10],[17,47],[30,85],[4,13],[3,20],[-1,18],[-3,68],[-5,120],[-4,80],[-3,57],[-2,18],[-4,20],[-6,25],[-16,31],[-6,9],[57,71],[69,117],[45,75]],[[8507,6699],[65,-90],[36,-129],[3,-43],[25,-226],[8,-230],[-63,-301],[-5,-158],[-5,-190],[-9,-331],[2,-145],[6,-370],[-2,-25],[57,-45],[67,-54],[16,-12],[6,-5],[28,-23],[89,-70],[22,-26],[22,-37],[18,-40],[3,-10],[-7,-7],[6,-14],[5,-13],[37,-48],[13,-25],[-1,-3],[5,-7],[8,-6],[43,34],[19,93],[15,1],[5,1],[10,1],[0,-2],[37,-7],[2,0],[24,9],[32,-29],[31,21],[34,6],[7,35],[8,19],[22,27],[24,38],[22,24],[26,20],[26,40],[16,-12],[56,-99],[61,23],[73,0],[40,-64],[19,-14],[41,-29],[9,-76],[183,47],[12,-7],[9,-9],[77,20],[4,-3],[16,-95],[-3,-4],[-43,-12],[8,-16],[19,-43],[32,-49],[21,-33],[-9,-13],[-2,-7],[-13,-19],[-3,-7],[-6,-8],[-11,-11],[-10,-13],[-8,-12],[-8,-17],[-8,-10],[-9,-21],[-7,-10],[-5,-9],[-11,-22],[-5,-7],[-4,-7],[-11,-12],[-5,-4],[-7,-9],[-1,-5],[-8,-13],[-4,-5],[1,-2],[-4,-5],[-2,-7],[-3,-4],[-4,-9],[-7,-16],[-1,-4],[-4,-5],[-1,-6],[-5,-5],[-1,-6],[-4,0],[0,-6],[-4,-4],[-2,-5],[-3,-4],[-2,-5],[-3,0],[-1,-4],[-2,-1],[-1,-5],[-7,-8],[-9,-19],[-16,-29],[-2,-4],[-7,-12],[-7,-9],[-1,-3],[-7,-11],[-3,-4],[-3,-5],[-4,-6],[-14,-31],[-4,-17],[-4,-6],[-1,-5],[-4,-2],[0,-6],[-4,-7],[-7,-7],[-3,-4],[-13,-33],[-3,-1],[-7,-8],[-5,-11],[-4,-3],[-6,-8],[-5,-10],[-6,-10],[-3,-3],[-7,-11],[-2,-2],[0,-4],[-4,-1],[-8,-11],[-7,-7],[-8,-7],[-15,-15],[-6,-10],[-5,-5],[-10,-6],[-6,-7],[-9,-12],[-5,-8],[-6,-7],[-1,-4],[-14,-13],[-4,-2],[-3,-5],[-13,-11],[-4,-2],[-2,-3],[-4,-2],[-2,-4],[-4,-1],[-2,-5],[-4,-5],[-1,-3],[-9,-8],[-2,-4],[-13,-15],[-8,-15],[-8,-9],[-2,-4],[-6,-5],[-1,-3],[-9,-9],[-19,-26],[-6,-4],[-7,-10],[-5,-11],[-6,-10],[-2,-7],[-7,-11],[-9,-11],[-11,-10],[-6,-5],[-9,-9],[-4,-7],[-3,-10],[-1,-8],[-4,-7],[-3,-3],[-6,-5],[-12,-6],[-10,-13],[-14,-20],[-6,-7],[-10,-13],[-9,-10],[-6,-5],[-3,-6],[-4,-4],[-8,-18],[-9,-9],[-5,-10],[-3,-3],[-8,-6],[-5,-5],[0,-2],[-10,-19],[-9,-11],[-2,-6],[-6,-3],[-8,-7],[-4,-2],[-5,-10],[-6,-12],[-3,-1],[-7,-6],[-7,1],[-6,-3],[-2,3],[-5,1],[-9,0],[-5,-2],[-6,-7],[-7,-2],[-4,-3],[-11,-11],[-8,-1],[0,-4],[-4,0],[-1,-6],[-3,-1],[-7,-10],[-11,-11],[-9,-5],[-2,-4],[-6,-4],[-7,-6],[-12,-14],[-10,-15],[-5,-5],[-6,-7],[-8,-9],[-3,-4],[-3,-3],[-3,-6],[-2,-1],[-2,-5],[-2,-2],[-9,-13],[-3,-6],[-7,-9],[-2,-4],[-6,-6],[-4,-10],[-9,-8],[-2,-4],[-4,-3],[-3,-4],[-6,1],[-3,4],[-3,-3],[-12,-6],[-7,-6],[-12,-14],[-5,-2],[-4,-3],[-4,-7],[1,-7],[-2,1],[-4,-4],[-4,0],[-7,-4],[-6,0],[-3,-4],[-8,-1],[-2,-4],[-1,-9],[-3,0],[-4,3],[-11,-1],[-13,-11],[-4,-5],[-11,-15],[-8,-4],[-5,-5],[-2,-5],[-7,-5],[-6,1],[-3,-2],[-13,0],[-4,-1],[-8,-8],[-4,-7],[-1,-3],[-7,-7],[-1,-6],[-3,-1],[-4,-6],[-2,2],[-6,-2],[-8,-9],[3,-2],[-2,-4],[-7,-2],[-7,2],[-4,0],[-2,-4],[-5,2],[-1,-5],[-4,-3],[-5,-1],[-4,-5],[-6,-9],[-3,-10],[-3,-3],[-2,-7],[-5,-3],[-5,-4],[-10,-2],[-5,-3],[-13,0],[-2,-3],[0,4],[-10,0],[-6,4],[-5,0],[-6,-2],[-5,4],[-3,5],[-1,4],[-3,2],[-16,1],[-8,-6],[-9,1],[-7,4],[-7,0],[-11,6],[-13,3],[-24,2],[-5,2],[-13,20],[-14,22],[-20,24],[-22,16],[-6,-2],[-5,6],[0,6],[7,2],[0,5],[-3,7],[-3,-1],[-4,9],[-8,3],[-5,-1],[-1,-8],[-3,1],[-6,4],[1,4],[-4,12],[-3,4],[-5,14],[1,7],[0,4],[-3,5],[-4,1],[-6,0],[-3,7],[-1,9],[-3,2],[-3,-1],[-4,3],[-1,5],[-3,5],[-7,6],[-8,1],[-8,-2],[-8,-6],[-8,-10],[-11,0],[-11,4],[-23,-7],[-10,-4],[-10,-1],[-1,1],[-16,5],[-15,1],[-11,-2],[-13,-4],[-8,-2],[-22,-10],[-26,-8],[-25,-7],[-13,-10],[-2,-6],[-4,-1],[-2,-3],[-6,-1],[-7,6],[1,5],[2,0],[-4,5],[-5,1],[-8,-3]],[[5979,650],[-2,0],[-3,5],[-6,1],[-19,-27],[-11,-14],[-11,-17],[-14,-18],[-27,-37],[-2,-4],[4,-6],[0,-4],[-5,-3],[2,-2],[-6,-5],[-9,2],[-5,-4],[-2,-5],[3,-9],[0,-4],[-3,-5],[-5,-2],[-3,0],[-7,-5],[-4,-7],[-10,-4],[-5,-1],[-5,3],[-5,7],[-5,3],[-5,-2],[-5,-9],[-3,-8],[-6,-10],[-5,-10],[-10,-7],[-10,-7],[-6,-8],[-14,-27],[-9,-9],[-10,-5],[-17,3],[-9,1],[-6,-4],[-10,3],[-12,0],[-15,-2],[-11,-4],[-11,-2],[-11,-5],[-7,-5],[-15,-14],[-3,-9],[-7,-6],[-15,-20],[-9,-10],[-9,-14],[-9,-2],[-15,-8],[-8,-6],[-11,-4],[-27,-16],[-13,-10],[-6,-6],[-12,-11],[-17,-12],[-19,-12],[-13,-5],[-16,-13]],[[5410,1357],[8,13],[8,5],[20,5],[5,3],[49,44],[0,2],[20,19],[9,9],[8,12],[18,34],[6,8],[7,7],[6,6],[15,7],[18,0],[37,-5],[15,-3],[21,-3],[22,-5],[22,-7],[3,-3],[4,-6],[5,-13]],[[6390,3522],[-4,-18],[-1,-5],[-3,-14],[-6,-13],[-7,-12],[-10,-14],[-9,-15],[-8,-17],[-2,-7],[-4,-20],[-2,-24],[-1,-30],[-1,-19],[-1,-35],[1,-13],[2,-9],[6,-25],[27,9],[1,-8],[2,-4],[6,-4],[6,1],[18,6],[11,-28],[-23,-5],[8,-16],[-26,-15],[-23,-42],[-10,-23],[-6,-39],[-11,9],[-11,6],[-39,19],[-11,-18],[-1,-9],[-7,-23],[-6,-15],[-4,-12],[-4,-10],[-5,-10],[-6,-9],[-4,-1],[-4,7],[-3,-2],[-10,-2],[-5,-5],[-6,-13],[-5,-12],[5,-4],[9,-7],[15,-18],[12,-12],[10,-12],[15,-27],[4,-15],[4,-6],[22,-23],[7,-6],[6,-5],[9,-4],[12,-2],[9,1],[27,7],[13,1],[10,0],[6,-2],[11,-3],[-7,-19],[19,-28],[4,-8],[1,-5],[0,-9],[-2,-8],[-6,-8],[-4,-3],[-4,-5],[-6,2],[-10,0],[-10,-3],[-8,-1],[-8,-4],[-6,-7],[-7,-2],[-6,-6],[4,-10],[-22,-14],[-8,-2],[-5,-4],[0,-7],[-5,-5],[-2,-4],[2,-8],[-3,-7],[-5,6],[0,-6],[-4,-3],[-5,-13],[-3,-3],[-10,-8],[-1,-5],[-3,-6],[-4,-16],[-3,-7],[-13,-11],[-6,-6],[-4,-7],[-8,-9],[-8,-6],[-6,1],[-3,-1],[-2,-7],[0,-8],[1,-3],[-1,-6],[-3,-5],[-3,-9],[2,-11],[15,-9],[13,-6],[4,-5],[9,5],[5,10],[4,-22],[-7,-42],[0,-7],[-3,-16],[-1,-8],[-2,-13],[-4,-5],[-4,-4],[-2,-4],[0,-10],[-14,4],[-12,1],[-7,2],[-1,-9],[-8,-2],[-10,-1],[-9,1],[-10,4],[-6,1],[-8,6],[-12,3],[-3,0],[-6,-2],[-4,0],[-3,7],[-4,4],[-2,7],[-1,8],[-3,14],[-2,14],[3,4],[-2,2],[-11,-10],[-15,-20],[-2,-6],[-34,-43],[-14,-17],[-5,-6],[-14,-13],[-17,-23],[-27,18],[-20,10],[1,8],[-2,5],[-4,4],[4,6],[-4,5],[-8,10],[-9,12],[-5,7],[-3,4],[-2,14],[-5,3],[-4,-11],[-3,1],[-5,-4],[0,-13],[8,-7],[-4,-3],[-2,1],[-7,7],[-4,1],[-14,-1],[-1,3],[-5,1],[-6,-3],[-8,-7],[-8,-6],[-4,-1],[3,-7],[-4,-4],[-8,-12],[-3,-7],[-10,2]],[[4751,2814],[-8,0],[-7,3],[-8,1],[-6,0],[-6,-5]],[[4814,3231],[-5,4],[-2,6],[3,2],[2,5],[-2,8],[1,6],[-3,4],[-1,7],[0,10],[-5,7],[-5,0],[-11,-9],[-3,-2],[-8,-1],[-8,-2],[-11,6],[-5,5],[-5,8],[0,8],[4,7],[8,5],[8,10],[3,6],[-1,9],[-1,4],[-4,3],[-2,4],[0,-4],[-7,4],[-10,-2],[-8,1],[-8,4],[-4,6],[-5,17],[-6,10],[-7,14],[-9,7],[-3,1],[-11,7],[-9,4],[-3,4],[-1,8],[8,11],[8,9],[-2,1],[-4,-2],[-7,-7],[0,5],[6,7],[11,6],[-2,4],[-3,2],[6,1],[8,6],[-9,-3],[-1,2],[5,3],[10,9],[9,5],[2,3],[-3,5],[10,8],[-1,5],[34,29],[7,6],[5,-2],[3,-5],[10,-1],[4,2],[4,0],[24,-2],[10,-4],[39,-26],[6,-3],[8,2],[12,7],[13,10],[11,14],[2,3],[10,7],[4,4],[-5,2],[3,8],[6,3],[1,8],[3,2],[1,7],[-5,3],[-1,2],[3,13],[1,8],[-2,19],[-2,6],[-1,11],[3,8],[1,6],[-2,11],[0,13],[-2,10],[1,9],[-4,6],[1,13],[6,21],[1,5],[1,19],[4,9],[4,6],[9,11],[6,12],[2,11],[1,10],[3,10],[13,12],[3,6],[1,9],[0,10],[-3,9],[-4,10],[-3,7],[-1,17],[-3,10],[-7,10],[-18,15],[1,4],[3,0],[5,-4],[9,-2],[6,-3],[9,1],[4,3],[13,14],[8,12],[7,14],[8,17]],[[5015,4006],[18,-9],[33,-12],[8,-3],[-6,-25],[-21,-89],[-9,-38],[-4,-17],[-9,-38],[-3,-12],[-6,-26],[-7,-31],[-2,-26],[0,-21],[16,-3],[14,5],[4,2],[7,9],[6,3],[15,4],[4,-2],[4,-5],[1,-9],[-2,-19],[3,-7],[9,1],[14,0],[11,-3],[15,7],[9,4],[9,-2],[7,-1],[12,-10],[11,-1],[6,1],[13,9],[11,11],[3,2],[8,0],[4,-2],[8,-6],[2,-4],[3,1]],[[6438,3663],[12,-7],[8,-1],[7,0],[11,5],[13,6],[10,2],[4,-16],[61,-9],[14,-10],[1,-2],[24,-68],[-1,-5],[5,-5],[27,-8],[17,0],[26,-15],[10,-1],[5,3],[10,2],[11,-1],[8,-1],[11,5],[7,3],[11,3],[9,-1],[8,-5],[9,-9],[12,-12],[8,-10],[6,-10],[14,-29],[6,-12],[13,-26],[6,-13],[2,-8],[3,-17],[3,-7],[15,-10],[15,-8],[13,-10],[9,-8],[33,-42],[12,-14],[11,-16],[9,-13],[21,-20],[10,-8],[11,-18],[5,-12],[3,-11],[1,-10],[0,-13],[-2,-18],[-1,-16],[-3,-25],[-3,-28],[-3,-13],[-2,-15],[2,-8],[2,-7],[4,-7],[4,-3],[4,-3],[18,-5],[5,-2],[8,-8],[3,-11],[4,-7],[5,-6],[5,-4],[17,-6],[-6,-19],[-2,-15],[-1,-4],[-1,-10],[10,-56]],[[5015,4006],[0,2],[3,10],[2,4],[4,10],[6,5],[2,-4],[3,5],[4,2],[8,0],[1,-2],[5,1],[4,-4],[5,4],[4,10],[4,7],[-7,0],[3,3],[-13,20],[1,4],[-3,0],[-3,5],[-1,6],[-1,12],[2,18],[-2,1],[0,10],[0,13],[2,14],[4,5],[1,9],[3,3],[5,11],[8,13],[1,3],[-3,6],[1,3],[6,-2],[5,-8],[7,-7],[5,-5],[2,-9],[14,-10],[4,-2],[10,-1],[0,2],[-16,5],[-9,9],[-5,13],[3,20],[2,9],[-2,11],[-6,6],[-5,-2],[-1,-3],[-3,-2],[-5,3],[-6,11],[2,9],[6,10],[-5,3],[-22,-6],[-10,4],[-3,1],[-7,4],[-6,1],[-2,3],[-4,2],[-7,-2],[-10,-10],[-6,-10],[-5,-2],[0,-3],[-5,-5],[-9,-13],[-1,-4],[-2,-1],[-4,-10],[0,-4],[3,-6],[2,-3],[0,-7],[0,-11],[3,-8],[4,-1],[5,-8],[4,-14],[3,-2],[1,-4],[-2,-6],[-4,-8],[-3,-6],[-4,1],[0,-3],[-3,-5],[-5,-4],[-4,0],[-7,2],[-9,-2],[-9,-8],[-8,-4],[1,-2],[6,1],[0,-1],[-7,-11],[-7,-6],[-4,1],[-5,-1],[-5,-4],[-3,3],[-10,5],[-6,0],[-10,-3],[-15,-9],[-6,-2],[-5,0],[-1,-3],[-21,-7],[-8,-7],[-2,-3],[0,-7],[-2,-8],[0,-7],[8,0],[4,-7],[1,-7],[-3,-5],[-7,2],[-11,10],[-7,5],[-5,5],[-11,7],[-7,5],[-8,9],[-16,11],[-4,5],[-5,6],[-10,18],[-5,11],[-14,61],[-12,-3],[-3,15],[5,2],[-1,1],[-7,28],[0,2],[6,3],[-3,17],[-7,21],[0,8],[-4,13],[-4,20],[0,14],[-1,2],[2,15],[0,15],[1,8],[-3,5],[1,5],[3,5],[2,2],[4,5],[6,19],[0,9],[4,8],[2,11],[2,5],[4,17],[5,6],[5,19],[2,2],[6,16],[0,7],[3,9],[3,5],[1,8],[4,-1],[1,2],[3,9],[4,6],[5,15],[1,10],[4,15],[1,6],[4,12],[4,9],[3,13],[13,29],[11,21],[-2,2],[13,26],[1,4],[3,12],[2,11],[5,11],[3,12],[3,0],[5,36],[3,31],[2,27],[3,18],[4,7],[3,2],[4,-1],[7,1],[4,2],[7,8],[16,15],[11,12],[2,12],[2,8],[6,12],[4,11],[1,6],[4,9],[11,7],[9,10],[8,0],[9,4],[8,2],[13,6],[5,4],[11,8],[7,5],[14,20],[3,8],[6,11],[6,13],[6,20],[5,8],[3,9],[7,15],[4,12],[5,19],[4,24],[2,21],[1,15],[-1,30],[0,26],[-3,8],[1,9],[1,3],[1,11],[1,25],[0,4],[-3,6],[-1,5],[3,6],[3,8],[2,8],[0,11],[-1,19],[-3,21],[-5,13],[-4,4],[0,6],[3,7],[0,6],[-4,13],[-2,9],[-6,3],[-1,6],[0,6],[3,4],[-1,12],[-4,26],[0,7],[1,9],[1,12],[-2,11],[-4,23],[-4,32],[-10,38],[-5,32],[-3,12],[0,7],[1,16],[-3,2],[-5,-2],[-4,1],[-2,3],[-1,9],[-3,3],[-3,-7],[-1,1],[11,20],[3,8],[0,4],[-3,3],[3,4],[5,2],[2,-4],[3,2],[5,3],[12,-4],[11,1],[6,4],[9,14],[11,12],[2,4],[2,6],[6,2],[14,18],[9,15],[5,11],[4,11]],[[4413,2446],[-1,2],[-23,-1],[-5,2],[-9,1],[0,3],[4,1],[2,3],[-1,6],[-2,36],[-1,18],[-1,9],[0,7],[0,7],[0,8],[4,2],[3,-2],[-2,-4],[0,-8],[5,-4],[9,14],[-1,13],[-5,7],[2,12],[-7,27],[-9,29],[-13,42],[-17,42],[-14,37],[-13,23],[-16,25],[-21,36],[-6,9],[-10,18],[-3,12],[-3,6],[-18,20],[-11,14],[-10,15],[-5,6]],[[7099,5230],[5,0],[8,8],[4,5],[8,8],[2,3],[6,2],[8,3],[3,-2],[2,-7],[7,3],[5,-3],[10,0],[8,1],[33,11],[12,2],[3,7],[4,2],[-2,17],[1,11],[6,20],[3,6],[6,1],[8,-3],[2,2],[1,7],[-3,11],[-5,35],[-6,20],[0,13],[-1,5],[-4,5],[-6,1],[-1,3],[1,5],[3,3],[6,4],[12,11],[14,14],[12,17],[4,4],[10,6],[4,1],[8,6],[6,9],[3,8],[1,7],[-1,15],[-2,9],[-3,10],[-5,19],[-11,22],[-10,18],[-5,18],[-3,14],[1,7],[2,8],[9,11],[1,4],[7,9],[4,7],[2,8],[4,18],[2,3],[12,0],[8,-2],[12,0],[5,5],[5,1],[16,2],[5,2],[7,3],[8,5],[18,19],[22,35],[16,28],[12,14],[16,25],[18,26],[15,4],[16,-9],[18,-3],[16,2],[13,13],[10,27],[5,37],[-2,48],[-5,40],[-6,26],[-12,14],[-15,16],[-13,10],[-6,17],[-6,25],[-6,19],[-9,19],[-1,20],[2,29],[-3,23],[-2,23],[4,18],[12,16],[15,11],[53,62],[12,15],[8,18],[0,24],[-4,23],[8,31],[8,14],[5,20],[2,25],[1,29],[8,37],[11,21],[16,13],[15,8],[20,11],[21,16],[11,11],[6,17],[16,19],[23,17],[19,15],[13,19],[8,24],[0,39],[1,21],[7,19],[15,7],[14,6],[17,9],[10,14],[7,18],[7,30],[7,37],[0,18],[1,46],[11,-3],[14,1],[8,4],[11,-1],[11,-6],[18,-12],[14,-15],[42,-28],[10,-3],[25,-12],[26,-24],[8,-9],[7,-9],[2,-9],[4,-8],[6,-10],[12,-8],[8,-11],[7,-2],[16,6],[16,5],[21,10],[18,8],[16,3],[7,-3],[9,-8],[10,-12],[9,-15],[6,-6],[6,-5],[8,-4],[18,-13],[10,-11],[20,-19],[22,-16],[10,-8],[26,-31],[78,-42],[49,-58]]]} diff --git a/src/lib/municipalityProximity.ts b/src/lib/municipalityProximity.ts index dcbc396a..81b77bef 100644 --- a/src/lib/municipalityProximity.ts +++ b/src/lib/municipalityProximity.ts @@ -1,6 +1,7 @@ import type { Position } from 'geojson' import type { + BahiaGeometryFeature, BahiaMunicipalityFeature, MunicipalityGeometryModule, } from '@/lib/bahiaGeometriesTypes' @@ -105,7 +106,7 @@ const isPointInRing = (point: GeoPoint, ring: readonly Position[]): boolean => { return inside } -const polygonRingsOf = (feature: BahiaMunicipalityFeature): readonly Position[][][] => +const polygonRingsOf = (feature: BahiaGeometryFeature): readonly Position[][][] => feature.geometry.type === 'Polygon' ? [feature.geometry.coordinates] : feature.geometry.coordinates @@ -114,7 +115,7 @@ const polygonRingsOf = (feature: BahiaMunicipalityFeature): readonly Position[][ * Holes and MultiPolygons do not occur in the committed mesh (417 plain * Polygons), but a rebuild at finer quality would introduce them. */ -export const featureContainsPoint = (feature: BahiaMunicipalityFeature, point: GeoPoint): boolean => +export const featureContainsPoint = (feature: BahiaGeometryFeature, point: GeoPoint): boolean => polygonRingsOf(feature).some(([outerRing, ...holes]) => { if (!outerRing || !isPointInRing(point, outerRing)) return false return !holes.some((hole) => isPointInRing(point, hole)) @@ -125,7 +126,7 @@ export const featureContainsPoint = (feature: BahiaMunicipalityFeature, point: G * at Bahia's latitudes is far below the precision this feeds). Holes are * ignored: they move the centre by less than the rounding of the label. */ -export const featureCentroid = (feature: BahiaMunicipalityFeature): GeoPoint => { +export const featureCentroid = (feature: BahiaGeometryFeature): GeoPoint => { let weightedLng = 0 let weightedLat = 0 let totalArea = 0 diff --git a/src/lib/municipalityZoneNeighborhoods.ts b/src/lib/municipalityZoneNeighborhoods.ts index 9f078b76..d40f02ba 100644 --- a/src/lib/municipalityZoneNeighborhoods.ts +++ b/src/lib/municipalityZoneNeighborhoods.ts @@ -6,6 +6,10 @@ * https://www.tre-ba.jus.br/legislacao/compilada/resolucao/2017/resolucao-administrativa-no-2-de-10-de-maio-de-2017 * (accessed 2026-07-21) * + * Rows author the slug and the neighborhood list only: `city` and `zoneNumber` + * are hydrated from `municipalityCatalog`, which is also what makes an unknown + * or non-zona slug fail at import instead of drifting silently. + * * Coverage is independently checked by tests/fixtures/municipality-zone-neighborhoods.official.json. */ @@ -13,11 +17,9 @@ import { getMunicipalityCatalogEntry } from '@/lib/municipalityCatalog' export type MunicipalityZoneNeighborhoodSource = 'tre-ra-02-2017' -type MunicipalityZoneNeighborhoodCity = 'Salvador' - export type MunicipalityZoneNeighborhoodEntry = { readonly municipalitySlug: string - readonly city: MunicipalityZoneNeighborhoodCity + readonly city: string readonly zoneNumber: number readonly source: MunicipalityZoneNeighborhoodSource readonly neighborhoods: readonly string[] @@ -28,20 +30,25 @@ const sortNeighborhoods = (names: readonly string[]): readonly string[] => const entry = ( municipalitySlug: string, - city: MunicipalityZoneNeighborhoodCity, - zoneNumber: number, source: MunicipalityZoneNeighborhoodSource, neighborhoods: readonly string[], -): MunicipalityZoneNeighborhoodEntry => ({ - municipalitySlug, - city, - zoneNumber, - source, - neighborhoods: sortNeighborhoods(neighborhoods), -}) +): MunicipalityZoneNeighborhoodEntry => { + const catalogEntry = getMunicipalityCatalogEntry(municipalitySlug) + if (!catalogEntry || catalogEntry.kind !== 'zona' || catalogEntry.zoneNumber === undefined) { + throw new Error(`Missing zona municipality catalog entry for slug: ${municipalitySlug}`) + } + + return { + municipalitySlug, + city: catalogEntry.city, + zoneNumber: catalogEntry.zoneNumber, + source, + neighborhoods: sortNeighborhoods(neighborhoods), + } +} export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoodEntry[] = [ - entry('salvador-ze-1', 'Salvador', 1, 'tre-ra-02-2017', [ + entry('salvador-ze-1', 'tre-ra-02-2017', [ 'Alto das Pombas', 'Barra', 'Calabar', @@ -52,7 +59,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Graça', 'Vitória', ]), - entry('salvador-ze-2', 'Salvador', 2, 'tre-ra-02-2017', [ + entry('salvador-ze-2', 'tre-ra-02-2017', [ 'Amaralina', 'Chapada do Rio Vermelho', 'Engenho Velho da Federação', @@ -60,7 +67,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Ondina', 'Rio Vermelho', ]), - entry('salvador-ze-3', 'Salvador', 3, 'tre-ra-02-2017', [ + entry('salvador-ze-3', 'tre-ra-02-2017', [ 'Baixa de Quintas', 'Cidade Nova', 'Fazenda Grande do Retiro', @@ -69,7 +76,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Pero Vaz', 'Santa Mônica', ]), - entry('salvador-ze-4', 'Salvador', 4, 'tre-ra-02-2017', [ + entry('salvador-ze-4', 'tre-ra-02-2017', [ 'Coutos', 'Fazenda Coutos', 'Ilha de Bom Jesus dos Passos', @@ -80,7 +87,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Periperi (parte ao norte da Rua das Pedrinhas)', 'São Tomé', ]), - entry('salvador-ze-5', 'Salvador', 5, 'tre-ra-02-2017', [ + entry('salvador-ze-5', 'tre-ra-02-2017', [ 'Barreiras', 'Cabula', 'Engomadeira', @@ -89,14 +96,14 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Retiro', 'São Gonçalo', ]), - entry('salvador-ze-6', 'Salvador', 6, 'tre-ra-02-2017', [ + entry('salvador-ze-6', 'tre-ra-02-2017', [ 'Acupe', 'Boa Vista de Brotas', 'Brotas', 'Candeal', 'Engenho Velho de Brotas', ]), - entry('salvador-ze-7', 'Salvador', 7, 'tre-ra-02-2017', [ + entry('salvador-ze-7', 'tre-ra-02-2017', [ 'Barbalho', 'Barris', 'Comércio', @@ -111,7 +118,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Tororó', 'Vila Laura', ]), - entry('salvador-ze-8', 'Salvador', 8, 'tre-ra-02-2017', [ + entry('salvador-ze-8', 'tre-ra-02-2017', [ 'Cajazeiras II', 'Cajazeiras IV', 'Cajazeiras V', @@ -125,7 +132,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Pirajá', 'Valéria', ]), - entry('salvador-ze-9', 'Salvador', 9, 'tre-ra-02-2017', [ + entry('salvador-ze-9', 'tre-ra-02-2017', [ 'Boa Viagem', 'Bonfim', 'Jardim Cruzeiro', @@ -137,7 +144,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Uruguai', 'Vila Ruy Barbosa', ]), - entry('salvador-ze-10', 'Salvador', 10, 'tre-ra-02-2017', [ + entry('salvador-ze-10', 'tre-ra-02-2017', [ 'Bairro da Paz', 'Boca do Rio', 'Costa Azul', @@ -148,7 +155,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Pituaçu', 'STIEP', ]), - entry('salvador-ze-11', 'Salvador', 11, 'tre-ra-02-2017', [ + entry('salvador-ze-11', 'tre-ra-02-2017', [ 'Águas Claras', 'Calabetão', 'Castelo Branco', @@ -161,7 +168,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Sete de Abril', 'Vila Canária', ]), - entry('salvador-ze-12', 'Salvador', 12, 'tre-ra-02-2017', [ + entry('salvador-ze-12', 'tre-ra-02-2017', [ 'Aeroporto', 'Alto do Coqueirinho', 'Areia Branca', @@ -171,7 +178,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'São Cristóvão', 'Stella Maris', ]), - entry('salvador-ze-13', 'Salvador', 13, 'tre-ra-02-2017', [ + entry('salvador-ze-13', 'tre-ra-02-2017', [ 'Caminho das Árvores', 'Itaigara', 'Nordeste de Amaralina', @@ -179,14 +186,14 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Santa Cruz', 'Vale das Pedrinhas', ]), - entry('salvador-ze-14', 'Salvador', 14, 'tre-ra-02-2017', [ + entry('salvador-ze-14', 'tre-ra-02-2017', [ 'Arenoso', 'Novo Horizonte', 'São Marcos', 'Sussuarana', 'Tancredo Neves', ]), - entry('salvador-ze-15', 'Salvador', 15, 'tre-ra-02-2017', [ + entry('salvador-ze-15', 'tre-ra-02-2017', [ 'Arraial do Retiro', 'Boa Vista de São Caetano', 'Bom Juá', @@ -194,7 +201,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Lobato', 'São Caetano', ]), - entry('salvador-ze-16', 'Salvador', 16, 'tre-ra-02-2017', [ + entry('salvador-ze-16', 'tre-ra-02-2017', [ 'Cabula VI', 'Canabrava', 'Centro Administrativo da Bahia', @@ -207,7 +214,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Saramandaia', 'Vale dos Lagos', ]), - entry('salvador-ze-17', 'Salvador', 17, 'tre-ra-02-2017', [ + entry('salvador-ze-17', 'tre-ra-02-2017', [ 'Alto da Terezinha', 'Alto do Cabrito', 'Itacaranha', @@ -217,7 +224,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Rio Sena', 'São João do Cabrito', ]), - entry('salvador-ze-18', 'Salvador', 18, 'tre-ra-02-2017', [ + entry('salvador-ze-18', 'tre-ra-02-2017', [ "Caixa D'água", 'Calçada', 'Caminho de Areia', @@ -227,7 +234,7 @@ export const municipalityZoneNeighborhoods: readonly MunicipalityZoneNeighborhoo 'Mares', 'Roma', ]), - entry('salvador-ze-19', 'Salvador', 19, 'tre-ra-02-2017', [ + entry('salvador-ze-19', 'tre-ra-02-2017', [ 'Boca da Mata', 'Cajazeiras X', 'Cajazeiras XI', @@ -264,19 +271,3 @@ export const municipalityZoneNeighborhoodSourceLabel = ( return source satisfies never } } - -const assertMunicipalityZoneNeighborhoodCatalogIntegrity = (): void => { - for (const record of municipalityZoneNeighborhoods) { - const catalogEntry = getMunicipalityCatalogEntry(record.municipalitySlug) - if (!catalogEntry || catalogEntry.kind !== 'zona') { - throw new Error( - `Missing zona municipality catalog entry for slug: ${record.municipalitySlug}`, - ) - } - if (catalogEntry.city !== record.city || catalogEntry.zoneNumber !== record.zoneNumber) { - throw new Error(`Catalog mismatch for slug: ${record.municipalitySlug}`) - } - } -} - -assertMunicipalityZoneNeighborhoodCatalogIntegrity() diff --git a/src/utilities/municipalityMapContract.ts b/src/utilities/municipalityMapContract.ts index 53543958..ef009450 100644 --- a/src/utilities/municipalityMapContract.ts +++ b/src/utilities/municipalityMapContract.ts @@ -1,7 +1,7 @@ import type { FederalCompetitiveRank } from '@/lib/bahiaElectionAggregates' import { HISTORICAL_SERIES_YEARS } from '@/lib/electionResults' import type { VoteEstimateScenario } from '@/lib/voteEstimate' -import type { MunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation' +import type { MunicipalitiesByMapKey } from '@/utilities/municipalityMapNavigation' import type { MunicipalityTerritorialClass } from '@/utilities/municipalityTerritorialClass' /** @@ -89,16 +89,22 @@ type MunicipalityZoneBreakdownRow = { export type MunicipalityMapComparison = { candidateNumber: number candidateName: string - /** TSE year (as string) → codarea → (sollaVotes − otherVotes). */ + /** TSE year (as string) → map key → (sollaVotes − otherVotes). */ diffByYear: Record> } +/** + * Every record below is keyed by MAP KEY, not by IBGE codarea: since B8 F2 the + * map paints one polygon per catalog unit, so Salvador's 19 zones are addressed + * by slug (`salvador-ze-3`) and every other município by its codarea. See + * `mapKeyForMunicipality`. + */ export type MunicipalityMapBundle = { - /** year (as string) → codarea → value. 2026 = cenário médio (central). */ + /** year (as string) → map key → value. 2026 = cenário médio (central). */ valuesByYear: Record> - /** 2026 totals per estimate scenario (codarea → votes). */ + /** 2026 totals per estimate scenario (map key → votes). */ values2026ByScenario: Record> - /** year (as string) → codarea → votosValidos (federal T1). 2026 reuses 2022. */ + /** year (as string) → map key → votosValidos (federal T1). 2026 reuses 2022. */ validVotesByYear: Record> /** * year (as string) → his own STATEWIDE share of valid votes: the "1×" the LQ @@ -107,14 +113,18 @@ export type MunicipalityMapBundle = { * the same standard. 2026 reuses the 2022 standard, like `validVotesByYear`. */ statewideShareByYear: Record - /** codarea → E10 territorial class, aggregated over the slugs in scope. */ - territorialClassByCode: Record - /** codarea → his placement among the federal-deputy candidates voted there. */ + /** map key → E10 territorial class of that unit. */ + territorialClassByMapKey: Record + /** + * map key → his placement among the federal-deputy candidates voted there. The + * TSE artifact ranks by codarea only, so Salvador's zones all carry the city's + * position (the legend says so). + */ competitiveRankByYear: Record> - /** codarea → projected 2026 valid votes (E8) — the size of the proportional symbol. */ - projectedValidVotesByCode: Record - /** IBGE codarea → accessible municipality slugs for map click navigation. */ - municipalitiesByIbgeCode: MunicipalitiesByIbgeCode + /** map key → projected 2026 valid votes (E8) — the size of the proportional symbol. */ + projectedValidVotesByMapKey: Record + /** map key → the municipality it opens on click. */ + municipalitiesByMapKey: MunicipalitiesByMapKey /** Zone municipalities in scope (Salvador) with per-year values. */ zoneBreakdown: MunicipalityZoneBreakdownRow[] candidateName: string diff --git a/src/utilities/municipalityMapData.ts b/src/utilities/municipalityMapData.ts index ea533cfe..3ac9ccb1 100644 --- a/src/utilities/municipalityMapData.ts +++ b/src/utilities/municipalityMapData.ts @@ -36,10 +36,13 @@ import type { MunicipalityMapBundle, MunicipalityMapComparison, } from '@/utilities/municipalityMapContract' -import { buildMunicipalitiesByIbgeCode } from '@/utilities/municipalityMapNavigation' +import { + buildMunicipalitiesByMapKey, + mapKeyForMunicipality, +} from '@/utilities/municipalityMapNavigation' import { projectedValidVotes } from '@/utilities/municipalityPotential' import { - computeAggregateTerritorialClass, + computeMunicipalityTerritorialClass, type MunicipalityTerritorialClass, } from '@/utilities/municipalityTerritorialClass' import { @@ -104,9 +107,9 @@ const buildMunicipalityMapBundleFromMunicipalities = async ( const statewideShareByYear: Record = {} const competitiveRankByYear: Record> = {} const zoneVotesBySlug = new Map>() - // One polygon per IBGE code, N catalog slugs behind it (Salvador is 19) — - // the same index the map uses to navigate, reused here for the rollups. - const municipalitiesByIbgeCode = buildMunicipalitiesByIbgeCode(municipalities) + // One polygon per catalog unit since B8 F2, so this index — the same one the + // map navigates by — is also the key of every rollup below. + const municipalitiesByMapKey = buildMunicipalitiesByMapKey(municipalities) // Historical years come from the committed artifact (immutable TSE data, // pre-aggregated per municipality) — zero database work. @@ -114,11 +117,11 @@ const buildMunicipalityMapBundleFromMunicipalities = async ( const values: Record = {} const validValues: Record = {} for (const municipality of municipalities) { + const mapKey = mapKeyForMunicipality(municipality) const baseline = getMunicipalityFederalBaseline(municipality.slug) const votes = baseline.votesByYear[String(year)] ?? 0 - values[municipality.ibgeCode] = (values[municipality.ibgeCode] ?? 0) + votes - const valid = baseline.validVotesByYear[String(year)] ?? 0 - validValues[municipality.ibgeCode] = (validValues[municipality.ibgeCode] ?? 0) + valid + values[mapKey] = votes + validValues[mapKey] = baseline.validVotesByYear[String(year)] ?? 0 if (municipality.kind === 'zona') { const bySlug = zoneVotesBySlug.get(municipality.slug) ?? {} bySlug[String(year)] = votes @@ -135,10 +138,12 @@ const buildMunicipalityMapBundleFromMunicipalities = async ( statewideShareByYear[String(year)] = statewide.validVotes > 0 ? statewide.ownVotes / statewide.validVotes : 0 + // Rank exists per city only, so every zone of Salvador carries the city's + // position — stated in the legend rather than faked per zone. const ranks: Record = {} - for (const ibgeCode of Object.keys(municipalitiesByIbgeCode)) { - const rank = getFederalCompetitiveRank(ibgeCode, year) - if (rank) ranks[ibgeCode] = rank + for (const municipality of municipalities) { + const rank = getFederalCompetitiveRank(municipality.ibgeCode, year) + if (rank) ranks[mapKeyForMunicipality(municipality)] = rank } competitiveRankByYear[String(year)] = ranks } @@ -148,14 +153,12 @@ const buildMunicipalityMapBundleFromMunicipalities = async ( // substitution `validVotesByYear` already makes. statewideShareByYear['2026'] = statewideShareByYear['2022'] ?? 0 - const territorialClassByCode: Record = {} - const projectedValidVotesByCode: Record = {} - for (const [ibgeCode, entries] of Object.entries(municipalitiesByIbgeCode)) { - const slugs = entries.map((entry) => entry.slug) - territorialClassByCode[ibgeCode] = computeAggregateTerritorialClass(slugs).class - projectedValidVotesByCode[ibgeCode] = slugs.reduce( - (total, slug) => total + projectedValidVotes(getMunicipalityFederalBaseline(slug)), - 0, + const territorialClassByMapKey: Record = {} + const projectedValidVotesByMapKey: Record = {} + for (const [mapKey, entry] of Object.entries(municipalitiesByMapKey)) { + territorialClassByMapKey[mapKey] = computeMunicipalityTerritorialClass(entry.slug).class + projectedValidVotesByMapKey[mapKey] = projectedValidVotes( + getMunicipalityFederalBaseline(entry.slug), ) } @@ -178,8 +181,7 @@ const buildMunicipalityMapBundleFromMunicipalities = async ( scenario, ) if (votes > 0) { - pledgeValuesByScenario[scenario][municipality.ibgeCode] = - (pledgeValuesByScenario[scenario][municipality.ibgeCode] ?? 0) + votes + pledgeValuesByScenario[scenario][mapKeyForMunicipality(municipality)] = votes } if (municipality.kind === 'zona') { const byScenario = zoneVotes2026BySlug.get(municipality.slug) ?? emptyZoneScenarioVotes() @@ -239,7 +241,7 @@ const buildMunicipalityMapBundleFromMunicipalities = async ( const solla = getMunicipalityFederalBaseline(municipality.slug).votesByYear[String(year)] ?? 0 const other = sumVotesForGeography(otherVotes, municipality.geography) - values[municipality.ibgeCode] = (values[municipality.ibgeCode] ?? 0) + (solla - other) + values[mapKeyForMunicipality(municipality)] = solla - other } diffByYear[String(year)] = values }), @@ -258,10 +260,10 @@ const buildMunicipalityMapBundleFromMunicipalities = async ( values2026ByScenario: pledgeValuesByScenario, validVotesByYear, statewideShareByYear, - territorialClassByCode, + territorialClassByMapKey, competitiveRankByYear, - projectedValidVotesByCode, - municipalitiesByIbgeCode, + projectedValidVotesByMapKey, + municipalitiesByMapKey, zoneBreakdown, candidateName: BASELINE_TICKET_2022.candidate.name, comparison, diff --git a/src/utilities/municipalityMapNavigation.ts b/src/utilities/municipalityMapNavigation.ts index caa33974..53dad921 100644 --- a/src/utilities/municipalityMapNavigation.ts +++ b/src/utilities/municipalityMapNavigation.ts @@ -5,12 +5,28 @@ type MunicipalityMapSlugEntry = { name: string } +/** IBGE codarea → the accessible catalog units inside it (Salvador has 19). */ export type MunicipalitiesByIbgeCode = Record -export type MunicipalityMapNavigation = - | { kind: 'none' } - | { kind: 'navigate'; slug: string } - | { kind: 'zones' } +/** + * Map key → the single unit it paints. Since B8 F2 the map draws one polygon per + * catalog unit — zone municipalities by slug, everything else by IBGE codarea — + * so this index is 1:1 where `MunicipalitiesByIbgeCode` is 1:N. + */ +export type MunicipalitiesByMapKey = Record + +export type MunicipalityMapNavigation = { kind: 'none' } | { kind: 'navigate'; slug: string } + +/** + * What the choropleth paints a unit by: a zone municipality has no code of its + * own (the whole city shares one), so it is keyed by its immutable catalog slug, + * which is also the polygon's key in `bahia-municipality-zones.topo.json`. + */ +export const mapKeyForMunicipality = (municipality: { + kind: 'municipio' | 'zona' + slug: string + ibgeCode: string +}): string => (municipality.kind === 'zona' ? municipality.slug : municipality.ibgeCode) export const buildMunicipalitiesByIbgeCode = ( municipalities: readonly AccessibleMunicipality[], @@ -30,12 +46,25 @@ export const buildMunicipalitiesByIbgeCode = ( return byIbge } -export const resolveMunicipalityMapNavigation = ( - ibgeCode: string, - municipalitiesByIbgeCode: MunicipalitiesByIbgeCode, -): MunicipalityMapNavigation => { - const entries = municipalitiesByIbgeCode[ibgeCode] - if (!entries || entries.length === 0) return { kind: 'none' } - if (entries.length === 1) return { kind: 'navigate', slug: entries[0].slug } - return { kind: 'zones' } +export const buildMunicipalitiesByMapKey = ( + municipalities: ReadonlyArray, +): MunicipalitiesByMapKey => { + const byMapKey: MunicipalitiesByMapKey = {} + + for (const municipality of municipalities) { + byMapKey[mapKeyForMunicipality(municipality)] = { + slug: municipality.slug, + name: municipality.name, + } + } + + return byMapKey +} + +export const resolveMunicipalityMapNavigation = ( + mapKey: string, + municipalitiesByMapKey: MunicipalitiesByMapKey, +): MunicipalityMapNavigation => { + const entry = municipalitiesByMapKey[mapKey] + return entry ? { kind: 'navigate', slug: entry.slug } : { kind: 'none' } } diff --git a/tests/e2e/campaignZoneMap.e2e.spec.ts b/tests/e2e/campaignZoneMap.e2e.spec.ts new file mode 100644 index 00000000..b8787391 --- /dev/null +++ b/tests/e2e/campaignZoneMap.e2e.spec.ts @@ -0,0 +1,63 @@ +import { expect, test } from './fixtures/campaignE2EFixtures.js' + +/** + * B8 F2 — Salvador used to be one polygon carrying the whole city's votes, so + * the choropleth could not answer "which piece of Salvador is weak / is mine". + * This pins the two halves of that fix: the zone mesh is drawn (one path per + * ZE, the city's own polygon left underneath as a non-interactive base) and a + * zone is addressable on its own — the readout names it and opens it. + */ +test('the dashboard map paints Salvador zone by zone and opens one', async ({ campaign, page }) => { + const { fixtures } = campaign + const password = fixtures.value('senha') + const coordinator = await campaign.payload.create({ + collection: 'campaignUser', + data: { + name: fixtures.value('Coordenadora Mapa'), + email: `${fixtures.value('map-coordinator')}@example.com`, + password, + role: 'coordinator', + }, + depth: 0, + }) + + await campaign.login(page, coordinator.email!, password) + + await expect(page.getByRole('heading', { name: 'Mapa dos Municípios' })).toBeVisible({ + timeout: 60_000, + }) + await page.waitForSelector('path.leaflet-interactive', { timeout: 60_000 }) + + // 417 municípios + 19 Salvador zones; Salvador's own polygon is the one path + // that stays out of the pointer's way. + await expect + .poll( + () => + page.evaluate(() => { + const paths = Array.from(document.querySelectorAll('.leaflet-overlay-pane path')) + return { + total: paths.length, + interactive: paths.filter((path) => path.classList.contains('leaflet-interactive')) + .length, + } + }), + { timeout: 30_000 }, + ) + .toEqual({ total: 436, interactive: 435 }) + + // Insertion order is z-order, so the zone mesh is the tail of the pane. + await page.evaluate(() => { + const paths = Array.from(document.querySelectorAll('.leaflet-overlay-pane path')) + paths.at(-1)?.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })) + }) + + const readout = page.locator('div[aria-live="polite"]').first() + await expect(readout).toContainText(/Salvador — ZE \d+/) + + await readout.getByRole('link', { name: 'Abrir município' }).click() + // The slug is the assertion: the detail page's own

    reads the municipality + // ROW, which other specs rename, while the polygon's name comes from the + // static catalog. + await expect(page).toHaveURL(/\/campanha\/municipios\/salvador-ze-\d+$/) + await expect(page.getByRole('heading', { level: 1 })).toBeVisible() +}) diff --git a/tests/int/bahiaGeometries.int.spec.ts b/tests/int/bahiaGeometries.int.spec.ts index 3c5579bb..87375554 100644 --- a/tests/int/bahiaGeometries.int.spec.ts +++ b/tests/int/bahiaGeometries.int.spec.ts @@ -4,14 +4,22 @@ import { statSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' +import type { Position } from 'geojson' import { describe, expect, it } from 'vitest' -import { loadMunicipalityGeometryModule, loadTerritoryGeometryModule } from '@/lib/bahiaGeometries' +import type { BahiaGeometryFeature } from '@/lib/bahiaGeometriesTypes' + +import { + loadMunicipalityGeometryModule, + loadMunicipalityZoneGeometryModule, + loadTerritoryGeometryModule, +} from '@/lib/bahiaGeometries' import { bahiaMunicipalityCodes } from '@/lib/bahiaMunicipalityCodes' import { bahiaIdentityTerritoryRecords } from '@/lib/bahiaTerritories' import { municipalityCatalog } from '@/lib/municipalityCatalog' import { featureCentroid, + featureContainsPoint, findContainingMunicipality, resolveNearbyMunicipality, } from '@/lib/municipalityProximity' @@ -21,6 +29,9 @@ import { featureBounds } from '../helpers/featureBounds' /** Soft ceiling against accidental unsimplified meshes (plan target ≤ ~600 KB). */ const MAX_TOPO_BYTES = 600 * 1024 +/** Square degrees → km² at Salvador's latitude — good to a few percent, which is all this needs. */ +const SQUARE_DEGREE_KM2 = 110.57 * 108.5 + const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '../..') const hasNonEmptyGeometry = ( @@ -31,6 +42,32 @@ const hasNonEmptyGeometry = ( return true } +/** Planar shoelace in square degrees — only ever compared against itself. */ +const featureArea = (feature: BahiaGeometryFeature): number => { + const polygons = + feature.geometry.type === 'Polygon' + ? [feature.geometry.coordinates] + : feature.geometry.coordinates + + return polygons.reduce((total, [outerRing, ...holes]) => { + const ringArea = (ring: readonly Position[]): number => { + let sum = 0 + for ( + let current = 0, previous = ring.length - 1; + current < ring.length; + previous = current++ + ) { + sum += ring[previous][0] * ring[current][1] - ring[current][0] * ring[previous][1] + } + return Math.abs(sum) / 2 + } + + return ( + total + ringArea(outerRing) - holes.reduce((holeTotal, hole) => holeTotal + ringArea(hole), 0) + ) + }, 0) +} + describe('Bahia static geometries', () => { it('keeps committed TopoJSON under the size budget', () => { const municipalityBytes = statSync( @@ -39,11 +76,16 @@ describe('Bahia static geometries', () => { const territoryBytes = statSync( join(repoRoot, 'src/lib/geometries/bahia-identity-territories.topo.json'), ).size + const zoneBytes = statSync( + join(repoRoot, 'src/lib/geometries/bahia-municipality-zones.topo.json'), + ).size expect(municipalityBytes).toBeGreaterThan(10_000) expect(territoryBytes).toBeGreaterThan(1_000) + expect(zoneBytes).toBeGreaterThan(10_000) expect(municipalityBytes).toBeLessThanOrEqual(MAX_TOPO_BYTES) expect(territoryBytes).toBeLessThanOrEqual(MAX_TOPO_BYTES) + expect(zoneBytes).toBeLessThanOrEqual(MAX_TOPO_BYTES) }) it('exposes 417 municipality features whose codarea set matches the code table', async () => { @@ -87,6 +129,72 @@ describe('Bahia static geometries', () => { expect(getTerritoryFeature('99')).toBeUndefined() }) + /** + * B8 F2 — the 19 Salvador zones are dissolved from the IBGE neighborhood mesh, + * a different source than the municipal mesh above, so what is pinned here is + * the contract the map depends on: one feature per catalog zona, keyed by slug, + * geometrically inside the city it decomposes. + */ + describe('zone municipality geometries (B8 F2)', () => { + const zoneCatalog = municipalityCatalog.filter((entry) => entry.kind === 'zona') + + it('exposes one feature per zona municipality, keyed by catalog slug', async () => { + const { topology, features, getMunicipalityZoneFeature } = + await loadMunicipalityZoneGeometryModule() + + expect(Object.keys(topology.objects)).toEqual(['municipalityZones']) + expect(topology.objects.municipalityZones.geometries).toHaveLength(zoneCatalog.length) + expect(features).toHaveLength(zoneCatalog.length) + + for (const entry of zoneCatalog) { + const zone = getMunicipalityZoneFeature(entry.slug) + expect(zone, entry.slug).toBeDefined() + expect(zone?.properties.name).toBe(entry.name) + expect(zone?.properties.zoneNumber).toBe(entry.zoneNumber) + expect(zone?.properties.ibgeCode).toBe(entry.ibgeCode) + expect(hasNonEmptyGeometry(zone?.geometry)).toBe(true) + } + + expect(getMunicipalityZoneFeature('feira-de-santana')).toBeUndefined() + }) + + it('keeps every zone inside the municipality it decomposes', async () => { + const [{ getMunicipalityFeature }, { features }] = await Promise.all([ + loadMunicipalityGeometryModule(), + loadMunicipalityZoneGeometryModule(), + ]) + + for (const zone of features) { + const city = getMunicipalityFeature(zone.properties.ibgeCode) + expect(city, zone.properties.municipalitySlug).toBeDefined() + expect( + featureContainsPoint(city!, featureCentroid(zone)), + zone.properties.municipalitySlug, + ).toBe(true) + } + }) + + /** + * Salvador's land is ~324 km². It is NOT compared against the municipal + * polygon: that one measures ~690 km², matching the official IBGE area, + * because the municipal limits enclose the bay water around the islands — + * water the neighborhood mesh does not draw. + * + * Since each neighborhood polygon is assigned to exactly one zone, the sum is + * also the overlap check: a neighborhood counted twice, or dropped, moves this + * by its own area. + */ + it('sums to Salvador’s land area, so no neighborhood is dropped or counted twice', async () => { + const { features } = await loadMunicipalityZoneGeometryModule() + + const zoneKm2 = + features.reduce((total, zone) => total + featureArea(zone), 0) * SQUARE_DEGREE_KM2 + + expect(zoneKm2).toBeGreaterThan(280) + expect(zoneKm2).toBeLessThan(360) + }) + }) + /** * B14 resolves "onde estou" against this same mesh, so the containment used in * the field is pinned here with real coordinates — the synthetic squares in diff --git a/tests/int/municipalityMapData.int.spec.ts b/tests/int/municipalityMapData.int.spec.ts index 65805014..417d6496 100644 --- a/tests/int/municipalityMapData.int.spec.ts +++ b/tests/int/municipalityMapData.int.spec.ts @@ -10,10 +10,10 @@ import { loadMunicipalityMapBundle } from '@/utilities/municipalityMapData' import { installCampaignFixtures } from '../helpers/campaignFixtures' -const ZONE_MUNICIPALITY_COUNT = municipalityCatalog.filter((entry) => entry.kind === 'zona').length -const ZONE_MUNICIPALITY_CODES = new Set( - municipalityCatalog.filter((entry) => entry.kind === 'zona').map((entry) => entry.ibgeCode), -) +const zoneMunicipalities = municipalityCatalog.filter((entry) => entry.kind === 'zona') +const ZONE_MUNICIPALITY_COUNT = zoneMunicipalities.length +const ZONE_MUNICIPALITY_SLUGS = new Set(zoneMunicipalities.map((entry) => entry.slug)) +const ZONE_MUNICIPALITY_CODES = new Set(zoneMunicipalities.map((entry) => entry.ibgeCode)) let payload: Payload const campaignFixtures = installCampaignFixtures({ @@ -28,7 +28,7 @@ describe('loadMunicipalityMapBundle — list URL filters', () => { payload = await getPayload({ config: await config }) }) - it('scopes the map to zone municipalities when kind=zona is in the URL', async () => { + it('paints each zone under its own map key, never pooled under the city code', async () => { const coordinator = await campaignFixtures().createCampaignUser('coordinator') const bundle = await loadMunicipalityMapBundle(payload, coordinator, { kind: 'zona' }) @@ -36,11 +36,18 @@ describe('loadMunicipalityMapBundle — list URL filters', () => { expect(bundle).not.toBeNull() expect(bundle!.zoneBreakdown).toHaveLength(ZONE_MUNICIPALITY_COUNT) - const ibgeCodes = Object.keys(bundle!.valuesByYear['2022'] ?? {}) - expect(ibgeCodes.length).toBeGreaterThan(0) - for (const code of ibgeCodes) { - expect(ZONE_MUNICIPALITY_CODES.has(code)).toBe(true) + const mapKeys = Object.keys(bundle!.valuesByYear['2022'] ?? {}) + expect(mapKeys.length).toBeGreaterThan(0) + for (const key of mapKeys) { + expect(ZONE_MUNICIPALITY_SLUGS.has(key), key).toBe(true) + expect(ZONE_MUNICIPALITY_CODES.has(key), key).toBe(false) } + expect(Object.keys(bundle!.municipalitiesByMapKey).sort()).toEqual([...mapKeys].sort()) + + // The zones share the city's TSE rank — the artifact ranks by codarea only. + const ranks = bundle!.competitiveRankByYear['2022'] ?? {} + const positions = new Set(mapKeys.map((key) => ranks[key]?.rank)) + expect(positions.size).toBe(1) }) it('returns null when the filtered municipality set is empty', async () => { @@ -62,9 +69,9 @@ describe('loadMunicipalityMapBundle — list URL filters', () => { expect(included!.zoneBreakdown.some((row) => row.slug === administered.slug)).toBe( administered.kind === 'zona', ) - if (administered.kind !== 'zona') { - expect(Object.keys(included!.valuesByYear['2022'] ?? {})).toEqual([administered.ibgeCode]) - } + expect(Object.keys(included!.valuesByYear['2022'] ?? {})).toEqual([ + administered.kind === 'zona' ? administered.slug : administered.ibgeCode, + ]) const excluded = administered.kind === 'zona' diff --git a/tests/int/municipalityZoneNeighborhoods.int.spec.ts b/tests/int/municipalityZoneNeighborhoods.int.spec.ts index 4b1d174c..f52506e4 100644 --- a/tests/int/municipalityZoneNeighborhoods.int.spec.ts +++ b/tests/int/municipalityZoneNeighborhoods.int.spec.ts @@ -16,7 +16,8 @@ type OfficialEvidence = { zoneCount: number assignments: Array<{ municipalitySlug: string - city: 'Salvador' | 'Camaçari' + /** Hydrated from the catalog since Fase 0 of B8 F2, so the fixture no longer narrows it. */ + city: string zoneNumber: number source: 'tre-ra-02-2017' | 'tre-voting-locations-curated' neighborhoods: string[] @@ -60,6 +61,12 @@ describe('Municipality zone neighborhood catalog (Salvador only)', () => { } }) + it('lists zones in catalog order', () => { + expect(municipalityZoneNeighborhoods.map((entry) => entry.municipalitySlug)).toEqual( + zoneMunicipalities.map((entry) => entry.slug), + ) + }) + it('keeps Salvador neighborhoods exclusive across zones', () => { const seen = new Map() diff --git a/tests/unit/bahiaMapStyle.unit.spec.ts b/tests/unit/bahiaMapStyle.unit.spec.ts index cb493a50..e7cd68af 100644 --- a/tests/unit/bahiaMapStyle.unit.spec.ts +++ b/tests/unit/bahiaMapStyle.unit.spec.ts @@ -4,6 +4,7 @@ import { buildLayerStyleContext, canonicalMapKeysKey, computeChoroplethMax, + featureMapKey, resolvePathStyle, } from '@/lib/bahiaMapStyle' @@ -29,6 +30,34 @@ describe('bahiaMapStyle', () => { }) }) + describe('featureMapKey (B8 F2)', () => { + it('keys a zone municipality by its catalog slug', () => { + expect( + featureMapKey({ municipalitySlug: 'salvador-ze-7', ibgeCode: '2927408', zoneNumber: 7 }), + ).toBe('salvador-ze-7') + }) + + it('keys a município by codarea and an identity territory by code', () => { + expect(featureMapKey({ codarea: '2905701', name: 'Barreiras' })).toBe('2905701') + expect(featureMapKey({ code: 'chapada-diamantina', name: 'Chapada Diamantina' })).toBe( + 'chapada-diamantina', + ) + }) + + it('prefers the slug when the zone mesh also carries the city code', () => { + expect(featureMapKey({ municipalitySlug: 'salvador-ze-1', codarea: '2927408' })).toBe( + 'salvador-ze-1', + ) + }) + + it('returns undefined when no key property is usable', () => { + expect(featureMapKey(undefined)).toBeUndefined() + expect(featureMapKey({ name: 'Sem chave' })).toBeUndefined() + expect(featureMapKey({ codarea: '' })).toBeUndefined() + expect(featureMapKey({ codarea: 2905701 })).toBeUndefined() + }) + }) + describe('canonicalMapKeysKey', () => { it('sorts keys for stable identity', () => { expect(canonicalMapKeysKey(['2927408', '2905701'])).toBe('2905701,2927408') diff --git a/tests/unit/municipalityMapNavigation.unit.spec.ts b/tests/unit/municipalityMapNavigation.unit.spec.ts index 34446c72..a3e84cac 100644 --- a/tests/unit/municipalityMapNavigation.unit.spec.ts +++ b/tests/unit/municipalityMapNavigation.unit.spec.ts @@ -2,50 +2,69 @@ import { describe, expect, it } from 'vitest' import { buildMunicipalitiesByIbgeCode, + buildMunicipalitiesByMapKey, + mapKeyForMunicipality, resolveMunicipalityMapNavigation, } from '@/utilities/municipalityMapNavigation' +const abaira = { slug: 'abaira', name: 'Abaíra', ibgeCode: '2900108', kind: 'municipio' } as const +const salvadorZe1 = { + slug: 'salvador-ze-1', + name: 'Salvador — ZE 1', + ibgeCode: '2927408', + kind: 'zona', +} as const +const salvadorZe2 = { + slug: 'salvador-ze-2', + name: 'Salvador — ZE 2', + ibgeCode: '2927408', + kind: 'zona', +} as const + describe('municipalityMapNavigation', () => { it('buildMunicipalitiesByIbgeCode groups municipalities by ibgeCode and sorts by name', () => { - const result = buildMunicipalitiesByIbgeCode([ - { slug: 'salvador-ze-2', name: 'Salvador ZE 2', ibgeCode: '2927408' }, - { slug: 'salvador-ze-1', name: 'Salvador ZE 1', ibgeCode: '2927408' }, - { slug: 'abaira', name: 'Abaíra', ibgeCode: '2900108' }, - ]) + const result = buildMunicipalitiesByIbgeCode([salvadorZe2, salvadorZe1, abaira]) expect(result['2900108']).toEqual([{ slug: 'abaira', name: 'Abaíra' }]) expect(result['2927408']).toEqual([ - { slug: 'salvador-ze-1', name: 'Salvador ZE 1' }, - { slug: 'salvador-ze-2', name: 'Salvador ZE 2' }, + { slug: 'salvador-ze-1', name: 'Salvador — ZE 1' }, + { slug: 'salvador-ze-2', name: 'Salvador — ZE 2' }, ]) }) - it('resolveMunicipalityMapNavigation returns none when ibgeCode is missing or empty', () => { - const byIbge = buildMunicipalitiesByIbgeCode([ - { slug: 'abaira', name: 'Abaíra', ibgeCode: '2900108' }, - ]) - - expect(resolveMunicipalityMapNavigation('9999999', byIbge)).toEqual({ kind: 'none' }) - expect(resolveMunicipalityMapNavigation('2900108', {})).toEqual({ kind: 'none' }) + it('keys zone municipalities by slug and everything else by codarea', () => { + expect(mapKeyForMunicipality(abaira)).toBe('2900108') + expect(mapKeyForMunicipality(salvadorZe1)).toBe('salvador-ze-1') }) - it('resolveMunicipalityMapNavigation returns navigate for a single municipality', () => { - const byIbge = buildMunicipalitiesByIbgeCode([ - { slug: 'abaira', name: 'Abaíra', ibgeCode: '2900108' }, - ]) + it('gives every zone its own map key instead of pooling them under the city', () => { + const byMapKey = buildMunicipalitiesByMapKey([salvadorZe1, salvadorZe2, abaira]) - expect(resolveMunicipalityMapNavigation('2900108', byIbge)).toEqual({ - kind: 'navigate', - slug: 'abaira', + expect(Object.keys(byMapKey).sort()).toEqual(['2900108', 'salvador-ze-1', 'salvador-ze-2']) + expect(byMapKey['2927408']).toBeUndefined() + expect(byMapKey['salvador-ze-2']).toEqual({ + slug: 'salvador-ze-2', + name: 'Salvador — ZE 2', }) }) - it('resolveMunicipalityMapNavigation returns zones for multiple municipalities on the same ibgeCode', () => { - const byIbge = buildMunicipalitiesByIbgeCode([ - { slug: 'salvador-ze-1', name: 'Salvador ZE 1', ibgeCode: '2927408' }, - { slug: 'salvador-ze-2', name: 'Salvador ZE 2', ibgeCode: '2927408' }, - ]) + it('resolveMunicipalityMapNavigation returns none for an unknown or empty key', () => { + const byMapKey = buildMunicipalitiesByMapKey([abaira]) - expect(resolveMunicipalityMapNavigation('2927408', byIbge)).toEqual({ kind: 'zones' }) + expect(resolveMunicipalityMapNavigation('9999999', byMapKey)).toEqual({ kind: 'none' }) + expect(resolveMunicipalityMapNavigation('2900108', {})).toEqual({ kind: 'none' }) + }) + + it('resolveMunicipalityMapNavigation opens the unit behind the key, zone included', () => { + const byMapKey = buildMunicipalitiesByMapKey([abaira, salvadorZe1]) + + expect(resolveMunicipalityMapNavigation('2900108', byMapKey)).toEqual({ + kind: 'navigate', + slug: 'abaira', + }) + expect(resolveMunicipalityMapNavigation('salvador-ze-1', byMapKey)).toEqual({ + kind: 'navigate', + slug: 'salvador-ze-1', + }) }) })