@tileflow/static 0.1.0-alpha.20 → 0.1.0-alpha.22
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 +102 -12
- package/dist/chunk-EMCKSFZV.js +477 -0
- package/dist/{chunk-LGEYA6FQ.js → chunk-EPOYQNNT.js} +22 -6
- package/dist/{chunk-QCR76BUO.js → chunk-MSQ4OLKA.js} +74 -4
- package/dist/{chunk-YUG3PBK2.js → chunk-NM2JSFUF.js} +63 -9
- package/dist/chunk-TQMKOTBE.js +287 -0
- package/dist/client.d.ts +94 -5
- package/dist/client.js +11 -3
- package/dist/index.d.ts +4 -4
- package/dist/index.js +45 -5
- package/dist/manifest.d.ts +579 -8
- package/dist/manifest.js +25 -3
- package/dist/overlays.d.ts +5 -3
- package/dist/overlays.js +2 -2
- package/dist/{scene-contract-DzsUeJ_V.d.ts → scene-contract-OMIOisSB.d.ts} +66 -3
- package/dist/scene.d.ts +200 -5
- package/dist/scene.js +12 -2
- package/package.json +1 -1
- package/dist/chunk-HKYA7ZIU.js +0 -79
- package/dist/chunk-ROBEGDDY.js +0 -63
package/README.md
CHANGED
|
@@ -20,43 +20,133 @@ import {
|
|
|
20
20
|
const scene = validateStaticScene({
|
|
21
21
|
map: 'madrid',
|
|
22
22
|
theme: 'dark',
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
format: 'webp',
|
|
24
|
+
camera: {type: 'auto', padding: 24},
|
|
25
|
+
size: {width: 600, height: 400, dpr: 2},
|
|
25
26
|
overlays: [marker({coordinate: [-3.7038, 40.4168]})],
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
if (!scene.ok) throw new Error(scene.error);
|
|
29
30
|
|
|
30
|
-
await precacheStaticMap(scene.scene, {
|
|
31
|
+
const result = await precacheStaticMap(scene.scene, {
|
|
31
32
|
apiKey: process.env.TILEFLOW_API_KEY,
|
|
32
33
|
idempotencyKey: createStaticMapIdempotencyKey(),
|
|
33
34
|
});
|
|
35
|
+
|
|
36
|
+
console.log(result.attribution.entries);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`width` and `height` are logical CSS pixels. `dpr` accepts 1 or 2 and defaults to 1, so a 600 by
|
|
40
|
+
400 scene at DPR 2 produces a 1200 by 800 image without changing camera, padding, or overlay paint
|
|
41
|
+
sizes. Logical dimensions may reach 2048 per axis; the physical budget permits 4096 by 4096 output,
|
|
42
|
+
or 16,777,216 total pixels.
|
|
43
|
+
|
|
44
|
+
`format` accepts `png`, `jpeg`, or `webp` and defaults to PNG. PNG's canonical default stays omitted,
|
|
45
|
+
so explicit and omitted PNG requests share identity. JPEG and WebP use a fixed encoder quality;
|
|
46
|
+
there is no public quality option.
|
|
47
|
+
|
|
48
|
+
## Attribution
|
|
49
|
+
|
|
50
|
+
New renders default to `attribution: {mode: 'embedded', position: 'auto'}` without adding that
|
|
51
|
+
omitted default to the normalized scene. Hosted derives notices from the sources referenced by the
|
|
52
|
+
exact selected Style and embeds a safe, compact block. Automatic placement prefers an unobstructed
|
|
53
|
+
corner, then the least interfering corner among declared request overlays. Explicit center and
|
|
54
|
+
bounds cameras never move.
|
|
55
|
+
|
|
56
|
+
Choose a corner when composition requires it:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
attribution: {mode: 'embedded', position: 'bottom-left'}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Or request structured external attribution:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const result = await createStaticMap(
|
|
66
|
+
{...scene.scene, attribution: {mode: 'external'}},
|
|
67
|
+
{
|
|
68
|
+
apiKey: process.env.TILEFLOW_API_KEY,
|
|
69
|
+
idempotencyKey: createStaticMapIdempotencyKey(),
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
for (const entry of result.attribution.entries) {
|
|
74
|
+
renderAttributionText(entry.text, entry.links);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
External mode leaves the image unchanged and returns `position: null`. The caller must retain and
|
|
79
|
+
display every returned entry beside the image. Entries contain bounded plain text, safe HTTP(S)
|
|
80
|
+
links, and `platform-notice` or `team-declared` authority; they never contain source HTML or private
|
|
81
|
+
resource IDs. Embedded results return the resolved corner. Changing mode or requested position
|
|
82
|
+
changes scene and artifact identity.
|
|
83
|
+
|
|
84
|
+
## Automatic framing
|
|
85
|
+
|
|
86
|
+
`camera.type: 'auto'` fits the complete nominal footprint of every declared marker, circle, line,
|
|
87
|
+
and polygon. It works with Mercator and Globe styles when the geometry can be shown. It never clips
|
|
88
|
+
silently.
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
camera: {
|
|
92
|
+
type: 'auto',
|
|
93
|
+
padding: {top: 24, right: 240, bottom: 24, left: 24},
|
|
94
|
+
maxZoom: 16,
|
|
95
|
+
bearing: 0,
|
|
96
|
+
}
|
|
34
97
|
```
|
|
35
98
|
|
|
99
|
+
Padding uses logical CSS pixels. A number applies to every side; omitted sides in an object are
|
|
100
|
+
zero. Without `padding`, Tileflow uses `min(32, ceil(5% * min(width, height)))`. `maxZoom` defaults
|
|
101
|
+
to 16 and `bearing` to 0. A single position still honors asymmetric padding, so its apparent center
|
|
102
|
+
can move away from the viewport center.
|
|
103
|
+
|
|
104
|
+
Point-like overlays around ±180° use the shortest longitude interval. Tileflow never reinterprets
|
|
105
|
+
vertices inside a line or polygon: an ambiguous crossing returns an error. Represent a correctly
|
|
106
|
+
cut `MultiLineString` or `MultiPolygon` as several `line` or `polygon` overlays with identical
|
|
107
|
+
styles; auto-fit may move each complete component to an adjacent world copy.
|
|
108
|
+
|
|
109
|
+
Current overlay primitives use MapLibre's GeoJSON pipeline and require
|
|
110
|
+
`abs(latitude) <= MAX_OVERLAY_LATITUDE` (`85.051129`). Tileflow rejects larger latitudes instead of
|
|
111
|
+
clamping them. A Globe basemap may display polar regions even though these overlay primitives cannot
|
|
112
|
+
yet be drawn there.
|
|
113
|
+
|
|
114
|
+
Deterministic request failures return `422`, set `retryable: false`, and preserve a stable code,
|
|
115
|
+
reason, and bounded details. `validateStaticScene` returns that document; request helpers throw
|
|
116
|
+
`StaticMapRequestError` either locally or from the Hosted response. The reasons distinguish an empty
|
|
117
|
+
scene, an ambiguous antimeridian path, an out-of-range overlay latitude, an insufficient viewport,
|
|
118
|
+
a Globe visibility conflict, an unresolvable camera, and failed post-fit containment. Correct the
|
|
119
|
+
scene; do not retry it unchanged. Unsupported attribution grammar or glyphs, excessive attribution,
|
|
120
|
+
and a block that cannot fit use the same non-retryable `422` boundary.
|
|
121
|
+
|
|
36
122
|
The package has four deliberately separate surfaces, all available from the root for compatibility:
|
|
37
123
|
|
|
38
124
|
- scene validation and normalization: `staticSceneSchema`, `validateStaticScene`, and
|
|
39
125
|
`normalizeStaticScene`;
|
|
40
126
|
- overlay authoring and MapLibre compilation: `line`, `circle`, `marker`, `polygon`, and
|
|
41
127
|
`compileStaticOverlays`;
|
|
42
|
-
- renderer identity: `
|
|
43
|
-
`hashRenderManifest`, and `hashStaticSceneRequest`;
|
|
128
|
+
- renderer identity: `createRenderManifestV1`, `createRenderManifestV2`,
|
|
129
|
+
`validateStaticRenderManifest`, `hashRenderManifest`, and `hashStaticSceneRequest`;
|
|
44
130
|
- bounded Hosted transport: `prepareStaticMapRequest`, `createStaticMap`, `precacheStaticMap`, and
|
|
45
131
|
`requestStaticMapUntilReady`.
|
|
46
132
|
|
|
47
133
|
Applications that need only one surface can use `@tileflow/static/scene`, `/overlays`, `/manifest`,
|
|
48
134
|
or `/client`. The root remains a compatibility entry point.
|
|
49
135
|
|
|
50
|
-
`StaticRenderManifest` is the immutable input contract between a resolved Hosted style and
|
|
51
|
-
Maps renderer.
|
|
52
|
-
|
|
53
|
-
|
|
136
|
+
`StaticRenderManifest` is the versioned immutable input contract between a resolved Hosted style and
|
|
137
|
+
a Static Maps renderer. Manifest v1 remains the exact historical no-attribution contract; new Hosted
|
|
138
|
+
work uses strict v2 with resolved safe entries and immutable provenance. `createRenderManifest`
|
|
139
|
+
remains the v1 compatibility alias. Its top-level `mapId` identifies the resolved destination while
|
|
140
|
+
`scene.map` retains the portable map name supplied by the application. It is unrelated to the
|
|
141
|
+
runtime delivery manifest generated by application build or deploy workflows.
|
|
54
142
|
|
|
55
143
|
Create one idempotency key per intentional create/precache action and retain it across network
|
|
56
144
|
retries. Reusing the key with the same normalized scene returns the stored result; reusing it with a
|
|
57
145
|
different scene returns 409. A successful paid operation reports `unitCost: 15`, shared
|
|
58
146
|
`remainingUnits`, its durable `operationId`, cache reuse, hash, and immutable image URL. An
|
|
59
|
-
unbounded Starter service reports `remainingUnits: null`.
|
|
147
|
+
unbounded Starter service reports `remainingUnits: null`. Creation helpers request strict result v2,
|
|
148
|
+
including `resultVersion: 2` and required attribution. They reject a legacy success rather than
|
|
149
|
+
claiming notices that were never produced. A live
|
|
60
150
|
equal-hash render may return `202`; the helper polls with the same body/key within its bounded wait.
|
|
61
151
|
`maxWaitMs` is the total client completion budget, including network, response parsing, and polling
|
|
62
152
|
delays; `signal` cancels the same complete operation. The helper aborts in-flight work when either
|
|
@@ -69,7 +159,7 @@ the same normalized scene for its `sceneKey` and HTTP body, so equivalent omitte
|
|
|
69
159
|
deduplicate consistently without duplicating the polling implementation.
|
|
70
160
|
|
|
71
161
|
Free cannot initiate Static Maps work. Starter consumes 15 shared API units for each successful
|
|
72
|
-
logical operation, including a distinct-key cache reuse. Downloading
|
|
73
|
-
zero units.
|
|
162
|
+
logical operation, including a distinct-key cache reuse. Downloading an immutable PNG, JPEG, or WebP
|
|
163
|
+
consumes zero units.
|
|
74
164
|
|
|
75
165
|
Docs: https://tileflow.dev/docs
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MAX_OVERLAY_LATITUDE,
|
|
3
|
+
jsonByteLength,
|
|
4
|
+
normalizeStaticCoordinate,
|
|
5
|
+
normalizeStaticOverlay,
|
|
6
|
+
roundNumber,
|
|
7
|
+
staticPaddingSchema,
|
|
8
|
+
staticSceneLimits,
|
|
9
|
+
staticSceneSchema,
|
|
10
|
+
stripUndefined
|
|
11
|
+
} from "./chunk-NM2JSFUF.js";
|
|
12
|
+
|
|
13
|
+
// src/auto-fit.ts
|
|
14
|
+
import { z } from "zod";
|
|
15
|
+
var autoFitOverlayReferenceSchema = z.object({
|
|
16
|
+
id: z.string().min(1).max(64),
|
|
17
|
+
index: z.number().int().min(0).max(23),
|
|
18
|
+
type: z.enum(["circle", "line", "marker", "polygon"])
|
|
19
|
+
}).strict();
|
|
20
|
+
var autoFitViewportSchema = z.object({
|
|
21
|
+
height: z.number().int().min(64).max(1280),
|
|
22
|
+
width: z.number().int().min(64).max(1280)
|
|
23
|
+
}).strict();
|
|
24
|
+
var autoFitDetailsSchema = z.object({
|
|
25
|
+
overlay: autoFitOverlayReferenceSchema.optional(),
|
|
26
|
+
padding: staticPaddingSchema.optional(),
|
|
27
|
+
projection: z.enum(["globe", "mercator"]).optional(),
|
|
28
|
+
requiredInsets: staticPaddingSchema.optional(),
|
|
29
|
+
ringIndex: z.number().int().min(0).max(15).optional(),
|
|
30
|
+
segmentIndex: z.number().int().min(0).max(1999).optional(),
|
|
31
|
+
viewport: autoFitViewportSchema.optional()
|
|
32
|
+
}).strict();
|
|
33
|
+
var staticOverlayInvalidDetailsSchema = z.object({
|
|
34
|
+
latitude: z.number().finite(),
|
|
35
|
+
limit: z.literal(MAX_OVERLAY_LATITUDE),
|
|
36
|
+
overlay: autoFitOverlayReferenceSchema
|
|
37
|
+
}).strict();
|
|
38
|
+
var autoFitErrorBase = {
|
|
39
|
+
error: z.string().min(1).max(512),
|
|
40
|
+
retryable: z.literal(false)
|
|
41
|
+
};
|
|
42
|
+
var staticMapRequestErrorResponseSchema = z.discriminatedUnion("code", [
|
|
43
|
+
z.object({
|
|
44
|
+
...autoFitErrorBase,
|
|
45
|
+
code: z.literal("AUTO_FIT_EMPTY"),
|
|
46
|
+
reason: z.literal("NO_FITTABLE_OVERLAYS")
|
|
47
|
+
}).strict(),
|
|
48
|
+
z.object({
|
|
49
|
+
...autoFitErrorBase,
|
|
50
|
+
code: z.literal("AUTO_FIT_AMBIGUOUS"),
|
|
51
|
+
details: autoFitDetailsSchema,
|
|
52
|
+
reason: z.literal("ANTIMERIDIAN_WRAP_REQUIRED")
|
|
53
|
+
}).strict(),
|
|
54
|
+
z.object({
|
|
55
|
+
...autoFitErrorBase,
|
|
56
|
+
code: z.literal("AUTO_FIT_IMPOSSIBLE"),
|
|
57
|
+
details: autoFitDetailsSchema.optional(),
|
|
58
|
+
reason: z.enum([
|
|
59
|
+
"GLOBE_NOT_SIMULTANEOUSLY_VISIBLE",
|
|
60
|
+
"INSUFFICIENT_VIEWPORT",
|
|
61
|
+
"CAMERA_UNRESOLVABLE",
|
|
62
|
+
"PROJECTED_FOOTPRINT_OUTSIDE_VIEWPORT"
|
|
63
|
+
])
|
|
64
|
+
}).strict(),
|
|
65
|
+
z.object({
|
|
66
|
+
...autoFitErrorBase,
|
|
67
|
+
code: z.literal("STATIC_OVERLAY_INVALID"),
|
|
68
|
+
details: staticOverlayInvalidDetailsSchema,
|
|
69
|
+
reason: z.literal("OVERLAY_LATITUDE_OUT_OF_RANGE")
|
|
70
|
+
}).strict(),
|
|
71
|
+
z.object({
|
|
72
|
+
...autoFitErrorBase,
|
|
73
|
+
code: z.literal("STATIC_MAP_ATTRIBUTION_UNSUPPORTED"),
|
|
74
|
+
reason: z.enum(["UNSUPPORTED_DECLARATION", "UNSUPPORTED_GLYPH"])
|
|
75
|
+
}).strict(),
|
|
76
|
+
z.object({
|
|
77
|
+
...autoFitErrorBase,
|
|
78
|
+
code: z.literal("STATIC_MAP_ATTRIBUTION_TOO_LARGE"),
|
|
79
|
+
reason: z.literal("CONTENT_LIMIT_EXCEEDED")
|
|
80
|
+
}).strict(),
|
|
81
|
+
z.object({
|
|
82
|
+
...autoFitErrorBase,
|
|
83
|
+
code: z.literal("STATIC_MAP_ATTRIBUTION_DOES_NOT_FIT"),
|
|
84
|
+
reason: z.literal("BLOCK_DOES_NOT_FIT")
|
|
85
|
+
}).strict()
|
|
86
|
+
]);
|
|
87
|
+
var StaticMapRequestError = class extends Error {
|
|
88
|
+
code;
|
|
89
|
+
details;
|
|
90
|
+
reason;
|
|
91
|
+
retryable = false;
|
|
92
|
+
status;
|
|
93
|
+
constructor(response, status = 422) {
|
|
94
|
+
super(response.error);
|
|
95
|
+
this.name = "StaticMapRequestError";
|
|
96
|
+
this.code = response.code;
|
|
97
|
+
this.details = "details" in response ? response.details : void 0;
|
|
98
|
+
this.reason = response.reason;
|
|
99
|
+
this.status = status;
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
var staticAutoFitPlanSchema = z.object({
|
|
103
|
+
bounds: z.tuple([
|
|
104
|
+
z.number().finite(),
|
|
105
|
+
z.number().finite().min(-90).max(90),
|
|
106
|
+
z.number().finite(),
|
|
107
|
+
z.number().finite().min(-90).max(90)
|
|
108
|
+
]),
|
|
109
|
+
longitudeOffsets: z.array(z.number().int().min(-360).max(360)).max(24),
|
|
110
|
+
nominalInsets: staticPaddingSchema,
|
|
111
|
+
requiredInsets: staticPaddingSchema
|
|
112
|
+
}).strict();
|
|
113
|
+
function analyzeStaticAutoFit(scene) {
|
|
114
|
+
if (scene.camera.type !== "auto") {
|
|
115
|
+
throw new Error("Static auto-fit analysis requires an auto camera");
|
|
116
|
+
}
|
|
117
|
+
if (scene.overlays.length === 0) {
|
|
118
|
+
return failure({
|
|
119
|
+
code: "AUTO_FIT_EMPTY",
|
|
120
|
+
error: "Auto-fit requires at least one overlay",
|
|
121
|
+
reason: "NO_FITTABLE_OVERLAYS",
|
|
122
|
+
retryable: false
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
const components = [];
|
|
126
|
+
let south = Number.POSITIVE_INFINITY;
|
|
127
|
+
let north = Number.NEGATIVE_INFINITY;
|
|
128
|
+
let nominalExpansion = 0;
|
|
129
|
+
for (const [overlayIndex, overlay] of scene.overlays.entries()) {
|
|
130
|
+
const coordinates = overlayCoordinates(overlay);
|
|
131
|
+
const ambiguity = findAmbiguousSegment(overlay);
|
|
132
|
+
if (ambiguity) {
|
|
133
|
+
return failure({
|
|
134
|
+
code: "AUTO_FIT_AMBIGUOUS",
|
|
135
|
+
details: {
|
|
136
|
+
overlay: {
|
|
137
|
+
id: overlay.id ?? `overlay-${overlayIndex + 1}`,
|
|
138
|
+
index: overlayIndex,
|
|
139
|
+
type: overlay.type
|
|
140
|
+
},
|
|
141
|
+
...ambiguity.ringIndex === void 0 ? {} : { ringIndex: ambiguity.ringIndex },
|
|
142
|
+
segmentIndex: ambiguity.segmentIndex
|
|
143
|
+
},
|
|
144
|
+
error: `Overlay overlays.${overlayIndex} requires an explicit antimeridian wrap decision`,
|
|
145
|
+
reason: "ANTIMERIDIAN_WRAP_REQUIRED",
|
|
146
|
+
retryable: false
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
const longitudes = coordinates.map(([longitude]) => longitude);
|
|
150
|
+
components.push({
|
|
151
|
+
east: Math.max(...longitudes),
|
|
152
|
+
overlayIndex,
|
|
153
|
+
west: Math.min(...longitudes)
|
|
154
|
+
});
|
|
155
|
+
for (const [, latitude] of coordinates) {
|
|
156
|
+
south = Math.min(south, latitude);
|
|
157
|
+
north = Math.max(north, latitude);
|
|
158
|
+
}
|
|
159
|
+
nominalExpansion = Math.max(nominalExpansion, nominalOverlayExpansion(overlay));
|
|
160
|
+
}
|
|
161
|
+
const longitudePlan = fitLongitudeComponents(components);
|
|
162
|
+
const nominalInset = Math.ceil(nominalExpansion);
|
|
163
|
+
const nominalInsets = allSides(nominalInset);
|
|
164
|
+
const padding = normalizedPadding(scene.camera.padding, scene.size);
|
|
165
|
+
if ([padding.bottom, padding.left, padding.right, padding.top].some(
|
|
166
|
+
(value) => !Number.isSafeInteger(value + nominalInset)
|
|
167
|
+
)) {
|
|
168
|
+
return failure({
|
|
169
|
+
code: "AUTO_FIT_IMPOSSIBLE",
|
|
170
|
+
details: {
|
|
171
|
+
padding,
|
|
172
|
+
viewport: { height: scene.size.height, width: scene.size.width }
|
|
173
|
+
},
|
|
174
|
+
error: "Auto-fit nominal footprint leaves no usable viewport",
|
|
175
|
+
reason: "INSUFFICIENT_VIEWPORT",
|
|
176
|
+
retryable: false
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
const requiredInsets = {
|
|
180
|
+
bottom: padding.bottom + nominalInset,
|
|
181
|
+
left: padding.left + nominalInset,
|
|
182
|
+
right: padding.right + nominalInset,
|
|
183
|
+
top: padding.top + nominalInset
|
|
184
|
+
};
|
|
185
|
+
if (requiredInsets.left + requiredInsets.right >= scene.size.width || requiredInsets.top + requiredInsets.bottom >= scene.size.height) {
|
|
186
|
+
return failure({
|
|
187
|
+
code: "AUTO_FIT_IMPOSSIBLE",
|
|
188
|
+
details: {
|
|
189
|
+
padding,
|
|
190
|
+
requiredInsets,
|
|
191
|
+
viewport: { height: scene.size.height, width: scene.size.width }
|
|
192
|
+
},
|
|
193
|
+
error: "Auto-fit nominal footprint leaves no usable viewport",
|
|
194
|
+
reason: "INSUFFICIENT_VIEWPORT",
|
|
195
|
+
retryable: false
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
ok: true,
|
|
200
|
+
plan: {
|
|
201
|
+
bounds: [
|
|
202
|
+
roundNumber(longitudePlan.west),
|
|
203
|
+
roundNumber(south),
|
|
204
|
+
roundNumber(longitudePlan.east),
|
|
205
|
+
roundNumber(north)
|
|
206
|
+
],
|
|
207
|
+
longitudeOffsets: longitudePlan.offsets,
|
|
208
|
+
nominalInsets,
|
|
209
|
+
requiredInsets
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function failure(response) {
|
|
214
|
+
return { ...response, ok: false };
|
|
215
|
+
}
|
|
216
|
+
function overlayCoordinates(overlay) {
|
|
217
|
+
if (overlay.type === "circle" || overlay.type === "marker") {
|
|
218
|
+
return [overlay.coordinate];
|
|
219
|
+
}
|
|
220
|
+
if (overlay.type === "line") {
|
|
221
|
+
return overlay.coordinates;
|
|
222
|
+
}
|
|
223
|
+
return overlay.coordinates.flat();
|
|
224
|
+
}
|
|
225
|
+
function findAmbiguousSegment(overlay) {
|
|
226
|
+
if (overlay.type === "circle" || overlay.type === "marker") return null;
|
|
227
|
+
const paths = overlay.type === "line" ? [overlay.coordinates] : overlay.coordinates;
|
|
228
|
+
for (const [ringIndex, path] of paths.entries()) {
|
|
229
|
+
for (let segmentIndex = 0; segmentIndex < path.length - 1; segmentIndex += 1) {
|
|
230
|
+
const start = path[segmentIndex];
|
|
231
|
+
const end = path[segmentIndex + 1];
|
|
232
|
+
if (start && end && Math.abs(end[0] - start[0]) >= 180) {
|
|
233
|
+
return {
|
|
234
|
+
...overlay.type === "polygon" ? { ringIndex } : {},
|
|
235
|
+
segmentIndex
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
function nominalOverlayExpansion(overlay) {
|
|
243
|
+
if (overlay.type === "circle" || overlay.type === "marker") {
|
|
244
|
+
return overlay.radius + overlay.strokeWidth;
|
|
245
|
+
}
|
|
246
|
+
if (overlay.type === "line") {
|
|
247
|
+
return overlay.width / 2 + 0.5;
|
|
248
|
+
}
|
|
249
|
+
return overlay.stroke && overlay.strokeWidth > 0 ? overlay.strokeWidth / 2 + 0.5 : 0;
|
|
250
|
+
}
|
|
251
|
+
function fitLongitudeComponents(components) {
|
|
252
|
+
const candidates = /* @__PURE__ */ new Set();
|
|
253
|
+
for (const component of components) {
|
|
254
|
+
candidates.add(toCircle(component.west));
|
|
255
|
+
candidates.add(toCircle(component.east));
|
|
256
|
+
}
|
|
257
|
+
let best;
|
|
258
|
+
for (const cut of [...candidates].sort((left, right) => left - right)) {
|
|
259
|
+
const assignedStarts = [];
|
|
260
|
+
let west = Number.POSITIVE_INFINITY;
|
|
261
|
+
let east = Number.NEGATIVE_INFINITY;
|
|
262
|
+
let valid = true;
|
|
263
|
+
for (const component of components) {
|
|
264
|
+
const width = component.east - component.west;
|
|
265
|
+
let start = toCircle(component.west);
|
|
266
|
+
if (start < cut) start += 360;
|
|
267
|
+
const end = start + width;
|
|
268
|
+
if (end > cut + 360 + 1e-9) {
|
|
269
|
+
valid = false;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
assignedStarts.push(start);
|
|
273
|
+
west = Math.min(west, start);
|
|
274
|
+
east = Math.max(east, end);
|
|
275
|
+
}
|
|
276
|
+
if (!valid) continue;
|
|
277
|
+
const span = east - west;
|
|
278
|
+
const canonicalWest2 = canonicalLongitude(west - 180);
|
|
279
|
+
const bestCanonicalWest = best ? canonicalLongitude(best.west - 180) : Number.POSITIVE_INFINITY;
|
|
280
|
+
if (!best || span < best.span - 1e-9 || Math.abs(span - best.span) <= 1e-9 && canonicalWest2 < bestCanonicalWest) {
|
|
281
|
+
best = { assignedStarts, east, span, west };
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (!best) {
|
|
285
|
+
const west = Math.min(...components.map((component) => component.west));
|
|
286
|
+
const east = Math.max(...components.map((component) => component.east));
|
|
287
|
+
return { east, offsets: components.map(() => 0), west };
|
|
288
|
+
}
|
|
289
|
+
const unshiftedWest = best.west - 180;
|
|
290
|
+
const canonicalWest = canonicalLongitude(unshiftedWest);
|
|
291
|
+
const globalOffset = canonicalWest - unshiftedWest;
|
|
292
|
+
const offsets = components.map((component, index) => {
|
|
293
|
+
const assignedLongitude = (best?.assignedStarts[index] ?? 0) - 180 + globalOffset;
|
|
294
|
+
return Math.round(assignedLongitude - component.west);
|
|
295
|
+
});
|
|
296
|
+
return {
|
|
297
|
+
east: canonicalWest + best.span,
|
|
298
|
+
offsets,
|
|
299
|
+
west: canonicalWest
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function toCircle(longitude) {
|
|
303
|
+
const value = (longitude + 180) % 360;
|
|
304
|
+
return value < 0 ? value + 360 : value;
|
|
305
|
+
}
|
|
306
|
+
function canonicalLongitude(longitude) {
|
|
307
|
+
const value = ((longitude + 180) % 360 + 360) % 360;
|
|
308
|
+
return value - 180;
|
|
309
|
+
}
|
|
310
|
+
function allSides(value) {
|
|
311
|
+
return { bottom: value, left: value, right: value, top: value };
|
|
312
|
+
}
|
|
313
|
+
function normalizedPadding(padding, size) {
|
|
314
|
+
if (padding === void 0) {
|
|
315
|
+
return allSides(Math.min(32, Math.ceil(Math.min(size.width, size.height) * 0.05)));
|
|
316
|
+
}
|
|
317
|
+
if (typeof padding === "number") return allSides(padding);
|
|
318
|
+
return {
|
|
319
|
+
bottom: padding.bottom ?? 0,
|
|
320
|
+
left: padding.left ?? 0,
|
|
321
|
+
right: padding.right ?? 0,
|
|
322
|
+
top: padding.top ?? 0
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
function autoFitErrorResponse(failure2) {
|
|
326
|
+
const { ok: _ok, ...response } = failure2;
|
|
327
|
+
return response;
|
|
328
|
+
}
|
|
329
|
+
function findStaticOverlayLatitudeFailure(input) {
|
|
330
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) return null;
|
|
331
|
+
const overlays = input.overlays;
|
|
332
|
+
if (!Array.isArray(overlays)) return null;
|
|
333
|
+
for (const [index, candidate] of overlays.entries()) {
|
|
334
|
+
if (index >= staticSceneLimits.maxOverlays) break;
|
|
335
|
+
if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) continue;
|
|
336
|
+
const overlay = candidate;
|
|
337
|
+
const type = overlay.type;
|
|
338
|
+
if (!["circle", "line", "marker", "polygon"].includes(String(type))) continue;
|
|
339
|
+
const coordinates = type === "circle" || type === "marker" ? [overlay.coordinate] : overlayCoordinateCandidates(type, overlay.coordinates);
|
|
340
|
+
for (const coordinate of coordinates) {
|
|
341
|
+
if (!Array.isArray(coordinate) || coordinate.length !== 2 || typeof coordinate[1] !== "number" || !Number.isFinite(coordinate[1]) || Math.abs(coordinate[1]) <= MAX_OVERLAY_LATITUDE) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
const id = typeof overlay.id === "string" && overlay.id.trim() ? overlay.id.trim() : `overlay-${index + 1}`;
|
|
345
|
+
const response = {
|
|
346
|
+
code: "STATIC_OVERLAY_INVALID",
|
|
347
|
+
details: {
|
|
348
|
+
latitude: coordinate[1],
|
|
349
|
+
limit: MAX_OVERLAY_LATITUDE,
|
|
350
|
+
overlay: {
|
|
351
|
+
id: id.slice(0, 64),
|
|
352
|
+
index,
|
|
353
|
+
type
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
error: `Overlay overlays.${index} latitude ${coordinate[1]} exceeds the supported \xB1${MAX_OVERLAY_LATITUDE}\xB0 range`,
|
|
357
|
+
reason: "OVERLAY_LATITUDE_OUT_OF_RANGE",
|
|
358
|
+
retryable: false
|
|
359
|
+
};
|
|
360
|
+
return { ...response, ok: false };
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
function overlayCoordinateCandidates(type, value) {
|
|
366
|
+
if (!Array.isArray(value)) return [];
|
|
367
|
+
if (type === "line") {
|
|
368
|
+
return value.slice(0, staticSceneLimits.maxPathCoordinates);
|
|
369
|
+
}
|
|
370
|
+
return value.slice(0, 16).flatMap(
|
|
371
|
+
(ring) => Array.isArray(ring) ? ring.slice(0, staticSceneLimits.maxPathCoordinates) : []
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// src/scene.ts
|
|
376
|
+
function validateStaticScene(input) {
|
|
377
|
+
const overlayLatitudeFailure = findStaticOverlayLatitudeFailure(input);
|
|
378
|
+
if (overlayLatitudeFailure) return overlayLatitudeFailure;
|
|
379
|
+
const parsed = staticSceneSchema.safeParse(input);
|
|
380
|
+
if (!parsed.success) {
|
|
381
|
+
return {
|
|
382
|
+
error: parsed.error.issues.map((issue) => `${issue.path.join(".") || "scene"}: ${issue.message}`).join("; "),
|
|
383
|
+
ok: false
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
if (jsonByteLength(parsed.data.overlays) > staticSceneLimits.maxGeoJsonBytes) {
|
|
387
|
+
return {
|
|
388
|
+
error: `overlays exceed ${staticSceneLimits.maxGeoJsonBytes} bytes`,
|
|
389
|
+
ok: false
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const scene = normalizeParsedStaticScene(parsed.data);
|
|
393
|
+
if (scene.camera.type === "auto") {
|
|
394
|
+
const analysis = analyzeStaticAutoFit(scene);
|
|
395
|
+
if (!analysis.ok) return analysis;
|
|
396
|
+
}
|
|
397
|
+
return { ok: true, scene };
|
|
398
|
+
}
|
|
399
|
+
function normalizeStaticScene(scene) {
|
|
400
|
+
const overlayLatitudeFailure = findStaticOverlayLatitudeFailure(scene);
|
|
401
|
+
if (overlayLatitudeFailure) {
|
|
402
|
+
const { ok: _ok, ...response } = overlayLatitudeFailure;
|
|
403
|
+
throw new StaticMapRequestError(response);
|
|
404
|
+
}
|
|
405
|
+
const normalized = normalizeParsedStaticScene(staticSceneSchema.parse(scene));
|
|
406
|
+
if (normalized.camera.type === "auto") {
|
|
407
|
+
const analysis = analyzeStaticAutoFit(normalized);
|
|
408
|
+
if (!analysis.ok) {
|
|
409
|
+
throw new StaticMapRequestError(autoFitErrorResponse(analysis));
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
return normalized;
|
|
413
|
+
}
|
|
414
|
+
function normalizeParsedStaticScene(parsed) {
|
|
415
|
+
return stripUndefined({
|
|
416
|
+
...parsed.attribution && Object.keys(parsed.attribution).length > 0 ? {
|
|
417
|
+
attribution: parsed.attribution.mode === "external" ? { mode: "external" } : stripUndefined({
|
|
418
|
+
mode: parsed.attribution.mode,
|
|
419
|
+
position: parsed.attribution.position
|
|
420
|
+
})
|
|
421
|
+
} : {},
|
|
422
|
+
camera: parsed.camera.type === "center" ? {
|
|
423
|
+
bearing: roundNumber(parsed.camera.bearing ?? 0),
|
|
424
|
+
center: normalizeStaticCoordinate(parsed.camera.center),
|
|
425
|
+
type: "center",
|
|
426
|
+
zoom: roundNumber(parsed.camera.zoom)
|
|
427
|
+
} : parsed.camera.type === "bounds" ? {
|
|
428
|
+
bearing: roundNumber(parsed.camera.bearing ?? 0),
|
|
429
|
+
bounds: parsed.camera.bounds.map(roundNumber),
|
|
430
|
+
padding: parsed.camera.padding ?? 32,
|
|
431
|
+
type: "bounds"
|
|
432
|
+
} : {
|
|
433
|
+
bearing: roundNumber(parsed.camera.bearing ?? 0),
|
|
434
|
+
maxZoom: roundNumber(parsed.camera.maxZoom ?? 16),
|
|
435
|
+
padding: normalizeAutoPadding(parsed.camera.padding, parsed.size),
|
|
436
|
+
type: "auto"
|
|
437
|
+
},
|
|
438
|
+
...parsed.format && parsed.format !== "png" ? { format: parsed.format } : {},
|
|
439
|
+
map: parsed.map,
|
|
440
|
+
overlays: parsed.overlays.map(
|
|
441
|
+
(overlay, index) => normalizeStaticOverlay({
|
|
442
|
+
...overlay,
|
|
443
|
+
id: overlay.id ?? `overlay-${index + 1}`
|
|
444
|
+
})
|
|
445
|
+
),
|
|
446
|
+
size: {
|
|
447
|
+
dpr: parsed.size.dpr ?? 1,
|
|
448
|
+
height: parsed.size.height,
|
|
449
|
+
width: parsed.size.width
|
|
450
|
+
},
|
|
451
|
+
theme: parsed.theme
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
function normalizeAutoPadding(padding, size) {
|
|
455
|
+
if (padding === void 0) {
|
|
456
|
+
const value = Math.min(32, Math.ceil(Math.min(size.width, size.height) * 0.05));
|
|
457
|
+
return { bottom: value, left: value, right: value, top: value };
|
|
458
|
+
}
|
|
459
|
+
if (typeof padding === "number") {
|
|
460
|
+
return { bottom: padding, left: padding, right: padding, top: padding };
|
|
461
|
+
}
|
|
462
|
+
return {
|
|
463
|
+
bottom: padding.bottom ?? 0,
|
|
464
|
+
left: padding.left ?? 0,
|
|
465
|
+
right: padding.right ?? 0,
|
|
466
|
+
top: padding.top ?? 0
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
export {
|
|
471
|
+
staticMapRequestErrorResponseSchema,
|
|
472
|
+
StaticMapRequestError,
|
|
473
|
+
staticAutoFitPlanSchema,
|
|
474
|
+
analyzeStaticAutoFit,
|
|
475
|
+
validateStaticScene,
|
|
476
|
+
normalizeStaticScene
|
|
477
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
-
normalizeStaticOverlay
|
|
3
|
-
|
|
2
|
+
normalizeStaticOverlay,
|
|
3
|
+
roundNumber
|
|
4
|
+
} from "./chunk-NM2JSFUF.js";
|
|
4
5
|
|
|
5
6
|
// src/overlays.ts
|
|
6
7
|
function line(input) {
|
|
@@ -15,15 +16,23 @@ function marker(input) {
|
|
|
15
16
|
function polygon(input) {
|
|
16
17
|
return normalizeStaticOverlay({ ...input, type: "polygon" });
|
|
17
18
|
}
|
|
18
|
-
function compileStaticOverlays(overlays) {
|
|
19
|
+
function compileStaticOverlays(overlays, options = {}) {
|
|
20
|
+
if (options.longitudeOffsets !== void 0 && (options.longitudeOffsets.length !== overlays.length || options.longitudeOffsets.some(
|
|
21
|
+
(offset) => !Number.isInteger(offset) || Math.abs(offset) > 360 || offset % 360 !== 0
|
|
22
|
+
))) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Static overlay longitude offsets must contain one -360, 0, or 360 per overlay"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
19
27
|
const sources = {};
|
|
20
28
|
const layers = [];
|
|
21
29
|
for (const [index, overlay] of overlays.entries()) {
|
|
30
|
+
const longitudeOffset = options.longitudeOffsets?.[index] ?? 0;
|
|
22
31
|
const id = safeLayerId(`overlay-${index + 1}-${overlay.id ?? overlay.type}`);
|
|
23
32
|
const sourceId = `${id}-source`;
|
|
24
33
|
if (overlay.type === "line") {
|
|
25
34
|
sources[sourceId] = {
|
|
26
|
-
data: feature("LineString", overlay.coordinates),
|
|
35
|
+
data: feature("LineString", shiftLongitudes(overlay.coordinates, longitudeOffset)),
|
|
27
36
|
type: "geojson"
|
|
28
37
|
};
|
|
29
38
|
layers.push({
|
|
@@ -41,7 +50,7 @@ function compileStaticOverlays(overlays) {
|
|
|
41
50
|
}
|
|
42
51
|
if (overlay.type === "polygon") {
|
|
43
52
|
sources[sourceId] = {
|
|
44
|
-
data: feature("Polygon", overlay.coordinates),
|
|
53
|
+
data: feature("Polygon", shiftLongitudes(overlay.coordinates, longitudeOffset)),
|
|
45
54
|
type: "geojson"
|
|
46
55
|
};
|
|
47
56
|
layers.push({
|
|
@@ -68,7 +77,7 @@ function compileStaticOverlays(overlays) {
|
|
|
68
77
|
continue;
|
|
69
78
|
}
|
|
70
79
|
sources[sourceId] = {
|
|
71
|
-
data: feature("Point", overlay.coordinate),
|
|
80
|
+
data: feature("Point", shiftLongitudes(overlay.coordinate, longitudeOffset)),
|
|
72
81
|
type: "geojson"
|
|
73
82
|
};
|
|
74
83
|
layers.push({
|
|
@@ -86,6 +95,13 @@ function compileStaticOverlays(overlays) {
|
|
|
86
95
|
}
|
|
87
96
|
return { layers, sources };
|
|
88
97
|
}
|
|
98
|
+
function shiftLongitudes(value, offset) {
|
|
99
|
+
if (!Array.isArray(value)) return value;
|
|
100
|
+
if (value.length === 2 && typeof value[0] === "number" && typeof value[1] === "number") {
|
|
101
|
+
return [roundNumber(value[0] + offset), value[1]];
|
|
102
|
+
}
|
|
103
|
+
return value.map((entry) => shiftLongitudes(entry, offset));
|
|
104
|
+
}
|
|
89
105
|
function feature(type, coordinates) {
|
|
90
106
|
return {
|
|
91
107
|
geometry: { coordinates, type },
|