mirror of
https://gitlab.com/PronounsPage/PronounsPage.git
synced 2025-09-24 05:05:20 -04:00
wip
This commit is contained in:
parent
bc61615066
commit
0ffc1852d3
@ -1,7 +1,9 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div id="map" :class="isDark ? 'dark' : ''"></div>
|
<div id="map" :class="isDark ? 'dark' : ''"></div>
|
||||||
{{ JSON.stringify(polygons) }}
|
<div v-for="polygon in polygons">
|
||||||
|
{{ JSON.stringify(polygon) }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -9,8 +11,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import dark from '../plugins/dark.ts';
|
import dark from '../plugins/dark.ts';
|
||||||
import walsLanguages from '~/assets/languages.csv';
|
import walsLanguages from '~/assets/languages.csv';
|
||||||
import polygons from '~/assets/shapes.json';
|
import polygonsByLocale from '~/assets/map.json';
|
||||||
import newMexicoPolygon from '~/assets/de.json';
|
|
||||||
import locales from '../locale/locales.ts';
|
import locales from '../locale/locales.ts';
|
||||||
import { clearUrl } from '~/src/helpers.ts';
|
import { clearUrl } from '~/src/helpers.ts';
|
||||||
import type { LocaleDescription } from '../locale/locales.ts';
|
import type { LocaleDescription } from '../locale/locales.ts';
|
||||||
@ -38,49 +39,9 @@ declare module 'leaflet' {
|
|||||||
export default dark.extend({
|
export default dark.extend({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
inputCurve: null as any,
|
polygons: {} as Record<string, L.Polygon[]>,
|
||||||
curve: null as any,
|
|
||||||
controls: [],
|
|
||||||
polygons: newMexicoPolygon,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
watch: {
|
|
||||||
polygons() {
|
|
||||||
this.update();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
update() {
|
|
||||||
const inputPath = [];
|
|
||||||
const polygonPath = [];
|
|
||||||
for (const polygon of this.polygons) {
|
|
||||||
if (polygon.length >= 1) {
|
|
||||||
inputPath.push('M', polygon[0]);
|
|
||||||
}
|
|
||||||
for (let i = 1; i < polygon.length; i++) {
|
|
||||||
inputPath.push('L', polygon[i]);
|
|
||||||
}
|
|
||||||
inputPath.push('Z');
|
|
||||||
/*const middleOf = (a: [number, number], b: [number, number]) => {
|
|
||||||
return [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
|
|
||||||
};
|
|
||||||
|
|
||||||
if (polygon.length >= 3) {
|
|
||||||
polygonPath.push('M', middleOf(polygon[0], polygon[1]));
|
|
||||||
polygonPath.push('Q', polygon[1], middleOf(polygon[1], polygon[2]));
|
|
||||||
|
|
||||||
for (let i = 2; i < polygon.length - 1; i++) {
|
|
||||||
polygonPath.push('T', middleOf(polygon[i], polygon[i + 1]));
|
|
||||||
}
|
|
||||||
|
|
||||||
polygonPath.push('T', middleOf(polygon[polygon.length - 1], polygon[0]));
|
|
||||||
polygonPath.push('T', middleOf(polygon[0], polygon[1]));
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
// this.inputCurve.setPath(inputPath);
|
|
||||||
this.curve.setPath(inputPath);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
async mounted() {
|
async mounted() {
|
||||||
const { default: L } = await import('leaflet');
|
const { default: L } = await import('leaflet');
|
||||||
await import ('@elfalem/leaflet-curve');
|
await import ('@elfalem/leaflet-curve');
|
||||||
@ -95,29 +56,13 @@ export default dark.extend({
|
|||||||
const map = L.map('map', {
|
const map = L.map('map', {
|
||||||
attributionControl: false,
|
attributionControl: false,
|
||||||
worldCopyJump: true,
|
worldCopyJump: true,
|
||||||
center: [50, 12.8],
|
center: [47, 10],
|
||||||
zoom: 6,
|
zoom: 4,
|
||||||
minZoom: 2,
|
minZoom: 2,
|
||||||
maxZoom: 6,
|
maxZoom: 7,
|
||||||
maxBounds: [[-75, Number.NEGATIVE_INFINITY], [85, Number.POSITIVE_INFINITY]],
|
maxBounds: [[-75, Number.NEGATIVE_INFINITY], [85, Number.POSITIVE_INFINITY]],
|
||||||
});
|
});
|
||||||
|
|
||||||
map.addEventListener('click', (event) => {
|
|
||||||
this.polygons[this.polygons.length - 1].push(toPoint(event.latlng));
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('keydown', (event) => {
|
|
||||||
switch (event.key) {
|
|
||||||
case 'Enter':
|
|
||||||
this.polygons.push([]);
|
|
||||||
return false;
|
|
||||||
case 'Backspace':
|
|
||||||
this.polygons[this.polygons.length - 1].pop();
|
|
||||||
event.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
L.control.attribution({ position: 'bottomright' })
|
L.control.attribution({ position: 'bottomright' })
|
||||||
.addAttribution('© <a href="https://doi.org/10.5281/zenodo.7385533" target="_blank" title="Dryer, Matthew S. & Haspelmath, Martin (eds.) 2013. The World Atlas of Language Structures Online. Leipzig: Max Planck Institute for Evolutionary Anthropology.">WALS</a>')
|
.addAttribution('© <a href="https://doi.org/10.5281/zenodo.7385533" target="_blank" title="Dryer, Matthew S. & Haspelmath, Martin (eds.) 2013. The World Atlas of Language Structures Online. Leipzig: Max Planck Institute for Evolutionary Anthropology.">WALS</a>')
|
||||||
.addAttribution('<a href="https://wals.info" target="_blank">wals.info</a>')
|
.addAttribution('<a href="https://wals.info" target="_blank">wals.info</a>')
|
||||||
@ -141,18 +86,20 @@ export default dark.extend({
|
|||||||
},
|
},
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
|
||||||
this.inputCurve = L.curve([], {
|
for (const [locale, polygons] of Object.entries(polygonsByLocale)) {
|
||||||
color: '#ffffff',
|
const polygonActors = polygons.map((polygon) => {
|
||||||
opacity: 0.5,
|
return L.polygon(polygon, {
|
||||||
}).addTo(map);
|
|
||||||
|
|
||||||
this.curve = L.curve([], {
|
|
||||||
color: '#971064',
|
color: '#971064',
|
||||||
|
weight: 1,
|
||||||
fill: true,
|
fill: true,
|
||||||
|
// fillColor: `hsl(${index * 360 / Object.values(polygonsByLocale).length}deg, 100%, 50%)`,
|
||||||
fillColor: '#c71585',
|
fillColor: '#c71585',
|
||||||
fillOpacity: 0.5,
|
fillOpacity: 0.2,
|
||||||
fillRule: 'nonzero',
|
fillRule: 'evenodd',
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
|
});
|
||||||
|
this.polygons[locale] = polygonActors;
|
||||||
|
}
|
||||||
|
|
||||||
/* for (let i = 0; i < this.polygons.length; i++) {
|
/* for (let i = 0; i < this.polygons.length; i++) {
|
||||||
for (let j = 0; j < this.polygons[i].length; j++) {
|
for (let j = 0; j < this.polygons[i].length; j++) {
|
||||||
@ -167,7 +114,7 @@ export default dark.extend({
|
|||||||
}
|
}
|
||||||
}*/
|
}*/
|
||||||
|
|
||||||
/*for (const walsLanguage of walsLanguages) {
|
for (const walsLanguage of walsLanguages) {
|
||||||
if (!localesByWalsCode.hasOwnProperty(walsLanguage.id)) {
|
if (!localesByWalsCode.hasOwnProperty(walsLanguage.id)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -181,12 +128,18 @@ export default dark.extend({
|
|||||||
radius: 300000 * Math.cos(walsLanguage.latitude * Math.PI / 180), // compensate for Mercator projection
|
radius: 300000 * Math.cos(walsLanguage.latitude * Math.PI / 180), // compensate for Mercator projection
|
||||||
}).addTo(map);
|
}).addTo(map);
|
||||||
circle.bindTooltip(`<strong>${locale.name}</strong><br/>${clearUrl(locale.url)}`, { opacity: 1 });
|
circle.bindTooltip(`<strong>${locale.name}</strong><br/>${clearUrl(locale.url)}`, { opacity: 1 });
|
||||||
|
circle.on('mouseover', () => {
|
||||||
|
this.polygons[locale.code].map((polygon) => polygon.setStyle({ fillOpacity: 1 }));
|
||||||
|
});
|
||||||
|
circle.on('mouseout', () => {
|
||||||
|
this.polygons[locale.code].map((polygon) => polygon.setStyle({ fillOpacity: 0.2 }));
|
||||||
|
});
|
||||||
circle.on('click', () => {
|
circle.on('click', () => {
|
||||||
window.open(locale.url);
|
window.open(locale.url);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}*/
|
}
|
||||||
this.update();
|
// this.update();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
@ -65,11 +65,13 @@
|
|||||||
"pageres": "^6.3.1",
|
"pageres": "^6.3.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"plausible-api": "https://github.com/avo7/plausible-api.git#main",
|
"plausible-api": "https://github.com/avo7/plausible-api.git#main",
|
||||||
|
"polygon-clipping": "^0.15.7",
|
||||||
"qr-code-styling": "^1.6.0-rc.1",
|
"qr-code-styling": "^1.6.0-rc.1",
|
||||||
"query-string": "^7.1.1",
|
"query-string": "^7.1.1",
|
||||||
"querystringify": "^2.2.0",
|
"querystringify": "^2.2.0",
|
||||||
"rtlcss": "^3.1.2",
|
"rtlcss": "^3.1.2",
|
||||||
"sha1": "^1.1.1",
|
"sha1": "^1.1.1",
|
||||||
|
"simplify-js": "^1.2.4",
|
||||||
"speakeasy": "^2.0.0",
|
"speakeasy": "^2.0.0",
|
||||||
"sql-template-strings": "^2.2.2",
|
"sql-template-strings": "^2.2.2",
|
||||||
"sqlite": "^4.0.12",
|
"sqlite": "^4.0.12",
|
||||||
|
25
pnpm-lock.yaml
generated
25
pnpm-lock.yaml
generated
@ -158,6 +158,9 @@ dependencies:
|
|||||||
plausible-api:
|
plausible-api:
|
||||||
specifier: https://github.com/avo7/plausible-api.git#main
|
specifier: https://github.com/avo7/plausible-api.git#main
|
||||||
version: github.com/avo7/plausible-api/0bb79ad1d26754a71b3ec1351255dbf5a32e6e2a
|
version: github.com/avo7/plausible-api/0bb79ad1d26754a71b3ec1351255dbf5a32e6e2a
|
||||||
|
polygon-clipping:
|
||||||
|
specifier: ^0.15.7
|
||||||
|
version: 0.15.7
|
||||||
qr-code-styling:
|
qr-code-styling:
|
||||||
specifier: ^1.6.0-rc.1
|
specifier: ^1.6.0-rc.1
|
||||||
version: 1.6.0-rc.1
|
version: 1.6.0-rc.1
|
||||||
@ -173,6 +176,9 @@ dependencies:
|
|||||||
sha1:
|
sha1:
|
||||||
specifier: ^1.1.1
|
specifier: ^1.1.1
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
|
simplify-js:
|
||||||
|
specifier: ^1.2.4
|
||||||
|
version: 1.2.4
|
||||||
speakeasy:
|
speakeasy:
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
@ -13146,6 +13152,13 @@ packages:
|
|||||||
- typescript
|
- typescript
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/polygon-clipping@0.15.7:
|
||||||
|
resolution: {integrity: sha512-nhfdr83ECBg6xtqOAJab1tbksbBAOMUltN60bU+llHVOL0e5Onm1WpAXXWXVB39L8AJFssoIhEVuy/S90MmotA==}
|
||||||
|
dependencies:
|
||||||
|
robust-predicates: 3.0.2
|
||||||
|
splaytree: 3.1.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/posix-character-classes@0.1.1:
|
/posix-character-classes@0.1.1:
|
||||||
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
|
resolution: {integrity: sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@ -14802,6 +14815,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==}
|
resolution: {integrity: sha512-fJhQQI5tLrQvYIYFpOnFinzv9dwmR7hRnUz1XqP3OJ1jIweTNOd6aTO4jwQSgcBSFUB+/KHJxuGneime+FdzOw==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/robust-predicates@3.0.2:
|
||||||
|
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/rrweb-cssom@0.6.0:
|
/rrweb-cssom@0.6.0:
|
||||||
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
|
resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
|
||||||
dev: false
|
dev: false
|
||||||
@ -15178,6 +15195,10 @@ packages:
|
|||||||
is-arrayish: 0.3.2
|
is-arrayish: 0.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/simplify-js@1.2.4:
|
||||||
|
resolution: {integrity: sha512-vITfSlwt7h/oyrU42R83mtzFpwYk3+mkH9bOHqq/Qw6n8rtR7aE3NZQ5fbcyCUVVmuMJR6ynsAhOfK2qoah8Jg==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/sirv@2.0.3:
|
/sirv@2.0.3:
|
||||||
resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==}
|
resolution: {integrity: sha512-O9jm9BsID1P+0HOi81VpXPoDxYP374pkOLzACAoyUQ/3OUVndNpsz6wMnY2z+yOxzbllCKZrM+9QrWsv4THnyA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
@ -15352,6 +15373,10 @@ packages:
|
|||||||
base32.js: 0.0.1
|
base32.js: 0.0.1
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/splaytree@3.1.2:
|
||||||
|
resolution: {integrity: sha512-4OM2BJgC5UzrhVnnJA4BkHKGtjXNzzUfpQjCO8I05xYPsfS/VuQDwjCGGMi8rYQilHEV4j8NBqTFbls/PZEE7A==}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/split-on-first@1.1.0:
|
/split-on-first@1.1.0:
|
||||||
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
@ -1,19 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<Page>
|
<Page>
|
||||||
<div class="list-group d-flex flex-wrap flex-row my-4">
|
|
||||||
<a
|
|
||||||
v-for="(options, locale) in $locales"
|
|
||||||
:key="locale"
|
|
||||||
:href="options.url"
|
|
||||||
class="list-group-item list-group-item-action list-group-item-hoverable w-md-50"
|
|
||||||
>
|
|
||||||
<div class="h3">
|
|
||||||
<LocaleIcon :locale="options" class="mx-2" />
|
|
||||||
{{ options.name }}
|
|
||||||
<small v-if="options.extra" class="text-muted">({{ options.extra }})</small>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<template #below>
|
<template #below>
|
||||||
<LanguageMap />
|
<LanguageMap />
|
||||||
<p class="small text-muted my-3">
|
<p class="small text-muted my-3">
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import polygonClipping from 'polygon-clipping';
|
||||||
|
import type { MultiPolygon, Polygon, Ring } from 'polygon-clipping';
|
||||||
|
import simplify from 'simplify-js';
|
||||||
|
|
||||||
const __dirname = new URL('.', import.meta.url).pathname;
|
const __dirname = new URL('.', import.meta.url).pathname;
|
||||||
|
|
||||||
const getFilePath = (code: string, level: number): string => {
|
const getFilePath = (code: string, level: number): string => {
|
||||||
@ -37,29 +41,37 @@ interface GeoJSONSubset {
|
|||||||
}[]
|
}[]
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCountryCode = (code: string): [string, 0 | 1 | 2] => {
|
const getLevel = (region: string): 0 | 1 | 2 => {
|
||||||
if (code.length === 3) {
|
if (region === 'ALL') {
|
||||||
return [code, 0];
|
return 0;
|
||||||
} else {
|
} else if (region.length === 3 || region.length === 5) {
|
||||||
const level = code.length === 5 ? 1 : 2;
|
return 1;
|
||||||
let countryCode;
|
|
||||||
switch (code.slice(0, 2)) {
|
|
||||||
case 'BE': countryCode = 'BEL';
|
|
||||||
break;
|
|
||||||
case 'CH': countryCode = 'CHE';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error(`Unknown ${countryCode}`);
|
|
||||||
}
|
|
||||||
return [countryCode, level];
|
|
||||||
}
|
}
|
||||||
|
return 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CoordinatesFetcher {
|
class CoordinatesFetcher {
|
||||||
adm0: Record<string, string> = {};
|
adm0: Record<string, string> = {};
|
||||||
adm1: Record<string, string> = {};
|
adm1: Record<string, string> = {};
|
||||||
adm2: Record<string, string> = {};
|
adm2: Record<string, string> = {};
|
||||||
coordinatesByCode: Record<string, [number, number][][]> = {};
|
coordinatesByCode: Record<string, Record<string, MultiPolygon>> = {};
|
||||||
|
|
||||||
|
async prime(codes: Record<string, Record<string, string[]>>) {
|
||||||
|
const neededLevelsByCountryCode: Record<string, Set<0 | 1 | 2>> = {};
|
||||||
|
for (const a of Object.values(codes)) {
|
||||||
|
for (const [countryCode, regionCodes] of Object.entries(a)) {
|
||||||
|
if (!Object.hasOwn(neededLevelsByCountryCode, countryCode)) {
|
||||||
|
neededLevelsByCountryCode[countryCode] = new Set();
|
||||||
|
}
|
||||||
|
for (const regionCode of regionCodes) {
|
||||||
|
neededLevelsByCountryCode[countryCode].add(getLevel(regionCode));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(Object.entries(neededLevelsByCountryCode).flatMap(([countryCode, levels]) => {
|
||||||
|
return [...levels].map((level) => this.fetchCoordinates(countryCode, level));
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
async getGeoJSONLink(code: string, level: number): Promise<string> {
|
async getGeoJSONLink(code: string, level: number): Promise<string> {
|
||||||
if (level === 0) {
|
if (level === 0) {
|
||||||
@ -84,36 +96,40 @@ class CoordinatesFetcher {
|
|||||||
const path = getFilePath(countryCode, level);
|
const path = getFilePath(countryCode, level);
|
||||||
if (!fs.existsSync(path)) {
|
if (!fs.existsSync(path)) {
|
||||||
const response = await fetch(await this.getGeoJSONLink(countryCode, level));
|
const response = await fetch(await this.getGeoJSONLink(countryCode, level));
|
||||||
fs.writeFileSync(path, await response.text());
|
await fs.promises.writeFile(path, await response.text());
|
||||||
}
|
}
|
||||||
const data = JSON.parse(fs.readFileSync(path, 'utf-8')) as GeoJSONSubset;
|
const data = JSON.parse(await fs.promises.readFile(path, 'utf-8')) as GeoJSONSubset;
|
||||||
const entries = data.features.map((feature) => {
|
const entries = data.features.map((feature) => {
|
||||||
|
let coordinates;
|
||||||
switch (feature.geometry.type) {
|
switch (feature.geometry.type) {
|
||||||
case 'Polygon':
|
case 'Polygon':
|
||||||
if (feature.geometry.coordinates.length !== 1) {
|
coordinates = [feature.geometry.coordinates];
|
||||||
process.stderr.write(`Assertion failed on ${feature.properties.shapeISO} (Polygon)\n`);
|
break;
|
||||||
}
|
|
||||||
return [feature.properties.shapeISO, feature.geometry.coordinates] as const;
|
|
||||||
case 'MultiPolygon':
|
case 'MultiPolygon':
|
||||||
if (feature.geometry.coordinates[0].length !== 1) {
|
coordinates = feature.geometry.coordinates;
|
||||||
process.stderr.write(`Assertion failed on ${feature.properties.shapeISO} (MultiPolygon)\n`);
|
break;
|
||||||
}
|
|
||||||
return [feature.properties.shapeISO, feature.geometry.coordinates.flatMap((a) => a)] as const;
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown type ${countryCode}`);
|
throw new Error(`Unknown type ${countryCode}`);
|
||||||
}
|
}
|
||||||
|
return [feature.properties.shapeISO, coordinates] as const;
|
||||||
});
|
});
|
||||||
for (const [code, coordinates] of entries) {
|
if (!Object.hasOwn(this.coordinatesByCode, countryCode)) {
|
||||||
this.coordinatesByCode[code] = coordinates;
|
this.coordinatesByCode[countryCode] = {};
|
||||||
|
}
|
||||||
|
for (const [regionCode, coordinates] of entries) {
|
||||||
|
this.coordinatesByCode[countryCode][regionCode] = coordinates;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCoordinates(code: string): Promise<[number, number][][]> {
|
getCoordinates(countryCode: string, regionCode: string): MultiPolygon {
|
||||||
if (!Object.hasOwn(this.coordinatesByCode, code)) {
|
if (regionCode === 'ALL') {
|
||||||
const [countryCode, level] = getCountryCode(code);
|
regionCode = countryCode;
|
||||||
await this.fetchCoordinates(countryCode, level);
|
|
||||||
}
|
}
|
||||||
return this.coordinatesByCode[code];
|
const coordinates = this.coordinatesByCode[countryCode][regionCode];
|
||||||
|
if (coordinates === undefined) {
|
||||||
|
throw new Error(`Cannot find ${countryCode} ${regionCode}`);
|
||||||
|
}
|
||||||
|
return coordinates;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,33 +142,145 @@ const transformCoordinate = (coordinate: [number, number]): [number, number] =>
|
|||||||
return [round(coordinate[1]), round(coordinate[0])];
|
return [round(coordinate[1]), round(coordinate[0])];
|
||||||
};
|
};
|
||||||
|
|
||||||
const countCoordinates = (polygons: [number, number][][]): number => {
|
const countCoordinates = (polygons: MultiPolygon): number => {
|
||||||
return polygons.reduce((count, polygon) => count + polygon.length, 0);
|
return polygons.flatMap((polygon) => polygon).reduce((count, ring) => count + ring.length, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
const makePolygons = async (fetcher: CoordinatesFetcher, code: string): Promise<[number, number][][]> => {
|
const makePolygons = (fetcher: CoordinatesFetcher, countryCode: string, regionCode: string): Polygon => {
|
||||||
const original = await fetcher.getCoordinates(code);
|
const original = fetcher.getCoordinates(countryCode, regionCode);
|
||||||
const transformed = original.map((polygon) => {
|
const transformed = original.map((polygon) => {
|
||||||
return polygon.map(transformCoordinate).reduce((acc, cur) => {
|
return polygon.map((ring) => ring.map(transformCoordinate));
|
||||||
|
/* return polygon.map((ring) => {
|
||||||
|
const transformed = ring.map(transformCoordinate).reduce((acc, cur) => {
|
||||||
// process.stdout.write(`${JSON.stringify(acc[acc.length - 1])} ${JSON.stringify(cur)}\n`);
|
// process.stdout.write(`${JSON.stringify(acc[acc.length - 1])} ${JSON.stringify(cur)}\n`);
|
||||||
if (acc.length >= 2 && (acc[acc.length - 2][0] === cur[0] && acc[acc.length - 1][0] === cur[0] || acc[acc.length - 2][1] === cur[1] && acc[acc.length - 1][1] === cur[1])) {
|
if (acc.length >= 2 && (acc[acc.length - 2][0] === cur[0] && acc[acc.length - 1][0] === cur[0] || acc[acc.length - 2][1] === cur[1] && acc[acc.length - 1][1] === cur[1])) {
|
||||||
acc.pop();
|
acc.pop();
|
||||||
}
|
}
|
||||||
if (acc.length === 0 || acc[acc.length - 1][0] !== cur[0] || acc[acc.length - 1][1] !== cur[1]) {
|
if (acc.length === 0 || !pointEquals(acc[acc.length - 1], cur)) {
|
||||||
// process.stdout.write('push\n');
|
// process.stdout.write('push\n');
|
||||||
acc.push(cur);
|
acc.push(cur);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, [] as [number, number][]);
|
}, [] as [number, number][]);
|
||||||
|
if (transformed.length >= 2 && pointEquals(transformed[0], transformed[transformed.length - 1])) {
|
||||||
|
transformed.pop();
|
||||||
|
}
|
||||||
|
return transformed;
|
||||||
|
});*/
|
||||||
});
|
});
|
||||||
process.stdout.write(`${code} has ${countCoordinates(original)} original and ${countCoordinates(transformed)} transformed coordinates\n`);
|
// process.stdout.write(`${countryCode} ${regionCode} has ${countCoordinates(original)} original and ${countCoordinates(transformed)} transformed coordinates\n`);
|
||||||
return transformed;
|
return transformed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const writePolygons = async (fetcher: CoordinatesFetcher, locale: string, codes: string[]): Promise<void> => {
|
const pointEquals = (pointA: [number, number] | undefined, pointB: [number, number] | undefined): boolean => {
|
||||||
const result = await Promise.all(codes.map((code) => makePolygons(fetcher, code)));
|
return pointA !== undefined && pointB !== undefined && pointA[0] === pointB[0] && pointA[1] === pointB[1];
|
||||||
const polygons = result.flatMap((polygons) => polygons);
|
};
|
||||||
fs.writeFileSync(`${__dirname}/../assets/${locale}.json`, JSON.stringify(polygons));
|
|
||||||
|
const pointNear = (pointA: [number, number] | undefined, pointB: [number, number] | undefined): boolean => {
|
||||||
|
const distance = 0.1;
|
||||||
|
return pointA !== undefined && pointB !== undefined && Math.sqrt((pointA[0] - pointB[0]) ** 2 + (pointA[1] - pointB[1]) ** 2) <= distance;
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrappingIndex = <T>(array: T[], index: number): T => {
|
||||||
|
return array[(index % array.length + array.length) % array.length];
|
||||||
|
};
|
||||||
|
|
||||||
|
const tryMergePolygons = (polygonA: [number, number][], polygonB: [number, number][]): [number, number][] | undefined => {
|
||||||
|
for (let i = 0; i < polygonA.length; i++) {
|
||||||
|
for (let j = 0; j < polygonB.length; j++) {
|
||||||
|
if (pointNear(polygonA[i], polygonB[j])) {
|
||||||
|
if (pointNear(polygonA[i + 1], polygonB[j + 1])) {
|
||||||
|
console.log(`found a match forwards ${i} ${j}`);
|
||||||
|
}
|
||||||
|
if (pointNear(polygonA[i + 1], polygonB[j - 1])) {
|
||||||
|
console.log(`found a match backwards ${i} (of ${polygonA.length}) ${j} (of ${polygonB.length})`);
|
||||||
|
let k = 2;
|
||||||
|
while (pointNear(polygonA[i + k], wrappingIndex(polygonB, j - k)) && k < polygonA.length) {
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
console.log(`length ${k}`);
|
||||||
|
/* console.log(`neighbors A: ${JSON.stringify(polygonA.slice(i - 2, i + 1))} ${JSON.stringify(polygonB.slice(j, j + 3))}`);
|
||||||
|
console.log(`neighbors B: ${JSON.stringify(polygonB.slice(j - k + 2, j - k + 1))} ${JSON.stringify(polygonA.slice(i + k - 1, i + k + 2))}`);
|
||||||
|
|
||||||
|
console.log(polygonA.slice(0, i));
|
||||||
|
console.log(polygonB.slice(j, polygonB.length));
|
||||||
|
console.log(polygonB.slice(j - k + 5, j - k));
|
||||||
|
console.log(polygonA.slice(i + k - 1, i + k + 4));*/
|
||||||
|
return [
|
||||||
|
...polygonA.slice(0, i),
|
||||||
|
...polygonB.slice(j, polygonB.length),
|
||||||
|
...polygonB.slice(0, j - k),
|
||||||
|
...polygonA.slice(i + k - 1, polygonA.length),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* const mergePolygons = (polygons: MultiPolygon): MultiPolygon=> {
|
||||||
|
return union(polygons);
|
||||||
|
for (let i = 0; i < polygons.length; i++) {
|
||||||
|
for (let j = i + 1; j < polygons.length; j++) {
|
||||||
|
// console.log(`trying to merge ${i} ${j}`);
|
||||||
|
const merged = tryMergePolygons(polygons[i], polygons[j]);
|
||||||
|
if (merged) {
|
||||||
|
console.log(`merged ${i} and ${j}`);
|
||||||
|
polygons[i] = merged;
|
||||||
|
polygons.splice(j, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return polygons;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tidyPolygon = (polygon: [number, number][]) => {
|
||||||
|
for (let i = 0; i < polygon.length; i++) {
|
||||||
|
for (let j = i + 1; j < polygon.length; j++) {
|
||||||
|
if (pointEquals(polygon[i], polygon[j])) {
|
||||||
|
if (pointEquals(polygon[i + 1], polygon[j + 1])) {
|
||||||
|
/* console.log(`found a tidy match forwards ${i} ${j}`);
|
||||||
|
let k = 2;
|
||||||
|
while (pointEquals(polygon[i + k], polygon[(j + k) % polygon.length]) && k < polygon.length) {
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
polygon.splice(j, k);
|
||||||
|
continue;*
|
||||||
|
}
|
||||||
|
if (pointEquals(polygon[i + 1], polygon[j - 1])) {
|
||||||
|
/* console.log(`found a tidy match backwards ${i} ${j}`);
|
||||||
|
let k = 2;
|
||||||
|
while (pointEquals(polygon[i + k], wrappingIndex(polygon, j + k)) && k < polygon.length) {
|
||||||
|
k++;
|
||||||
|
}
|
||||||
|
console.log(i, j, k, j - i);
|
||||||
|
console.log(polygon.slice(i - 3, i + 4));
|
||||||
|
console.log(polygon.slice(j - 3, j + 4));
|
||||||
|
// polygon.splice(j, k);
|
||||||
|
continue;*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return polygon;
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
|
const simplifyPolygon = (polygon: Polygon): Ring => {
|
||||||
|
const ring = polygon[0].map(([x, y]) => ({ x, y }));
|
||||||
|
const simplified = simplify(ring, 0.05);
|
||||||
|
return simplified.map(({ x, y }) => [x, y]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const writePolygons = async (fetcher: CoordinatesFetcher, locales: Record<string, Record<string, string[]>>): Promise<void> => {
|
||||||
|
const entries = Object.entries(locales).map(([locale, codes]) => {
|
||||||
|
let polygons = Object.entries(codes)
|
||||||
|
.flatMap(([code, regions]) => regions.flatMap((region) => makePolygons(fetcher, code, region)));
|
||||||
|
polygons = polygonClipping.union(polygons).map(simplifyPolygon);
|
||||||
|
// polygons = polygonClipping.union(polygons).map(simplifyPolygon);
|
||||||
|
return [locale, polygons];
|
||||||
|
});
|
||||||
|
fs.writeFileSync(`${__dirname}/../assets/map.json`, JSON.stringify(Object.fromEntries(entries)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const main = async (): Promise<void> => {
|
const main = async (): Promise<void> => {
|
||||||
@ -160,8 +288,40 @@ const main = async (): Promise<void> => {
|
|||||||
fs.mkdirSync(`${__dirname}/../assets/geo/`);
|
fs.mkdirSync(`${__dirname}/../assets/geo/`);
|
||||||
}
|
}
|
||||||
const fetcher = new CoordinatesFetcher();
|
const fetcher = new CoordinatesFetcher();
|
||||||
await fetcher.fetchCoordinates('BEL', 3);
|
const codes = {
|
||||||
await writePolygons(fetcher, 'de', ['POL', 'AUT', 'BE-WLG', 'DEU', 'LIE', 'LUX', 'CH-AG', 'CH-AR', 'CH-AI', 'CH-BL', 'CH-BS', 'CH-BE', 'CH-FR', 'CH-GL', 'CH-GR', 'CH-LU', 'CH-NW', 'CH-OW', 'CH-SG', 'CH-SH', 'CH-SO', 'CH-SZ', 'CH-TG', 'CH-UR', 'CH-VS', 'CH-ZG', 'CH-ZH']);
|
de: {
|
||||||
|
AUT: ['ALL'],
|
||||||
|
BEL: ['BE-WLG'],
|
||||||
|
DEU: ['ALL'],
|
||||||
|
LIE: ['ALL'],
|
||||||
|
LUX: ['ALL'],
|
||||||
|
CHE: ['CH-AG', 'CH-AR', 'CH-AI', 'CH-BL', 'CH-BS', 'CH-BE', 'CH-FR', 'CH-GL', 'CH-GR', 'CH-LU', 'CH-NW', 'CH-OW', 'CH-SG', 'CH-SH', 'CH-SO', 'CH-SZ', 'CH-TG', 'CH-UR', 'CH-VS', 'CH-ZG', 'CH-ZH'],
|
||||||
|
},
|
||||||
|
en: {
|
||||||
|
AUS: ['ALL'],
|
||||||
|
GBR: ['ALL'],
|
||||||
|
IRL: ['ALL'],
|
||||||
|
USA: ['ALL'],
|
||||||
|
},
|
||||||
|
es: {
|
||||||
|
ESP: ['ALL'],
|
||||||
|
},
|
||||||
|
fr: {
|
||||||
|
BEL: ['BRU', 'WAL'],
|
||||||
|
CHE: ['CH-BE', 'CH-FR', 'CH-GE', 'CH-JU', 'CH-NE', 'CH-VS', 'CH-VD'],
|
||||||
|
FRA: ['ALL'],
|
||||||
|
LUX: ['ALL'],
|
||||||
|
},
|
||||||
|
nl: {
|
||||||
|
BEL: ['BRU', 'VLG'],
|
||||||
|
NLD: ['ALL'],
|
||||||
|
},
|
||||||
|
pl: {
|
||||||
|
POL: ['ALL'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
await fetcher.prime(codes);
|
||||||
|
await writePolygons(fetcher, codes);
|
||||||
};
|
};
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user