@zappie/foundations 0.1.4 → 0.1.5
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/CHANGELOG.md +14 -0
- package/README.md +68 -13
- package/dist/branch-map/colors.js +27 -0
- package/dist/branch-map/colors.js.map +7 -0
- package/dist/branch-map/delivery-map.js +60 -0
- package/dist/branch-map/delivery-map.js.map +7 -0
- package/dist/branch-map/delivery-runtime.js +118 -0
- package/dist/branch-map/delivery-runtime.js.map +7 -0
- package/dist/branch-map/fit.js +123 -0
- package/dist/branch-map/fit.js.map +7 -0
- package/dist/branch-map/geometry.js +1 -30
- package/dist/branch-map/geometry.js.map +2 -2
- package/dist/branch-map/index.js +21 -53
- package/dist/branch-map/index.js.map +2 -2
- package/dist/branch-map/markers.js +143 -0
- package/dist/branch-map/markers.js.map +7 -0
- package/dist/branch-map/runtime.js +235 -99
- package/dist/branch-map/runtime.js.map +3 -3
- package/dist/branch-map/use-map-runtime.js +53 -0
- package/dist/branch-map/use-map-runtime.js.map +7 -0
- package/dist/index.d.ts +56 -10
- package/dist/index.js +2 -1
- package/dist/index.js.map +2 -2
- package/dist/item-form/root.js.map +1 -1
- package/dist/overlay/react.js +224 -13
- package/dist/overlay/react.js.map +2 -2
- package/dist/version.js +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @zappie/foundations
|
|
2
2
|
|
|
3
|
+
## 0.1.5
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 282781d: Maps draw the markers the default storefront draws. `BranchMap` shows each branch as the `pinImageUrl` logo in a white ring, 40 px for the focused branch and 32 px for the rest. The focused branch is the selected one or, once the shopper's location is known, the nearest. A shopper location adds a house marker labelled “You” and a line to the focused branch, and the map frames the two with 88 px of padding. Nearby branches group into numbered clusters that zoom in when clicked, and selecting a branch inside a cluster zooms in until its pin shows. Without a shopper the map frames every branch with 64 px of padding, and a single branch is centred at zoom 14. `compact` frames a small map. Coordinates of (0, 0) count as missing. Colour clusters, the house marker, the line and label borders with `--map-accent` and `--map-accent-foreground`.
|
|
8
|
+
|
|
9
|
+
The new `DeliveryMap` shows a delivery for a cart or checkout: the branch logo, the shopper's house and the line between them, fitted to all of it, with no controls. Pass `route` to draw a path instead of a straight line. Both maps load after mount, and the clusterer loads only with the branch map.
|
|
10
|
+
|
|
11
|
+
- 51d437f: An overlay's first open on a page paints on the tap. The first open used to run the overlay engine's modal code for the first time before anything showed, which took 60 to 90 ms on a slow phone. Now the backdrop and the panel with its content paint first, and the engine takes over on the next frame. Focus handling, scroll lock, Escape, outside presses and hiding the page from assistive technology work as before once it does. Until then the panel is hidden from assistive technology and holds every key, press and scroll, so nothing on the page can be reached. Escape and an outside press still close it. Later opens, nested overlays and `forceMount` content use the engine straight away.
|
|
12
|
+
- 1c3cbbf: `ItemForm.Root` accepts a typed `onInvalid={(groups: ItemFormInvalidGroup[]) => ...}` callback. `ItemFormProps` no longer inherits the DOM `onInvalid` handler, and the root never calls `onInvalid` for a DOM `invalid` event.
|
|
13
|
+
- Updated dependencies [e7cc5f0]
|
|
14
|
+
- Updated dependencies [1c3cbbf]
|
|
15
|
+
- @zappie/sdk@0.1.9
|
|
16
|
+
|
|
3
17
|
## 0.1.4
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -98,6 +98,13 @@ function Header() {
|
|
|
98
98
|
Importing the helper never touches `window`. On the server `Component` renders
|
|
99
99
|
nothing, and it works under `StrictMode`.
|
|
100
100
|
|
|
101
|
+
The first overlay you open on a page paints on the tap. Its backdrop and panel,
|
|
102
|
+
with the content, show in the engine's markup straight away, and the overlay's
|
|
103
|
+
focus handling, scroll lock and dismissal take over on the next frame. Until
|
|
104
|
+
then the panel is hidden from assistive technology and takes no keys, presses or
|
|
105
|
+
scrolls, apart from Escape and an outside press, which close it. Style the panel
|
|
106
|
+
and `[data-zappie-backdrop]` as usual: both frames match them.
|
|
107
|
+
|
|
101
108
|
## Item form focus
|
|
102
109
|
|
|
103
110
|
When an add fails validation, `ItemForm.Root` focuses the first invalid group
|
|
@@ -149,21 +156,36 @@ import { BranchMap } from "@zappie/foundations";
|
|
|
149
156
|
/>;
|
|
150
157
|
```
|
|
151
158
|
|
|
152
|
-
Set a height on `.branch-map`.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
`--branch-map-
|
|
157
|
-
|
|
159
|
+
Set a height on `.branch-map`. With a `pinImageUrl`, such as the business logo,
|
|
160
|
+
each branch pin is the logo in a white ring: 32 px, or 40 px for the focused
|
|
161
|
+
branch. The focused branch is the selected one or, once a `shopperLocation` is
|
|
162
|
+
known, the nearest. Without a `pinImageUrl`, pins are dots themed with
|
|
163
|
+
`--branch-map-pin`, `--branch-map-pin-selected`, `--branch-map-pin-border` and
|
|
164
|
+
`--branch-map-pin-size`. Pins expose `data-selected`, `data-focused` and
|
|
165
|
+
`aria-pressed`. Theme pin labels with `--branch-map-label-background`,
|
|
166
|
+
`--branch-map-label-color` and `--branch-map-label-border`. Labels are marked
|
|
167
|
+
`data-branch-map-pin-label` and hidden from assistive technology, because the pin
|
|
168
|
+
already carries the name. `pinLabel="selected"` labels the focused pin.
|
|
169
|
+
|
|
170
|
+
With a `shopperLocation`, the map shows a house marker labelled “You” and draws a
|
|
171
|
+
line from it to the focused branch. Nearby branches group into numbered
|
|
172
|
+
clusters. Clicking a cluster zooms into it, and selecting a branch inside a
|
|
173
|
+
cluster zooms in until its pin shows. Colour the clusters, the house marker, the
|
|
174
|
+
line and the label borders with `--map-accent`, and the cluster count and house
|
|
175
|
+
icon with `--map-accent-foreground`. The map reads these when it updates, because
|
|
176
|
+
Maps draws clusters and lines without CSS.
|
|
158
177
|
|
|
159
178
|
The map fits branches with valid coordinates, including across the date line.
|
|
160
|
-
With a `shopperLocation`, it fits the
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
179
|
+
Coordinates of (0, 0) count as missing. With a `shopperLocation`, it fits the
|
|
180
|
+
shopper and the focused branch with 88 px of padding, and fits again when the
|
|
181
|
+
focused branch changes. Without one, it fits every branch with 64 px of padding
|
|
182
|
+
and a new selection pans to that branch. A single point is centred at zoom 14.
|
|
183
|
+
`compact` frames a small map: the route gets 72 px of padding above, 52 px at
|
|
184
|
+
the sides and 36 px below, and a single point is centred at zoom 13. The map
|
|
185
|
+
fits again only when what it frames or `maxZoom` changes. Branches without
|
|
186
|
+
coordinates remain selectable with “Map location unavailable”. `maxZoom`
|
|
187
|
+
defaults to `15` with a shopper location and `14` without one. `mapTypeControl`
|
|
188
|
+
defaults to `false`.
|
|
167
189
|
|
|
168
190
|
The component renders nothing without a key. It loads its map runtime and the
|
|
169
191
|
Maps JavaScript API after mount, sharing one script across mounted maps.
|
|
@@ -172,3 +194,36 @@ With `renderLoadError({ error, retry })` you render the message, and `retry`
|
|
|
172
194
|
loads the map again. Without it the map shows “Map location unavailable”.
|
|
173
195
|
The optional location button pans to coordinates supplied by your app. Your app
|
|
174
196
|
requests geolocation permission and supplies those coordinates.
|
|
197
|
+
|
|
198
|
+
## Delivery map
|
|
199
|
+
|
|
200
|
+
`DeliveryMap` shows where an order goes: the branch logo marker, the shopper's
|
|
201
|
+
house marker and the line between them, fitted to all of it. Use it in a cart or
|
|
202
|
+
checkout in place of a static map image. It is a preview, so it has no controls
|
|
203
|
+
and ignores gestures. Pass `route`, such as a decoded driving route, to draw that
|
|
204
|
+
path instead of a straight line.
|
|
205
|
+
|
|
206
|
+
```tsx
|
|
207
|
+
import { DeliveryMap } from "@zappie/foundations";
|
|
208
|
+
|
|
209
|
+
<DeliveryMap
|
|
210
|
+
branch={branch}
|
|
211
|
+
shopper={deliveryAddress.coordinates}
|
|
212
|
+
browserKey={integrations.googleMaps?.browserKey}
|
|
213
|
+
pinImageUrl={store.brand.logoUrl}
|
|
214
|
+
renderLoadError={({ retry }) => (
|
|
215
|
+
<button type="button" onClick={retry}>
|
|
216
|
+
Try again
|
|
217
|
+
</button>
|
|
218
|
+
)}
|
|
219
|
+
className="delivery-map"
|
|
220
|
+
/>;
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Set a height on `.delivery-map`. The default storefront uses 180 px. The branch
|
|
224
|
+
marker is a 36 px logo, the house marker uses `--map-accent` and the line uses
|
|
225
|
+
`--delivery-map-route` at 60% opacity. Without a `pinImageUrl` the branch marker
|
|
226
|
+
is the themed dot. Like `BranchMap`, it renders nothing without a key, loads
|
|
227
|
+
Maps after mount, calls `onLoadError(error)` on a failure and shows “Map location
|
|
228
|
+
unavailable” when either point has no valid coordinates. It fits again when the
|
|
229
|
+
points, the route or its size change.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// src/branch-map/colors.ts
|
|
2
|
+
var DEFAULT_COLORS = {
|
|
3
|
+
accent: "#111111",
|
|
4
|
+
accentForeground: "#ffffff"
|
|
5
|
+
};
|
|
6
|
+
var COLOR_PROBE_STYLE = {
|
|
7
|
+
color: "var(--map-accent, currentColor)",
|
|
8
|
+
backgroundColor: "var(--map-accent-foreground, #ffffff)",
|
|
9
|
+
borderColor: "var(--delivery-map-route, #163300)"
|
|
10
|
+
};
|
|
11
|
+
function readColors(probe) {
|
|
12
|
+
if (!probe || typeof getComputedStyle !== "function") {
|
|
13
|
+
return DEFAULT_COLORS;
|
|
14
|
+
}
|
|
15
|
+
const style = getComputedStyle(probe);
|
|
16
|
+
return {
|
|
17
|
+
accent: style.color || DEFAULT_COLORS.accent,
|
|
18
|
+
accentForeground: style.backgroundColor || DEFAULT_COLORS.accentForeground,
|
|
19
|
+
route: style.borderTopColor || void 0
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export {
|
|
23
|
+
COLOR_PROBE_STYLE,
|
|
24
|
+
DEFAULT_COLORS,
|
|
25
|
+
readColors
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=colors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/colors.ts"],
|
|
4
|
+
"sourcesContent": ["/** Colours resolved from CSS, for map parts that cannot read CSS variables. */\nexport type MapColors = {\n accent: string;\n accentForeground: string;\n /** The delivery map's line. */\n route?: string | undefined;\n};\n\nexport const DEFAULT_COLORS: MapColors = {\n accent: \"#111111\",\n accentForeground: \"#ffffff\",\n};\n\n/** Inline style for the hidden element whose computed colours the map reads. */\nexport const COLOR_PROBE_STYLE = {\n color: \"var(--map-accent, currentColor)\",\n backgroundColor: \"var(--map-accent-foreground, #ffffff)\",\n borderColor: \"var(--delivery-map-route, #163300)\",\n} as const;\n\n// Cluster icons and lines are drawn by Maps, which cannot read CSS variables.\nexport function readColors(probe: HTMLElement | null): MapColors {\n if (!probe || typeof getComputedStyle !== \"function\") {\n return DEFAULT_COLORS;\n }\n const style = getComputedStyle(probe);\n return {\n accent: style.color || DEFAULT_COLORS.accent,\n accentForeground: style.backgroundColor || DEFAULT_COLORS.accentForeground,\n route: style.borderTopColor || undefined,\n };\n}\n"],
|
|
5
|
+
"mappings": ";AAQO,IAAM,iBAA4B;AAAA,EACvC,QAAQ;AAAA,EACR,kBAAkB;AACpB;AAGO,IAAM,oBAAoB;AAAA,EAC/B,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,aAAa;AACf;AAGO,SAAS,WAAW,OAAsC;AAC/D,MAAI,CAAC,SAAS,OAAO,qBAAqB,YAAY;AACpD,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,iBAAiB,KAAK;AACpC,SAAO;AAAA,IACL,QAAQ,MAAM,SAAS,eAAe;AAAA,IACtC,kBAAkB,MAAM,mBAAmB,eAAe;AAAA,IAC1D,OAAO,MAAM,kBAAkB;AAAA,EACjC;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/branch-map/delivery-map.tsx
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { COLOR_PROBE_STYLE, readColors } from "./colors.js";
|
|
6
|
+
import { validCoordinates } from "./geometry.js";
|
|
7
|
+
import { useMapRuntime } from "./use-map-runtime.js";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
var loadDeliveryMap = (target, key, signal) => import("./delivery-runtime.js").then(
|
|
10
|
+
({ createDeliveryMap }) => createDeliveryMap(target, key, signal)
|
|
11
|
+
);
|
|
12
|
+
function DeliveryMap({
|
|
13
|
+
branch,
|
|
14
|
+
shopper,
|
|
15
|
+
route,
|
|
16
|
+
browserKey,
|
|
17
|
+
pinImageUrl,
|
|
18
|
+
onLoadError,
|
|
19
|
+
renderLoadError,
|
|
20
|
+
...props
|
|
21
|
+
}) {
|
|
22
|
+
const probe = useRef(null);
|
|
23
|
+
const origin = validCoordinates(branch.coordinates) ? branch.coordinates : void 0;
|
|
24
|
+
const destination = validCoordinates(shopper) ? shopper : void 0;
|
|
25
|
+
const mappable = !!origin && !!destination;
|
|
26
|
+
const { element, runtime, failure, retry } = useMapRuntime(browserKey, mappable, loadDeliveryMap, onLoadError);
|
|
27
|
+
const failed = failure !== void 0;
|
|
28
|
+
const coordinates = branch.coordinates;
|
|
29
|
+
useEffect(() => {
|
|
30
|
+
if (runtime && validCoordinates(coordinates) && validCoordinates(shopper)) {
|
|
31
|
+
runtime.update({
|
|
32
|
+
branch: coordinates,
|
|
33
|
+
shopper,
|
|
34
|
+
route,
|
|
35
|
+
pinImageUrl,
|
|
36
|
+
colors: readColors(probe.current)
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}, [runtime, coordinates, shopper, route, pinImageUrl]);
|
|
40
|
+
if (!browserKey) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return /* @__PURE__ */ jsxs("div", { ...props, "data-delivery-map": "", children: [
|
|
44
|
+
/* @__PURE__ */ jsx("span", { ref: probe, hidden: true, style: COLOR_PROBE_STYLE }),
|
|
45
|
+
/* @__PURE__ */ jsx(
|
|
46
|
+
"div",
|
|
47
|
+
{
|
|
48
|
+
hidden: failed || !mappable,
|
|
49
|
+
ref: element,
|
|
50
|
+
"data-delivery-map-canvas": "",
|
|
51
|
+
style: { height: "100%" }
|
|
52
|
+
}
|
|
53
|
+
),
|
|
54
|
+
failure && renderLoadError ? renderLoadError({ error: failure.error, retry }) : failed || !mappable ? /* @__PURE__ */ jsx("p", { role: "status", children: "Map location unavailable" }) : null
|
|
55
|
+
] });
|
|
56
|
+
}
|
|
57
|
+
export {
|
|
58
|
+
DeliveryMap
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=delivery-map.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/delivery-map.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport type { Branch } from \"@zappie/sdk\";\nimport { useEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\nimport { COLOR_PROBE_STYLE, readColors } from \"./colors\";\nimport type { createDeliveryMap } from \"./delivery-runtime\";\nimport { validCoordinates, type Coordinates } from \"./geometry\";\nimport { useMapRuntime, type MapLoadErrorState } from \"./use-map-runtime\";\n\n/** The branch, the shopper's address and an optional route for DeliveryMap. */\nexport type DeliveryMapProps = HTMLAttributes<HTMLDivElement> & {\n /** The branch the order leaves from. Only its coordinates are read. */\n branch: Pick<Branch, \"coordinates\">;\n /** Coordinates of the delivery address. */\n shopper?: Coordinates | undefined;\n /**\n * The path to draw, such as a decoded driving route, from the branch to the\n * shopper. Without it the map draws a straight line.\n */\n route?: Coordinates[];\n browserKey?: string;\n /** Image shown inside the branch marker, such as the business logo. */\n pinImageUrl?: string;\n /** Called each time the map fails to load. */\n onLoadError?: (error: unknown) => void;\n /**\n * Render your own message when the map fails to load, such as a retry\n * button that calls `retry`. Without it the map shows \"Map location unavailable\".\n */\n renderLoadError?: (state: MapLoadErrorState) => ReactNode;\n};\n\nconst loadDeliveryMap = (\n target: HTMLElement,\n key: string,\n signal: AbortSignal,\n) =>\n import(\"./delivery-runtime\").then(({ createDeliveryMap }) =>\n createDeliveryMap(target, key, signal),\n );\n\n/**\n * A static map of a delivery: the branch marker, the shopper's house marker\n * and the line between them, fitted to both. It has no controls and ignores\n * gestures. Maps loads after mount when you supply a browser key. Set the\n * container height with CSS. Colour the house marker with --map-accent and\n * the line with --delivery-map-route.\n */\nexport function DeliveryMap({\n branch,\n shopper,\n route,\n browserKey,\n pinImageUrl,\n onLoadError,\n renderLoadError,\n ...props\n}: DeliveryMapProps) {\n const probe = useRef<HTMLSpanElement>(null);\n const origin = validCoordinates(branch.coordinates)\n ? branch.coordinates\n : undefined;\n const destination = validCoordinates(shopper) ? shopper : undefined;\n const mappable = !!origin && !!destination;\n const { element, runtime, failure, retry } = useMapRuntime<\n Awaited<ReturnType<typeof createDeliveryMap>>\n >(browserKey, mappable, loadDeliveryMap, onLoadError);\n const failed = failure !== undefined;\n const coordinates = branch.coordinates;\n useEffect(() => {\n if (runtime && validCoordinates(coordinates) && validCoordinates(shopper)) {\n runtime.update({\n branch: coordinates,\n shopper,\n route,\n pinImageUrl,\n colors: readColors(probe.current),\n });\n }\n }, [runtime, coordinates, shopper, route, pinImageUrl]);\n if (!browserKey) {\n return null;\n }\n return (\n <div {...props} data-delivery-map=\"\">\n <span ref={probe} hidden style={COLOR_PROBE_STYLE} />\n <div\n hidden={failed || !mappable}\n ref={element}\n data-delivery-map-canvas=\"\"\n style={{ height: \"100%\" }}\n />\n {failure && renderLoadError ? (\n renderLoadError({ error: failure.error, retry })\n ) : failed || !mappable ? (\n <p role=\"status\">Map location unavailable</p>\n ) : null}\n </div>\n );\n}\n"],
|
|
5
|
+
"mappings": ";;;AAEA,SAAS,WAAW,cAAmD;AACvE,SAAS,mBAAmB,kBAAkB;AAE9C,SAAS,wBAA0C;AACnD,SAAS,qBAA6C;AA6ElD,SACE,KADF;AApDJ,IAAM,kBAAkB,CACtB,QACA,KACA,WAEA,OAAO,uBAAoB,EAAE;AAAA,EAAK,CAAC,EAAE,kBAAkB,MACrD,kBAAkB,QAAQ,KAAK,MAAM;AACvC;AASK,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,QAAQ,OAAwB,IAAI;AAC1C,QAAM,SAAS,iBAAiB,OAAO,WAAW,IAC9C,OAAO,cACP;AACJ,QAAM,cAAc,iBAAiB,OAAO,IAAI,UAAU;AAC1D,QAAM,WAAW,CAAC,CAAC,UAAU,CAAC,CAAC;AAC/B,QAAM,EAAE,SAAS,SAAS,SAAS,MAAM,IAAI,cAE3C,YAAY,UAAU,iBAAiB,WAAW;AACpD,QAAM,SAAS,YAAY;AAC3B,QAAM,cAAc,OAAO;AAC3B,YAAU,MAAM;AACd,QAAI,WAAW,iBAAiB,WAAW,KAAK,iBAAiB,OAAO,GAAG;AACzE,cAAQ,OAAO;AAAA,QACb,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,WAAW,MAAM,OAAO;AAAA,MAClC,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,SAAS,aAAa,SAAS,OAAO,WAAW,CAAC;AACtD,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AACA,SACE,qBAAC,SAAK,GAAG,OAAO,qBAAkB,IAChC;AAAA,wBAAC,UAAK,KAAK,OAAO,QAAM,MAAC,OAAO,mBAAmB;AAAA,IACnD;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,UAAU,CAAC;AAAA,QACnB,KAAK;AAAA,QACL,4BAAyB;AAAA,QACzB,OAAO,EAAE,QAAQ,OAAO;AAAA;AAAA,IAC1B;AAAA,IACC,WAAW,kBACV,gBAAgB,EAAE,OAAO,QAAQ,OAAO,MAAM,CAAC,IAC7C,UAAU,CAAC,WACb,oBAAC,OAAE,MAAK,UAAS,sCAAwB,IACvC;AAAA,KACN;AAEJ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// src/branch-map/delivery-runtime.ts
|
|
2
|
+
import { pointBounds } from "./fit.js";
|
|
3
|
+
import { loadMaps } from "./loader.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_COLORS,
|
|
6
|
+
markerOverlayClass,
|
|
7
|
+
paintMarker
|
|
8
|
+
} from "./markers.js";
|
|
9
|
+
var DELIVERY_ROUTE = {
|
|
10
|
+
color: "#163300",
|
|
11
|
+
opacity: 0.6,
|
|
12
|
+
weight: 5
|
|
13
|
+
};
|
|
14
|
+
var DELIVERY_LOGO_SIZE = 36;
|
|
15
|
+
function deliveryPath(update) {
|
|
16
|
+
const via = (update.route ?? []).filter(
|
|
17
|
+
(point) => Number.isFinite(point.lat) && Number.isFinite(point.lng)
|
|
18
|
+
);
|
|
19
|
+
return via.length >= 2 ? via : [update.branch, update.shopper];
|
|
20
|
+
}
|
|
21
|
+
function deliveryBounds(update) {
|
|
22
|
+
return pointBounds([update.branch, update.shopper, ...deliveryPath(update)]);
|
|
23
|
+
}
|
|
24
|
+
async function createDeliveryMap(element, key, signal) {
|
|
25
|
+
const maps = await loadMaps(key);
|
|
26
|
+
signal.throwIfAborted();
|
|
27
|
+
const map = new maps.Map(element, {
|
|
28
|
+
center: { lat: 0, lng: 0 },
|
|
29
|
+
zoom: 12,
|
|
30
|
+
gestureHandling: "none",
|
|
31
|
+
disableDefaultUI: true,
|
|
32
|
+
keyboardShortcuts: false,
|
|
33
|
+
clickableIcons: false
|
|
34
|
+
});
|
|
35
|
+
const Marker = markerOverlayClass(maps);
|
|
36
|
+
let branchPin;
|
|
37
|
+
let shopperPin;
|
|
38
|
+
let line;
|
|
39
|
+
let framed;
|
|
40
|
+
let last;
|
|
41
|
+
const pin = (key2, position) => {
|
|
42
|
+
const node = document.createElement("div");
|
|
43
|
+
node.setAttribute("aria-hidden", "true");
|
|
44
|
+
node.dataset.deliveryMapPin = key2;
|
|
45
|
+
const marker = new Marker(key2, position, node);
|
|
46
|
+
marker.setMap(map);
|
|
47
|
+
return marker;
|
|
48
|
+
};
|
|
49
|
+
const frame = (update) => {
|
|
50
|
+
map.fitBounds(deliveryBounds(update));
|
|
51
|
+
};
|
|
52
|
+
const resize = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
|
|
53
|
+
if (last) {
|
|
54
|
+
frame(last);
|
|
55
|
+
}
|
|
56
|
+
}) : void 0;
|
|
57
|
+
resize?.observe(element);
|
|
58
|
+
return {
|
|
59
|
+
update(update) {
|
|
60
|
+
last = update;
|
|
61
|
+
const colors = update.colors ?? DEFAULT_COLORS;
|
|
62
|
+
branchPin ?? (branchPin = pin("branch", update.branch));
|
|
63
|
+
branchPin.setPosition(update.branch);
|
|
64
|
+
if (update.pinImageUrl) {
|
|
65
|
+
paintMarker(
|
|
66
|
+
branchPin.element,
|
|
67
|
+
{
|
|
68
|
+
kind: "logo",
|
|
69
|
+
size: DELIVERY_LOGO_SIZE,
|
|
70
|
+
imageUrl: update.pinImageUrl,
|
|
71
|
+
shadow: "sm",
|
|
72
|
+
lift: DELIVERY_LOGO_SIZE / 2
|
|
73
|
+
},
|
|
74
|
+
colors
|
|
75
|
+
);
|
|
76
|
+
} else {
|
|
77
|
+
paintMarker(branchPin.element, { kind: "dot", selected: true }, colors);
|
|
78
|
+
}
|
|
79
|
+
branchPin.element.style.cursor = "default";
|
|
80
|
+
shopperPin ?? (shopperPin = pin("shopper", update.shopper));
|
|
81
|
+
shopperPin.setPosition(update.shopper);
|
|
82
|
+
paintMarker(
|
|
83
|
+
shopperPin.element,
|
|
84
|
+
{ kind: "house", color: "#ffffff", lift: 16 },
|
|
85
|
+
colors
|
|
86
|
+
);
|
|
87
|
+
line ?? (line = new maps.Polyline({ map }));
|
|
88
|
+
line.setOptions({
|
|
89
|
+
path: deliveryPath(update),
|
|
90
|
+
strokeColor: update.colors?.route ?? DELIVERY_ROUTE.color,
|
|
91
|
+
strokeOpacity: DELIVERY_ROUTE.opacity,
|
|
92
|
+
strokeWeight: DELIVERY_ROUTE.weight,
|
|
93
|
+
clickable: false
|
|
94
|
+
});
|
|
95
|
+
const key2 = JSON.stringify(deliveryBounds(update));
|
|
96
|
+
if (key2 !== framed) {
|
|
97
|
+
framed = key2;
|
|
98
|
+
frame(update);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
destroy() {
|
|
102
|
+
resize?.disconnect();
|
|
103
|
+
branchPin?.setMap(null);
|
|
104
|
+
shopperPin?.setMap(null);
|
|
105
|
+
line?.setMap(null);
|
|
106
|
+
maps.event.clearInstanceListeners(map);
|
|
107
|
+
element.replaceChildren();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
export {
|
|
112
|
+
DELIVERY_LOGO_SIZE,
|
|
113
|
+
DELIVERY_ROUTE,
|
|
114
|
+
createDeliveryMap,
|
|
115
|
+
deliveryBounds,
|
|
116
|
+
deliveryPath
|
|
117
|
+
};
|
|
118
|
+
//# sourceMappingURL=delivery-runtime.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/delivery-runtime.ts"],
|
|
4
|
+
"sourcesContent": ["import { pointBounds } from \"./fit\";\nimport type { Coordinates } from \"./geometry\";\nimport { loadMaps } from \"./loader\";\nimport {\n DEFAULT_COLORS,\n markerOverlayClass,\n paintMarker,\n type MapColors,\n type MarkerOverlay,\n} from \"./markers\";\n\nexport type DeliveryMapUpdate = {\n branch: Coordinates;\n shopper: Coordinates;\n route?: Coordinates[] | undefined;\n pinImageUrl?: string | undefined;\n colors?: MapColors | undefined;\n};\n\n/** The line between the branch and the shopper. */\nexport const DELIVERY_ROUTE = {\n color: \"#163300\",\n opacity: 0.6,\n weight: 5,\n} as const;\n/** Diameter of the branch logo marker. */\nexport const DELIVERY_LOGO_SIZE = 36;\n\n/** The points the delivery map frames and the path it draws. */\nexport function deliveryPath(update: DeliveryMapUpdate) {\n const via = (update.route ?? []).filter(\n (point) => Number.isFinite(point.lat) && Number.isFinite(point.lng),\n );\n return via.length >= 2 ? via : [update.branch, update.shopper];\n}\n\nexport function deliveryBounds(update: DeliveryMapUpdate) {\n return pointBounds([update.branch, update.shopper, ...deliveryPath(update)])!;\n}\n\nexport async function createDeliveryMap(\n element: HTMLElement,\n key: string,\n signal: AbortSignal,\n) {\n const maps = await loadMaps(key);\n signal.throwIfAborted();\n // A static preview: no panning, zooming, dragging or controls.\n const map = new maps.Map(element, {\n center: { lat: 0, lng: 0 },\n zoom: 12,\n gestureHandling: \"none\",\n disableDefaultUI: true,\n keyboardShortcuts: false,\n clickableIcons: false,\n });\n const Marker = markerOverlayClass(maps);\n let branchPin: MarkerOverlay | undefined;\n let shopperPin: MarkerOverlay | undefined;\n let line: google.maps.Polyline | undefined;\n let framed: string | undefined;\n let last: DeliveryMapUpdate | undefined;\n const pin = (key: string, position: Coordinates) => {\n const node = document.createElement(\"div\");\n node.setAttribute(\"aria-hidden\", \"true\");\n node.dataset.deliveryMapPin = key;\n const marker = new Marker(key, position, node);\n marker.setMap(map);\n return marker;\n };\n const frame = (update: DeliveryMapUpdate) => {\n map.fitBounds(deliveryBounds(update));\n };\n const resize =\n typeof ResizeObserver === \"function\"\n ? new ResizeObserver(() => {\n if (last) {\n frame(last);\n }\n })\n : undefined;\n resize?.observe(element);\n return {\n update(update: DeliveryMapUpdate) {\n last = update;\n const colors = update.colors ?? DEFAULT_COLORS;\n branchPin ??= pin(\"branch\", update.branch);\n branchPin.setPosition(update.branch);\n if (update.pinImageUrl) {\n // The logo is the marker itself, so its centre sits half its size above the location.\n paintMarker(\n branchPin.element,\n {\n kind: \"logo\",\n size: DELIVERY_LOGO_SIZE,\n imageUrl: update.pinImageUrl,\n shadow: \"sm\",\n lift: DELIVERY_LOGO_SIZE / 2,\n },\n colors,\n );\n } else {\n paintMarker(branchPin.element, { kind: \"dot\", selected: true }, colors);\n }\n branchPin.element.style.cursor = \"default\";\n shopperPin ??= pin(\"shopper\", update.shopper);\n shopperPin.setPosition(update.shopper);\n paintMarker(\n shopperPin.element,\n { kind: \"house\", color: \"#ffffff\", lift: 16 },\n colors,\n );\n line ??= new maps.Polyline({ map });\n line.setOptions({\n path: deliveryPath(update),\n strokeColor: update.colors?.route ?? DELIVERY_ROUTE.color,\n strokeOpacity: DELIVERY_ROUTE.opacity,\n strokeWeight: DELIVERY_ROUTE.weight,\n clickable: false,\n });\n const key = JSON.stringify(deliveryBounds(update));\n if (key !== framed) {\n framed = key;\n frame(update);\n }\n },\n destroy() {\n resize?.disconnect();\n branchPin?.setMap(null);\n shopperPin?.setMap(null);\n line?.setMap(null);\n maps.event.clearInstanceListeners(map);\n element.replaceChildren();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["key"]
|
|
7
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// src/branch-map/fit.ts
|
|
2
|
+
import {
|
|
3
|
+
validCoordinates,
|
|
4
|
+
visibleBranches
|
|
5
|
+
} from "./geometry.js";
|
|
6
|
+
var OVERVIEW_PADDING = 64;
|
|
7
|
+
var ROUTE_PADDING = {
|
|
8
|
+
top: 88,
|
|
9
|
+
right: 88,
|
|
10
|
+
bottom: 88,
|
|
11
|
+
left: 88
|
|
12
|
+
};
|
|
13
|
+
var COMPACT_ROUTE_PADDING = {
|
|
14
|
+
top: 72,
|
|
15
|
+
right: 52,
|
|
16
|
+
bottom: 36,
|
|
17
|
+
left: 52
|
|
18
|
+
};
|
|
19
|
+
var SINGLE_POINT_ZOOM = 14;
|
|
20
|
+
var COMPACT_SINGLE_POINT_ZOOM = 13;
|
|
21
|
+
function distanceKm(from, to) {
|
|
22
|
+
const radians = (degrees) => degrees * Math.PI / 180;
|
|
23
|
+
const dLat = radians(to.lat - from.lat);
|
|
24
|
+
const dLng = radians(to.lng - from.lng);
|
|
25
|
+
const a = Math.sin(dLat / 2) ** 2 + Math.cos(radians(from.lat)) * Math.cos(radians(to.lat)) * Math.sin(dLng / 2) ** 2;
|
|
26
|
+
return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
|
27
|
+
}
|
|
28
|
+
function focusedBranch(branches, selectedId, shopper) {
|
|
29
|
+
const visible = visibleBranches(branches);
|
|
30
|
+
const selected = visible.find((branch) => branch.id === selectedId);
|
|
31
|
+
if (selected || !validCoordinates(shopper)) {
|
|
32
|
+
return selected;
|
|
33
|
+
}
|
|
34
|
+
let nearest;
|
|
35
|
+
let best = Infinity;
|
|
36
|
+
for (const branch of visible) {
|
|
37
|
+
const distance = distanceKm(shopper, branch.coordinates);
|
|
38
|
+
if (distance < best) {
|
|
39
|
+
best = distance;
|
|
40
|
+
nearest = branch;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return nearest;
|
|
44
|
+
}
|
|
45
|
+
function clusterSize(count) {
|
|
46
|
+
return count < 10 ? 40 : count < 50 ? 48 : 56;
|
|
47
|
+
}
|
|
48
|
+
function pointBounds(points) {
|
|
49
|
+
if (!points.length) {
|
|
50
|
+
return void 0;
|
|
51
|
+
}
|
|
52
|
+
const wrapped = (lng) => (lng + 360) % 360;
|
|
53
|
+
const longitudes = points.map((point) => point.lng).sort((a, b) => wrapped(a) - wrapped(b));
|
|
54
|
+
let gap = -1;
|
|
55
|
+
let start = 0;
|
|
56
|
+
for (let index = 0; index < longitudes.length; index++) {
|
|
57
|
+
const next = wrapped(longitudes[(index + 1) % longitudes.length]) + (index === longitudes.length - 1 ? 360 : 0);
|
|
58
|
+
if (next - wrapped(longitudes[index]) > gap) {
|
|
59
|
+
gap = next - wrapped(longitudes[index]);
|
|
60
|
+
start = (index + 1) % longitudes.length;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
north: Math.max(...points.map((point) => point.lat)),
|
|
65
|
+
south: Math.min(...points.map((point) => point.lat)),
|
|
66
|
+
west: longitudes[start],
|
|
67
|
+
east: longitudes[(start + longitudes.length - 1) % longitudes.length]
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function branchBounds(branches, location) {
|
|
71
|
+
const points = visibleBranches(branches).map((branch) => branch.coordinates);
|
|
72
|
+
if (validCoordinates(location)) {
|
|
73
|
+
points.push(location);
|
|
74
|
+
}
|
|
75
|
+
return pointBounds(points);
|
|
76
|
+
}
|
|
77
|
+
function fitPlan({
|
|
78
|
+
branches,
|
|
79
|
+
shopper,
|
|
80
|
+
focused,
|
|
81
|
+
maxZoom,
|
|
82
|
+
compact = false
|
|
83
|
+
}) {
|
|
84
|
+
const location = validCoordinates(shopper) ? shopper : void 0;
|
|
85
|
+
const points = location && focused ? [location, focused.coordinates] : [
|
|
86
|
+
...location ? [location] : [],
|
|
87
|
+
...visibleBranches(branches).map((branch) => branch.coordinates)
|
|
88
|
+
];
|
|
89
|
+
if (!points.length) {
|
|
90
|
+
return void 0;
|
|
91
|
+
}
|
|
92
|
+
const route = !!(location && focused);
|
|
93
|
+
if (points.length === 1) {
|
|
94
|
+
return {
|
|
95
|
+
kind: "center",
|
|
96
|
+
center: points[0],
|
|
97
|
+
zoom: Math.min(
|
|
98
|
+
maxZoom,
|
|
99
|
+
compact ? COMPACT_SINGLE_POINT_ZOOM : SINGLE_POINT_ZOOM
|
|
100
|
+
)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
kind: "bounds",
|
|
105
|
+
bounds: pointBounds(points),
|
|
106
|
+
padding: route ? compact ? COMPACT_ROUTE_PADDING : ROUTE_PADDING : OVERVIEW_PADDING,
|
|
107
|
+
maxZoom
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
export {
|
|
111
|
+
COMPACT_ROUTE_PADDING,
|
|
112
|
+
COMPACT_SINGLE_POINT_ZOOM,
|
|
113
|
+
OVERVIEW_PADDING,
|
|
114
|
+
ROUTE_PADDING,
|
|
115
|
+
SINGLE_POINT_ZOOM,
|
|
116
|
+
branchBounds,
|
|
117
|
+
clusterSize,
|
|
118
|
+
distanceKm,
|
|
119
|
+
fitPlan,
|
|
120
|
+
focusedBranch,
|
|
121
|
+
pointBounds
|
|
122
|
+
};
|
|
123
|
+
//# sourceMappingURL=fit.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/fit.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Branch } from \"@zappie/sdk\";\nimport {\n validCoordinates,\n visibleBranches,\n type Bounds,\n type Coordinates,\n} from \"./geometry\";\n\nexport type Padding =\n number | { top: number; right: number; bottom: number; left: number };\n\n/** Padding around every fitted point when no route is drawn. */\nexport const OVERVIEW_PADDING = 64;\n/** Padding around the shopper-to-branch route. */\nexport const ROUTE_PADDING: Padding = {\n top: 88,\n right: 88,\n bottom: 88,\n left: 88,\n};\n/** Route padding in a compact map, with room for both pin labels. */\nexport const COMPACT_ROUTE_PADDING: Padding = {\n top: 72,\n right: 52,\n bottom: 36,\n left: 52,\n};\n/** Zoom for a single point, in a full and a compact map. */\nexport const SINGLE_POINT_ZOOM = 14;\nexport const COMPACT_SINGLE_POINT_ZOOM = 13;\n\n/** Great-circle distance in kilometres. */\nexport function distanceKm(from: Coordinates, to: Coordinates) {\n const radians = (degrees: number) => (degrees * Math.PI) / 180;\n const dLat = radians(to.lat - from.lat);\n const dLng = radians(to.lng - from.lng);\n const a =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(radians(from.lat)) *\n Math.cos(radians(to.lat)) *\n Math.sin(dLng / 2) ** 2;\n return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n\n/**\n * The branch the map highlights: the selected one, or, once the shopper's\n * location is known, the nearest branch.\n */\nexport function focusedBranch(\n branches: Branch[],\n selectedId: string | undefined,\n shopper: Coordinates | undefined,\n) {\n const visible = visibleBranches(branches);\n const selected = visible.find((branch) => branch.id === selectedId);\n if (selected || !validCoordinates(shopper)) {\n return selected;\n }\n let nearest: (typeof visible)[number] | undefined;\n let best = Infinity;\n for (const branch of visible) {\n const distance = distanceKm(shopper, branch.coordinates);\n if (distance < best) {\n best = distance;\n nearest = branch;\n }\n }\n return nearest;\n}\n\n/** Diameter of a cluster icon for the number of branches it holds. */\nexport function clusterSize(count: number) {\n return count < 10 ? 40 : count < 50 ? 48 : 56;\n}\n\nexport function pointBounds(points: Coordinates[]): Bounds | undefined {\n if (!points.length) {\n return undefined;\n }\n // The largest longitude gap lies outside the bounds, including across the date line.\n // Sort by the wrapped longitude but report the original values, so they stay exact.\n const wrapped = (lng: number) => (lng + 360) % 360;\n const longitudes = points\n .map((point) => point.lng)\n .sort((a, b) => wrapped(a) - wrapped(b));\n let gap = -1;\n let start = 0;\n for (let index = 0; index < longitudes.length; index++) {\n const next =\n wrapped(longitudes[(index + 1) % longitudes.length]!) +\n (index === longitudes.length - 1 ? 360 : 0);\n if (next - wrapped(longitudes[index]!) > gap) {\n gap = next - wrapped(longitudes[index]!);\n start = (index + 1) % longitudes.length;\n }\n }\n return {\n north: Math.max(...points.map((point) => point.lat)),\n south: Math.min(...points.map((point) => point.lat)),\n west: longitudes[start]!,\n east: longitudes[(start + longitudes.length - 1) % longitudes.length]!,\n };\n}\n\nexport function branchBounds(branches: Branch[], location?: Coordinates) {\n const points = visibleBranches(branches).map((branch) => branch.coordinates);\n if (validCoordinates(location)) {\n points.push(location);\n }\n return pointBounds(points);\n}\n\n/** How the map frames its points. */\nexport type FitPlan =\n | { kind: \"center\"; center: Coordinates; zoom: number }\n | { kind: \"bounds\"; bounds: Bounds; padding: Padding; maxZoom: number };\n\n/**\n * With a route, frame the shopper and the focused branch. Otherwise frame the\n * shopper and every branch. A single point is centred at a fixed zoom.\n */\nexport function fitPlan({\n branches,\n shopper,\n focused,\n maxZoom,\n compact = false,\n}: {\n branches: Branch[];\n shopper?: Coordinates | undefined;\n focused?: (Branch & { coordinates: Coordinates }) | undefined;\n maxZoom: number;\n compact?: boolean;\n}): FitPlan | undefined {\n const location = validCoordinates(shopper) ? shopper : undefined;\n const points =\n location && focused\n ? [location, focused.coordinates]\n : [\n ...(location ? [location] : []),\n ...visibleBranches(branches).map((branch) => branch.coordinates),\n ];\n if (!points.length) {\n return undefined;\n }\n const route = !!(location && focused);\n if (points.length === 1) {\n return {\n kind: \"center\",\n center: points[0]!,\n zoom: Math.min(\n maxZoom,\n compact ? COMPACT_SINGLE_POINT_ZOOM : SINGLE_POINT_ZOOM,\n ),\n };\n }\n return {\n kind: \"bounds\",\n bounds: pointBounds(points)!,\n padding: route\n ? compact\n ? COMPACT_ROUTE_PADDING\n : ROUTE_PADDING\n : OVERVIEW_PADDING,\n maxZoom,\n };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -4,39 +4,10 @@ function validCoordinates(value) {
|
|
|
4
4
|
}
|
|
5
5
|
function visibleBranches(branches) {
|
|
6
6
|
return branches.filter(
|
|
7
|
-
(branch) => validCoordinates(branch.coordinates)
|
|
7
|
+
(branch) => validCoordinates(branch.coordinates) && (branch.coordinates.lat !== 0 || branch.coordinates.lng !== 0)
|
|
8
8
|
);
|
|
9
9
|
}
|
|
10
|
-
function branchBounds(branches, location) {
|
|
11
|
-
const points = visibleBranches(branches).map((branch) => branch.coordinates);
|
|
12
|
-
if (validCoordinates(location)) {
|
|
13
|
-
points.push(location);
|
|
14
|
-
}
|
|
15
|
-
if (!points.length) {
|
|
16
|
-
return void 0;
|
|
17
|
-
}
|
|
18
|
-
const longitudes = points.map((point) => (point.lng + 360) % 360).sort((a, b) => a - b);
|
|
19
|
-
let gap = -1;
|
|
20
|
-
let start = 0;
|
|
21
|
-
for (let index = 0; index < longitudes.length; index++) {
|
|
22
|
-
const next = longitudes[(index + 1) % longitudes.length] + (index === longitudes.length - 1 ? 360 : 0);
|
|
23
|
-
if (next - longitudes[index] > gap) {
|
|
24
|
-
gap = next - longitudes[index];
|
|
25
|
-
start = (index + 1) % longitudes.length;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
const normalize = (lng) => lng > 180 ? lng - 360 : lng;
|
|
29
|
-
return {
|
|
30
|
-
north: Math.max(...points.map((point) => point.lat)),
|
|
31
|
-
south: Math.min(...points.map((point) => point.lat)),
|
|
32
|
-
west: normalize(longitudes[start]),
|
|
33
|
-
east: normalize(
|
|
34
|
-
longitudes[(start + longitudes.length - 1) % longitudes.length]
|
|
35
|
-
)
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
10
|
export {
|
|
39
|
-
branchBounds,
|
|
40
11
|
validCoordinates,
|
|
41
12
|
visibleBranches
|
|
42
13
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/branch-map/geometry.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Branch } from \"@zappie/sdk\";\n\nexport type Coordinates = { lat: number; lng: number };\nexport function validCoordinates(value?: Coordinates): value is Coordinates {\n return (\n !!value &&\n Number.isFinite(value.lat) &&\n Number.isFinite(value.lng) &&\n Math.abs(value.lat) <= 90 &&\n Math.abs(value.lng) <= 180\n );\n}\nexport function visibleBranches(branches: Branch[]) {\n return branches.filter(\n (branch): branch is Branch & { coordinates: Coordinates } =>\n validCoordinates(branch.coordinates)
|
|
5
|
-
"mappings": ";
|
|
4
|
+
"sourcesContent": ["import type { Branch } from \"@zappie/sdk\";\n\nexport type Coordinates = { lat: number; lng: number };\nexport type Bounds = {\n north: number;\n south: number;\n east: number;\n west: number;\n};\n\nexport function validCoordinates(value?: Coordinates): value is Coordinates {\n return (\n !!value &&\n Number.isFinite(value.lat) &&\n Number.isFinite(value.lng) &&\n Math.abs(value.lat) <= 90 &&\n Math.abs(value.lng) <= 180\n );\n}\n\n/** Branches with a real location. (0, 0) is the placeholder for \"no location on file\". */\nexport function visibleBranches(branches: Branch[]) {\n return branches.filter(\n (branch): branch is Branch & { coordinates: Coordinates } =>\n validCoordinates(branch.coordinates) &&\n (branch.coordinates.lat !== 0 || branch.coordinates.lng !== 0),\n );\n}\n"],
|
|
5
|
+
"mappings": ";AAUO,SAAS,iBAAiB,OAA2C;AAC1E,SACE,CAAC,CAAC,SACF,OAAO,SAAS,MAAM,GAAG,KACzB,OAAO,SAAS,MAAM,GAAG,KACzB,KAAK,IAAI,MAAM,GAAG,KAAK,MACvB,KAAK,IAAI,MAAM,GAAG,KAAK;AAE3B;AAGO,SAAS,gBAAgB,UAAoB;AAClD,SAAO,SAAS;AAAA,IACd,CAAC,WACC,iBAAiB,OAAO,WAAW,MAClC,OAAO,YAAY,QAAQ,KAAK,OAAO,YAAY,QAAQ;AAAA,EAChE;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|