@expofp/offline 3.7.16 → 3.8.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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Box, projectLocalToGps } from '@expofp/geometry';
|
|
1
2
|
import { resolve } from '@expofp/resolve';
|
|
2
3
|
import debug from 'debug';
|
|
3
4
|
import { execScriptInSandbox } from './exec-script-in-sandbox.js';
|
|
@@ -103,12 +104,16 @@ async function derivePaddedVenueBbox(manifest, fpSvgContext, signal) {
|
|
|
103
104
|
throw new Error('Offline map generation requires fpGeo.properties.config.');
|
|
104
105
|
}
|
|
105
106
|
const svgArea = deriveSvgArea(ctx);
|
|
107
|
+
const [anchorA, anchorB] = geoAnchors(config);
|
|
106
108
|
const corners = [
|
|
107
|
-
[svgArea.
|
|
108
|
-
[svgArea.
|
|
109
|
-
[svgArea.
|
|
110
|
-
[svgArea.
|
|
111
|
-
].map(([x, y]) =>
|
|
109
|
+
[svgArea.min.x, svgArea.min.y],
|
|
110
|
+
[svgArea.max.x, svgArea.min.y],
|
|
111
|
+
[svgArea.max.x, svgArea.max.y],
|
|
112
|
+
[svgArea.min.x, svgArea.max.y],
|
|
113
|
+
].map(([x, y]) => {
|
|
114
|
+
const { lat, lng } = projectLocalToGps({ x, y }, anchorA, anchorB);
|
|
115
|
+
return [lng, lat];
|
|
116
|
+
});
|
|
112
117
|
if (!corners.every(([lng, lat]) => Number.isFinite(lng) && Number.isFinite(lat))) {
|
|
113
118
|
throw new Error('Offline map generation produced invalid venue GPS bounds.');
|
|
114
119
|
}
|
|
@@ -134,7 +139,7 @@ async function resolveFpSvgContext(manifest, signal) {
|
|
|
134
139
|
function deriveSvgArea(ctx) {
|
|
135
140
|
const viewboxObj = ctx.__viewbox;
|
|
136
141
|
if (viewboxObj) {
|
|
137
|
-
return
|
|
142
|
+
return new Box(viewboxObj.x, viewboxObj.y, viewboxObj.width, viewboxObj.height);
|
|
138
143
|
}
|
|
139
144
|
const fpSvg = ctx.__fp;
|
|
140
145
|
if (typeof fpSvg !== 'string') {
|
|
@@ -147,7 +152,10 @@ function deriveSvgArea(ctx) {
|
|
|
147
152
|
if (!svgViewBox) {
|
|
148
153
|
throw new Error('Offline map generation could not derive the SVG viewBox.');
|
|
149
154
|
}
|
|
150
|
-
|
|
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);
|
|
151
159
|
}
|
|
152
160
|
function getSvgViewBox(svg) {
|
|
153
161
|
const match = svg.match(/\bviewBox\s*=\s*["']([^"']+)["']/i);
|
|
@@ -159,7 +167,7 @@ function getSvgViewBox(svg) {
|
|
|
159
167
|
.map(Number);
|
|
160
168
|
if (![x, y, w, h].every(Number.isFinite))
|
|
161
169
|
return null;
|
|
162
|
-
return
|
|
170
|
+
return new Box(x, y, w, h);
|
|
163
171
|
}
|
|
164
172
|
function getSvgRectById(svg, id) {
|
|
165
173
|
const rectMatch = svg.match(new RegExp(`<rect\\b(?=[^>]*\\bid=["']${id}["'])[^>]*/?>`, 'i'));
|
|
@@ -172,7 +180,7 @@ function getSvgRectById(svg, id) {
|
|
|
172
180
|
const h = Number(getSvgAttribute(rect, 'height'));
|
|
173
181
|
if (![x, y, w, h].every(Number.isFinite))
|
|
174
182
|
return null;
|
|
175
|
-
return
|
|
183
|
+
return new Box(x, y, w, h);
|
|
176
184
|
}
|
|
177
185
|
function getSvgAttribute(element, name) {
|
|
178
186
|
return element.match(new RegExp(`\\b${name}\\s*=\\s*["']([^"']+)["']`, 'i'))?.[1];
|
|
@@ -404,100 +412,13 @@ function createMapStyle() {
|
|
|
404
412
|
],
|
|
405
413
|
};
|
|
406
414
|
}
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
};
|
|
417
|
-
}
|
|
418
|
-
function width(rect) {
|
|
419
|
-
return Math.abs(rect.x2 - rect.x1);
|
|
420
|
-
}
|
|
421
|
-
function height(rect) {
|
|
422
|
-
return Math.abs(rect.y2 - rect.y1);
|
|
423
|
-
}
|
|
424
|
-
function withPadding(rect, x, y = x) {
|
|
425
|
-
if (width(rect) < x * 2 || height(rect) < y * 2)
|
|
426
|
-
return rect;
|
|
427
|
-
const cx = (rect.x1 + rect.x2) / 2;
|
|
428
|
-
const cy = (rect.y1 + rect.y2) / 2;
|
|
429
|
-
const w = width(rect) - x * 2;
|
|
430
|
-
const h = height(rect) - y * 2;
|
|
431
|
-
return normalizeRect({ x1: cx - w / 2, y1: cy - h / 2, x2: cx + w / 2, y2: cy + h / 2 });
|
|
432
|
-
}
|
|
433
|
-
const earthRadius = 6371000.0;
|
|
434
|
-
const minLongitude = -180.0;
|
|
435
|
-
const maxLongitude = 180.0;
|
|
436
|
-
const toRad = (value) => (value * Math.PI) / 180;
|
|
437
|
-
const toDeg = (value) => (value * 180) / Math.PI;
|
|
438
|
-
function convertLocalToGps(x, y, geoConfig) {
|
|
439
|
-
const diagAngle = -getAngle(geoConfig.p0, geoConfig.p2, {
|
|
440
|
-
x: geoConfig.p0.x + 10000,
|
|
441
|
-
y: geoConfig.p0.y,
|
|
442
|
-
});
|
|
443
|
-
const pointAngle = -getAngle(geoConfig.p0, { x, y }, { x: geoConfig.p0.x + 10000, y: geoConfig.p0.y });
|
|
444
|
-
const delta = pointAngle - diagAngle;
|
|
445
|
-
const diagLen = lineLength(geoConfig.p2, geoConfig.p0);
|
|
446
|
-
const pointLen = lineLength(geoConfig.p0, { x, y });
|
|
447
|
-
const perc = pointLen / diagLen;
|
|
448
|
-
const fullDistance = distance(geoConfig.p0.lat, geoConfig.p0.lng, geoConfig.p2.lat, geoConfig.p2.lng);
|
|
449
|
-
const pointDist = perc * fullDistance;
|
|
450
|
-
const baseBearing = bearing(geoConfig.p0.lat, geoConfig.p0.lng, geoConfig.p2.lat, geoConfig.p2.lng);
|
|
451
|
-
return destinationPoint(geoConfig.p0.lat, geoConfig.p0.lng, pointDist, baseBearing + delta);
|
|
452
|
-
}
|
|
453
|
-
function lineLength(p1, p2) {
|
|
454
|
-
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
|
|
455
|
-
}
|
|
456
|
-
function getAngle(centerPoint, startPoint, endPoint) {
|
|
457
|
-
const a = lineLength(startPoint, centerPoint);
|
|
458
|
-
const b = lineLength(endPoint, centerPoint);
|
|
459
|
-
const c = lineLength(startPoint, endPoint);
|
|
460
|
-
const cos = (a ** 2 + b ** 2 - c ** 2) / (2 * a * b);
|
|
461
|
-
const direction = getDirection(centerPoint, startPoint, endPoint);
|
|
462
|
-
return (direction * (Math.acos(Math.max(-1, Math.min(1, cos))) * 180)) / Math.PI || 0;
|
|
463
|
-
}
|
|
464
|
-
function getDirection(centerPoint, startPoint, endPoint) {
|
|
465
|
-
return (startPoint.x - centerPoint.x) * (endPoint.y - centerPoint.y) -
|
|
466
|
-
(startPoint.y - centerPoint.y) * (endPoint.x - centerPoint.x) <
|
|
467
|
-
0
|
|
468
|
-
? -1
|
|
469
|
-
: 1;
|
|
470
|
-
}
|
|
471
|
-
function distance(lat1, lng1, lat2, lng2) {
|
|
472
|
-
const phi1 = toRad(lat1);
|
|
473
|
-
const phi2 = toRad(lat2);
|
|
474
|
-
const deltaPhi = toRad(lat2 - lat1);
|
|
475
|
-
const deltaLambda = toRad(lng2 - lng1);
|
|
476
|
-
const a = Math.sin(deltaPhi / 2) * Math.sin(deltaPhi / 2) +
|
|
477
|
-
Math.cos(phi1) * Math.cos(phi2) * Math.sin(deltaLambda / 2) * Math.sin(deltaLambda / 2);
|
|
478
|
-
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
479
|
-
return earthRadius * c;
|
|
480
|
-
}
|
|
481
|
-
function bearing(lat1, lng1, lat2, lng2) {
|
|
482
|
-
const phi1 = toRad(lat1);
|
|
483
|
-
const phi2 = toRad(lat2);
|
|
484
|
-
const deltaLambda = toRad(lng2 - lng1);
|
|
485
|
-
const y = Math.sin(deltaLambda) * Math.cos(phi2);
|
|
486
|
-
const x = Math.cos(phi1) * Math.sin(phi2) - Math.sin(phi1) * Math.cos(phi2) * Math.cos(deltaLambda);
|
|
487
|
-
return (toDeg(Math.atan2(y, x)) + 360) % 360;
|
|
488
|
-
}
|
|
489
|
-
function destinationPoint(lat, lng, distanceMeters, pointBearing) {
|
|
490
|
-
const delta = distanceMeters / earthRadius;
|
|
491
|
-
const theta = toRad(pointBearing);
|
|
492
|
-
const phi1 = toRad(lat);
|
|
493
|
-
const lambda1 = toRad(lng);
|
|
494
|
-
const phi2 = Math.asin(Math.sin(phi1) * Math.cos(delta) + Math.cos(phi1) * Math.sin(delta) * Math.cos(theta));
|
|
495
|
-
let lambda2 = lambda1 +
|
|
496
|
-
Math.atan2(Math.sin(theta) * Math.sin(delta) * Math.cos(phi1), Math.cos(delta) - Math.sin(phi1) * Math.sin(phi2));
|
|
497
|
-
let longitude = toDeg(lambda2);
|
|
498
|
-
if (longitude < minLongitude || longitude > maxLongitude) {
|
|
499
|
-
lambda2 = ((lambda2 + 3 * Math.PI) % (2 * Math.PI)) - Math.PI;
|
|
500
|
-
longitude = toDeg(lambda2);
|
|
501
|
-
}
|
|
502
|
-
return [longitude, toDeg(phi2)];
|
|
415
|
+
// Builds the two-anchor affine bridge (p0 -> anchor a, p2 -> anchor b) used to
|
|
416
|
+
// project the SVG-area corners into GPS. All GeoConfig parsing / SVG extraction /
|
|
417
|
+
// anchor validation stays at the callsite; only the pure projection math lives in
|
|
418
|
+
// @expofp/geometry's geo module.
|
|
419
|
+
function geoAnchors(config) {
|
|
420
|
+
return [
|
|
421
|
+
{ local: { x: config.p0.x, y: config.p0.y }, geo: { lat: config.p0.lat, lng: config.p0.lng } },
|
|
422
|
+
{ local: { x: config.p2.x, y: config.p2.y }, geo: { lat: config.p2.lat, lng: config.p2.lng } },
|
|
423
|
+
];
|
|
503
424
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expofp/offline",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
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,9 +31,10 @@
|
|
|
31
31
|
"dependencies": {
|
|
32
32
|
"debug": "^4.4.3",
|
|
33
33
|
"tslib": "^2.3.0",
|
|
34
|
-
"@expofp/
|
|
35
|
-
"@expofp/
|
|
36
|
-
"@expofp/
|
|
37
|
-
"@expofp/
|
|
34
|
+
"@expofp/floorplan": "3.8.0",
|
|
35
|
+
"@expofp/data": "3.8.0",
|
|
36
|
+
"@expofp/geometry": "3.8.0",
|
|
37
|
+
"@expofp/resolve": "3.8.0",
|
|
38
|
+
"@expofp/utils": "3.8.0"
|
|
38
39
|
}
|
|
39
40
|
}
|