@expofp/offline 3.10.1 → 3.10.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.
|
@@ -7,8 +7,19 @@ const MAPS_BASE_PATH = 'maps/';
|
|
|
7
7
|
const MAP_STYLE_PATH = `${MAPS_BASE_PATH}style.json`;
|
|
8
8
|
const MAP_PMTILES_PATH = `${MAPS_BASE_PATH}basemap.pmtiles`;
|
|
9
9
|
const MAP_PMTILES_STYLE_URL = './basemap.pmtiles';
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Pannable-bounds padding for the offline tile region — sized off the viewport at
|
|
11
|
+
// max zoom-out (not the canvas span), so a small area map is extended enough to be
|
|
12
|
+
// seen fully when zoomed out, while a large one only gets a rotation margin. The
|
|
13
|
+
// online runtime pans freely (no maxBounds), so this only bounds how far out
|
|
14
|
+
// offline tiles are pre-generated; a safety factor keeps that region comfortably
|
|
15
|
+
// covered across screen sizes and out to the panned edges.
|
|
16
|
+
const MAP_MAX_ZOOM_OUT = 14;
|
|
17
|
+
const MERCATOR_M_PER_PX_Z0 = 78271.5168;
|
|
18
|
+
const M_PER_DEG_LAT = 110540;
|
|
19
|
+
const OFFLINE_REF_VIEWPORT_W = 3840;
|
|
20
|
+
const OFFLINE_REF_VIEWPORT_H = 2160;
|
|
21
|
+
const MAP_BOUNDS_MIN_PADDING_FRACTION = 0.05;
|
|
22
|
+
const OFFLINE_PADDING_SAFETY = 1.5;
|
|
12
23
|
const DEFAULT_MAP_SOURCE_BASE = 'https://build.protomaps.com';
|
|
13
24
|
const DEFAULT_MAP_SOURCE_LOOKBACK_DAYS = 7;
|
|
14
25
|
const PROTOMAPS_SPRITE_BASE = 'https://protomaps.github.io/basemaps-assets/sprites/v4/white';
|
|
@@ -103,19 +114,19 @@ async function derivePaddedVenueBbox(manifest, fpSvgContext, signal) {
|
|
|
103
114
|
if (!config) {
|
|
104
115
|
throw new Error('Offline map generation requires fpGeo.properties.config.');
|
|
105
116
|
}
|
|
106
|
-
const
|
|
117
|
+
const mapBounds = deriveMapBoundsRect(ctx);
|
|
107
118
|
const [anchorA, anchorB] = geoAnchors(config);
|
|
108
119
|
const corners = [
|
|
109
|
-
[
|
|
110
|
-
[
|
|
111
|
-
[
|
|
112
|
-
[
|
|
120
|
+
[mapBounds.min.x, mapBounds.min.y],
|
|
121
|
+
[mapBounds.max.x, mapBounds.min.y],
|
|
122
|
+
[mapBounds.max.x, mapBounds.max.y],
|
|
123
|
+
[mapBounds.min.x, mapBounds.max.y],
|
|
113
124
|
].map(([x, y]) => {
|
|
114
125
|
const { lat, lng } = projectLocalToGps({ x, y }, anchorA, anchorB);
|
|
115
126
|
return [lng, lat];
|
|
116
127
|
});
|
|
117
128
|
if (!corners.every(([lng, lat]) => Number.isFinite(lng) && Number.isFinite(lat))) {
|
|
118
|
-
throw new Error('Offline map generation produced invalid
|
|
129
|
+
throw new Error('Offline map generation produced invalid map GPS bounds.');
|
|
119
130
|
}
|
|
120
131
|
const lngs = corners.map(([lng]) => lng);
|
|
121
132
|
const lats = corners.map(([, lat]) => lat);
|
|
@@ -123,8 +134,27 @@ async function derivePaddedVenueBbox(manifest, fpSvgContext, signal) {
|
|
|
123
134
|
const maxLng = Math.max(...lngs);
|
|
124
135
|
const minLat = Math.min(...lats);
|
|
125
136
|
const maxLat = Math.max(...lats);
|
|
126
|
-
const
|
|
127
|
-
const
|
|
137
|
+
const centerLat = (minLat + maxLat) / 2;
|
|
138
|
+
const cosLat = Math.max(0.01, Math.cos((centerLat * Math.PI) / 180));
|
|
139
|
+
const mPerDegLng = 111320 * cosLat;
|
|
140
|
+
// Work in metres (Mercator is isotropic in metres). Content span = canvas AABB.
|
|
141
|
+
const contentW = (maxLng - minLng) * mPerDegLng;
|
|
142
|
+
const contentH = (maxLat - minLat) * M_PER_DEG_LAT;
|
|
143
|
+
// Viewport span at max zoom-out, for a generous reference screen.
|
|
144
|
+
const mPerPx = (MERCATOR_M_PER_PX_Z0 * cosLat) / 2 ** MAP_MAX_ZOOM_OUT;
|
|
145
|
+
const viewW = OFFLINE_REF_VIEWPORT_W * mPerPx;
|
|
146
|
+
const viewH = OFFLINE_REF_VIEWPORT_H * mPerPx;
|
|
147
|
+
// That viewport rotated by the view bearing has a larger axis-aligned footprint.
|
|
148
|
+
const bearing = config.bearing ?? 0;
|
|
149
|
+
const c = Math.abs(Math.cos((bearing * Math.PI) / 180));
|
|
150
|
+
const s = Math.abs(Math.sin((bearing * Math.PI) / 180));
|
|
151
|
+
const viewRotW = viewW * c + viewH * s;
|
|
152
|
+
const viewRotH = viewW * s + viewH * c;
|
|
153
|
+
// Same rule as online (extend small / constant for large / floor), times safety.
|
|
154
|
+
const padW = Math.max((viewRotW - contentW) / 2, (viewRotW - viewW) / 2, viewW * MAP_BOUNDS_MIN_PADDING_FRACTION) * OFFLINE_PADDING_SAFETY;
|
|
155
|
+
const padH = Math.max((viewRotH - contentH) / 2, (viewRotH - viewH) / 2, viewH * MAP_BOUNDS_MIN_PADDING_FRACTION) * OFFLINE_PADDING_SAFETY;
|
|
156
|
+
const lngPadding = padW / mPerDegLng;
|
|
157
|
+
const latPadding = padH / M_PER_DEG_LAT;
|
|
128
158
|
return [minLng - lngPadding, minLat - latPadding, maxLng + lngPadding, maxLat + latPadding];
|
|
129
159
|
}
|
|
130
160
|
async function resolveFpSvgContext(manifest, signal) {
|
|
@@ -136,26 +166,36 @@ async function resolveFpSvgContext(manifest, signal) {
|
|
|
136
166
|
.catch(() => Date.now().toString());
|
|
137
167
|
return await execScriptInSandbox(`${manifest.legacyDataUrlBase}fp.svg.js?v=${version}`, signal);
|
|
138
168
|
}
|
|
139
|
-
|
|
169
|
+
// The offline map extent — the whole SVG canvas, NOT the fit box: the full
|
|
170
|
+
// viewBox unioned with the explicit authored area (`__viewbox` / `rect#VIEWBOX`).
|
|
171
|
+
// Basing this on the
|
|
172
|
+
// authored area alone would, for a small venue `__viewbox`, preload only the
|
|
173
|
+
// venue's tiles and leave the rest of the pannable area blank offline.
|
|
174
|
+
function deriveMapBoundsRect(ctx) {
|
|
175
|
+
const fpSvg = ctx.__fp;
|
|
176
|
+
const viewBox = typeof fpSvg === 'string' ? getSvgViewBox(fpSvg) : null;
|
|
177
|
+
const explicit = deriveExplicitArea(ctx, fpSvg);
|
|
178
|
+
if (viewBox && explicit)
|
|
179
|
+
return Box.fromUnion(viewBox, explicit);
|
|
180
|
+
if (viewBox)
|
|
181
|
+
return viewBox;
|
|
182
|
+
if (explicit)
|
|
183
|
+
return explicit;
|
|
184
|
+
throw new Error('Offline map generation could not derive the map bounds (no SVG viewBox or VIEWBOX rect).');
|
|
185
|
+
}
|
|
186
|
+
// The explicit author-drawn area, if any: the `__viewbox` global or a
|
|
187
|
+
// `<rect id="VIEWBOX">` element. Null when neither is present.
|
|
188
|
+
function deriveExplicitArea(ctx, fpSvg) {
|
|
140
189
|
const viewboxObj = ctx.__viewbox;
|
|
141
190
|
if (viewboxObj) {
|
|
142
191
|
return new Box(viewboxObj.x, viewboxObj.y, viewboxObj.width, viewboxObj.height);
|
|
143
192
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
193
|
+
if (typeof fpSvg === 'string') {
|
|
194
|
+
const viewboxRect = getSvgRectById(fpSvg, 'VIEWBOX');
|
|
195
|
+
if (viewboxRect)
|
|
196
|
+
return viewboxRect;
|
|
147
197
|
}
|
|
148
|
-
|
|
149
|
-
if (viewboxRect)
|
|
150
|
-
return viewboxRect;
|
|
151
|
-
const svgViewBox = getSvgViewBox(fpSvg);
|
|
152
|
-
if (!svgViewBox) {
|
|
153
|
-
throw new Error('Offline map generation could not derive the SVG viewBox.');
|
|
154
|
-
}
|
|
155
|
-
// Inflate the viewBox by 10% about its centre (a 5% margin per side), matching
|
|
156
|
-
// the legacy withPadding(svgViewBox, -w*0.05, -h*0.05): a NEGATIVE padding GREW
|
|
157
|
-
// the extent per side.
|
|
158
|
-
return svgViewBox.scale(1.1);
|
|
198
|
+
return null;
|
|
159
199
|
}
|
|
160
200
|
function getSvgViewBox(svg) {
|
|
161
201
|
const match = svg.match(/\bviewBox\s*=\s*["']([^"']+)["']/i);
|
|
@@ -309,28 +349,45 @@ function createMapStyle() {
|
|
|
309
349
|
{
|
|
310
350
|
id: 'background',
|
|
311
351
|
type: 'background',
|
|
312
|
-
paint: { 'background-color': '#
|
|
352
|
+
paint: { 'background-color': '#f5f1ef' },
|
|
313
353
|
},
|
|
314
354
|
{
|
|
315
355
|
id: 'earth',
|
|
316
356
|
type: 'fill',
|
|
317
357
|
source: 'protomaps',
|
|
318
358
|
'source-layer': 'earth',
|
|
319
|
-
paint: { 'fill-color': '#
|
|
359
|
+
paint: { 'fill-color': '#f5f1ef' },
|
|
320
360
|
},
|
|
321
361
|
{
|
|
322
362
|
id: 'landuse',
|
|
323
363
|
type: 'fill',
|
|
324
364
|
source: 'protomaps',
|
|
325
365
|
'source-layer': 'landuse',
|
|
326
|
-
paint: {
|
|
366
|
+
paint: {
|
|
367
|
+
'fill-color': [
|
|
368
|
+
'match',
|
|
369
|
+
['get', 'kind'],
|
|
370
|
+
['park', 'nature_reserve', 'forest', 'wood', 'grass'],
|
|
371
|
+
'#bdebb3',
|
|
372
|
+
['hospital'],
|
|
373
|
+
'#f2dfdf',
|
|
374
|
+
['school', 'university'],
|
|
375
|
+
'#e6dccf',
|
|
376
|
+
['industrial'],
|
|
377
|
+
'#e2e3eb',
|
|
378
|
+
['commercial'],
|
|
379
|
+
'#f5efdf',
|
|
380
|
+
'#e3d9d4',
|
|
381
|
+
],
|
|
382
|
+
'fill-opacity': 0.75,
|
|
383
|
+
},
|
|
327
384
|
},
|
|
328
385
|
{
|
|
329
386
|
id: 'water',
|
|
330
387
|
type: 'fill',
|
|
331
388
|
source: 'protomaps',
|
|
332
389
|
'source-layer': 'water',
|
|
333
|
-
paint: { 'fill-color': '#
|
|
390
|
+
paint: { 'fill-color': '#99ddff' },
|
|
334
391
|
},
|
|
335
392
|
{
|
|
336
393
|
id: 'buildings',
|
|
@@ -338,7 +395,41 @@ function createMapStyle() {
|
|
|
338
395
|
source: 'protomaps',
|
|
339
396
|
'source-layer': 'buildings',
|
|
340
397
|
minzoom: 13,
|
|
341
|
-
paint: { 'fill-color': '#
|
|
398
|
+
paint: { 'fill-color': '#ded6d3', 'fill-opacity': 0.75 },
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
id: 'roads-minor-casing',
|
|
402
|
+
type: 'line',
|
|
403
|
+
source: 'protomaps',
|
|
404
|
+
'source-layer': 'roads',
|
|
405
|
+
minzoom: 12,
|
|
406
|
+
filter: ['in', ['get', 'kind'], ['literal', ['minor_road', 'path', 'other']]],
|
|
407
|
+
paint: {
|
|
408
|
+
'line-color': '#d2d6df',
|
|
409
|
+
'line-width': ['interpolate', ['linear'], ['zoom'], 12, 1, 16, 3.5, 19, 10],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
id: 'roads-major-casing',
|
|
414
|
+
type: 'line',
|
|
415
|
+
source: 'protomaps',
|
|
416
|
+
'source-layer': 'roads',
|
|
417
|
+
filter: ['==', ['get', 'kind'], 'major_road'],
|
|
418
|
+
paint: {
|
|
419
|
+
'line-color': '#d2d6df',
|
|
420
|
+
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1.4, 14, 4.5, 18, 14],
|
|
421
|
+
},
|
|
422
|
+
},
|
|
423
|
+
{
|
|
424
|
+
id: 'roads-highway-casing',
|
|
425
|
+
type: 'line',
|
|
426
|
+
source: 'protomaps',
|
|
427
|
+
'source-layer': 'roads',
|
|
428
|
+
filter: ['==', ['get', 'kind'], 'highway'],
|
|
429
|
+
paint: {
|
|
430
|
+
'line-color': '#f9fafc',
|
|
431
|
+
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 1.6, 14, 5, 18, 16],
|
|
432
|
+
},
|
|
342
433
|
},
|
|
343
434
|
{
|
|
344
435
|
id: 'roads-minor',
|
|
@@ -357,12 +448,23 @@ function createMapStyle() {
|
|
|
357
448
|
type: 'line',
|
|
358
449
|
source: 'protomaps',
|
|
359
450
|
'source-layer': 'roads',
|
|
360
|
-
filter: ['
|
|
451
|
+
filter: ['==', ['get', 'kind'], 'major_road'],
|
|
361
452
|
paint: {
|
|
362
453
|
'line-color': '#ffffff',
|
|
363
454
|
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.8, 14, 3, 18, 12],
|
|
364
455
|
},
|
|
365
456
|
},
|
|
457
|
+
{
|
|
458
|
+
id: 'roads-highway',
|
|
459
|
+
type: 'line',
|
|
460
|
+
source: 'protomaps',
|
|
461
|
+
'source-layer': 'roads',
|
|
462
|
+
filter: ['==', ['get', 'kind'], 'highway'],
|
|
463
|
+
paint: {
|
|
464
|
+
'line-color': '#ffb366',
|
|
465
|
+
'line-width': ['interpolate', ['linear'], ['zoom'], 8, 0.8, 14, 3, 18, 12],
|
|
466
|
+
},
|
|
467
|
+
},
|
|
366
468
|
{
|
|
367
469
|
id: 'road-labels',
|
|
368
470
|
type: 'symbol',
|
|
@@ -377,7 +479,7 @@ function createMapStyle() {
|
|
|
377
479
|
'text-size': ['interpolate', ['linear'], ['zoom'], 14, 10, 18, 13],
|
|
378
480
|
},
|
|
379
481
|
paint: {
|
|
380
|
-
'text-color': '#
|
|
482
|
+
'text-color': '#000000',
|
|
381
483
|
'text-halo-color': '#ffffff',
|
|
382
484
|
'text-halo-width': 1,
|
|
383
485
|
},
|
|
@@ -404,7 +506,7 @@ function createMapStyle() {
|
|
|
404
506
|
'text-offset': [0, 0.8],
|
|
405
507
|
},
|
|
406
508
|
paint: {
|
|
407
|
-
'text-color': '#
|
|
509
|
+
'text-color': '#333333',
|
|
408
510
|
'text-halo-color': '#ffffff',
|
|
409
511
|
'text-halo-width': 1.25,
|
|
410
512
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/offline",
|
|
3
|
-
"version": "3.10.
|
|
3
|
+
"version": "3.10.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for creating offline copies of ExpoFP floor plans",
|
|
6
6
|
"homepage": "https://developer.expofp.com/",
|
|
@@ -31,10 +31,10 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"debug": "^4.4.3",
|
|
33
33
|
"tslib": "^2.3.0",
|
|
34
|
-
"@expofp/
|
|
35
|
-
"@expofp/
|
|
36
|
-
"@expofp/geometry": "3.10.
|
|
37
|
-
"@expofp/
|
|
38
|
-
"@expofp/
|
|
34
|
+
"@expofp/floorplan": "3.10.2",
|
|
35
|
+
"@expofp/resolve": "3.10.2",
|
|
36
|
+
"@expofp/geometry": "3.10.2",
|
|
37
|
+
"@expofp/utils": "3.10.2",
|
|
38
|
+
"@expofp/data": "3.10.2"
|
|
39
39
|
}
|
|
40
40
|
}
|