@bitbaum/geo-kit 0.2.0 → 0.3.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 CHANGED
@@ -130,7 +130,8 @@ map.flyTo(cameraForBounds([5.96, 45.82, 10.49, 47.81], limits));
130
130
  - **Layers.** The palette's sphere, graticule, glow and limb shade are each
131
131
  optional, so a lit globe of the world and a plain map where the data is the
132
132
  picture are the same painter. `over` draws the caller's own layer (sites,
133
- routes, figures) on top each frame, with the frame's projection.
133
+ routes, figures) on top each frame, with the frame's projection, and
134
+ `pickFirst` lets that layer take a tap or a hover before the features.
134
135
  - **Pure parts.** The camera, motion, culling, picking and the fast
135
136
  projection are exported and tested without a browser.
136
137
 
@@ -24,6 +24,8 @@ export interface FrameInfo {
24
24
  flight: number | null;
25
25
  /** True while the map moves and paints its coarse shapes. */
26
26
  moving: boolean;
27
+ /** Milliseconds since the reader last touched the map. */
28
+ sinceTouch: number;
27
29
  }
28
30
  export interface MapOptions<P = Record<string, unknown>> {
29
31
  shapes: MapShapes<P>;
@@ -42,8 +44,16 @@ export interface MapOptions<P = Record<string, unknown>> {
42
44
  idleTurn?: boolean;
43
45
  /** A feature was tapped or chosen by keyboard; null for the sea. */
44
46
  onSelect?: (id: string | null) => void;
45
- /** The mouse is over a feature (null when over none), at a canvas point. */
47
+ /** The mouse is over a feature (null when over none, or when a gesture starts), at a canvas point. */
46
48
  onHover?: (id: string | null, x: number, y: number) => void;
49
+ /**
50
+ * Asked first on a tap and on hover, before the features: true when the
51
+ * caller's own layer (a site, a marker) takes the point, so no feature is
52
+ * selected or outlined there.
53
+ */
54
+ pickFirst?: (x: number, y: number, projection: GeoProjection, kind: "tap" | "hover") => boolean;
55
+ /** The keyboard turned the map: the feature now facing the reader, or null. */
56
+ onFacing?: (id: string | null) => void;
47
57
  /** Draw on top of the map each frame. */
48
58
  over?: (ctx: CanvasRenderingContext2D, basis: Basis, projection: GeoProjection, frame: FrameInfo) => void;
49
59
  /** True while the overlay animates and needs another frame. */
@@ -62,6 +72,8 @@ export interface MapController {
62
72
  fly?: boolean;
63
73
  }): void;
64
74
  flyTo(camera: Camera, duration?: number): void;
75
+ /** Go to a view at once, with no flight. */
76
+ jumpTo(camera: Camera): void;
65
77
  zoomBy(factor: number | "home"): void;
66
78
  camera(): Camera;
67
79
  projection(): GeoProjection | null;
@@ -102,7 +102,9 @@ export function createMap(canvas, options) {
102
102
  const dpr = pixelRatio();
103
103
  ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
104
104
  const flightAt = motion.flight ? (now - motion.flight.start) / motion.flight.f.duration : null;
105
- paintMap(ctx, size, proj, moving ? shapes.coarse : shapes, paint, { hover: feature(hover), selected: feature(selected) }, options.over ? (c, basis, p) => options.over(c, basis, p, { now, flight: flightAt, moving }) : undefined);
105
+ paintMap(ctx, size, proj, moving ? shapes.coarse : shapes, paint, { hover: feature(hover), selected: feature(selected) }, options.over
106
+ ? (c, basis, p) => options.over(c, basis, p, { now, flight: flightAt, moving, sinceTouch: now - touched })
107
+ : undefined);
106
108
  }
107
109
  if (again)
108
110
  raf = requestAnimationFrame(tick);
@@ -144,6 +146,17 @@ export function createMap(canvas, options) {
144
146
  return proj ? pickAt(index, proj, x, y) : null;
145
147
  };
146
148
  const choose = (id) => options.onSelect?.(id);
149
+ const takenFirst = (x, y, kind) => {
150
+ const proj = projection();
151
+ return !!proj && !!options.pickFirst?.(x, y, proj, kind);
152
+ };
153
+ const facing = () => {
154
+ const view = viewAt(performance.now());
155
+ if (!view)
156
+ return null;
157
+ const d = disc(view);
158
+ return pick(d.cx, d.cy)?.id ?? null;
159
+ };
147
160
  /* ------------------------------------------------------------ input */
148
161
  const pointers = new Map();
149
162
  let gesture = { moved: false, vx: 0, vy: 0, t: 0, startX: 0, startY: 0 };
@@ -155,7 +168,16 @@ export function createMap(canvas, options) {
155
168
  keydown(e) {
156
169
  const step = e.shiftKey ? 30 : 10;
157
170
  const perStep = step / Math.max(camera.k, 1);
158
- const turn = (dLon, dLat) => flyTo({ center: [camera.center[0] + dLon * perStep, camera.center[1] + dLat * perStep], k: camera.k }, 220);
171
+ const turn = (dLon, dLat) => {
172
+ motion = {};
173
+ touched = performance.now();
174
+ camera = {
175
+ center: [wrapLon(camera.center[0] + dLon * perStep), clampTilt(camera.center[1] + dLat * perStep, limits)],
176
+ k: camera.k,
177
+ };
178
+ invalidate();
179
+ options.onFacing?.(facing());
180
+ };
159
181
  const keys = {
160
182
  ArrowLeft: () => turn(-1, 0),
161
183
  ArrowRight: () => turn(1, 0),
@@ -166,13 +188,7 @@ export function createMap(canvas, options) {
166
188
  "-": () => zoomBy(1 / 1.5),
167
189
  "0": () => zoomBy("home"),
168
190
  Home: () => zoomBy("home"),
169
- Enter: () => {
170
- const view = viewAt(performance.now());
171
- if (!view)
172
- return;
173
- const d = disc(view);
174
- choose(pick(d.cx, d.cy)?.id ?? null);
175
- },
191
+ Enter: () => choose(facing()),
176
192
  };
177
193
  const run = keys[e.key];
178
194
  if (!run)
@@ -197,19 +213,27 @@ export function createMap(canvas, options) {
197
213
  gesture.moved = true; // a second finger is never a tap
198
214
  grab();
199
215
  grabbed = true;
216
+ if (hover !== null) {
217
+ hover = null;
218
+ options.onHover?.(null, at.x, at.y);
219
+ }
200
220
  },
201
221
  pointermove(e) {
202
222
  const at = local(e);
203
223
  const before = pointers.get(e.pointerId);
204
224
  if (!before) {
205
225
  if (e.pointerType === "mouse") {
206
- const id = pick(at.x, at.y)?.id ?? null;
207
- canvas.style.cursor = id ? "pointer" : "";
226
+ touched = performance.now();
227
+ const taken = takenFirst(at.x, at.y, "hover");
228
+ const id = taken ? null : (pick(at.x, at.y)?.id ?? null);
229
+ canvas.style.cursor = taken || id ? "pointer" : "";
208
230
  if (id !== hover) {
209
231
  hover = id;
210
232
  invalidate();
211
233
  }
212
- options.onHover?.(id, at.x, at.y);
234
+ // The caller's own layer answered this point; its hover is its own to show.
235
+ if (!taken)
236
+ options.onHover?.(id, at.x, at.y);
213
237
  }
214
238
  return;
215
239
  }
@@ -248,7 +272,8 @@ export function createMap(canvas, options) {
248
272
  const g = gesture;
249
273
  if (!g.moved) {
250
274
  const at = local(e);
251
- choose(pick(at.x, at.y)?.id ?? null);
275
+ if (!takenFirst(at.x, at.y, "tap"))
276
+ choose(pick(at.x, at.y)?.id ?? null);
252
277
  return;
253
278
  }
254
279
  const fresh = e.timeStamp - g.t < 80;
@@ -327,6 +352,12 @@ export function createMap(canvas, options) {
327
352
  invalidate();
328
353
  },
329
354
  flyTo,
355
+ jumpTo(to) {
356
+ motion = {};
357
+ touched = performance.now();
358
+ camera = { center: [wrapLon(to.center[0]), clampTilt(to.center[1], limits)], k: clampZoom(to.k, limits) };
359
+ invalidate();
360
+ },
330
361
  zoomBy,
331
362
  camera: () => camera,
332
363
  projection: () => projection(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bitbaum/geo-kit",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Versioned, on-demand geography resources and sourced boundary assertions for web applications, and a Canvas 2D renderer for them (globe or map).",
5
5
  "author": "BitBaum",
6
6
  "homepage": "https://github.com/bitbaum/geo-kit#readme",