@versatiles/svelte 2.1.0 → 2.1.2

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.
@@ -4,40 +4,59 @@
4
4
  import { getCountryName } from '../../utils/location.js';
5
5
  import BasicMap from '../BasicMap/BasicMap.svelte';
6
6
  import { isDarkMode } from '../../utils/map_style.js';
7
- import type { BBox } from 'geojson';
8
7
  import { loadBBoxes } from './BBoxMap.js';
9
- import { BBoxDrawer } from './lib/bbox.js';
8
+ import { BBoxDrawer, isSameBBox, type BBox } from './lib/bbox_drawer.js';
10
9
 
11
10
  let { selectedBBox = $bindable() }: { selectedBBox?: BBox } = $props();
11
+
12
12
  const startTime = Date.now();
13
- let bboxDrawer: BBoxDrawer;
13
+ let bboxDrawer: BBoxDrawer | undefined;
14
14
  let map: MaplibreMapType | undefined = $state();
15
15
  let bboxes: { key: string; value: BBox }[] | undefined = $state();
16
16
  let mapContainer: HTMLElement;
17
+ let disableZoomTimeout: ReturnType<typeof setTimeout> | undefined;
17
18
 
18
19
  async function onMapInit(_map: MaplibreMapType) {
19
20
  map = _map;
20
21
  mapContainer = map.getContainer();
21
22
  map.setPadding({ top: 42, right: 10, bottom: 15, left: 10 });
22
- bboxDrawer = new BBoxDrawer(map!, [-180, -85, 180, 85], isDarkMode(mapContainer) ? '#FFFFFF' : '#000000');
23
23
  bboxes = await loadBBoxes();
24
- // If an initial bbox is already provided by the parent, display it instead of guessing the user's country
25
- if (selectedBBox) flyToBBox(selectedBBox);
26
- bboxDrawer.bbox.subscribe((bbox) => (selectedBBox = bbox));
24
+
25
+ bboxDrawer = new BBoxDrawer(
26
+ map!,
27
+ selectedBBox ?? [-180, -85, 180, 85],
28
+ isDarkMode(mapContainer) ? '#FFFFFF' : '#000000'
29
+ );
30
+
31
+ if (selectedBBox) {
32
+ zoom();
33
+ }
34
+
35
+ bboxDrawer.on('dragEnd', (bbox) => {
36
+ disableZoomTemporarily();
37
+ if (!selectedBBox || !isSameBBox(bbox, selectedBBox)) {
38
+ selectedBBox = bbox;
39
+ }
40
+ });
27
41
  }
28
42
 
