@tileflow/static 0.1.0-alpha.20 → 0.1.0-alpha.21

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 CHANGED
@@ -20,7 +20,7 @@ import {
20
20
  const scene = validateStaticScene({
21
21
  map: 'madrid',
22
22
  theme: 'dark',
23
- camera: {type: 'center', center: [-3.7038, 40.4168], zoom: 12},
23
+ camera: {type: 'auto', padding: 24},
24
24
  size: {width: 1200, height: 800},
25
25
  overlays: [marker({coordinate: [-3.7038, 40.4168]})],
26
26
  });
@@ -33,6 +33,43 @@ await precacheStaticMap(scene.scene, {
33
33
  });
34
34
  ```
35
35
 
36
+ ## Automatic framing
37
+
38
+ `camera.type: 'auto'` fits the complete nominal footprint of every declared marker, circle, line,
39
+ and polygon. It works with Mercator and Globe styles when the geometry can be shown. It never clips
40
+ silently.
41
+
42
+ ```ts
43
+ camera: {
44
+ type: 'auto',
45
+ padding: {top: 24, right: 240, bottom: 24, left: 24},
46
+ maxZoom: 16,
47
+ bearing: 0,
48
+ }
49
+ ```
50
+
51
+ Padding uses logical CSS pixels. A number applies to every side; omitted sides in an object are
52
+ zero. Without `padding`, Tileflow uses `min(32, ceil(5% * min(width, height)))`. `maxZoom` defaults
53
+ to 16 and `bearing` to 0. A single position still honors asymmetric padding, so its apparent center
54
+ can move away from the viewport center.
55
+
56
+ Point-like overlays around ±180° use the shortest longitude interval. Tileflow never reinterprets
57
+ vertices inside a line or polygon: an ambiguous crossing returns an error. Represent a correctly
58
+ cut `MultiLineString` or `MultiPolygon` as several `line` or `polygon` overlays with identical
59
+ styles; auto-fit may move each complete component to an adjacent world copy.
60
+
61
+ Current overlay primitives use MapLibre's GeoJSON pipeline and require
62
+ `abs(latitude) <= MAX_OVERLAY_LATITUDE` (`85.051129`). Tileflow rejects larger latitudes instead of
63
+ clamping them. A Globe basemap may display polar regions even though these overlay primitives cannot
64
+ yet be drawn there.
65
+
66
+ Deterministic request failures return `422`, set `retryable: false`, and preserve a stable code,
67
+ reason, and bounded details. `validateStaticScene` returns that document; request helpers throw
68
+ `StaticMapRequestError` either locally or from the Hosted response. The reasons distinguish an empty
69
+ scene, an ambiguous antimeridian path, an out-of-range overlay latitude, an insufficient viewport,
70
+ a Globe visibility conflict, an unresolvable camera, and failed post-fit containment. Correct the
71
+ scene; do not retry it unchanged.
72
+
36
73
  The package has four deliberately separate surfaces, all available from the root for compatibility:
37
74
 
38
75
  - scene validation and normalization: `staticSceneSchema`, `validateStaticScene`, and
@@ -1,10 +1,12 @@
1
1
  import {
2
+ StaticMapRequestError,
3
+ staticMapRequestErrorResponseSchema,
2
4
  validateStaticScene
3
- } from "./chunk-ROBEGDDY.js";
5
+ } from "./chunk-NHQK5UZ2.js";
4
6
  import {
5
7
  isSafeHttpUrl,
6
8
  stableStringify
7
- } from "./chunk-YUG3PBK2.js";
9
+ } from "./chunk-MGQCHCU3.js";
8
10
 
9
11
  // src/client.ts
10
12
  import { z } from "zod";
@@ -44,6 +46,10 @@ function createStaticMapIdempotencyKey() {
44
46
  function prepareStaticMapRequest(scene) {
45
47
  const validation = validateStaticScene(scene);
46
48
  if (!validation.ok) {
49
+ if (isStaticMapRequestFailure(validation)) {
50
+ const { ok: _ok, ...response } = validation;
51
+ throw new StaticMapRequestError(response);
52
+ }
47
53
  throw new Error(`Invalid Tileflow static scene: ${validation.error}`);
48
54
  }
49
55
  const prepared = Object.freeze({
@@ -103,8 +109,11 @@ async function requestStaticMapUntilReady(request, options) {
103
109
  });
104
110
  throwIfAborted(signal);
105
111
  if (!response.ok) {
106
- await discardBoundedResponse(response, 8 * 1024);
112
+ const requestError = response.status === 422 ? await readStaticMapRequestErrorResponse(response, 8 * 1024) : await discardBoundedResponse(response, 8 * 1024);
107
113
  throwIfAborted(signal);
114
+ if (requestError) {
115
+ throw new StaticMapRequestError(requestError, response.status);
116
+ }
108
117
  throw new Error(`Tileflow static map failed (${response.status}).`);
109
118
  }
110
119
  const json = await readJsonResponse(response);
@@ -148,11 +157,26 @@ async function readJsonResponse(response) {
148
157
  throw new Error("Tileflow static map returned an invalid response: expected JSON");
149
158
  }
150
159
  }
160
+ async function readStaticMapRequestErrorResponse(response, maximumBytes) {
161
+ try {
162
+ const source = await readBoundedResponseText(response, maximumBytes, `${maximumBytes} bytes`);
163
+ const parsed = staticMapRequestErrorResponseSchema.safeParse(JSON.parse(source));
164
+ return parsed.success ? parsed.data : null;
165
+ } catch {
166
+ return null;
167
+ }
168
+ }
151
169
  async function discardBoundedResponse(response, maximumBytes) {
152
170
  try {
153
171
  await readBoundedResponseText(response, maximumBytes, `${maximumBytes} bytes`);
154
172
  } catch {
155
173
  }
174
+ return null;
175
+ }
176
+ function isStaticMapRequestFailure(value) {
177
+ if (!("code" in value)) return false;
178
+ const { ok: _ok, ...response } = value;
179
+ return staticMapRequestErrorResponseSchema.safeParse(response).success;
156
180
  }
157
181
  async function readBoundedResponseText(response, maximumBytes, displayLimit) {
158
182
  if (!response.body) return "";
@@ -1,6 +1,7 @@
1
1
  import {
2
- normalizeStaticOverlay
3
- } from "./chunk-YUG3PBK2.js";
2
+ normalizeStaticOverlay,
3
+ roundNumber
4
+ } from "./chunk-MGQCHCU3.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 },
@@ -49,6 +49,7 @@ function base64Url(bytes) {
49
49
  // src/scene-contract.ts
50
50
  import { z } from "zod";
51
51
  var staticSceneSchemaVersion = 1;
52
+ var MAX_OVERLAY_LATITUDE = 85.051129;
52
53
  var staticSceneLimits = {
53
54
  maxDimension: 1280,
54
55
  maxGeoJsonBytes: 96e3,
@@ -60,6 +61,12 @@ var staticSceneLimits = {
60
61
  var coordinateSchema = z.tuple([z.number().finite(), z.number().finite()]).refine(([lng, lat]) => lng >= -180 && lng <= 180 && lat >= -90 && lat <= 90, {
61
62
  message: "Expected [lng, lat] within world bounds"
62
63
  });
64
+ var overlayCoordinateSchema = coordinateSchema.refine(
65
+ ([, latitude]) => Math.abs(latitude) <= MAX_OVERLAY_LATITUDE,
66
+ {
67
+ message: `Static overlay latitude must be within \xB1${MAX_OVERLAY_LATITUDE}\xB0`
68
+ }
69
+ );
63
70
  var colorSchema = z.string().regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, {
64
71
  message: "Expected a hex color like #C6A15B"
65
72
  });
@@ -99,9 +106,30 @@ var boundsCameraSchema = z.object({
99
106
  }).refine((camera) => camera.bounds[3] > camera.bounds[1], {
100
107
  message: "Bounds north must be greater than south"
101
108
  });
109
+ var staticPaddingSchema = z.object({
110
+ bottom: z.number().int().nonnegative(),
111
+ left: z.number().int().nonnegative(),
112
+ right: z.number().int().nonnegative(),
113
+ top: z.number().int().nonnegative()
114
+ }).strict();
115
+ var autoPaddingInputSchema = z.union([
116
+ z.number().int().nonnegative(),
117
+ z.object({
118
+ bottom: z.number().int().nonnegative().optional(),
119
+ left: z.number().int().nonnegative().optional(),
120
+ right: z.number().int().nonnegative().optional(),
121
+ top: z.number().int().nonnegative().optional()
122
+ }).strict()
123
+ ]);
124
+ var autoCameraSchema = z.object({
125
+ bearing: z.number().finite().min(-180).max(180).optional(),
126
+ maxZoom: z.number().finite().min(0).max(22).optional(),
127
+ padding: autoPaddingInputSchema.optional(),
128
+ type: z.literal("auto")
129
+ });
102
130
  var lineOverlaySchema = z.object({
103
131
  color: colorSchema.default("#C6A15B"),
104
- coordinates: z.array(coordinateSchema).min(2).max(staticSceneLimits.maxPathCoordinates),
132
+ coordinates: z.array(overlayCoordinateSchema).min(2).max(staticSceneLimits.maxPathCoordinates),
105
133
  id: z.string().trim().min(1).max(64).optional(),
106
134
  opacity: z.number().finite().min(0).max(1).default(1),
107
135
  type: z.literal("line"),
@@ -109,7 +137,7 @@ var lineOverlaySchema = z.object({
109
137
  });
110
138
  var circleOverlaySchema = z.object({
111
139
  color: colorSchema.default("#C6A15B"),
112
- coordinate: coordinateSchema,
140
+ coordinate: overlayCoordinateSchema,
113
141
  id: z.string().trim().min(1).max(64).optional(),
114
142
  opacity: z.number().finite().min(0).max(1).default(1),
115
143
  radius: z.number().finite().min(1).max(64).default(6),
@@ -119,14 +147,14 @@ var circleOverlaySchema = z.object({
119
147
  });
120
148
  var markerOverlaySchema = z.object({
121
149
  color: colorSchema.default("#C6A15B"),
122
- coordinate: coordinateSchema,
150
+ coordinate: overlayCoordinateSchema,
123
151
  id: z.string().trim().min(1).max(64).optional(),
124
152
  radius: z.number().finite().min(2).max(64).default(8),
125
153
  strokeColor: colorSchema.default("#ffffff"),
126
154
  strokeWidth: z.number().finite().min(0).max(16).default(2),
127
155
  type: z.literal("marker")
128
156
  });
129
- var polygonRingSchema = z.array(coordinateSchema).min(4).max(staticSceneLimits.maxPathCoordinates).refine(
157
+ var polygonRingSchema = z.array(overlayCoordinateSchema).min(4).max(staticSceneLimits.maxPathCoordinates).refine(
130
158
  (ring) => {
131
159
  const first = ring[0];
132
160
  const last = ring[ring.length - 1];
@@ -150,7 +178,7 @@ var staticOverlaySchema = z.discriminatedUnion("type", [
150
178
  polygonOverlaySchema
151
179
  ]);
152
180
  var staticSceneSchema = z.object({
153
- camera: z.discriminatedUnion("type", [centerCameraSchema, boundsCameraSchema]),
181
+ camera: z.discriminatedUnion("type", [centerCameraSchema, boundsCameraSchema, autoCameraSchema]),
154
182
  map: portableIdSchema,
155
183
  overlays: z.array(staticOverlaySchema).max(staticSceneLimits.maxOverlays).default([]),
156
184
  size: sizeSchema,
@@ -190,7 +218,9 @@ export {
190
218
  hashStableValue,
191
219
  isSafeHttpUrl,
192
220
  staticSceneSchemaVersion,
221
+ MAX_OVERLAY_LATITUDE,
193
222
  staticSceneLimits,
223
+ staticPaddingSchema,
194
224
  staticOverlaySchema,
195
225
  staticSceneSchema,
196
226
  normalizeStaticOverlay,