@fusion.dev/helix-map 0.110.3 → 0.110.4

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.
@@ -29,7 +29,8 @@ export const cameraOf = (view) => {
29
29
  },
30
30
  toWorld: ([sx, sy]) => {
31
31
  const x = sx - view.width / 2;
32
- const y = sy - view.height / 2;
32
+ const horizon = tilt > 0 ? -distance / Math.tan(tilt) : -Infinity;
33
+ const y = Math.max(sy - view.height / 2, horizon * 0.95);
33
34
  const toward = (y * distance) / (distance * Math.cos(tilt) + y * Math.sin(tilt));
34
35
  const across = (x * (distance - toward * Math.sin(tilt))) / distance;
35
36
  return [
@@ -47,6 +47,8 @@ export declare const widthAt: ([[z0, w0], [z1, w1]]: readonly [readonly [number,
47
47
  export interface SnapshotOptions {
48
48
  /** The fetch the tiles are asked for with, where a server has its own, such as one that caches. */
49
49
  fetch?: typeof fetch | undefined;
50
+ /** How long each request may take, in milliseconds, before the snapshot is given up. 3000 by default. */
51
+ timeout?: number | undefined;
50
52
  /** A TileJSON URL for vector tiles in the OpenMapTiles schema: the one given to `mapLibreStyle`. OpenFreeMap's by default. */
51
53
  tiles?: string | undefined;
52
54
  }
@@ -68,7 +70,11 @@ export interface SnapshotOptions {
68
70
  * toward a token in OKLab, so it is drawn in whichever theme the page is in,
69
71
  * light or dark, and changes with it.
70
72
  *
71
- * Resolves to an empty string where the tiles cannot be read, which a page
72
- * renders as nothing.
73
+ * **Whole or nothing.** Resolves to an empty string where any tile the view
74
+ * needs cannot be read in time, or the view is not numbers, which a page
75
+ * renders as nothing and the renderer then draws. A snapshot with a tile
76
+ * missing looks finished and is not, and one that waited on a slow server
77
+ * held the page: each request is given `timeout` milliseconds. A tile the
78
+ * server says it does not have, a 404, is drawn empty, as the sea is.
73
79
  */
74
- export declare const mapSnapshot: (view: MapView, options?: SnapshotOptions) => Promise<string>;
80
+ export declare const mapSnapshot: (asked: MapView, options?: SnapshotOptions) => Promise<string>;
@@ -152,8 +152,33 @@ const simplified = (points) => {
152
152
  const OPENFREEMAP = 'https://tiles.openfreemap.org/planet';
153
153
  const MOST_TILES = 40;
154
154
  const MARGIN = 16;
155
- export const mapSnapshot = async (view, options = {}) => {
156
- const get = options.fetch ?? fetch;
155
+ export const mapSnapshot = async (asked, options = {}) => {
156
+ const view = {
157
+ bearing: Number(asked.bearing ?? 0),
158
+ center: { lat: Number(asked.center?.lat), lng: Number(asked.center?.lng) },
159
+ height: Math.round(Number(asked.height)),
160
+ pitch: Number(asked.pitch ?? 0),
161
+ width: Math.round(Number(asked.width)),
162
+ zoom: Number(asked.zoom),
163
+ };
164
+ if (![view.bearing, view.center.lat, view.center.lng, view.pitch, view.zoom].every(Number.isFinite) ||
165
+ !(view.width > 0 && view.height > 0)) {
166
+ return '';
167
+ }
168
+ const wait = options.timeout ?? 3000;
169
+ const get = (url) => {
170
+ const control = new AbortController();
171
+ let timer;
172
+ return Promise.race([
173
+ (options.fetch ?? fetch)(url, { signal: control.signal }),
174
+ new Promise((_, refuse) => {
175
+ timer = setTimeout(() => {
176
+ control.abort();
177
+ refuse(new Error(`${url} took longer than ${wait} ms`));
178
+ }, wait);
179
+ }),
180
+ ]).finally(() => clearTimeout(timer));
181
+ };
157
182
  const tiles = await get(options.tiles ?? OPENFREEMAP)
158
183
  .then((response) => (response.ok ? response.json() : undefined))
159
184
  .catch(() => undefined);
@@ -179,7 +204,7 @@ export const mapSnapshot = async (view, options = {}) => {
179
204
  const wanted = [];
180
205
  for (let x = Math.min(...xs); x <= Math.max(...xs); x++) {
181
206
  for (let y = Math.max(0, Math.min(...ys)); y <= Math.min(count - 1, Math.max(...ys)); y++) {
182
- wanted.push([((x % count) + count) % count, y]);
207
+ wanted.push([x, y]);
183
208
  }
184
209
  }
185
210
  return wanted;
@@ -235,18 +260,32 @@ export const mapSnapshot = async (view, options = {}) => {
235
260
  }
236
261
  return path;
237
262
  };
263
+ let whole = true;
264
+ const count = 2 ** depth;
238
265
  await Promise.all(wanted.map(async (at) => {
239
266
  const url = template
240
267
  .replace('{z}', String(depth))
241
- .replace('{x}', String(at[0]))
268
+ .replace('{x}', String(((at[0] % count) + count) % count))
242
269
  .replace('{y}', String(at[1]));
243
270
  const bytes = await get(url)
244
- .then((response) => (response.ok ? response.arrayBuffer() : undefined))
271
+ .then((response) => response.ok
272
+ ? response.arrayBuffer()
273
+ : response.status === 404
274
+ ? new ArrayBuffer(0)
275
+ : undefined)
245
276
  .catch(() => undefined);
246
277
  if (!bytes) {
278
+ whole = false;
247
279
  return;
248
280
  }
249
- const tile = new VectorTile(new PbfReader(new Uint8Array(bytes)));
281
+ try {
282
+ drawTile(new VectorTile(new PbfReader(new Uint8Array(bytes))), at);
283
+ }
284
+ catch {
285
+ whole = false;
286
+ }
287
+ }));
288
+ function drawTile(tile, at) {
250
289
  for (const layer of shown) {
251
290
  const source = tile.layers[layer.source];
252
291
  if (!source) {
@@ -267,7 +306,10 @@ export const mapSnapshot = async (view, options = {}) => {
267
306
  }
268
307
  }
269
308
  }
270
- }));
309
+ }
310
+ if (!whole) {
311
+ return '';
312
+ }
271
313
  const body = shown
272
314
  .map((layer) => {
273
315
  const paths = drawn.get(layer.id) ?? [];
@@ -276,7 +318,8 @@ export const mapSnapshot = async (view, options = {}) => {
276
318
  }
277
319
  const color = colorOf(layer);
278
320
  return layer.kind === 'fill'
279
- ? `<path fill-rule="evenodd" style="fill:${color}" d="${paths.join('')}"/>`
321
+ ?
322
+ `<path style="fill:${color}" d="${paths.join('')}"/>`
280
323
  : `<path style="fill:none;stroke:${color};stroke-width:${+widthAt(layer.width ?? [
281
324
  [0, 1],
282
325
  [1, 1],
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@fusion.dev/helix-map",
3
3
  "description": "Connects the Helix Map frame to a map renderer: MapLibre, Leaflet, or a driver of your own.",
4
- "version": "0.110.3",
4
+ "version": "0.110.4",
5
5
  "license": "MIT",
6
6
  "author": "Ross Sheppard <ross.sheppard@fusion.dev>",
7
7
  "repository": {
@@ -43,14 +43,14 @@
43
43
  },
44
44
  "bugs": "https://github.com/project-fusion/fusion/issues",
45
45
  "dependencies": {
46
- "@fusion.dev/helix-core": "0.110.3",
47
- "@fusion.dev/helix-icons": "0.110.3",
46
+ "@fusion.dev/helix-core": "0.110.4",
47
+ "@fusion.dev/helix-icons": "0.110.4",
48
48
  "@mapbox/vector-tile": "^3.0.0",
49
49
  "pbf": "^5.1.2"
50
50
  },
51
51
  "devDependencies": {
52
- "@fusion.dev/helix-headless": "0.110.3",
53
- "@fusion.dev/helix-testing": "0.110.3",
52
+ "@fusion.dev/helix-headless": "0.110.4",
53
+ "@fusion.dev/helix-testing": "0.110.4",
54
54
  "@maplibre/maplibre-gl-style-spec": "^26.4.4",
55
55
  "@types/bun": "^1.3.0",
56
56
  "@types/leaflet": "^1.9.20",