29
- function flyToBBox(bbox: BBox) {
30
- if (!map || !bbox) return;
43
+ $effect(() => {
44
+ if (!selectedBBox) return;
45
+ zoom();
46
+ if (bboxDrawer && selectedBBox) bboxDrawer.bbox = selectedBBox;
47
+ });
31
48
 
32
- bboxDrawer.setGeometry(bbox);
49
+ function zoom() {
50
+ if (!bboxDrawer || !map || !selectedBBox) return;
51
+ if (disableZoomTimeout) return;
33
52
 
34
- const transform = map.cameraForBounds(bboxDrawer.getBounds()) as CameraOptions;
53
+ const transform = map.cameraForBounds(selectedBBox) as CameraOptions;
35
54
  if (transform == null) return;
36
55
  transform.zoom = transform.zoom ?? 0 - 0.5;
37
56
  transform.bearing = 0;
38
57
  transform.pitch = 0;
39
58
 
40
- if (Date.now() - startTime < 1000) {
59
+ if (Date.now() - startTime < 3000) {
41
60
  map.jumpTo(transform);
42
61
  } else {
43
62
  map.flyTo({ ...transform, essential: true, speed: 5 });
@@ -58,6 +77,11 @@
58
77
  }
59
78
  return query;
60
79
  }
80
+
81
+ function disableZoomTemporarily() {
82
+ if (disableZoomTimeout) clearTimeout(disableZoomTimeout);
83
+ disableZoomTimeout = setTimeout(() => (disableZoomTimeout = undefined), 100);
84
+ }
61
85
  </script>
62
86
 
63
87
  <div class="container">
@@ -66,19 +90,21 @@
66
90
  <AutoComplete
67
91
  items={bboxes}
68
92
  placeholder="Find country, region or city …"
69
- change={(bbox) => flyToBBox(bbox)}
93
+ change={(bbox) => (selectedBBox = bbox)}
70
94
  initialInputText={getInitialInputText()}
71
95
  />
72
96
  </div>
73
97
  {/if}
74
- <BasicMap {onMapInit} emptyStyle={true}></BasicMap>
98
+ <BasicMap {onMapInit}></BasicMap>
75
99
  </div>
76
100
 
77
101
  <style>
78
102
  .container {
103
+ position: absolute;
79
104
  width: 100%;
80
105
  height: 100%;
81
- position: relative;
106
+ left: 0;
107
+ top: 0;
82
108
  min-height: 6em;
83
109
  }
84
110
  .input {
@@ -1,4 +1,4 @@
1
- import type { BBox } from 'geojson';
1
+ import { type BBox } from './lib/bbox_drawer.js';
2
2
  type $$ComponentProps = {
3
3
  selectedBBox?: BBox;
4
4
  };
@@ -1,6 +1,6 @@
1
1
  import type geojson from 'geojson';
2
- import { type Writable } from 'svelte/store';
3
- import maplibregl from 'maplibre-gl';
2
+ import type maplibregl from 'maplibre-gl';
3
+ import { EventHandler } from '../../../utils/event_handler.js';
4
4
  export type DragPoint = 'n' | 'ne' | 'e' | 'se' | 's' | 'sw' | 'w' | 'nw' | false;
5
5
  export declare const DragPointMap: Map<DragPoint, {
6
6
  cursor: string;
@@ -8,22 +8,26 @@ export declare const DragPointMap: Map<DragPoint, {
8
8
  flipV: DragPoint;
9
9
  }>;
10
10
  export type BBox = [number, number, number, number];
11
- export declare class BBoxDrawer {
11
+ export declare class BBoxDrawer extends EventHandler<{
12
+ drag: BBox;
13
+ dragEnd: BBox;
14
+ }> {
15
+ #private;
12
16
  private sourceId;
13
17
  private dragPoint;
14
18
  private isDragging;
15
19
  private map;
16
20
  private canvas;
17
- private inverted;
18
- readonly bbox: Writable<BBox>;
19
- constructor(map: maplibregl.Map, bbox: BBox, color: string, inverted?: boolean);
21
+ private insideOut;
22
+ constructor(map: maplibregl.Map, initialBBox: BBox, color: string, insideOut?: boolean);
20
23
  private updateDragPoint;
21
24
  private getAsFeatureCollection;
22
- setGeometry(bbox: geojson.BBox): void;
23
- getBounds(): maplibregl.LngLatBounds;
25
+ set bbox(bbox: BBox);
26
+ get bbox(): BBox;
24
27
  private redraw;
25
28
  private getAsPixel;
26
29
  private checkDragPointAt;
27
30
  private getCursor;
28
31
  private doDrag;
29
32
  }
33
+ export declare function isSameBBox(a: geojson.BBox, b: geojson.BBox): boolean;
@@ -1,6 +1,4 @@
1
- import { get, writable } from 'svelte/store';
2
- import maplibregl from 'maplibre-gl';
3
- import { getMapStyle } from '../../../utils/map_style.js';
1
+ import { EventHandler } from '../../../utils/event_handler.js';
4
2
  // prettier-ignore
5
3
  export const DragPointMap = new Map([
6
4
  ['n', { cursor: 'ns-resize', flipH: 'n', flipV: 's' }],
@@ -14,23 +12,22 @@ export const DragPointMap = new Map([
14
12
  [false, { cursor: 'default', flipH: false, flipV: false }],
15
13
  ]);
16
14
  const worldBBox = [-180, -85, 180, 85];
17
- export class BBoxDrawer {
15
+ export class BBoxDrawer extends EventHandler {
18
16
  sourceId;
19
17
  dragPoint = false;
20
18
  isDragging = false;
21
19
  map;
22
20
  canvas;
23
- inverted;
24
- bbox;
25
- constructor(map, bbox, color, inverted) {
26
- this.bbox = writable(bbox);
27
- this.inverted = inverted ?? true;
21
+ insideOut;
22
+ #bbox;
23
+ constructor(map, initialBBox, color, insideOut) {
24
+ super();
25
+ this.#bbox = [...initialBBox];
26
+ this.insideOut = insideOut ?? true;
28
27
  this.map = map;
29
28
  this.sourceId = 'bbox_' + Math.random().toString(36).slice(2);
30
- const style = getMapStyle();
31
- style.transition = { duration: 0, delay: 0 };
32
- style.sources[this.sourceId] = { type: 'geojson', data: this.getAsFeatureCollection() };
33
- style.layers.push({
29
+ map.addSource(this.sourceId, { type: 'geojson', data: this.getAsFeatureCollection() });
30
+ map.addLayer({
34
31
  id: 'bbox-line_' + Math.random().toString(36).slice(2),
35
32
  type: 'line',
36
33
  source: this.sourceId,
@@ -38,7 +35,7 @@ export class BBoxDrawer {
38
35
  layout: { 'line-cap': 'round', 'line-join': 'round' },
39
36
  paint: { 'line-color': color }
40
37
  });
41
- style.layers.push({
38
+ map.addLayer({
42
39
  id: 'bbox-fill_' + Math.random().toString(36).slice(2),
43
40
  type: 'fill',
44
41
  source: this.sourceId,
@@ -46,7 +43,6 @@ export class BBoxDrawer {
46
43
  layout: {},
47
44
  paint: { 'fill-color': color, 'fill-opacity': 0.2 }
48
45
  });
49
- map.setStyle(style);
50
46
  this.canvas = map.getCanvasContainer();
51
47
  map.on('mousemove', (e) => {
52
48
  if (e.originalEvent.buttons % 2 === 0)
@@ -58,6 +54,7 @@ export class BBoxDrawer {
58
54
  this.doDrag(e.lngLat);
59
55
  this.redraw();
60
56
  e.preventDefault();
57
+ this.emit('drag', [...this.#bbox]);
61
58
  });
62
59
  map.on('mousedown', (e) => {
63
60
  if (this.isDragging)
@@ -73,6 +70,7 @@ export class BBoxDrawer {
73
70
  map.on('mouseup', () => {
74
71
  this.isDragging = false;
75
72
  this.updateDragPoint(false);
73
+ this.emit('dragEnd', [...this.#bbox]);
76
74
  });
77
75
  }
78
76
  updateDragPoint(dragPoint) {
@@ -82,10 +80,10 @@ export class BBoxDrawer {
82
80
  this.canvas.style.cursor = this.getCursor(dragPoint);
83
81
  }
84
82
  getAsFeatureCollection() {
85
- const ring = getRing(get(this.bbox));
83
+ const ring = getRing(this.#bbox);
86
84
  return {
87
85
  type: 'FeatureCollection',
88
- features: [this.inverted ? polygon(getRing(worldBBox), ring) : polygon(ring), linestring(ring)]
86
+ features: [this.insideOut ? polygon(getRing(worldBBox), ring) : polygon(ring), linestring(ring)]
89
87
  };
90
88
  function getRing(bbox) {
91
89
  const x0 = Math.min(bbox[0], bbox[2]);
@@ -102,23 +100,23 @@ export class BBoxDrawer {
102
100
  return { type: 'Feature', geometry: { type: 'LineString', coordinates }, properties: {} };
103
101
  }
104
102
  }
105
- setGeometry(bbox) {
106
- this.bbox.set(bbox.slice(0, 4));
103
+ set bbox(bbox) {
104
+ if (isSameBBox(this.#bbox, bbox))
105
+ return;
106
+ this.#bbox = [...bbox];
107
107
  this.redraw();
108
108
  }
109
- getBounds() {
110
- return new maplibregl.LngLatBounds(get(this.bbox));
109
+ get bbox() {
110
+ return [...this.#bbox];
111
111
  }
112
112
  redraw() {
113
113
  const source = this.map.getSource(this.sourceId);
114
- if (!source)
114
+ if (!source || source.type !== 'geojson')
115
115
  return;
116
- if (source instanceof maplibregl.GeoJSONSource) {
117
- source.setData(this.getAsFeatureCollection());
118
- }
116
+ source.setData(this.getAsFeatureCollection());
119
117
  }
120
118
  getAsPixel() {
121
- const bbox = get(this.bbox);
119
+ const bbox = this.#bbox;
122
120
  const p0 = this.map.project([bbox[0], bbox[1]]);
123
121
  const p1 = this.map.project([bbox[2], bbox[3]]);
124
122
  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)];
@@ -149,7 +147,7 @@ export class BBoxDrawer {
149
147
  return DragPointMap.get(drag)?.cursor ?? 'default';
150
148
  }
151
149
  doDrag(lngLat) {
152
- this.bbox.update((bbox) => {
150
+ this.#bbox = ((bbox) => {
153
151
  const x = Math.round(lngLat.lng * 1e3) / 1e3;
154
152
  const y = Math.round(lngLat.lat * 1e3) / 1e3;
155
153
  // prettier-ignore
@@ -195,6 +193,9 @@ export class BBoxDrawer {
195
193
  this.updateDragPoint(DragPointMap.get(this.dragPoint)?.flipV ?? false);
196
194
  }
197
195
  return bbox;
198
- });
196
+ })(this.#bbox);
199
197
  }
200
198
  }
199
+ export function isSameBBox(a, b) {
200
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2] && a[3] === b[3];
201
+ }
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { EventHandler } from '../lib/utils/event_handler.js';
3
+ import { EventHandler } from '../../../utils/event_handler.js';
4
4
 
5
5
  let {
6
6
  children = $bindable(),
@@ -15,7 +15,10 @@
15
15
  } = $props();
16
16
 
17
17
  let dialog: HTMLDialogElement | null = null;
18
- export const eventHandler = new EventHandler();
18
+ export const eventHandler = new EventHandler<{
19
+ open: void;
20
+ close: void;
21
+ }>();
19
22
 
20
23
  export function getNode(): HTMLDialogElement {
21
24
  return dialog!;
@@ -1,5 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
- import { EventHandler } from '../lib/utils/event_handler.js';
2
+ import { EventHandler } from '../../../utils/event_handler.js';
3
3
  type $$ComponentProps = {
4
4
  children?: Snippet;
5
5
  size?: 'big' | 'fullscreen' | 'small';
@@ -7,7 +7,10 @@ type $$ComponentProps = {
7
7
  onclose?: () => void;
8
8
  };
9
9
  declare const Dialog: import("svelte").Component<$$ComponentProps, {
10
- eventHandler: EventHandler;
10
+ eventHandler: EventHandler<{
11
+ open: void;
12
+ close: void;
13
+ }>;
11
14
  getNode: () => HTMLDialogElement;
12
15
  open: () => void;
13
16
  close: () => void;
@@ -1,13 +1,16 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
3
  import Dialog from './Dialog.svelte';
4
- import { EventHandler } from '../lib/utils/event_handler.js';
4
+ import { EventHandler } from '../../../utils/event_handler.js';
5
5
 
6
6
  type Mode = 'download' | 'new' | null;
7
7
  let mode: Mode = $state(null);
8
8
  let dialog: Dialog | null = null;
9
9
  let input: HTMLInputElement | null = $state(null);
10
- let eventHandler = new EventHandler();
10
+ let eventHandler = new EventHandler<{
11
+ A: void;
12
+ B: void;
13
+ }>();
11
14
 
12
15
  async function openDialog(newMode: Mode) {
13
16
  if (!dialog) return;
@@ -19,8 +19,8 @@ export declare class MapLayerSymbol extends MapLayer<LayerSymbol> {
19
19
  label: Writable<string>;
20
20
  labelAlign: Writable<number>;
21
21
  symbolInfo: import("svelte/store").Readable<import("../symbols.js").SymbolInfo>;
22
- textAnchor: import("svelte/store").Readable<"center" | "top" | "bottom" | "right" | "left" | undefined>;
23
- textVariableAnchor: import("svelte/store").Readable<("center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "top" | "bottom" | "right" | "left")[] | undefined>;
22
+ textAnchor: import("svelte/store").Readable<"center" | "left" | "right" | "top" | "bottom" | undefined>;
23
+ textVariableAnchor: import("svelte/store").Readable<("center" | "top-left" | "top-right" | "bottom-left" | "bottom-right" | "left" | "right" | "top" | "bottom")[] | undefined>;
24
24
  constructor(manager: GeometryManager, id: string, source: string);
25
25
  getState(): StateStyle | undefined;
26
26
  setState(state: StateStyle): void;
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ import BBoxMap from './components/BBoxMap/BBoxMap.svelte';
3
3
  import LocatorMap from './components/LocatorMap/LocatorMap.svelte';
4
4
  import MapEditor from './components/MapEditor/MapEditor.svelte';
5
5
  export { BasicMap, BBoxMap, LocatorMap, MapEditor };
6
+ export { type BBox } from './components/BBoxMap/lib/bbox_drawer.js';
package/dist/index.js CHANGED
@@ -3,3 +3,4 @@ import BBoxMap from './components/BBoxMap/BBoxMap.svelte';
3
3
  import LocatorMap from './components/LocatorMap/LocatorMap.svelte';
4
4
  import MapEditor from './components/MapEditor/MapEditor.svelte';
5
5
  export { BasicMap, BBoxMap, LocatorMap, MapEditor };
6
+ export {} from './components/BBoxMap/lib/bbox_drawer.js';
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Callback type that accepts a payload only if the event actually carries data.
3
+ * For data‑less events (`void` or `undefined`), the callback takes no argument.
4
+ */
5
+ type EventCallback<T> = T extends void | undefined ? () => void : (data: T) => void;
6
+ export declare class EventHandler<Events extends Record<string, unknown> = {}> {
7
+ /** monotonically increasing id for every registered callback */
8
+ private index;
9
+ /**
10
+ * Per‑event registry that keeps the correct data type for every callback
11
+ * (`drag` maps to `{x: number; y: number}`, `dragEnd` to the same, etc.).
12
+ */
13
+ private events;
14
+ /** Emit an event (with data only if the event defines some) */
15
+ emit<K extends keyof Events>(name: K, ...args: Events[K] extends void | undefined ? [] : [data: Events[K]]): void;
16
+ /** Register a new listener; returns its numeric id so it can be removed later */
17
+ on<K extends keyof Events>(name: K, callback: EventCallback<Events[K]>): number;
18
+ /** Register a listener that will fire exactly once */
19
+ once<K extends keyof Events>(name: K, callback: EventCallback<Events[K]>): number;
20
+ /** Remove a single listener by id, or all listeners for an event */
21
+ off<K extends keyof Events>(name: K, id?: number): void;
22
+ /** Clear the entire registry */
23
+ clear(): void;
24
+ }
25
+ export {};
@@ -0,0 +1,50 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-empty-object-type
2
+ export class EventHandler {
3
+ /** monotonically increasing id for every registered callback */
4
+ index = 0;
5
+ /**
6
+ * Per‑event registry that keeps the correct data type for every callback
7
+ * (`drag` maps to `{x: number; y: number}`, `dragEnd` to the same, etc.).
8
+ */
9
+ events = {};
10
+ /** Emit an event (with data only if the event defines some) */
11
+ emit(name, ...args) {
12
+ const payload = args[0];
13
+ this.events[name]?.forEach((cb) => cb(payload));
14
+ }
15
+ /** Register a new listener; returns its numeric id so it can be removed later */
16
+ on(name, callback) {
17
+ if (!callback)
18
+ throw new Error('Callback is required');
19
+ const bucket = (this.events[name] ??= new Map());
20
+ const id = ++this.index;
21
+ bucket.set(id, callback);
22
+ return id;
23
+ }
24
+ /** Register a listener that will fire exactly once */
25
+ once(name, callback) {
26
+ const id = this.on(name, ((data) => {
27
+ this.off(name, id);
28
+ callback(data);
29
+ }));
30
+ return id;
31
+ }
32
+ /** Remove a single listener by id, or all listeners for an event */
33
+ off(name, id) {
34
+ const bucket = this.events[name];
35
+ if (!bucket)
36
+ return;
37
+ if (id !== undefined) {
38
+ bucket.delete(id);
39
+ }
40
+ else {
41
+ delete this.events[name];
42
+ }
43
+ }
44
+ /** Clear the entire registry */
45
+ clear() {
46
+ for (const key in this.events) {
47
+ delete this.events[key];
48
+ }
49
+ }
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@versatiles/svelte",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "license": "MIT",
5
5
  "scripts": {
6
6
  "build": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json && vite build && npm run package",
@@ -1,10 +0,0 @@
1
- export declare class EventHandler {
2
- private index;
3
- private events;
4
- constructor();
5
- emit(event: string): void;
6
- on(name: string, callback: () => void): number;
7
- once(name: string, callback: () => void): number;
8
- off(name: string, index?: number): void;
9
- clear(): void;
10
- }
@@ -1,39 +0,0 @@
1
- export class EventHandler {
2
- index = 0;
3
- events = new Map();
4
- constructor() { }
5
- emit(event) {
6
- this.events.get(event)?.forEach((callback) => callback());
7
- }
8
- on(name, callback) {
9
- if (!callback)
10
- throw new Error('Callback is required');
11
- if (!this.events.has(name))
12
- this.events.set(name, new Map());
13
- this.index++;
14
- this.events.get(name).set(this.index, callback);
15
- return this.index;
16
- }
17
- once(name, callback) {
18
- if (!callback)
19
- throw new Error('Callback is required');
20
- const index = this.on(name, () => {
21
- this.off(name, index);
22
- callback();
23
- });
24
- return index;
25
- }
26
- off(name, index) {
27
- if (!this.events.has(name))
28
- return;
29
- if (index) {
30
- this.events.get(name)?.delete(index);
31
- }
32
- else {
33
- this.events.delete(name);
34
- }
35
- }
36
- clear() {
37
- this.events.clear();
38
- }
39
- }