@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.
Files changed (60) hide show
  1. package/README.md +21 -4
  2. package/dist/components/BBoxMap/AutoComplete.svelte +176 -0
  3. package/dist/components/{AutoComplete.svelte.d.ts → BBoxMap/AutoComplete.svelte.d.ts} +12 -18
  4. package/dist/components/BBoxMap/BBoxMap.d.ts +1 -12
  5. package/dist/components/BBoxMap/BBoxMap.js +1 -200
  6. package/dist/components/BBoxMap/BBoxMap.svelte +43 -127
  7. package/dist/components/BBoxMap/BBoxMap.svelte.d.ts +4 -20
  8. package/dist/components/BasicMap/BasicMap.svelte +34 -37
  9. package/dist/components/BasicMap/BasicMap.svelte.d.ts +9 -24
  10. package/dist/components/LocatorMap/LocatorMap.svelte +75 -0
  11. package/dist/components/LocatorMap/LocatorMap.svelte.d.ts +19 -0
  12. package/dist/components/MapEditor/Editor.svelte +25 -0
  13. package/dist/components/MapEditor/Editor.svelte.d.ts +7 -0
  14. package/dist/components/MapEditor/EditorLine.svelte +27 -0
  15. package/dist/components/MapEditor/EditorLine.svelte.d.ts +7 -0
  16. package/dist/components/MapEditor/EditorMarker.svelte +42 -0
  17. package/dist/components/MapEditor/EditorMarker.svelte.d.ts +7 -0
  18. package/dist/components/MapEditor/MapEditor.svelte +99 -0
  19. package/dist/components/MapEditor/MapEditor.svelte.d.ts +4 -0
  20. package/dist/components/MapEditor/editor.scss +16 -0
  21. package/dist/components/MapEditor/lib/element_abstract.d.ts +20 -0
  22. package/dist/components/MapEditor/lib/element_abstract.js +58 -0
  23. package/dist/components/MapEditor/lib/element_line.d.ts +14 -0
  24. package/dist/components/MapEditor/lib/element_line.js +76 -0
  25. package/dist/components/MapEditor/lib/element_marker.d.ts +20 -0
  26. package/dist/components/MapEditor/lib/element_marker.js +191 -0
  27. package/dist/components/MapEditor/lib/geocoder.d.ts +1 -0
  28. package/dist/components/MapEditor/lib/geocoder.js +8 -0
  29. package/dist/components/MapEditor/lib/geometry_manager.d.ts +18 -0
  30. package/dist/components/MapEditor/lib/geometry_manager.js +117 -0
  31. package/dist/components/MapEditor/lib/map_layer.d.ts +14 -0
  32. package/dist/components/MapEditor/lib/map_layer.js +61 -0
  33. package/dist/components/MapEditor/lib/types.d.ts +112 -0
  34. package/dist/components/MapEditor/lib/types.js +1 -0
  35. package/dist/components/MapEditor/lib/utils.d.ts +2 -0
  36. package/dist/components/MapEditor/lib/utils.js +11 -0
  37. package/dist/index.d.ts +3 -1
  38. package/dist/index.js +3 -1
  39. package/dist/utils/draw/bbox.d.ts +28 -0
  40. package/dist/utils/draw/bbox.js +193 -0
  41. package/dist/utils/location.d.ts +2 -1
  42. package/dist/utils/location.js +19 -10
  43. package/dist/utils/map_style.d.ts +6 -0
  44. package/dist/utils/map_style.js +28 -0
  45. package/dist/utils/sprite_library.d.ts +19 -0
  46. package/dist/utils/sprite_library.js +30 -0
  47. package/package.json +19 -14
  48. package/dist/components/AutoComplete.svelte +0 -197
  49. package/dist/components/BBoxMap/README.md +0 -70
  50. package/dist/components/BBoxMap/data/countries.jsonl +0 -258
  51. package/dist/components/BBoxMap/data/eu.jsonl +0 -1876
  52. package/dist/components/BBoxMap/data/us.jsonl +0 -52
  53. package/dist/components/BBoxMap/data/world.jsonl +0 -7
  54. package/dist/components/BBoxMap/helpers/geojson2bboxes.d.ts +0 -2
  55. package/dist/components/BBoxMap/helpers/geojson2bboxes.js +0 -183
  56. package/dist/components/BBoxMap/helpers/merge_bboxes.d.ts +0 -2
  57. package/dist/components/BBoxMap/helpers/merge_bboxes.js +0 -84
  58. package/dist/components/BBoxMap/helpers/population.raw.br +0 -0
  59. package/dist/utils/style.d.ts +0 -3
  60. 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
+ }
@@ -1,3 +1,4 @@
1
1
  import type { Language } from '@versatiles/style';
2
- export declare function getCountry(): string;
2
+ export declare function getCountryName(): string | null;
3
+ export declare function getCountryCode(): string | null;
3
4
  export declare function getLanguage(): Language;
@@ -1,22 +1,32 @@
1
1
  import { timezone2countrycode } from './zones.js';
2
- export function getCountry() {
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
- const region = new Intl.DisplayNames(['en-GB'], { type: 'region' });
9
- const country = region.of(countryCode);
10
- return country || '';
21
+ return countryCode || null;
11
22
  }
12
- catch (error) {
13
- console.error('Could not determine country from timezone:', error);
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 (error) {
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.2.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": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
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": "npm run test:integration && npm run test:unit",
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-auto": "^4.0.0",
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.10.10",
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
- "split2": "^4.2.0",
60
- "svelte": "^5.19.2",
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.21.0",
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.5",
73
- "maplibre-gl": "^5.0.1"
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`.