@versatiles/svelte 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +21 -4
- package/dist/components/BBoxMap/AutoComplete.svelte +176 -0
- package/dist/components/{AutoComplete.svelte.d.ts → BBoxMap/AutoComplete.svelte.d.ts} +12 -18
- package/dist/components/BBoxMap/BBoxMap.d.ts +1 -12
- package/dist/components/BBoxMap/BBoxMap.js +1 -200
- package/dist/components/BBoxMap/BBoxMap.svelte +43 -127
- package/dist/components/BBoxMap/BBoxMap.svelte.d.ts +4 -20
- package/dist/components/BasicMap/BasicMap.svelte +34 -37
- package/dist/components/BasicMap/BasicMap.svelte.d.ts +9 -24
- package/dist/components/LocatorMap/LocatorMap.svelte +75 -0
- package/dist/components/LocatorMap/LocatorMap.svelte.d.ts +19 -0
- package/dist/components/MapEditor/Editor.svelte +25 -0
- package/dist/components/MapEditor/Editor.svelte.d.ts +7 -0
- package/dist/components/MapEditor/EditorLine.svelte +27 -0
- package/dist/components/MapEditor/EditorLine.svelte.d.ts +7 -0
- package/dist/components/MapEditor/EditorMarker.svelte +42 -0
- package/dist/components/MapEditor/EditorMarker.svelte.d.ts +7 -0
- package/dist/components/MapEditor/MapEditor.svelte +99 -0
- package/dist/components/MapEditor/MapEditor.svelte.d.ts +4 -0
- package/dist/components/MapEditor/editor.scss +16 -0
- package/dist/components/MapEditor/lib/element_abstract.d.ts +20 -0
- package/dist/components/MapEditor/lib/element_abstract.js +58 -0
- package/dist/components/MapEditor/lib/element_line.d.ts +14 -0
- package/dist/components/MapEditor/lib/element_line.js +76 -0
- package/dist/components/MapEditor/lib/element_marker.d.ts +20 -0
- package/dist/components/MapEditor/lib/element_marker.js +191 -0
- package/dist/components/MapEditor/lib/geocoder.d.ts +1 -0
- package/dist/components/MapEditor/lib/geocoder.js +8 -0
- package/dist/components/MapEditor/lib/geometry_manager.d.ts +18 -0
- package/dist/components/MapEditor/lib/geometry_manager.js +117 -0
- package/dist/components/MapEditor/lib/map_layer.d.ts +14 -0
- package/dist/components/MapEditor/lib/map_layer.js +61 -0
- package/dist/components/MapEditor/lib/types.d.ts +112 -0
- package/dist/components/MapEditor/lib/types.js +1 -0
- package/dist/components/MapEditor/lib/utils.d.ts +2 -0
- package/dist/components/MapEditor/lib/utils.js +11 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/utils/draw/bbox.d.ts +28 -0
- package/dist/utils/draw/bbox.js +193 -0
- package/dist/utils/location.d.ts +2 -1
- package/dist/utils/location.js +19 -10
- package/dist/utils/map_style.d.ts +6 -0
- package/dist/utils/map_style.js +28 -0
- package/dist/utils/sprite_library.d.ts +19 -0
- package/dist/utils/sprite_library.js +30 -0
- package/package.json +19 -14
- package/dist/components/AutoComplete.svelte +0 -197
- package/dist/components/BBoxMap/README.md +0 -70
- package/dist/components/BBoxMap/data/countries.jsonl +0 -258
- package/dist/components/BBoxMap/data/eu.jsonl +0 -1876
- package/dist/components/BBoxMap/data/us.jsonl +0 -52
- package/dist/components/BBoxMap/data/world.jsonl +0 -7
- package/dist/components/BBoxMap/helpers/geojson2bboxes.d.ts +0 -2
- package/dist/components/BBoxMap/helpers/geojson2bboxes.js +0 -183
- package/dist/components/BBoxMap/helpers/merge_bboxes.d.ts +0 -2
- package/dist/components/BBoxMap/helpers/merge_bboxes.js +0 -84
- package/dist/components/BBoxMap/helpers/population.raw.br +0 -0
- package/dist/utils/style.d.ts +0 -3
- package/dist/utils/style.js +0 -21
@@ -0,0 +1,193 @@
|
|
1
|
+
const maplibregl = await import('maplibre-gl');
|
2
|
+
const { LngLatBounds } = maplibregl;
|
3
|
+
// prettier-ignore
|
4
|
+
export const DragPointMap = new Map([
|
5
|
+
['n', { cursor: 'ns-resize', flipH: 'n', flipV: 's' }],
|
6
|
+
['ne', { cursor: 'nesw-resize', flipH: 'n', flipV: 'se' }],
|
7
|
+
['e', { cursor: 'ew-resize', flipH: 'w', flipV: 'e' }],
|
8
|
+
['se', { cursor: 'nwse-resize', flipH: 'sw', flipV: 'ne' }],
|
9
|
+
['s', { cursor: 'ns-resize', flipH: 's', flipV: 'n' }],
|
10
|
+
['sw', { cursor: 'nesw-resize', flipH: 'se', flipV: 'nw' }],
|
11
|
+
['w', { cursor: 'ew-resize', flipH: 'e', flipV: 'w' }],
|
12
|
+
['nw', { cursor: 'nwse-resize', flipH: 'ne', flipV: 'sw' }],
|
13
|
+
[false, { cursor: 'default', flipH: false, flipV: false }],
|
14
|
+
]);
|
15
|
+
const worldBBox = [-180, -85, 180, 85];
|
16
|
+
export class BBoxDrawer {
|
17
|
+
source;
|
18
|
+
dragPoint = false;
|
19
|
+
isDragging = false;
|
20
|
+
map;
|
21
|
+
canvas;
|
22
|
+
inverted;
|
23
|
+
bbox;
|
24
|
+
constructor(map, bbox, color, inverted) {
|
25
|
+
this.bbox = bbox;
|
26
|
+
this.inverted = inverted ?? true;
|
27
|
+
this.map = map;
|
28
|
+
const sourceId = 'bbox_' + Math.random().toString(36).slice(2);
|
29
|
+
if (this.source)
|
30
|
+
throw new Error('BBoxDrawer already added to map');
|
31
|
+
map.addSource(sourceId, { type: 'geojson', data: this.getAsFeatureCollection() });
|
32
|
+
map.addLayer({
|
33
|
+
id: 'bbox-line_' + Math.random().toString(36).slice(2),
|
34
|
+
type: 'line',
|
35
|
+
source: sourceId,
|
36
|
+
filter: ['==', '$type', 'LineString'],
|
37
|
+
layout: { 'line-cap': 'round', 'line-join': 'round' },
|
38
|
+
paint: { 'line-color': color }
|
39
|
+
});
|
40
|
+
map.addLayer({
|
41
|
+
id: 'bbox-fill_' + Math.random().toString(36).slice(2),
|
42
|
+
type: 'fill',
|
43
|
+
source: sourceId,
|
44
|
+
filter: ['==', '$type', 'Polygon'],
|
45
|
+
layout: {},
|
46
|
+
paint: { 'fill-color': color, 'fill-opacity': 0.2 }
|
47
|
+
});
|
48
|
+
this.source = map.getSource(sourceId);
|
49
|
+
this.canvas = map.getCanvasContainer();
|
50
|
+
map.on('mousemove', (e) => {
|
51
|
+
if (e.originalEvent.buttons % 2 === 0)
|
52
|
+
return this.checkDragPointAt(e.point);
|
53
|
+
if (!this.isDragging)
|
54
|
+
return;
|
55
|
+
if (!this.dragPoint)
|
56
|
+
return this.checkDragPointAt(e.point);
|
57
|
+
this.doDrag(e.lngLat);
|
58
|
+
this.redraw();
|
59
|
+
e.preventDefault();
|
60
|
+
});
|
61
|
+
map.on('mousedown', (e) => {
|
62
|
+
if (this.isDragging)
|
63
|
+
return;
|
64
|
+
if (e.originalEvent.buttons % 2) {
|
65
|
+
this.checkDragPointAt(e.point);
|
66
|
+
if (this.dragPoint)
|
67
|
+
this.isDragging = true;
|
68
|
+
if (this.isDragging)
|
69
|
+
e.preventDefault();
|
70
|
+
}
|
71
|
+
});
|
72
|
+
map.on('mouseup', () => {
|
73
|
+
this.isDragging = false;
|
74
|
+
this.updateDragPoint(false);
|
75
|
+
});
|
76
|
+
}
|
77
|
+
updateDragPoint(dragPoint) {
|
78
|
+
if (this.dragPoint === dragPoint)
|
79
|
+
return;
|
80
|
+
this.dragPoint = dragPoint;
|
81
|
+
this.canvas.style.cursor = this.getCursor(dragPoint);
|
82
|
+
}
|
83
|
+
getAsFeatureCollection() {
|
84
|
+
const ring = getRing(this.bbox);
|
85
|
+
return {
|
86
|
+
type: 'FeatureCollection',
|
87
|
+
features: [
|
88
|
+
this.inverted ? polygon(getRing(worldBBox), ring) : polygon(ring),
|
89
|
+
linestring(ring)
|
90
|
+
]
|
91
|
+
};
|
92
|
+
function getRing(bbox) {
|
93
|
+
const x0 = Math.min(bbox[0], bbox[2]);
|
94
|
+
const x1 = Math.max(bbox[0], bbox[2]);
|
95
|
+
const y0 = Math.min(bbox[1], bbox[3]);
|
96
|
+
const y1 = Math.max(bbox[1], bbox[3]);
|
97
|
+
// prettier-ignore
|
98
|
+
return [[x0, y0], [x1, y0], [x1, y1], [x0, y1], [x0, y0]];
|
99
|
+
}
|
100
|
+
function polygon(...coordinates) {
|
101
|
+
return { type: 'Feature', geometry: { type: 'Polygon', coordinates }, properties: {} };
|
102
|
+
}
|
103
|
+
function linestring(coordinates) {
|
104
|
+
return { type: 'Feature', geometry: { type: 'LineString', coordinates }, properties: {} };
|
105
|
+
}
|
106
|
+
}
|
107
|
+
setGeometry(bbox) {
|
108
|
+
this.bbox = bbox.slice(0, 4);
|
109
|
+
this.redraw();
|
110
|
+
}
|
111
|
+
getBounds() {
|
112
|
+
return new LngLatBounds(this.bbox);
|
113
|
+
}
|
114
|
+
redraw() {
|
115
|
+
this.source?.setData(this.getAsFeatureCollection());
|
116
|
+
}
|
117
|
+
getAsPixel() {
|
118
|
+
const p0 = this.map.project([this.bbox[0], this.bbox[1]]);
|
119
|
+
const p1 = this.map.project([this.bbox[2], this.bbox[3]]);
|
120
|
+
return [Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)];
|
121
|
+
}
|
122
|
+
checkDragPointAt(point) {
|
123
|
+
const maxDistance = 5;
|
124
|
+
const { x, y } = point;
|
125
|
+
const [x0, y0, x1, y1] = this.getAsPixel();
|
126
|
+
// Don't think outside the box
|
127
|
+
if (x < x0 - maxDistance)
|
128
|
+
return this.updateDragPoint(false);
|
129
|
+
if (x > x1 + maxDistance)
|
130
|
+
return this.updateDragPoint(false);
|
131
|
+
if (y < y0 - maxDistance)
|
132
|
+
return this.updateDragPoint(false);
|
133
|
+
if (y > y1 + maxDistance)
|
134
|
+
return this.updateDragPoint(false);
|
135
|
+
let dragX = (Math.abs(x0 - x) < maxDistance ? 1 : 0) + (Math.abs(x1 - x) < maxDistance ? 2 : 0);
|
136
|
+
let dragY = (Math.abs(y0 - y) < maxDistance ? 1 : 0) + (Math.abs(y1 - y) < maxDistance ? 2 : 0);
|
137
|
+
if (dragX === 3)
|
138
|
+
dragX = Math.abs(x0 - x) < Math.abs(x1 - x) ? 1 : 2;
|
139
|
+
if (dragY === 3)
|
140
|
+
dragY = Math.abs(y0 - y) < Math.abs(y1 - y) ? 1 : 2;
|
141
|
+
const directions = [false, 'w', 'e', 'n', 'nw', 'ne', 's', 'sw', 'se'];
|
142
|
+
this.updateDragPoint(directions[dragX + dragY * 3]);
|
143
|
+
}
|
144
|
+
getCursor(drag) {
|
145
|
+
return DragPointMap.get(drag)?.cursor ?? 'default';
|
146
|
+
}
|
147
|
+
doDrag(lngLat) {
|
148
|
+
const x = Math.round(lngLat.lng * 1e3) / 1e3;
|
149
|
+
const y = Math.round(lngLat.lat * 1e3) / 1e3;
|
150
|
+
// prettier-ignore
|
151
|
+
switch (this.dragPoint) {
|
152
|
+
case 'n':
|
153
|
+
this.bbox[3] = y;
|
154
|
+
break;
|
155
|
+
case 'ne':
|
156
|
+
this.bbox[2] = x;
|
157
|
+
this.bbox[3] = y;
|
158
|
+
break;
|
159
|
+
case 'e':
|
160
|
+
this.bbox[2] = x;
|
161
|
+
break;
|
162
|
+
case 'se':
|
163
|
+
this.bbox[2] = x;
|
164
|
+
this.bbox[1] = y;
|
165
|
+
break;
|
166
|
+
case 's':
|
167
|
+
this.bbox[1] = y;
|
168
|
+
break;
|
169
|
+
case 'sw':
|
170
|
+
this.bbox[0] = x;
|
171
|
+
this.bbox[1] = y;
|
172
|
+
break;
|
173
|
+
case 'w':
|
174
|
+
this.bbox[0] = x;
|
175
|
+
break;
|
176
|
+
case 'nw':
|
177
|
+
this.bbox[0] = x;
|
178
|
+
this.bbox[3] = y;
|
179
|
+
break;
|
180
|
+
default: return;
|
181
|
+
}
|
182
|
+
if (this.bbox[2] < this.bbox[0]) {
|
183
|
+
// flip horizontal
|
184
|
+
this.bbox = [this.bbox[2], this.bbox[1], this.bbox[0], this.bbox[3]];
|
185
|
+
this.updateDragPoint(DragPointMap.get(this.dragPoint)?.flipH ?? false);
|
186
|
+
}
|
187
|
+
if (this.bbox[3] < this.bbox[1]) {
|
188
|
+
// flip vertical
|
189
|
+
this.bbox = [this.bbox[0], this.bbox[3], this.bbox[2], this.bbox[1]];
|
190
|
+
this.updateDragPoint(DragPointMap.get(this.dragPoint)?.flipV ?? false);
|
191
|
+
}
|
192
|
+
}
|
193
|
+
}
|
package/dist/utils/location.d.ts
CHANGED
package/dist/utils/location.js
CHANGED
@@ -1,22 +1,32 @@
|
|
1
1
|
import { timezone2countrycode } from './zones.js';
|
2
|
-
export function
|
2
|
+
export function getCountryName() {
|
3
|
+
try {
|
4
|
+
const countryCode = getCountryCode();
|
5
|
+
if (!countryCode)
|
6
|
+
return null;
|
7
|
+
const region = new Intl.DisplayNames(['en-GB'], { type: 'region' });
|
8
|
+
const country = region.of(countryCode);
|
9
|
+
return country || null;
|
10
|
+
}
|
11
|
+
catch {
|
12
|
+
return null;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
export function getCountryCode() {
|
3
16
|
try {
|
4
17
|
const options = Intl.DateTimeFormat().resolvedOptions();
|
5
18
|
let countryCode = timezone2countrycode(options.timeZone);
|
6
19
|
if (!countryCode)
|
7
20
|
countryCode = navigator.language.split('-')[1];
|
8
|
-
|
9
|
-
const country = region.of(countryCode);
|
10
|
-
return country || '';
|
21
|
+
return countryCode || null;
|
11
22
|
}
|
12
|
-
catch
|
13
|
-
|
14
|
-
return ''; // Fallback if no country can be determined
|
23
|
+
catch {
|
24
|
+
return null;
|
15
25
|
}
|
16
26
|
}
|
17
27
|
export function getLanguage() {
|
18
28
|
try {
|
19
|
-
const language = Intl.DateTimeFormat().resolvedOptions().locale.split('-')[0];
|
29
|
+
const language = Intl.DateTimeFormat().resolvedOptions().locale.split('-')[0].toLowerCase();
|
20
30
|
switch (language) {
|
21
31
|
case 'en':
|
22
32
|
case 'de':
|
@@ -24,8 +34,7 @@ export function getLanguage() {
|
|
24
34
|
}
|
25
35
|
return null;
|
26
36
|
}
|
27
|
-
catch
|
28
|
-
console.error('Could not determine country from timezone:', error);
|
37
|
+
catch {
|
29
38
|
return null; // Fallback if no country can be determined
|
30
39
|
}
|
31
40
|
}
|
@@ -0,0 +1,6 @@
|
|
1
|
+
import { type StyleBuilderOptions } from '@versatiles/style';
|
2
|
+
export declare function getMapStyle(darkMode: boolean, styleOptions?: StyleBuilderOptions & {
|
3
|
+
transitionDuration?: number;
|
4
|
+
disableDarkMode?: boolean;
|
5
|
+
}): import("maplibre-gl").StyleSpecification;
|
6
|
+
export declare function isDarkMode(element?: HTMLElement): boolean;
|
@@ -0,0 +1,28 @@
|
|
1
|
+
import { styles } from '@versatiles/style';
|
2
|
+
import { getLanguage } from './location.js';
|
3
|
+
export function getMapStyle(darkMode, styleOptions = {}) {
|
4
|
+
darkMode = darkMode && !styleOptions.disableDarkMode;
|
5
|
+
const style = styles.colorful({
|
6
|
+
baseUrl: 'https://tiles.versatiles.org',
|
7
|
+
language: getLanguage(),
|
8
|
+
recolor: {
|
9
|
+
invertBrightness: darkMode,
|
10
|
+
gamma: darkMode ? 0.5 : 1
|
11
|
+
},
|
12
|
+
...styleOptions
|
13
|
+
});
|
14
|
+
if (styleOptions.transitionDuration != null) {
|
15
|
+
style.transition = { duration: styleOptions.transitionDuration, delay: 0 };
|
16
|
+
}
|
17
|
+
return style;
|
18
|
+
}
|
19
|
+
export function isDarkMode(element) {
|
20
|
+
if (element != null) {
|
21
|
+
const colorScheme = getComputedStyle(element).getPropertyValue('color-scheme');
|
22
|
+
if (colorScheme.includes('dark'))
|
23
|
+
return true;
|
24
|
+
if (colorScheme.includes('light'))
|
25
|
+
return false;
|
26
|
+
}
|
27
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches;
|
28
|
+
}
|
@@ -0,0 +1,19 @@
|
|
1
|
+
interface SpriteEntry {
|
2
|
+
width: number;
|
3
|
+
height: number;
|
4
|
+
x: number;
|
5
|
+
y: number;
|
6
|
+
pixelRatio: number;
|
7
|
+
sdf: boolean;
|
8
|
+
id: string;
|
9
|
+
name: string;
|
10
|
+
group: string;
|
11
|
+
}
|
12
|
+
export declare class SpriteLibrary {
|
13
|
+
private spriteList;
|
14
|
+
private pixelRatio;
|
15
|
+
constructor(pixelRatio?: number);
|
16
|
+
getSpriteList(): Promise<SpriteEntry[]>;
|
17
|
+
private loadSpriteList;
|
18
|
+
}
|
19
|
+
export default SpriteLibrary;
|
@@ -0,0 +1,30 @@
|
|
1
|
+
export class SpriteLibrary {
|
2
|
+
spriteList = [];
|
3
|
+
pixelRatio;
|
4
|
+
constructor(pixelRatio = 2) {
|
5
|
+
this.pixelRatio = pixelRatio;
|
6
|
+
}
|
7
|
+
async getSpriteList() {
|
8
|
+
await this.loadSpriteList();
|
9
|
+
return this.spriteList;
|
10
|
+
}
|
11
|
+
async loadSpriteList() {
|
12
|
+
if (this.spriteList.length)
|
13
|
+
return;
|
14
|
+
const spriteGroupList = (await fetch('https://tiles.versatiles.org/assets/sprites/index.json').then((r) => r.json()));
|
15
|
+
await Promise.all(spriteGroupList.map(async (group) => {
|
16
|
+
const spriteGroup = (await fetch(`https://tiles.versatiles.org/assets/sprites/${group}/sprites@${this.pixelRatio}x.json`).then((r) => r.json()));
|
17
|
+
Object.entries(spriteGroup).forEach(([name, entry]) => {
|
18
|
+
this.spriteList.push({
|
19
|
+
...entry,
|
20
|
+
pixelRatio: entry.pixelRatio ?? 1,
|
21
|
+
sdf: entry.sdf ?? false,
|
22
|
+
id: `${group}/${name}`,
|
23
|
+
group,
|
24
|
+
name
|
25
|
+
});
|
26
|
+
});
|
27
|
+
}));
|
28
|
+
}
|
29
|
+
}
|
30
|
+
export default SpriteLibrary;
|
package/package.json
CHANGED
@@ -1,22 +1,24 @@
|
|
1
1
|
{
|
2
2
|
"name": "@versatiles/svelte",
|
3
|
-
"version": "0.
|
3
|
+
"version": "1.0.0",
|
4
|
+
"license": "MIT",
|
4
5
|
"scripts": {
|
5
|
-
"build": "vite build && npm run package",
|
6
|
+
"build": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && vite build && npm run package",
|
6
7
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
7
|
-
"check": "
|
8
|
+
"check": "npm run lint && npm run build && npm run test",
|
8
9
|
"dev": "vite dev",
|
9
10
|
"format": "prettier --write .",
|
10
|
-
"lint": "prettier --check . && eslint .",
|
11
|
+
"lint": "prettier --check . && eslint --color .",
|
11
12
|
"package": "svelte-kit sync && svelte-package && publint",
|
12
13
|
"prepack": "npm run package",
|
13
14
|
"prepublishOnly": "npm run package",
|
14
15
|
"preview": "vite preview",
|
15
|
-
"screenshots": "npm run build && npx tsx ./scripts/screenshots.ts",
|
16
|
+
"screenshots": "npm run build && npx tsx ./scripts/screenshots.ts && pngquant --nofs --force --ext .png screenshots/*.png && optipng -quiet screenshots/*.png",
|
16
17
|
"release": "npm run build && npx vrt release-npm",
|
17
18
|
"test:integration": "playwright test",
|
18
19
|
"test:unit": "vitest run",
|
19
|
-
"test": "
|
20
|
+
"test:coverage": "vitest run --coverage",
|
21
|
+
"test": "npm run test:unit && npm run test:integration",
|
20
22
|
"upgrade": "npm-check-updates -u && rm -f package-lock.json; rm -rf node_modules; npm update --save && npm install"
|
21
23
|
},
|
22
24
|
"exports": {
|
@@ -37,31 +39,34 @@
|
|
37
39
|
},
|
38
40
|
"devDependencies": {
|
39
41
|
"@playwright/test": "^1.50.0",
|
40
|
-
"@sveltejs/adapter-
|
42
|
+
"@sveltejs/adapter-static": "^3.0.8",
|
41
43
|
"@sveltejs/kit": "^2.16.1",
|
42
44
|
"@sveltejs/package": "^2.3.9",
|
43
45
|
"@sveltejs/vite-plugin-svelte": "^5.0.3",
|
44
46
|
"@turf/turf": "^7.2.0",
|
45
47
|
"@types/eslint": "^9.6.1",
|
46
|
-
"@types/node": "^22.
|
47
|
-
"@types/split2": "^4.2.3",
|
48
|
+
"@types/node": "^22.12.0",
|
48
49
|
"@versatiles/release-tool": "^1.2.6",
|
50
|
+
"@vitest/coverage-v8": "^3.0.4",
|
49
51
|
"cookie": "^1.0.2",
|
50
52
|
"eslint": "^9.19.0",
|
51
53
|
"eslint-config-prettier": "^10.0.1",
|
52
54
|
"eslint-plugin-svelte": "^2.46.1",
|
53
55
|
"geojson": "^0.5.0",
|
54
56
|
"globals": "^15.14.0",
|
57
|
+
"happy-dom": "^16.7.3",
|
55
58
|
"npm-check-updates": "^17.1.14",
|
56
59
|
"prettier": "^3.4.2",
|
57
60
|
"prettier-plugin-svelte": "^3.3.3",
|
58
61
|
"publint": "^0.3.2",
|
59
|
-
"
|
60
|
-
"
|
62
|
+
"sass": "^1.83.4",
|
63
|
+
"sass-embedded": "^1.83.4",
|
64
|
+
"svelte": "^5.19.6",
|
61
65
|
"svelte-check": "^4.1.4",
|
66
|
+
"svelte-preprocess": "^6.0.3",
|
62
67
|
"tsx": "^4.19.2",
|
63
68
|
"typescript": "^5.7.3",
|
64
|
-
"typescript-eslint": "^8.
|
69
|
+
"typescript-eslint": "^8.22.0",
|
65
70
|
"vite": "^6.0.11",
|
66
71
|
"vitest": "^3.0.4"
|
67
72
|
},
|
@@ -69,7 +74,7 @@
|
|
69
74
|
"types": "./dist/index.d.ts",
|
70
75
|
"type": "module",
|
71
76
|
"dependencies": {
|
72
|
-
"@versatiles/style": "^5.2.
|
73
|
-
"maplibre-gl": "^5.0
|
77
|
+
"@versatiles/style": "^5.2.9",
|
78
|
+
"maplibre-gl": "^5.1.0"
|
74
79
|
}
|
75
80
|
}
|
@@ -1,197 +0,0 @@
|
|
1
|
-
<!-- AutoComplete.svelte -->
|
2
|
-
<script lang="ts" generics="T">
|
3
|
-
import { isDarkMode } from '../utils/style.js';
|
4
|
-
/* eslint svelte/no-at-html-tags: off */
|
5
|
-
|
6
|
-
import { createEventDispatcher, onMount } from 'svelte';
|
7
|
-
const dispatch = createEventDispatcher();
|
8
|
-
|
9
|
-
type Item = { key: string; value: T };
|
10
|
-
type ResultItem = Item & { _label: string };
|
11
|
-
|
12
|
-
// Component properties
|
13
|
-
export let placeholder: string = '';
|
14
|
-
export let minChar: number = 0;
|
15
|
-
export let maxItems: number = 10;
|
16
|
-
|
17
|
-
// Reactive variables
|
18
|
-
export let items: Item[];
|
19
|
-
|
20
|
-
let inputElement: HTMLInputElement; // Reference to DOM element
|
21
|
-
let autocompleteElement: HTMLDivElement; // Reference to DOM element
|
22
|
-
let isOpen = false;
|
23
|
-
let results: ResultItem[] = [];
|
24
|
-
let selectedIndex = 0;
|
25
|
-
let inputText: string = '';
|
26
|
-
|
27
|
-
// Escape special characters in search string for use in regex
|
28
|
-
const regExpEscape = (s: string) => s.replace(/[-\\^$*+?.()|[\]{}]/g, '\\$&');
|
29
|
-
|
30
|
-
if (inputText.length >= minChar) {
|
31
|
-
const r = filterResults();
|
32
|
-
if (r.length > 0) {
|
33
|
-
const { key, value } = r[0];
|
34
|
-
inputText = key;
|
35
|
-
setTimeout(() => dispatch('change', JSON.parse(JSON.stringify(value))), 0);
|
36
|
-
} else {
|
37
|
-
inputText = '';
|
38
|
-
}
|
39
|
-
}
|
40
|
-
|
41
|
-
export function setInputText(text: string) {
|
42
|
-
console.log(text);
|
43
|
-
inputText = text;
|
44
|
-
results = filterResults();
|
45
|
-
close(0);
|
46
|
-
}
|
47
|
-
|
48
|
-
// Handle input change
|
49
|
-
function onChange() {
|
50
|
-
if (inputText.length >= minChar) {
|
51
|
-
results = filterResults();
|
52
|
-
selectedIndex = 0;
|
53
|
-
isOpen = true;
|
54
|
-
} else {
|
55
|
-
isOpen = false;
|
56
|
-
}
|
57
|
-
}
|
58
|
-
|
59
|
-
function onFocus() {
|
60
|
-
inputElement.setSelectionRange(0, 1000);
|
61
|
-
}
|
62
|
-
|
63
|
-
// Filter results based on search query
|
64
|
-
function filterResults(): ResultItem[] {
|
65
|
-
const searchText = inputText.trim();
|
66
|
-
const searchTextUpper = searchText.toUpperCase();
|
67
|
-
const searchReg = RegExp(regExpEscape(searchText), 'i');
|
68
|
-
return items
|
69
|
-
.filter((item) => item.key.toUpperCase().includes(searchTextUpper))
|
70
|
-
.slice(0, maxItems)
|
71
|
-
.map((item) => ({
|
72
|
-
...item,
|
73
|
-
_label: item.key.replace(searchReg, '<span>$&</span>')
|
74
|
-
}));
|
75
|
-
}
|
76
|
-
|
77
|
-
// Handle keyboard navigation
|
78
|
-
function onKeyDown(event: KeyboardEvent) {
|
79
|
-
switch (event.key) {
|
80
|
-
case 'ArrowDown':
|
81
|
-
if (selectedIndex < results.length - 1) selectedIndex += 1;
|
82
|
-
break;
|
83
|
-
case 'ArrowUp':
|
84
|
-
if (selectedIndex > 0) selectedIndex -= 1;
|
85
|
-
break;
|
86
|
-
case 'Enter':
|
87
|
-
event.preventDefault();
|
88
|
-
close(selectedIndex);
|
89
|
-
break;
|
90
|
-
case 'Escape':
|
91
|
-
event.preventDefault();
|
92
|
-
close();
|
93
|
-
break;
|
94
|
-
}
|
95
|
-
}
|
96
|
-
|
97
|
-
// Close the autocomplete and select an item
|
98
|
-
function close(index = -1) {
|
99
|
-
isOpen = false;
|
100
|
-
if (index > -1 && results[index]) {
|
101
|
-
const { key, value } = results[index];
|
102
|
-
inputText = key;
|
103
|
-
dispatch('change', JSON.parse(JSON.stringify(value)));
|
104
|
-
}
|
105
|
-
}
|
106
|
-
|
107
|
-
onMount(() => {
|
108
|
-
const darkMode = isDarkMode(autocompleteElement);
|
109
|
-
autocompleteElement.style.setProperty('--bg-color', darkMode ? '#000' : '#fff');
|
110
|
-
autocompleteElement.style.setProperty('--fg-color', darkMode ? '#fff' : '#000');
|
111
|
-
});
|
112
|
-
</script>
|
113
|
-
|
114
|
-
<svelte:window on:click={() => close()} />
|
115
|
-
|
116
|
-
<div class="autocomplete" bind:this={autocompleteElement}>
|
117
|
-
<input
|
118
|
-
type="text"
|
119
|
-
bind:value={inputText}
|
120
|
-
{placeholder}
|
121
|
-
autocomplete="off"
|
122
|
-
on:input={onChange}
|
123
|
-
on:keydown={onKeyDown}
|
124
|
-
on:focusin={onFocus}
|
125
|
-
on:click={(e) => e.stopPropagation()}
|
126
|
-
bind:this={inputElement}
|
127
|
-
aria-activedescendant={isOpen ? `result-${selectedIndex}` : undefined}
|
128
|
-
aria-autocomplete="list"
|
129
|
-
aria-controls="autocomplete-results"
|
130
|
-
/>
|
131
|
-
<div class="autocomplete-results" class:hide-results={!isOpen}>
|
132
|
-
{#each results as result, i}
|
133
|
-
<button
|
134
|
-
on:click={() => close(i)}
|
135
|
-
class={i === selectedIndex ? ' is-active' : ''}
|
136
|
-
role="option"
|
137
|
-
aria-selected={i === selectedIndex}
|
138
|
-
>
|
139
|
-
{@html result._label}
|
140
|
-
</button>
|
141
|
-
{/each}
|
142
|
-
</div>
|
143
|
-
</div>
|
144
|
-
|
145
|
-
<style>
|
146
|
-
.autocomplete {
|
147
|
-
position: relative;
|
148
|
-
border-radius: 0.5em;
|
149
|
-
background: color-mix(in srgb, var(--bg-color) 80%, transparent);
|
150
|
-
box-sizing: border-box;
|
151
|
-
line-height: normal;
|
152
|
-
}
|
153
|
-
|
154
|
-
input {
|
155
|
-
width: 100%;
|
156
|
-
display: block;
|
157
|
-
box-sizing: border-box;
|
158
|
-
padding: 0.3em 0.6em;
|
159
|
-
border: none;
|
160
|
-
background: none;
|
161
|
-
color: var(--fg-color);
|
162
|
-
}
|
163
|
-
|
164
|
-
.autocomplete-results {
|
165
|
-
padding: 0;
|
166
|
-
margin: 0;
|
167
|
-
background: none;
|
168
|
-
width: 100%;
|
169
|
-
display: block;
|
170
|
-
border-radius: 0 0 0.5em 0.5em;
|
171
|
-
}
|
172
|
-
|
173
|
-
.autocomplete-results.hide-results {
|
174
|
-
display: none;
|
175
|
-
}
|
176
|
-
|
177
|
-
button {
|
178
|
-
padding: 0.2rem 0.5rem;
|
179
|
-
cursor: pointer;
|
180
|
-
border: none;
|
181
|
-
display: block;
|
182
|
-
background: transparent;
|
183
|
-
font-weight: normal;
|
184
|
-
color: color-mix(in srgb, var(--fg-color) 50%, transparent);
|
185
|
-
width: 100%;
|
186
|
-
text-align: left;
|
187
|
-
}
|
188
|
-
|
189
|
-
button > :global(span) {
|
190
|
-
color: var(--fg-color);
|
191
|
-
}
|
192
|
-
|
193
|
-
button.is-active,
|
194
|
-
button:hover {
|
195
|
-
background-color: color-mix(in srgb, var(--fg-color) 15%, transparent);
|
196
|
-
}
|
197
|
-
</style>
|
@@ -1,70 +0,0 @@
|
|
1
|
-
# BBox Map
|
2
|
-
|
3
|
-
This SvelteKit component generates an interactive world map with a search field that allows users to easily search for geographic bounding boxes (BBoxes) for various regions, including continents, countries, and cities. The selected bounding box can also be manipulated by dragging the edges.
|
4
|
-
|
5
|
-
## Improving Bounding Boxes
|
6
|
-
|
7
|
-
Bounding boxes for different regions (continents, countries, cities, etc.) are stored in the `bboxes.json` file. This file is generated by the helper script `helpers/merge_bboxes.ts`, which processes the `*.jsonl` files in the `data` directory.
|
8
|
-
|
9
|
-
### Editing Bounding Boxes
|
10
|
-
|
11
|
-
To edit or add new bounding boxes:
|
12
|
-
|
13
|
-
1. **Modify or Add a JSONL File**: Edit existing JSONL files in the `data` directory or create new ones with the required bounding box data.
|
14
|
-
2. **Run the `merge_bboxes.ts` Script**: After making changes, run the `merge_bboxes.ts` script to update the `bboxes.json` file.
|
15
|
-
|
16
|
-
### Helper Script: `merge_bboxes.ts`
|
17
|
-
|
18
|
-
This script consolidates bounding box data from multiple JSONL files in the `data` directory, checks for duplicate labels, and outputs a single, sorted JSON file (`bboxes.json`). The bounding box coordinates are also rounded to an appropriate precision based on their size.
|
19
|
-
|
20
|
-
#### Usage
|
21
|
-
|
22
|
-
Simply run the script:
|
23
|
-
|
24
|
-
```bash
|
25
|
-
./helpers/merge_bboxes.ts
|
26
|
-
```
|
27
|
-
|
28
|
-
#### Output
|
29
|
-
|
30
|
-
The script writes the processed data to `bboxes.json` in the parent directory. The entries in this file are sorted by population and formatted as:
|
31
|
-
|
32
|
-
```json
|
33
|
-
["Label", lngMin, latMin, lngWidth, latHeight]
|
34
|
-
```
|
35
|
-
|
36
|
-
### Helper Script: `geojson2bboxes.ts`
|
37
|
-
|
38
|
-
This script processes GeoJSON or GeoJSONL files to extract bounding boxes for each geographic feature, generate labels, and either extract or estimate the population for each feature. The results are saved in JSONL format, which can be used by the `merge_bboxes.ts` script.
|
39
|
-
|
40
|
-
#### Usage
|
41
|
-
|
42
|
-
```bash
|
43
|
-
./helpers/geojson2bboxes.ts <FILENAME> <LABEL_TEMPLATE> [POPULATION_KEY]
|
44
|
-
```
|
45
|
-
|
46
|
-
- `<FILENAME>`: The name of the input GeoJSON or GeoJSONL file. The file can be optionally compressed with `.br` (Brotli) or `.gz` (Gzip).
|
47
|
-
- `<LABEL_TEMPLATE>`: A template string used to generate labels for each feature. Placeholders in the form `{propertyName}` will be replaced by the corresponding property value from each feature.
|
48
|
-
- `[POPULATION_KEY]`: (Optional) The key in the feature's properties that contains the population value. If not provided, the script will estimate the population.
|
49
|
-
|
50
|
-
#### Example
|
51
|
-
|
52
|
-
```bash
|
53
|
-
./helpers/geojson2bboxes.ts africa.geojson "{country}, {city}"
|
54
|
-
```
|
55
|
-
|
56
|
-
This command processes the `africa.geojson` file, generating labels in the format `"country, city"` and estimating the population for each region.
|
57
|
-
|
58
|
-
#### Output
|
59
|
-
|
60
|
-
The script outputs a `.jsonl` file with entries formatted as follows:
|
61
|
-
|
62
|
-
```json
|
63
|
-
{
|
64
|
-
"label": "Country - City",
|
65
|
-
"population": 123456,
|
66
|
-
"bbox": [minLng, minLat, maxLng, maxLat]
|
67
|
-
}
|
68
|
-
```
|
69
|
-
|
70
|
-
Place this `.jsonl` file in the `data` directory to include it in the `bboxes.json` file when running `merge_bboxes.ts`.
|