riverbankcms 0.22.2 → 0.24.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.
- package/dist/acceptance/matrix.mjs +3 -3
- package/dist/cli/index.mjs +2750 -1731
- package/dist/cli/init-docs/content/cli-reference.md +2 -2
- package/dist/cli/init-docs/content/content-management.md +32 -7
- package/dist/cli/init-docs/content/workflow-custom-block.md +4 -4
- package/dist/cli/init-docs/content/workflow-publish.md +11 -5
- package/dist/{entryCommands-BGVgMNMw.mjs → entryCommands-C0xbLW5L.mjs} +308 -173
- package/dist/{eventUpsertInputSchema-CdX--hHz.mjs → eventUpsertInputSchema-C-cDv9vR.mjs} +74 -32
- package/dist/load-config-BGzlsmtt.mjs +4 -0
- package/dist/{load-config-BV9Ig2CH.mjs → load-config-h2Ri_tDO.mjs} +2 -2
- package/dist/{plan-CFMj_Hee.mjs → plan-Dyv1rMTT.mjs} +1 -1
- package/dist/scaffold/index.mjs +2 -2
- package/dist/{sdk-event-upsert-zWw5VolM.mjs → sdk-event-upsert-DmNSeHgP.mjs} +3706 -2373
- package/dist/{src-ZACBG29u.mjs → src-B03aw4Tv.mjs} +1476 -139
- package/dist/testing.mjs +4 -4
- package/package.json +1 -1
- package/dist/load-config-JWz__Dkx.mjs +0 -4
|
@@ -52,6 +52,7 @@ const unsafeAsPageId = (id) => id;
|
|
|
52
52
|
const unsafeAsPageSurfaceId = (id) => id;
|
|
53
53
|
const unsafeAsEditLeaseToken = (id) => id;
|
|
54
54
|
const unsafeAsEditorClientId = (id) => id;
|
|
55
|
+
const unsafeAsPageDocumentCheckpointId = (id) => id;
|
|
55
56
|
const unsafeAsOccurrenceId = (id) => id;
|
|
56
57
|
const unsafeAsEventOfferingId = (id) => id;
|
|
57
58
|
const unsafeAsEventSeriesId = (id) => id;
|
|
@@ -166,6 +167,7 @@ const parsePageId = (value) => parseUuidBrand(value, unsafeAsPageId);
|
|
|
166
167
|
const parsePageSurfaceId = (value) => parseUuidBrand(value, unsafeAsPageSurfaceId);
|
|
167
168
|
const parseEditLeaseToken = (value) => parseUuidBrand(value, unsafeAsEditLeaseToken);
|
|
168
169
|
const parseEditorClientId = (value) => parseUuidBrand(value, unsafeAsEditorClientId);
|
|
170
|
+
const parsePageDocumentCheckpointId = (value) => parseUuidBrand(value, unsafeAsPageDocumentCheckpointId);
|
|
169
171
|
const parseOccurrenceId = (value) => parseUuidBrand(value, unsafeAsOccurrenceId);
|
|
170
172
|
const parseEventOfferingId = (value) => parseUuidBrand(value, unsafeAsEventOfferingId);
|
|
171
173
|
const parseEventAttendeeTicketLineId = (value) => parseUuidBrand(value, unsafeAsEventAttendeeTicketLineId);
|
|
@@ -1385,7 +1387,7 @@ function parseEventPageSurfaceDestinationMode(value) {
|
|
|
1385
1387
|
return value === "cms-managed" || value === "system-managed" ? value : undefined;
|
|
1386
1388
|
}
|
|
1387
1389
|
function parseEventPageSurfaceSubject(value) {
|
|
1388
|
-
if (!isRecord$
|
|
1390
|
+
if (!isRecord$10(value)) return err({ kind: "notObject" });
|
|
1389
1391
|
const parsed = value.kind === "eventSeries" ? parseEventSeriesPageSurfaceSubject(value) : value.kind === "eventOffering" ? parseEventOfferingPageSurfaceSubject(value) : err({
|
|
1390
1392
|
kind: "unsupportedSubject",
|
|
1391
1393
|
subjectKind: value.kind
|
|
@@ -1458,7 +1460,7 @@ function parseEventSeriesIdList(values) {
|
|
|
1458
1460
|
}
|
|
1459
1461
|
return ok(parsed);
|
|
1460
1462
|
}
|
|
1461
|
-
function isRecord$
|
|
1463
|
+
function isRecord$10(value) {
|
|
1462
1464
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1463
1465
|
}
|
|
1464
1466
|
|
|
@@ -1476,7 +1478,7 @@ function toPositiveTicketQuantity(value) {
|
|
|
1476
1478
|
* null when the total is not a positive whole number.
|
|
1477
1479
|
*/
|
|
1478
1480
|
function deriveTotalTicketQuantity(lines) {
|
|
1479
|
-
const total = lines.reduce((sum, line) => sum + line.quantity, 0);
|
|
1481
|
+
const total = lines.reduce((sum, line$1) => sum + line$1.quantity, 0);
|
|
1480
1482
|
const parsed = toPositiveTicketQuantity(total);
|
|
1481
1483
|
return parsed.ok ? parsed.value : null;
|
|
1482
1484
|
}
|
|
@@ -1999,8 +2001,8 @@ function resolveBookingTimezone(venueTimezone, siteDefaultTimezone) {
|
|
|
1999
2001
|
return venueTimezone ?? siteDefaultTimezone;
|
|
2000
2002
|
}
|
|
2001
2003
|
function resolveBookingTimeZoneId(...candidates) {
|
|
2002
|
-
for (const candidate of candidates) {
|
|
2003
|
-
const normalized = typeof candidate === "string" ? candidate.trim() : "";
|
|
2004
|
+
for (const candidate$1 of candidates) {
|
|
2005
|
+
const normalized = typeof candidate$1 === "string" ? candidate$1.trim() : "";
|
|
2004
2006
|
if (normalized && isValidBookingTimeZone(normalized)) {
|
|
2005
2007
|
return normalized;
|
|
2006
2008
|
}
|
|
@@ -2220,8 +2222,8 @@ function parseBookingLocalDate(value) {
|
|
|
2220
2222
|
const year = Number(match.groups.year);
|
|
2221
2223
|
const month = Number(match.groups.month);
|
|
2222
2224
|
const day = Number(match.groups.day);
|
|
2223
|
-
const candidate = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
2224
|
-
if (Number.isNaN(candidate.getTime()) || candidate.getUTCFullYear() !== year || candidate.getUTCMonth() + 1 !== month || candidate.getUTCDate() !== day) {
|
|
2225
|
+
const candidate$1 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
2226
|
+
if (Number.isNaN(candidate$1.getTime()) || candidate$1.getUTCFullYear() !== year || candidate$1.getUTCMonth() + 1 !== month || candidate$1.getUTCDate() !== day) {
|
|
2225
2227
|
throw new Error(`Invalid booking date format: "${value}" (expected YYYY-MM-DD)`);
|
|
2226
2228
|
}
|
|
2227
2229
|
return {
|
|
@@ -2565,12 +2567,12 @@ function decideRangeLabels(span, context, profile) {
|
|
|
2565
2567
|
label: start
|
|
2566
2568
|
};
|
|
2567
2569
|
}
|
|
2568
|
-
function presentRangeEndpoint(value, context, profile, label) {
|
|
2570
|
+
function presentRangeEndpoint(value, context, profile, label$1) {
|
|
2569
2571
|
const date = presentDate({
|
|
2570
2572
|
kind: "instant",
|
|
2571
2573
|
value
|
|
2572
2574
|
}, context, profile);
|
|
2573
|
-
const time = label === undefined ? presentTime(value, context, profile) : `${presentClock(value, context, profile.clock)} ${label}`;
|
|
2575
|
+
const time = label$1 === undefined ? presentTime(value, context, profile) : `${presentClock(value, context, profile.clock)} ${label$1}`;
|
|
2574
2576
|
return `${date}, ${time}`;
|
|
2575
2577
|
}
|
|
2576
2578
|
function presentNativeRange(span, context, format) {
|
|
@@ -3022,7 +3024,7 @@ function selectCategoryView(categories, services, filterCategoryId) {
|
|
|
3022
3024
|
layout: publicLayout()
|
|
3023
3025
|
};
|
|
3024
3026
|
}
|
|
3025
|
-
const category = categories.find((candidate) => candidate.id === filterCategoryId);
|
|
3027
|
+
const category = categories.find((candidate$1) => candidate$1.id === filterCategoryId);
|
|
3026
3028
|
if (category === undefined) {
|
|
3027
3029
|
return {
|
|
3028
3030
|
kind: "fallback-all",
|
|
@@ -3112,26 +3114,167 @@ const RECTIFICATION_HISTORY_PROPAGATION_PLANS = {
|
|
|
3112
3114
|
};
|
|
3113
3115
|
|
|
3114
3116
|
//#endregion
|
|
3115
|
-
//#region ../core/src/
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3117
|
+
//#region ../core/src/map-drawing/model.ts
|
|
3118
|
+
const MAX_DRAWING_BYTES = 1024 * 1024;
|
|
3119
|
+
const DRAWING_SIZE = 800;
|
|
3120
|
+
const DRAWING_SCHEMA_VERSION = 2;
|
|
3121
|
+
const DRAWING_GENERATOR_VERSION = 3;
|
|
3122
|
+
/** Current real city extracts fit with >2x headroom; reservations precede allocation. */
|
|
3123
|
+
const DRAWING_DECODE_LIMITS = {
|
|
3124
|
+
pathsPerFeature: 2048,
|
|
3125
|
+
verticesPerPath: 8192,
|
|
3126
|
+
verticesPerFeature: 32768,
|
|
3127
|
+
featuresPerLayer: 1e4,
|
|
3128
|
+
featuresPerTile: 16e3,
|
|
3129
|
+
featuresPerAttempt: 6e4,
|
|
3130
|
+
verticesPerAttempt: 1e6
|
|
3131
|
+
};
|
|
3132
|
+
/**
|
|
3133
|
+
* The two fixed framings. Neighbourhood draws 1 m per view-box unit, wider 4 m, so the same stroke and label
|
|
3134
|
+
* sizes read well in both. The wider view leaves out buildings, distant paths and small green patches, which turn to
|
|
3135
|
+
* noise at that scale. `minParkArea` is in square view-box units. Labels go to the nearest streets first, with a
|
|
3136
|
+
* minor street counted as `minorLabelDistance` times further away than it is, so the wider view names main roads.
|
|
3137
|
+
*/
|
|
3138
|
+
const FRAMINGS = [{
|
|
3139
|
+
name: "neighbourhood",
|
|
3140
|
+
radiusMeters: 400,
|
|
3141
|
+
zoom: 15,
|
|
3142
|
+
roads: [
|
|
3143
|
+
"major",
|
|
3144
|
+
"minor",
|
|
3145
|
+
"path"
|
|
3146
|
+
],
|
|
3147
|
+
maxPathDistance: 800,
|
|
3148
|
+
buildings: true,
|
|
3149
|
+
minParkArea: .3,
|
|
3150
|
+
maxLabels: 6,
|
|
3151
|
+
minorLabelDistance: 1.5
|
|
3152
|
+
}, {
|
|
3153
|
+
name: "wider",
|
|
3154
|
+
radiusMeters: 1600,
|
|
3155
|
+
zoom: 14,
|
|
3156
|
+
roads: [
|
|
3157
|
+
"major",
|
|
3158
|
+
"minor",
|
|
3159
|
+
"path"
|
|
3160
|
+
],
|
|
3161
|
+
maxPathDistance: 100,
|
|
3162
|
+
buildings: false,
|
|
3163
|
+
minParkArea: 40,
|
|
3164
|
+
maxLabels: 5,
|
|
3165
|
+
minorLabelDistance: 4
|
|
3166
|
+
}];
|
|
3167
|
+
const tilePoint = z.tuple([z.number().finite().min(-8192).max(12288), z.number().finite().min(-8192).max(12288)]).readonly();
|
|
3168
|
+
const paths = z.array(z.array(tilePoint).min(2).max(DRAWING_DECODE_LIMITS.verticesPerPath).readonly()).min(1).max(DRAWING_DECODE_LIMITS.pathsPerFeature).readonly();
|
|
3169
|
+
const featureFields = {
|
|
3170
|
+
kind: z.string().max(80),
|
|
3171
|
+
name: z.string().trim().max(60).regex(/^[^<>\u0000-\u001f\u007f]*$/).nullish().transform((value) => value === undefined || value === null || value === "" ? null : value),
|
|
3172
|
+
sortRank: z.number().finite().min(-1e4).max(1e4).default(0),
|
|
3173
|
+
paths: paths.refine((value) => value.reduce((n, path) => n + path.length, 0) <= DRAWING_DECODE_LIMITS.verticesPerFeature)
|
|
3174
|
+
};
|
|
3175
|
+
/** An unusable optional label must not discard otherwise valid road geometry. */
|
|
3176
|
+
function normalizeMapDrawingName(value) {
|
|
3177
|
+
const parsed = featureFields.name.safeParse(value);
|
|
3178
|
+
return parsed.success ? parsed.data : null;
|
|
3179
|
+
}
|
|
3180
|
+
const feature = z.discriminatedUnion("geometry", [z.strictObject({
|
|
3181
|
+
...featureFields,
|
|
3182
|
+
layer: z.literal("roads"),
|
|
3183
|
+
geometry: z.literal("line")
|
|
3184
|
+
}).readonly(), z.strictObject({
|
|
3185
|
+
...featureFields,
|
|
3186
|
+
layer: z.enum([
|
|
3187
|
+
"earth",
|
|
3188
|
+
"water",
|
|
3189
|
+
"landuse",
|
|
3190
|
+
"buildings"
|
|
3191
|
+
]),
|
|
3192
|
+
geometry: z.literal("polygon")
|
|
3193
|
+
}).refine((value) => value.paths.every((ring) => ring.length >= 3)).readonly()]);
|
|
3194
|
+
const tile = z.strictObject({
|
|
3195
|
+
z: z.union([z.literal(14), z.literal(15)]),
|
|
3196
|
+
x: z.number().int().nonnegative(),
|
|
3197
|
+
y: z.number().int().nonnegative(),
|
|
3198
|
+
extent: z.literal(4096),
|
|
3199
|
+
features: z.array(feature).max(DRAWING_DECODE_LIMITS.featuresPerTile).readonly()
|
|
3200
|
+
}).refine((value) => value.x < 2 ** value.z && value.y < 2 ** value.z).readonly();
|
|
3201
|
+
const mapDrawingInputSchema = z.strictObject({
|
|
3202
|
+
buildId: z.string().regex(/^\d{8}$/),
|
|
3203
|
+
tileSchemaVersion: z.string().regex(/^4\.\d+\.\d+$/),
|
|
3204
|
+
pin: z.lazy(() => locationPinSchema),
|
|
3205
|
+
tiles: z.array(tile).min(1).max(96).readonly()
|
|
3206
|
+
}).superRefine((value, ctx) => {
|
|
3207
|
+
const features = value.tiles.flatMap((item) => item.features);
|
|
3208
|
+
if (features.length > DRAWING_DECODE_LIMITS.featuresPerAttempt || features.reduce((sum, item) => sum + item.paths.reduce((count, path) => count + path.length, 0), 0) > DRAWING_DECODE_LIMITS.verticesPerAttempt) {
|
|
3209
|
+
ctx.addIssue({
|
|
3210
|
+
code: "custom",
|
|
3211
|
+
message: "Decoded geometry exceeds the attempt budget."
|
|
3212
|
+
});
|
|
3129
3213
|
}
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3214
|
+
}).readonly();
|
|
3215
|
+
const point = z.tuple([z.number().min(0).max(DRAWING_SIZE), z.number().min(0).max(DRAWING_SIZE)]).readonly();
|
|
3216
|
+
const polygon = z.strictObject({
|
|
3217
|
+
kind: z.literal("polygon"),
|
|
3218
|
+
fillRule: z.literal("evenodd"),
|
|
3219
|
+
className: z.enum([
|
|
3220
|
+
"map-land",
|
|
3221
|
+
"map-water",
|
|
3222
|
+
"map-park",
|
|
3223
|
+
"map-building"
|
|
3224
|
+
]),
|
|
3225
|
+
rings: z.array(z.array(point).min(3).max(8192).readonly()).min(1).max(DRAWING_DECODE_LIMITS.pathsPerFeature).readonly()
|
|
3226
|
+
}).readonly();
|
|
3227
|
+
const line = z.strictObject({
|
|
3228
|
+
kind: z.literal("line"),
|
|
3229
|
+
className: z.enum([
|
|
3230
|
+
"map-road-major-casing",
|
|
3231
|
+
"map-road-major",
|
|
3232
|
+
"map-road-minor-casing",
|
|
3233
|
+
"map-road-minor",
|
|
3234
|
+
"map-road-path"
|
|
3235
|
+
]),
|
|
3236
|
+
points: z.array(point).min(2).max(8192).readonly()
|
|
3237
|
+
}).readonly();
|
|
3238
|
+
const drawingPathSchema = z.discriminatedUnion("kind", [polygon, line]);
|
|
3239
|
+
const label = z.strictObject({
|
|
3240
|
+
className: z.literal("map-label"),
|
|
3241
|
+
haloClassName: z.literal("map-label-halo"),
|
|
3242
|
+
text: z.string().trim().min(1).max(60).regex(/^[^<>\u0000-\u001f\u007f]*$/),
|
|
3243
|
+
point,
|
|
3244
|
+
angle: z.number().min(-90).max(90)
|
|
3245
|
+
}).readonly();
|
|
3246
|
+
const drawingFrameSchema = z.strictObject({
|
|
3247
|
+
radiusMeters: z.union([z.literal(FRAMINGS[0].radiusMeters), z.literal(FRAMINGS[1].radiusMeters)]),
|
|
3248
|
+
viewBox: z.tuple([
|
|
3249
|
+
z.literal(0),
|
|
3250
|
+
z.literal(0),
|
|
3251
|
+
z.literal(800),
|
|
3252
|
+
z.literal(800)
|
|
3253
|
+
]).readonly(),
|
|
3254
|
+
backgroundClass: z.literal("map-water"),
|
|
3255
|
+
paths: z.array(drawingPathSchema).max(1800).readonly(),
|
|
3256
|
+
labels: z.array(label).max(12).readonly(),
|
|
3257
|
+
pin: z.strictObject({
|
|
3258
|
+
className: z.literal("map-pin"),
|
|
3259
|
+
point: z.tuple([z.literal(400), z.literal(400)]).readonly()
|
|
3260
|
+
}).readonly()
|
|
3261
|
+
}).superRefine((value, ctx) => {
|
|
3262
|
+
const count = value.paths.reduce((sum, path) => sum + (path.kind === "line" ? path.points.length : path.rings.reduce((n, ring) => n + ring.length, 0)), 0);
|
|
3263
|
+
if (count > 5e4) ctx.addIssue({
|
|
3264
|
+
code: "custom",
|
|
3265
|
+
message: "Drawing exceeds its vertex budget."
|
|
3266
|
+
});
|
|
3267
|
+
}).readonly();
|
|
3268
|
+
const mapDrawingSchema = z.strictObject({
|
|
3269
|
+
schemaVersion: z.literal(DRAWING_SCHEMA_VERSION),
|
|
3270
|
+
generatorVersion: z.literal(DRAWING_GENERATOR_VERSION),
|
|
3271
|
+
buildId: z.string().regex(/^\d{8}$/),
|
|
3272
|
+
tileSchemaVersion: z.string().regex(/^4\.\d+\.\d+$/),
|
|
3273
|
+
frames: z.strictObject({
|
|
3274
|
+
neighbourhood: drawingFrameSchema,
|
|
3275
|
+
wider: drawingFrameSchema
|
|
3276
|
+
}).readonly()
|
|
3277
|
+
}).refine((value) => value.frames.neighbourhood.radiusMeters === FRAMINGS[0].radiusMeters && value.frames.wider.radiusMeters === FRAMINGS[1].radiusMeters).refine((value) => new TextEncoder().encode(JSON.stringify(value)).byteLength <= MAX_DRAWING_BYTES, { error: "Drawing exceeds its byte budget." }).readonly();
|
|
3135
3278
|
|
|
3136
3279
|
//#endregion
|
|
3137
3280
|
//#region ../core/src/string.ts
|
|
@@ -3182,6 +3325,490 @@ function capitalize(str) {
|
|
|
3182
3325
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
3183
3326
|
}
|
|
3184
3327
|
|
|
3328
|
+
//#endregion
|
|
3329
|
+
//#region ../core/src/location.ts
|
|
3330
|
+
/** One complete, finite, bounded arrival pin; zero is a valid coordinate. */
|
|
3331
|
+
const locationPinSchema = z.strictObject({
|
|
3332
|
+
lat: z.number().min(-90).max(90),
|
|
3333
|
+
lng: z.number().min(-180).max(180)
|
|
3334
|
+
}).readonly().brand();
|
|
3335
|
+
const addressPart = z.string().trim().max(200).nullish().transform((value) => trimmedStringOrNull(value));
|
|
3336
|
+
/** Building identifies the place; unit/floor refine it without moving it. */
|
|
3337
|
+
const locationAddressSchema = z.strictObject({
|
|
3338
|
+
building: addressPart,
|
|
3339
|
+
unit: addressPart,
|
|
3340
|
+
floor: addressPart,
|
|
3341
|
+
street: addressPart,
|
|
3342
|
+
locality: addressPart,
|
|
3343
|
+
region: addressPart,
|
|
3344
|
+
postalCode: addressPart,
|
|
3345
|
+
country: addressPart.transform((value) => value?.length === 2 ? value.toUpperCase() : value)
|
|
3346
|
+
}).readonly().transform((address) => Object.values(address).some((part) => part !== null) ? address : null);
|
|
3347
|
+
const locationAddressMatchSchema = z.strictObject({
|
|
3348
|
+
point: locationPinSchema,
|
|
3349
|
+
precision: z.enum([
|
|
3350
|
+
"building",
|
|
3351
|
+
"street",
|
|
3352
|
+
"area",
|
|
3353
|
+
"unknown"
|
|
3354
|
+
]),
|
|
3355
|
+
placeId: z.string().trim().max(1024).regex(/^[a-z][a-z0-9-]*:\S+$/),
|
|
3356
|
+
attribution: z.array(z.strictObject({
|
|
3357
|
+
text: z.string().trim().min(1).max(500),
|
|
3358
|
+
url: z.url().max(2048).refine((value) => /^https?:\/\//.test(value))
|
|
3359
|
+
}).readonly()).min(1).max(4).readonly()
|
|
3360
|
+
}).readonly();
|
|
3361
|
+
const locationValueSchema = z.strictObject({
|
|
3362
|
+
address: locationAddressSchema.nullable().default(null),
|
|
3363
|
+
pin: locationPinSchema.nullable().default(null),
|
|
3364
|
+
doorNote: z.string().trim().max(1e3).nullish().transform(trimmedStringOrNull),
|
|
3365
|
+
addressMatch: locationAddressMatchSchema.nullable().default(null),
|
|
3366
|
+
addressCheckSettled: z.literal(true).optional(),
|
|
3367
|
+
ownerConfirmedPinAndAddress: z.boolean().default(false)
|
|
3368
|
+
}).superRefine((value, context) => {
|
|
3369
|
+
if (value.address === null && value.pin === null) {
|
|
3370
|
+
context.addIssue({
|
|
3371
|
+
code: "custom",
|
|
3372
|
+
path: ["address"],
|
|
3373
|
+
message: "Use null to remove a location."
|
|
3374
|
+
});
|
|
3375
|
+
}
|
|
3376
|
+
if (value.address === null && value.addressMatch !== null) {
|
|
3377
|
+
context.addIssue({
|
|
3378
|
+
code: "custom",
|
|
3379
|
+
path: ["addressMatch"],
|
|
3380
|
+
message: "Address evidence requires an address."
|
|
3381
|
+
});
|
|
3382
|
+
}
|
|
3383
|
+
if (value.address === null && value.addressCheckSettled) {
|
|
3384
|
+
context.addIssue({
|
|
3385
|
+
code: "custom",
|
|
3386
|
+
path: ["addressCheckSettled"],
|
|
3387
|
+
message: "A settled address check requires an address."
|
|
3388
|
+
});
|
|
3389
|
+
}
|
|
3390
|
+
if (value.ownerConfirmedPinAndAddress && (value.address === null || value.pin === null)) {
|
|
3391
|
+
context.addIssue({
|
|
3392
|
+
code: "custom",
|
|
3393
|
+
path: ["ownerConfirmedPinAndAddress"],
|
|
3394
|
+
message: "Confirmation requires both address and pin."
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
}).readonly();
|
|
3398
|
+
/** API/DB ingress parses once. Removed locations are represented by null. */
|
|
3399
|
+
function parseLocationValue(input) {
|
|
3400
|
+
const parsed = locationValueSchema.nullable().safeParse(input);
|
|
3401
|
+
return parsed.success ? ok(parsed.data) : err({
|
|
3402
|
+
kind: "invalid_location",
|
|
3403
|
+
issues: parsed.error.issues
|
|
3404
|
+
});
|
|
3405
|
+
}
|
|
3406
|
+
/** One display formatter: a known matching site country is redundant. */
|
|
3407
|
+
function formatLocationAddress(address, siteCountry) {
|
|
3408
|
+
if (address === null) return null;
|
|
3409
|
+
const parts = [
|
|
3410
|
+
address.building,
|
|
3411
|
+
address.street,
|
|
3412
|
+
address.unit,
|
|
3413
|
+
address.floor,
|
|
3414
|
+
address.locality,
|
|
3415
|
+
address.region,
|
|
3416
|
+
address.postalCode,
|
|
3417
|
+
sameLocationCountry(address.country, siteCountry) ? null : address.country
|
|
3418
|
+
];
|
|
3419
|
+
return parts.filter((part) => part !== null).join(", ") || null;
|
|
3420
|
+
}
|
|
3421
|
+
const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
|
3422
|
+
const countryIdentity = (country) => {
|
|
3423
|
+
const text = country.trim();
|
|
3424
|
+
const code = text.toUpperCase() === "UK" ? "GB" : text.toUpperCase();
|
|
3425
|
+
return (code.length === 2 && /^[A-Z]{2}$/.test(code) ? regionNames.of(code) ?? text : text).toLowerCase();
|
|
3426
|
+
};
|
|
3427
|
+
function sameLocationCountry(country, siteCountry) {
|
|
3428
|
+
return country !== null && Boolean(siteCountry?.trim()) && countryIdentity(country) === countryIdentity(siteCountry ?? "");
|
|
3429
|
+
}
|
|
3430
|
+
const publicLocationSchema = z.strictObject({
|
|
3431
|
+
drawing: z.lazy(() => mapDrawingSchema).optional().catch(undefined),
|
|
3432
|
+
address: locationAddressSchema.nullable(),
|
|
3433
|
+
displayAddress: z.string().nullable().default(null),
|
|
3434
|
+
pin: locationPinSchema.nullable(),
|
|
3435
|
+
doorNote: z.string().max(1e3).nullable(),
|
|
3436
|
+
attribution: z.array(locationAddressMatchSchema.unwrap().shape.attribution.unwrap().element).max(4).readonly()
|
|
3437
|
+
}).readonly();
|
|
3438
|
+
/** Permission belongs to the resource/source owner before this projection. */
|
|
3439
|
+
function projectPublicLocation(location, siteCountry) {
|
|
3440
|
+
return {
|
|
3441
|
+
address: location.address,
|
|
3442
|
+
displayAddress: formatLocationAddress(location.address, siteCountry),
|
|
3443
|
+
pin: location.pin,
|
|
3444
|
+
doorNote: location.doorNote,
|
|
3445
|
+
attribution: location.addressMatch?.attribution ?? []
|
|
3446
|
+
};
|
|
3447
|
+
}
|
|
3448
|
+
function normalizeLocationAddressText(value) {
|
|
3449
|
+
if (typeof value !== "string") {
|
|
3450
|
+
return null;
|
|
3451
|
+
}
|
|
3452
|
+
return trimmedStringOrNull(value);
|
|
3453
|
+
}
|
|
3454
|
+
function hasLocationCoordinates(value) {
|
|
3455
|
+
return value != null && typeof value.lat === "number" && Number.isFinite(value.lat) && Math.abs(value.lat) <= 90 && typeof value.lng === "number" && Number.isFinite(value.lng) && Math.abs(value.lng) <= 180;
|
|
3456
|
+
}
|
|
3457
|
+
function toLocationCoordinates(value) {
|
|
3458
|
+
if (!hasLocationCoordinates(value)) {
|
|
3459
|
+
return null;
|
|
3460
|
+
}
|
|
3461
|
+
return {
|
|
3462
|
+
lat: value.lat,
|
|
3463
|
+
lng: value.lng
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
/** Decide permission before loading or projecting the selected source. */
|
|
3467
|
+
function decideLocationSourcePermission(source, businessAddressVisibility) {
|
|
3468
|
+
if (source === "business_address" && businessAddressVisibility === undefined) return { kind: "unresolved" };
|
|
3469
|
+
return source === "business_address" && businessAddressVisibility === "hidden" ? { kind: "hidden" } : {
|
|
3470
|
+
kind: "permitted",
|
|
3471
|
+
source
|
|
3472
|
+
};
|
|
3473
|
+
}
|
|
3474
|
+
function resolvePermittedLocation(permission, location, siteCountry) {
|
|
3475
|
+
if (permission.kind !== "permitted") return permission;
|
|
3476
|
+
return location === null ? {
|
|
3477
|
+
kind: "missing",
|
|
3478
|
+
source: permission.source
|
|
3479
|
+
} : {
|
|
3480
|
+
kind: "visible",
|
|
3481
|
+
source: permission.source,
|
|
3482
|
+
location: projectPublicLocation(location, siteCountry)
|
|
3483
|
+
};
|
|
3484
|
+
}
|
|
3485
|
+
|
|
3486
|
+
//#endregion
|
|
3487
|
+
//#region ../core/src/locationLinks.ts
|
|
3488
|
+
/** Both outbound actions use the arrival pin, never match evidence. */
|
|
3489
|
+
function locationMapLinks(input) {
|
|
3490
|
+
const target = input.location != null ? `${input.location.lat},${input.location.lng}` : normalizeLocationAddressText(input.addressText);
|
|
3491
|
+
if (target === null) return {
|
|
3492
|
+
directions: null,
|
|
3493
|
+
googleMaps: null
|
|
3494
|
+
};
|
|
3495
|
+
const directions = new URL("https://www.google.com/maps/dir/");
|
|
3496
|
+
directions.searchParams.set("api", "1");
|
|
3497
|
+
directions.searchParams.set("destination", target);
|
|
3498
|
+
const search = new URL("https://www.google.com/maps/search/");
|
|
3499
|
+
search.searchParams.set("api", "1");
|
|
3500
|
+
search.searchParams.set("query", target);
|
|
3501
|
+
return {
|
|
3502
|
+
directions: directions.toString(),
|
|
3503
|
+
googleMaps: search.toString()
|
|
3504
|
+
};
|
|
3505
|
+
}
|
|
3506
|
+
|
|
3507
|
+
//#endregion
|
|
3508
|
+
//#region ../core/src/stable-json.ts
|
|
3509
|
+
function stableJsonStringify(value) {
|
|
3510
|
+
return JSON.stringify(sortStableJsonValue(value));
|
|
3511
|
+
}
|
|
3512
|
+
function sortStableJsonValue(value) {
|
|
3513
|
+
if (value === null) return null;
|
|
3514
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
3515
|
+
if (typeof value === "number") {
|
|
3516
|
+
if (!Number.isFinite(value)) {
|
|
3517
|
+
throw new TypeError("Stable JSON only supports finite numbers");
|
|
3518
|
+
}
|
|
3519
|
+
return value;
|
|
3520
|
+
}
|
|
3521
|
+
if (Array.isArray(value)) {
|
|
3522
|
+
return value.map(sortStableJsonValue);
|
|
3523
|
+
}
|
|
3524
|
+
if (typeof value === "object") {
|
|
3525
|
+
const prototype = Object.getPrototypeOf(value);
|
|
3526
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
3527
|
+
throw new TypeError("Stable JSON only supports plain objects");
|
|
3528
|
+
}
|
|
3529
|
+
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sortStableJsonValue(child)]));
|
|
3530
|
+
}
|
|
3531
|
+
throw new TypeError(`Stable JSON does not support ${typeof value}`);
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3534
|
+
//#endregion
|
|
3535
|
+
//#region ../core/src/location-relationship.ts
|
|
3536
|
+
/** Initial bounded policy; Opus can tune these values without changing callers. */
|
|
3537
|
+
const LOCATION_MATCH_THRESHOLDS_METRES = {
|
|
3538
|
+
building: 100,
|
|
3539
|
+
street: 300,
|
|
3540
|
+
area: 1e3
|
|
3541
|
+
};
|
|
3542
|
+
function locationDistanceMetres(a, b) {
|
|
3543
|
+
const radians = Math.PI / 180;
|
|
3544
|
+
const haversine = Math.sin((b.lat - a.lat) * radians / 2) ** 2 + Math.cos(a.lat * radians) * Math.cos(b.lat * radians) * Math.sin((b.lng - a.lng) * radians / 2) ** 2;
|
|
3545
|
+
return 6371e3 * 2 * Math.asin(Math.sqrt(Math.min(1, Math.max(0, haversine))));
|
|
3546
|
+
}
|
|
3547
|
+
/** Arrival pins are never substituted for missing provider address points. */
|
|
3548
|
+
function decideLocationRelationship(location) {
|
|
3549
|
+
if (location === null) return { kind: "empty" };
|
|
3550
|
+
if (location.pin === null) return { kind: "address_only" };
|
|
3551
|
+
if (location.address === null) return { kind: "pin_only" };
|
|
3552
|
+
if (location.addressMatch === null) {
|
|
3553
|
+
return { kind: "unchecked" };
|
|
3554
|
+
}
|
|
3555
|
+
const { point: point$1, precision } = location.addressMatch;
|
|
3556
|
+
const distanceMetres = locationDistanceMetres(point$1, location.pin);
|
|
3557
|
+
const thresholdMetres = LOCATION_MATCH_THRESHOLDS_METRES[precision === "unknown" ? "building" : precision];
|
|
3558
|
+
const beyondThreshold = distanceMetres > thresholdMetres * (1 + 8 * Number.EPSILON);
|
|
3559
|
+
return {
|
|
3560
|
+
kind: beyondThreshold ? "far" : precision === "building" || precision === "unknown" ? "near" : "approximate",
|
|
3561
|
+
distanceMetres,
|
|
3562
|
+
thresholdMetres,
|
|
3563
|
+
precision
|
|
3564
|
+
};
|
|
3565
|
+
}
|
|
3566
|
+
const spatialAddressParts = [
|
|
3567
|
+
"building",
|
|
3568
|
+
"street",
|
|
3569
|
+
"locality",
|
|
3570
|
+
"postalCode",
|
|
3571
|
+
"country"
|
|
3572
|
+
];
|
|
3573
|
+
const comparisonText = (text) => (text ?? "").trim().replace(/\s+/g, " ").toLowerCase();
|
|
3574
|
+
/** Address evidence identity excludes refinements such as unit, floor and region. */
|
|
3575
|
+
function sameMeaningfulAddress(a, b) {
|
|
3576
|
+
if (a === null || b === null) return a === b;
|
|
3577
|
+
return spatialAddressParts.every((part) => comparisonText(a[part]) === comparisonText(b[part]));
|
|
3578
|
+
}
|
|
3579
|
+
function samePin(a, b) {
|
|
3580
|
+
return a === null || b === null ? a === b : a.lat === b.lat && a.lng === b.lng;
|
|
3581
|
+
}
|
|
3582
|
+
/** Spatial identity ignores unit/floor/region/note edits; picker reply tags share this rule. */
|
|
3583
|
+
function sameLocationSpatialPair(a, b) {
|
|
3584
|
+
return a === null || b === null ? a === b : sameMeaningfulAddress(a.address, b.address) && samePin(a.pin, b.pin);
|
|
3585
|
+
}
|
|
3586
|
+
/** Edits cannot carry forward old confirmation or address evidence by accident. */
|
|
3587
|
+
function editLocationValue(previous, details) {
|
|
3588
|
+
if (details.address === null && details.pin === null) return ok(null);
|
|
3589
|
+
const authored = {
|
|
3590
|
+
address: details.address,
|
|
3591
|
+
pin: details.pin,
|
|
3592
|
+
doorNote: details.doorNote
|
|
3593
|
+
};
|
|
3594
|
+
if (previous === null) {
|
|
3595
|
+
return parseLocationValue({
|
|
3596
|
+
...authored,
|
|
3597
|
+
addressMatch: null,
|
|
3598
|
+
ownerConfirmedPinAndAddress: false
|
|
3599
|
+
});
|
|
3600
|
+
}
|
|
3601
|
+
const sameAddress = sameMeaningfulAddress(previous.address, details.address);
|
|
3602
|
+
const samePair = sameAddress && samePin(previous.pin, details.pin);
|
|
3603
|
+
return parseLocationValue({
|
|
3604
|
+
...authored,
|
|
3605
|
+
addressMatch: sameAddress ? previous.addressMatch : null,
|
|
3606
|
+
...sameAddress && previous.addressCheckSettled ? { addressCheckSettled: true } : {},
|
|
3607
|
+
ownerConfirmedPinAndAddress: samePair && previous.ownerConfirmedPinAndAddress
|
|
3608
|
+
});
|
|
3609
|
+
}
|
|
3610
|
+
function confirmLocationPair(location) {
|
|
3611
|
+
return location.address !== null && location.pin !== null ? ok({
|
|
3612
|
+
...location,
|
|
3613
|
+
ownerConfirmedPinAndAddress: true
|
|
3614
|
+
}) : err({ kind: "missing_address_or_pin" });
|
|
3615
|
+
}
|
|
3616
|
+
/** Forms receive the decided save value, rather than a collection of flags. */
|
|
3617
|
+
function decideLocationSave(location, activity) {
|
|
3618
|
+
if (activity === "address-check") return {
|
|
3619
|
+
kind: "blocked",
|
|
3620
|
+
reason: "pending_address_check"
|
|
3621
|
+
};
|
|
3622
|
+
if (activity === "reverse") return {
|
|
3623
|
+
kind: "blocked",
|
|
3624
|
+
reason: "pending_reverse"
|
|
3625
|
+
};
|
|
3626
|
+
const relationship = decideLocationRelationship(location);
|
|
3627
|
+
return relationship.kind === "far" && location?.ownerConfirmedPinAndAddress !== true ? {
|
|
3628
|
+
kind: "blocked",
|
|
3629
|
+
reason: "unconfirmed_mismatch"
|
|
3630
|
+
} : {
|
|
3631
|
+
kind: "ready",
|
|
3632
|
+
location
|
|
3633
|
+
};
|
|
3634
|
+
}
|
|
3635
|
+
/** Opening lookup evidence may change provider facts only, never the owner's saved place. */
|
|
3636
|
+
function planLocationOpeningEvidence(previous, evidence) {
|
|
3637
|
+
const parsed = parseLocationValue(evidence);
|
|
3638
|
+
if (previous === null || !parsed.ok || parsed.value === null) return err({ kind: "invalid_location" });
|
|
3639
|
+
const next = parsed.value;
|
|
3640
|
+
const authored = (value) => ({
|
|
3641
|
+
address: value.address,
|
|
3642
|
+
pin: value.pin,
|
|
3643
|
+
doorNote: value.doorNote,
|
|
3644
|
+
ownerConfirmedPinAndAddress: value.ownerConfirmedPinAndAddress
|
|
3645
|
+
});
|
|
3646
|
+
if (stableJsonStringify(authored(previous)) !== stableJsonStringify(authored(next)) || next.addressMatch === null && next.addressCheckSettled !== true) return err({ kind: "invalid_location" });
|
|
3647
|
+
if (stableJsonStringify(previous) === stableJsonStringify(next)) return ok(next);
|
|
3648
|
+
return previous.addressMatch !== null || previous.addressCheckSettled === true ? err({ kind: "invalid_location" }) : ok(next);
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
//#endregion
|
|
3652
|
+
//#region ../core/src/location-migration.ts
|
|
3653
|
+
/** Offline expand/contract conversions, not runtime fallback readers. */
|
|
3654
|
+
const legacyText = z.string().nullable().optional();
|
|
3655
|
+
const legacyCoordinatesSchema = z.strictObject({
|
|
3656
|
+
lat: z.number().min(-90).max(90).nullable().optional(),
|
|
3657
|
+
lng: z.number().min(-180).max(180).nullable().optional()
|
|
3658
|
+
}).superRefine((value, context) => {
|
|
3659
|
+
if (value.lat == null !== (value.lng == null)) {
|
|
3660
|
+
context.addIssue({
|
|
3661
|
+
code: "custom",
|
|
3662
|
+
message: "Legacy pin is incomplete."
|
|
3663
|
+
});
|
|
3664
|
+
}
|
|
3665
|
+
});
|
|
3666
|
+
const venueAddressSchema = z.strictObject({
|
|
3667
|
+
street: legacyText,
|
|
3668
|
+
line1: legacyText,
|
|
3669
|
+
line2: legacyText,
|
|
3670
|
+
city: legacyText,
|
|
3671
|
+
state: legacyText,
|
|
3672
|
+
region: legacyText,
|
|
3673
|
+
postalCode: legacyText,
|
|
3674
|
+
postal_code: legacyText,
|
|
3675
|
+
postcode: legacyText,
|
|
3676
|
+
country: legacyText,
|
|
3677
|
+
formatted: legacyText,
|
|
3678
|
+
summary: legacyText
|
|
3679
|
+
}).superRefine((value, context) => {
|
|
3680
|
+
const aliases = [
|
|
3681
|
+
[value.street, value.line1],
|
|
3682
|
+
[value.state, value.region],
|
|
3683
|
+
[
|
|
3684
|
+
value.postalCode,
|
|
3685
|
+
value.postal_code,
|
|
3686
|
+
value.postcode
|
|
3687
|
+
],
|
|
3688
|
+
[value.formatted, value.summary]
|
|
3689
|
+
];
|
|
3690
|
+
aliases.forEach((parts) => {
|
|
3691
|
+
const meaningful = parts.filter((part) => displayComparison(part) !== "");
|
|
3692
|
+
if (meaningful.some((part) => displayComparison(part) !== displayComparison(meaningful[0]))) {
|
|
3693
|
+
context.addIssue({
|
|
3694
|
+
code: "custom",
|
|
3695
|
+
message: "Legacy address aliases disagree."
|
|
3696
|
+
});
|
|
3697
|
+
}
|
|
3698
|
+
});
|
|
3699
|
+
}).nullable();
|
|
3700
|
+
const businessAddressSchema = legacyCoordinatesSchema.safeExtend({
|
|
3701
|
+
addressLine1: legacyText,
|
|
3702
|
+
addressLine2: legacyText,
|
|
3703
|
+
locality: legacyText,
|
|
3704
|
+
region: legacyText,
|
|
3705
|
+
postalCode: legacyText,
|
|
3706
|
+
countryCode: legacyText,
|
|
3707
|
+
displayAddress: legacyText
|
|
3708
|
+
});
|
|
3709
|
+
const customLocationSchema = legacyCoordinatesSchema.safeExtend({ addressText: legacyText });
|
|
3710
|
+
function finishConversion(address, coordinates) {
|
|
3711
|
+
const parsedAddress = locationAddressSchema.nullable().safeParse(address);
|
|
3712
|
+
if (!parsedAddress.success) return err({
|
|
3713
|
+
kind: "invalid_legacy_location",
|
|
3714
|
+
issues: parsedAddress.error.issues
|
|
3715
|
+
});
|
|
3716
|
+
const pin = coordinates.lat == null || coordinates.lng == null ? null : {
|
|
3717
|
+
lat: coordinates.lat,
|
|
3718
|
+
lng: coordinates.lng
|
|
3719
|
+
};
|
|
3720
|
+
if (parsedAddress.data === null && pin === null) return ok(null);
|
|
3721
|
+
const result = parseLocationValue({
|
|
3722
|
+
address: parsedAddress.data,
|
|
3723
|
+
pin,
|
|
3724
|
+
addressMatch: null,
|
|
3725
|
+
ownerConfirmedPinAndAddress: false
|
|
3726
|
+
});
|
|
3727
|
+
return result.ok ? result : err({
|
|
3728
|
+
kind: "invalid_legacy_location",
|
|
3729
|
+
issues: result.error.issues
|
|
3730
|
+
});
|
|
3731
|
+
}
|
|
3732
|
+
function convertVenue(input) {
|
|
3733
|
+
const address = venueAddressSchema.safeParse(input.addressJson ?? null);
|
|
3734
|
+
const coordinates = legacyCoordinatesSchema.safeParse(input.locationJson ?? {});
|
|
3735
|
+
if (!address.success) return err({
|
|
3736
|
+
kind: "invalid_legacy_location",
|
|
3737
|
+
issues: address.error.issues
|
|
3738
|
+
});
|
|
3739
|
+
if (!coordinates.success) return err({
|
|
3740
|
+
kind: "invalid_legacy_location",
|
|
3741
|
+
issues: coordinates.error.issues
|
|
3742
|
+
});
|
|
3743
|
+
const result = finishConversion(mapVenueAddress(address.data), coordinates.data);
|
|
3744
|
+
return checkDisplayText(result, firstText(address.data?.formatted, address.data?.summary));
|
|
3745
|
+
}
|
|
3746
|
+
const displayComparison = (text) => (text ?? "").trim().replace(/\s+/g, " ").toLowerCase();
|
|
3747
|
+
const firstText = (...parts) => parts.find((part) => part != null && part.trim().length > 0)?.trim() ?? null;
|
|
3748
|
+
function mapVenueAddress(address) {
|
|
3749
|
+
if (address === null) return null;
|
|
3750
|
+
const structured = {
|
|
3751
|
+
street: firstText(address.street, address.line1),
|
|
3752
|
+
unit: address.line2,
|
|
3753
|
+
locality: address.city,
|
|
3754
|
+
region: firstText(address.state, address.region),
|
|
3755
|
+
postalCode: firstText(address.postalCode, address.postal_code, address.postcode),
|
|
3756
|
+
country: address.country
|
|
3757
|
+
};
|
|
3758
|
+
return Object.values(structured).some((part) => displayComparison(part) !== "") ? structured : { street: firstText(address.formatted, address.summary) };
|
|
3759
|
+
}
|
|
3760
|
+
const displayTokens = (text) => (text ?? "").toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
3761
|
+
/** Only redundant trailing country text is disposable; ordered address words must all survive. */
|
|
3762
|
+
function comparableDisplay(text, countries) {
|
|
3763
|
+
const tokens = displayTokens(text);
|
|
3764
|
+
const suffixIndex = tokens.findIndex((_token, index) => countries.some((country) => sameLocationCountry(tokens.slice(index).join(" "), country)));
|
|
3765
|
+
return (suffixIndex < 0 ? tokens : tokens.slice(0, suffixIndex)).join(" ");
|
|
3766
|
+
}
|
|
3767
|
+
function checkDisplayText(result, text, siteCountry) {
|
|
3768
|
+
if (!result.ok) return result;
|
|
3769
|
+
const original = text?.trim() ?? "";
|
|
3770
|
+
const derived = formatLocationAddress(result.value?.address ?? null);
|
|
3771
|
+
const countries = [result.value?.address?.country, siteCountry];
|
|
3772
|
+
return original.length > 0 && comparableDisplay(original, countries) !== comparableDisplay(derived, countries) ? err({
|
|
3773
|
+
kind: "display_address_conflict",
|
|
3774
|
+
original,
|
|
3775
|
+
derived
|
|
3776
|
+
}) : result;
|
|
3777
|
+
}
|
|
3778
|
+
function convertBusiness(value, siteCountry) {
|
|
3779
|
+
if (value == null) return ok(null);
|
|
3780
|
+
const parsed = businessAddressSchema.safeParse(value);
|
|
3781
|
+
if (!parsed.success) return err({
|
|
3782
|
+
kind: "invalid_legacy_location",
|
|
3783
|
+
issues: parsed.error.issues
|
|
3784
|
+
});
|
|
3785
|
+
const result = finishConversion({
|
|
3786
|
+
street: parsed.data.addressLine1,
|
|
3787
|
+
unit: parsed.data.addressLine2,
|
|
3788
|
+
locality: parsed.data.locality,
|
|
3789
|
+
region: parsed.data.region,
|
|
3790
|
+
postalCode: parsed.data.postalCode,
|
|
3791
|
+
country: parsed.data.countryCode
|
|
3792
|
+
}, parsed.data);
|
|
3793
|
+
return checkDisplayText(result, parsed.data.displayAddress, siteCountry);
|
|
3794
|
+
}
|
|
3795
|
+
function convertCustom(value) {
|
|
3796
|
+
if (value == null) return ok(null);
|
|
3797
|
+
const parsed = customLocationSchema.safeParse(value);
|
|
3798
|
+
return parsed.success ? finishConversion({ street: parsed.data.addressText }, parsed.data) : err({
|
|
3799
|
+
kind: "invalid_legacy_location",
|
|
3800
|
+
issues: parsed.error.issues
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
/** Preserve pins exactly; never split free text or fabricate match evidence. */
|
|
3804
|
+
function convertLegacyLocation(input) {
|
|
3805
|
+
switch (input.kind) {
|
|
3806
|
+
case "venue": return convertVenue(input);
|
|
3807
|
+
case "business": return convertBusiness(input.value, input.siteCountry);
|
|
3808
|
+
case "custom": return convertCustom(input.value);
|
|
3809
|
+
}
|
|
3810
|
+
}
|
|
3811
|
+
|
|
3185
3812
|
//#endregion
|
|
3186
3813
|
//#region ../core/src/business-address.ts
|
|
3187
3814
|
function normalizeCountryCode(value) {
|
|
@@ -3189,7 +3816,7 @@ function normalizeCountryCode(value) {
|
|
|
3189
3816
|
return trimmed !== null ? trimmed.toUpperCase() : null;
|
|
3190
3817
|
}
|
|
3191
3818
|
function normalizeCoordinate(value) {
|
|
3192
|
-
return typeof value === "number"
|
|
3819
|
+
return typeof value === "number" ? value : null;
|
|
3193
3820
|
}
|
|
3194
3821
|
function isLatitude(value) {
|
|
3195
3822
|
return value >= -90 && value <= 90;
|
|
@@ -3197,16 +3824,17 @@ function isLatitude(value) {
|
|
|
3197
3824
|
function isLongitude(value) {
|
|
3198
3825
|
return value >= -180 && value <= 180;
|
|
3199
3826
|
}
|
|
3200
|
-
function buildBusinessAddressDisplayText(input) {
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
trimmedStringOrNull(input.
|
|
3204
|
-
trimmedStringOrNull(input.
|
|
3205
|
-
|
|
3206
|
-
trimmedStringOrNull(input.
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3827
|
+
function buildBusinessAddressDisplayText(input, siteCountry) {
|
|
3828
|
+
return formatLocationAddress({
|
|
3829
|
+
building: null,
|
|
3830
|
+
street: trimmedStringOrNull(input.addressLine1),
|
|
3831
|
+
unit: trimmedStringOrNull(input.addressLine2),
|
|
3832
|
+
floor: null,
|
|
3833
|
+
locality: trimmedStringOrNull(input.locality),
|
|
3834
|
+
region: trimmedStringOrNull(input.region),
|
|
3835
|
+
postalCode: trimmedStringOrNull(input.postalCode),
|
|
3836
|
+
country: normalizeCountryCode(input.countryCode)
|
|
3837
|
+
}, siteCountry);
|
|
3210
3838
|
}
|
|
3211
3839
|
function validateBusinessAddressDraft(input) {
|
|
3212
3840
|
const addressLine1 = trimmedStringOrNull(input.addressLine1);
|
|
@@ -3218,16 +3846,13 @@ function validateBusinessAddressDraft(input) {
|
|
|
3218
3846
|
const displayAddress = trimmedStringOrNull(input.displayAddress) ?? buildBusinessAddressDisplayText(input);
|
|
3219
3847
|
const lat = normalizeCoordinate(input.lat);
|
|
3220
3848
|
const lng = normalizeCoordinate(input.lng);
|
|
3221
|
-
const errors = [
|
|
3222
|
-
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
if (lng === null) errors.push("missing_longitude");
|
|
3229
|
-
else if (!isLongitude(lng)) errors.push("invalid_longitude");
|
|
3230
|
-
if (errors.length > 0 || addressLine1 === null || locality === null || countryCode === null || displayAddress === null || lat === null || lng === null) {
|
|
3849
|
+
const errors = [...missingPostalFields({
|
|
3850
|
+
addressLine1,
|
|
3851
|
+
locality,
|
|
3852
|
+
countryCode,
|
|
3853
|
+
displayAddress
|
|
3854
|
+
}), ...businessPinErrors(lat, lng)];
|
|
3855
|
+
if (errors.length > 0 || addressLine1 === null || locality === null || countryCode === null || displayAddress === null) {
|
|
3231
3856
|
return {
|
|
3232
3857
|
success: false,
|
|
3233
3858
|
errors
|
|
@@ -3260,6 +3885,95 @@ function businessAddressToAddressedLocation(address) {
|
|
|
3260
3885
|
lng: address.lng
|
|
3261
3886
|
};
|
|
3262
3887
|
}
|
|
3888
|
+
/** Temporary postal/editor response adapter. Canonical storage never uses this shape. */
|
|
3889
|
+
function businessAddressFromLocation(location, siteCountry) {
|
|
3890
|
+
if (location === null || location.address === null) return null;
|
|
3891
|
+
const address = location.address;
|
|
3892
|
+
const displayAddress = formatLocationAddress(address, siteCountry);
|
|
3893
|
+
if (displayAddress === null) return null;
|
|
3894
|
+
const pin = location.pin ?? {
|
|
3895
|
+
lat: null,
|
|
3896
|
+
lng: null
|
|
3897
|
+
};
|
|
3898
|
+
return {
|
|
3899
|
+
addressLine1: [address.building, address.street].filter((part) => part !== null).join(", "),
|
|
3900
|
+
addressLine2: [address.unit, address.floor].filter((part) => part !== null).join(", ") || null,
|
|
3901
|
+
locality: address.locality ?? "",
|
|
3902
|
+
region: address.region,
|
|
3903
|
+
postalCode: address.postalCode,
|
|
3904
|
+
countryCode: address.country ?? "",
|
|
3905
|
+
displayAddress,
|
|
3906
|
+
lat: pin.lat,
|
|
3907
|
+
lng: pin.lng
|
|
3908
|
+
};
|
|
3909
|
+
}
|
|
3910
|
+
/** Legacy editor inputs are explicit write adapters, never fallback storage readers. */
|
|
3911
|
+
function planBusinessLocationWrite(previous, input) {
|
|
3912
|
+
if ("locationValue" in input && input.expectedLocationValue !== undefined) {
|
|
3913
|
+
if (stableJsonStringify(previous) !== stableJsonStringify(input.expectedLocationValue)) return err({ kind: "invalid_location" });
|
|
3914
|
+
return planLocationOpeningEvidence(previous, input.locationValue);
|
|
3915
|
+
}
|
|
3916
|
+
const planned = "locationValue" in input ? parseLocationValue(input.locationValue) : planLegacyBusinessLocation(previous, input.businessAddress);
|
|
3917
|
+
if (!planned.ok) return err({ kind: "locationValue" in input ? "invalid_location" : "invalid_legacy_location" });
|
|
3918
|
+
const decision = decideLocationSave(planned.value, "idle");
|
|
3919
|
+
return decision.kind === "blocked" ? err({ kind: "unconfirmed_mismatch" }) : ok(decision.location);
|
|
3920
|
+
}
|
|
3921
|
+
function planLegacyBusinessLocation(previous, address) {
|
|
3922
|
+
if (address === null) return ok(null);
|
|
3923
|
+
if (previous === null) return convertLegacyBusinessEdit(previous, address);
|
|
3924
|
+
const projection = businessAddressFromLocation(previous);
|
|
3925
|
+
return projection !== null && sameBusinessPostalProjection(projection, address) ? editBusinessPin(previous, address) : convertLegacyBusinessEdit(previous, address);
|
|
3926
|
+
}
|
|
3927
|
+
function editBusinessPin(previous, address) {
|
|
3928
|
+
const pin = locationPinSchema.nullable().safeParse(address.lat === null && address.lng === null ? null : {
|
|
3929
|
+
lat: address.lat,
|
|
3930
|
+
lng: address.lng
|
|
3931
|
+
});
|
|
3932
|
+
return pin.success ? editLocationValue(previous, {
|
|
3933
|
+
address: previous.address,
|
|
3934
|
+
pin: pin.data,
|
|
3935
|
+
doorNote: previous.doorNote
|
|
3936
|
+
}) : err({ kind: "invalid_location" });
|
|
3937
|
+
}
|
|
3938
|
+
function convertLegacyBusinessEdit(previous, address) {
|
|
3939
|
+
const converted = convertLegacyLocation({
|
|
3940
|
+
kind: "business",
|
|
3941
|
+
value: address
|
|
3942
|
+
});
|
|
3943
|
+
if (!converted.ok || converted.value === null) return converted;
|
|
3944
|
+
return editLocationValue(previous, {
|
|
3945
|
+
address: converted.value.address,
|
|
3946
|
+
pin: converted.value.pin,
|
|
3947
|
+
doorNote: previous?.doorNote ?? null
|
|
3948
|
+
});
|
|
3949
|
+
}
|
|
3950
|
+
function sameBusinessPostalProjection(a, b) {
|
|
3951
|
+
const fields = [
|
|
3952
|
+
"addressLine1",
|
|
3953
|
+
"addressLine2",
|
|
3954
|
+
"locality",
|
|
3955
|
+
"region",
|
|
3956
|
+
"postalCode",
|
|
3957
|
+
"countryCode",
|
|
3958
|
+
"displayAddress"
|
|
3959
|
+
];
|
|
3960
|
+
return fields.every((field) => (a[field] ?? "").trim().replace(/\s+/g, " ").toLowerCase() === (b[field] ?? "").trim().replace(/\s+/g, " ").toLowerCase());
|
|
3961
|
+
}
|
|
3962
|
+
function missingPostalFields(fields) {
|
|
3963
|
+
const required = [
|
|
3964
|
+
[fields.addressLine1, "missing_address_line_1"],
|
|
3965
|
+
[fields.locality, "missing_locality"],
|
|
3966
|
+
[fields.countryCode, "missing_country_code"],
|
|
3967
|
+
[fields.displayAddress, "missing_display_address"]
|
|
3968
|
+
];
|
|
3969
|
+
return required.flatMap(([value, code]) => value === null ? [code] : []);
|
|
3970
|
+
}
|
|
3971
|
+
function businessPinErrors(lat, lng) {
|
|
3972
|
+
if (lat === null && lng === null) return [];
|
|
3973
|
+
const latitude = lat === null ? ["missing_latitude"] : !isLatitude(lat) ? ["invalid_latitude"] : [];
|
|
3974
|
+
const longitude = lng === null ? ["missing_longitude"] : !isLongitude(lng) ? ["invalid_longitude"] : [];
|
|
3975
|
+
return [...latitude, ...longitude];
|
|
3976
|
+
}
|
|
3263
3977
|
|
|
3264
3978
|
//#endregion
|
|
3265
3979
|
//#region ../core/src/try-async.ts
|
|
@@ -3378,6 +4092,18 @@ function syncStateOf(sync) {
|
|
|
3378
4092
|
* reload handover is only worth presenting within it (issue 1570).
|
|
3379
4093
|
*/
|
|
3380
4094
|
const EDIT_LEASE_TTL_SECONDS = 60;
|
|
4095
|
+
/** One-use navigation transport for the same previous-client acquire contract. */
|
|
4096
|
+
const EDIT_LEASE_HANDOVER_QUERY_PARAM = "edit_lease_handover";
|
|
4097
|
+
/** Frozen at editor mount; reacquiring a lease never advances this session's admission stamp. */
|
|
4098
|
+
const PAGE_DOCUMENT_SESSION_CHECKPOINT_HEADER = "X-Riverbank-Page-Checkpoint";
|
|
4099
|
+
function parsePageDocumentSessionCheckpoint(value) {
|
|
4100
|
+
if (value === "legacy") return { kind: "legacy" };
|
|
4101
|
+
const parsed = parsePageDocumentCheckpointId(value);
|
|
4102
|
+
return parsed.ok ? {
|
|
4103
|
+
kind: "document",
|
|
4104
|
+
checkpointId: parsed.value
|
|
4105
|
+
} : undefined;
|
|
4106
|
+
}
|
|
3381
4107
|
function parseEditLeaseScope(input) {
|
|
3382
4108
|
switch (input.nodeType) {
|
|
3383
4109
|
case "page": {
|
|
@@ -4056,7 +4782,7 @@ const knownSocialPlatformLabels = {
|
|
|
4056
4782
|
x: "X",
|
|
4057
4783
|
youtube: "YouTube"
|
|
4058
4784
|
};
|
|
4059
|
-
function isRecord$
|
|
4785
|
+
function isRecord$9(value) {
|
|
4060
4786
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4061
4787
|
}
|
|
4062
4788
|
function trimmedOptionalString(value) {
|
|
@@ -4066,7 +4792,7 @@ function trimmedOptionalString(value) {
|
|
|
4066
4792
|
return trimmed.length > 0 ? trimmed : null;
|
|
4067
4793
|
}
|
|
4068
4794
|
function parsePublicContactDetails(value) {
|
|
4069
|
-
if (!isRecord$
|
|
4795
|
+
if (!isRecord$9(value)) {
|
|
4070
4796
|
return err({
|
|
4071
4797
|
code: "invalid_shape",
|
|
4072
4798
|
path: "contact",
|
|
@@ -4162,7 +4888,7 @@ function minutesSinceMidnight(value) {
|
|
|
4162
4888
|
}
|
|
4163
4889
|
function parseOpeningHoursDay(value, expectedWeekday, index) {
|
|
4164
4890
|
const path = `openingHours.days.${index}`;
|
|
4165
|
-
if (!isRecord$
|
|
4891
|
+
if (!isRecord$9(value) || value.weekday !== expectedWeekday) {
|
|
4166
4892
|
return err({
|
|
4167
4893
|
code: "invalid_weekdays",
|
|
4168
4894
|
path,
|
|
@@ -4183,7 +4909,7 @@ function parseOpeningHoursDay(value, expectedWeekday, index) {
|
|
|
4183
4909
|
const intervals = [];
|
|
4184
4910
|
for (const [intervalIndex, rawInterval] of value.intervals.entries()) {
|
|
4185
4911
|
const intervalPath = `${path}.intervals.${intervalIndex}`;
|
|
4186
|
-
if (!isRecord$
|
|
4912
|
+
if (!isRecord$9(rawInterval)) {
|
|
4187
4913
|
return err({
|
|
4188
4914
|
code: "invalid_interval",
|
|
4189
4915
|
path: intervalPath,
|
|
@@ -4255,7 +4981,7 @@ function parseOpeningHoursWeek(rawDays) {
|
|
|
4255
4981
|
]);
|
|
4256
4982
|
}
|
|
4257
4983
|
function parseWeeklyOpeningHours(value) {
|
|
4258
|
-
if (!isRecord$
|
|
4984
|
+
if (!isRecord$9(value) || !Array.isArray(value.days) || value.days.length !== publicProfileWeekdays.length) {
|
|
4259
4985
|
return err({
|
|
4260
4986
|
code: "invalid_weekdays",
|
|
4261
4987
|
path: "openingHours.days",
|
|
@@ -4324,7 +5050,7 @@ function parsePublicSocialLinks(value) {
|
|
|
4324
5050
|
const ids = new Set();
|
|
4325
5051
|
for (const [index, rawLink] of value.entries()) {
|
|
4326
5052
|
const path = `socialLinks.${index}`;
|
|
4327
|
-
if (!isRecord$
|
|
5053
|
+
if (!isRecord$9(rawLink)) {
|
|
4328
5054
|
return err({
|
|
4329
5055
|
code: "invalid_social_link",
|
|
4330
5056
|
path,
|
|
@@ -4352,12 +5078,12 @@ function parsePublicSocialLinks(value) {
|
|
|
4352
5078
|
});
|
|
4353
5079
|
continue;
|
|
4354
5080
|
}
|
|
4355
|
-
const label = trimmedOptionalString(rawLink.label);
|
|
4356
|
-
if (rawLink.kind === "custom" && label !== null && label !== undefined) {
|
|
5081
|
+
const label$1 = trimmedOptionalString(rawLink.label);
|
|
5082
|
+
if (rawLink.kind === "custom" && label$1 !== null && label$1 !== undefined) {
|
|
4357
5083
|
links.push({
|
|
4358
5084
|
id: id.value,
|
|
4359
5085
|
kind: "custom",
|
|
4360
|
-
label,
|
|
5086
|
+
label: label$1,
|
|
4361
5087
|
url: url.value
|
|
4362
5088
|
});
|
|
4363
5089
|
continue;
|
|
@@ -4397,7 +5123,7 @@ function parsePublicProfilePresentation(value) {
|
|
|
4397
5123
|
});
|
|
4398
5124
|
}
|
|
4399
5125
|
function parsePublicProfile(value) {
|
|
4400
|
-
if (!isRecord$
|
|
5126
|
+
if (!isRecord$9(value)) {
|
|
4401
5127
|
return err({
|
|
4402
5128
|
code: "invalid_shape",
|
|
4403
5129
|
path: "",
|
|
@@ -4435,7 +5161,7 @@ function parseContactVisibility(value) {
|
|
|
4435
5161
|
email: "shown"
|
|
4436
5162
|
});
|
|
4437
5163
|
}
|
|
4438
|
-
if (!isRecord$
|
|
5164
|
+
if (!isRecord$9(value)) {
|
|
4439
5165
|
return err({
|
|
4440
5166
|
code: "invalid_contact_visibility",
|
|
4441
5167
|
path: "contactVisibility",
|
|
@@ -4579,7 +5305,7 @@ function parsePublicExperience(value) {
|
|
|
4579
5305
|
|
|
4580
5306
|
//#endregion
|
|
4581
5307
|
//#region ../core/src/rich-text.ts
|
|
4582
|
-
function isRecord$
|
|
5308
|
+
function isRecord$8(value) {
|
|
4583
5309
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4584
5310
|
}
|
|
4585
5311
|
function hasOnlyKeys(value, allowed) {
|
|
@@ -4601,10 +5327,10 @@ function isDenseArrayOf(value, predicate) {
|
|
|
4601
5327
|
return true;
|
|
4602
5328
|
}
|
|
4603
5329
|
function isTipTapMark(value) {
|
|
4604
|
-
return isRecord$
|
|
5330
|
+
return isRecord$8(value) && hasOnlyKeys(value, markKeys) && typeof value.type === "string" && value.type.length > 0 && (value.attrs === undefined || isRecord$8(value.attrs));
|
|
4605
5331
|
}
|
|
4606
5332
|
function isTipTapNode(value) {
|
|
4607
|
-
if (!isRecord$
|
|
5333
|
+
if (!isRecord$8(value) || !hasOnlyKeys(value, nodeKeys) || typeof value.type !== "string" || value.type.length === 0 || value.type === "doc" || value.attrs !== undefined && !isRecord$8(value.attrs) || value.marks !== undefined && (!Array.isArray(value.marks) || !isDenseArrayOf(value.marks, isTipTapMark)) || value.content !== undefined && (!Array.isArray(value.content) || !isDenseArrayOf(value.content, isTipTapNode))) {
|
|
4608
5334
|
return false;
|
|
4609
5335
|
}
|
|
4610
5336
|
if (value.type === "text") {
|
|
@@ -4613,7 +5339,7 @@ function isTipTapNode(value) {
|
|
|
4613
5339
|
return value.text === undefined;
|
|
4614
5340
|
}
|
|
4615
5341
|
function isCanonicalRichTextDoc(value) {
|
|
4616
|
-
return isRecord$
|
|
5342
|
+
return isRecord$8(value) && hasOnlyKeys(value, rootKeys) && value.type === "doc" && Array.isArray(value.content) && isDenseArrayOf(value.content, isTipTapNode);
|
|
4617
5343
|
}
|
|
4618
5344
|
|
|
4619
5345
|
//#endregion
|
|
@@ -4853,10 +5579,10 @@ function presentVisitorSelection(selection, optionCount) {
|
|
|
4853
5579
|
//#region ../core/src/mailing-list-targeting-parser.ts
|
|
4854
5580
|
function parseMailingListPlacementTargeting(value) {
|
|
4855
5581
|
if (value == null) return ok(DEFAULT_MAILING_LIST_TARGETING);
|
|
4856
|
-
if (!isRecord$
|
|
5582
|
+
if (!isRecord$7(value)) return targetingError("shape");
|
|
4857
5583
|
if (value.mode === "default") return ok({ mode: "default" });
|
|
4858
5584
|
if (value.mode !== "custom") return targetingError("mode");
|
|
4859
|
-
if (!isRecord$
|
|
5585
|
+
if (!isRecord$7(value.selection)) return targetingError("selection");
|
|
4860
5586
|
switch (value.provider) {
|
|
4861
5587
|
case "native": return parseNativeTargeting(value.selection);
|
|
4862
5588
|
case "brevo": return parseBrevoTargeting(value.selection);
|
|
@@ -4865,7 +5591,7 @@ function parseMailingListPlacementTargeting(value) {
|
|
|
4865
5591
|
}
|
|
4866
5592
|
}
|
|
4867
5593
|
function parseMailingListVisitorSelection(value) {
|
|
4868
|
-
if (!isRecord$
|
|
5594
|
+
if (!isRecord$7(value)) return visitorSelectionError("shape");
|
|
4869
5595
|
switch (value.provider) {
|
|
4870
5596
|
case "native": {
|
|
4871
5597
|
if (Object.keys(value).some((key) => key !== "provider" && key !== "topicIds")) return visitorSelectionError("shape");
|
|
@@ -4996,7 +5722,7 @@ function parseVisitorIds(value, parseId) {
|
|
|
4996
5722
|
const parsed = parseNonEmptyUniqueIds(value, parseId);
|
|
4997
5723
|
return parsed.ok ? parsed.value : null;
|
|
4998
5724
|
}
|
|
4999
|
-
function isRecord$
|
|
5725
|
+
function isRecord$7(value) {
|
|
5000
5726
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5001
5727
|
}
|
|
5002
5728
|
function targetingError(field) {
|
|
@@ -5014,7 +5740,7 @@ function visitorSelectionError(field) {
|
|
|
5014
5740
|
|
|
5015
5741
|
//#endregion
|
|
5016
5742
|
//#region ../core/src/footer-engagement.ts
|
|
5017
|
-
function isRecord$
|
|
5743
|
+
function isRecord$6(value) {
|
|
5018
5744
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5019
5745
|
}
|
|
5020
5746
|
function invalid(path, message) {
|
|
@@ -5048,7 +5774,7 @@ function parseEngagementBase(value, base, path) {
|
|
|
5048
5774
|
}
|
|
5049
5775
|
const treatment = parseTreatment(value.treatment, `${path}.treatment`);
|
|
5050
5776
|
if (!treatment.ok) return treatment;
|
|
5051
|
-
if (!isRecord$
|
|
5777
|
+
if (!isRecord$6(value.source)) {
|
|
5052
5778
|
return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
|
|
5053
5779
|
}
|
|
5054
5780
|
const intro = parseBoundedText(value.source.intro, `${path}.source.intro`, {
|
|
@@ -5095,7 +5821,7 @@ function parseFooterEngagementSectionValue(value, sectionBase, path) {
|
|
|
5095
5821
|
const base = parseEngagementBase(value, sectionBase, path);
|
|
5096
5822
|
if (!base.ok) return base;
|
|
5097
5823
|
const source = value.source;
|
|
5098
|
-
if (!isRecord$
|
|
5824
|
+
if (!isRecord$6(source)) {
|
|
5099
5825
|
return err(invalid(`${path}.source`, "Footer engagement source is invalid."));
|
|
5100
5826
|
}
|
|
5101
5827
|
switch (source.kind) {
|
|
@@ -5256,7 +5982,7 @@ function unsafeAsFooterModuleId(value) {
|
|
|
5256
5982
|
function parseFooterModuleId(value) {
|
|
5257
5983
|
return parseSectionId(value, "sectionId");
|
|
5258
5984
|
}
|
|
5259
|
-
function isRecord$
|
|
5985
|
+
function isRecord$5(value) {
|
|
5260
5986
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5261
5987
|
}
|
|
5262
5988
|
function isRichText(value) {
|
|
@@ -5286,7 +6012,7 @@ function parseHeading(value, path) {
|
|
|
5286
6012
|
}
|
|
5287
6013
|
function parseFooterSection(value, index) {
|
|
5288
6014
|
const path = `sections.${index}`;
|
|
5289
|
-
if (!isRecord$
|
|
6015
|
+
if (!isRecord$5(value)) {
|
|
5290
6016
|
return err({
|
|
5291
6017
|
code: "invalid_section",
|
|
5292
6018
|
path,
|
|
@@ -5321,7 +6047,7 @@ function parseFooterSection(value, index) {
|
|
|
5321
6047
|
}
|
|
5322
6048
|
break;
|
|
5323
6049
|
case "navigation":
|
|
5324
|
-
if (isRecord$
|
|
6050
|
+
if (isRecord$5(value.source) && value.source.kind === "menu-role" && value.source.role === "main") {
|
|
5325
6051
|
return ok({
|
|
5326
6052
|
...base,
|
|
5327
6053
|
kind: "navigation",
|
|
@@ -5334,7 +6060,7 @@ function parseFooterSection(value, index) {
|
|
|
5334
6060
|
}
|
|
5335
6061
|
break;
|
|
5336
6062
|
case "legal-navigation":
|
|
5337
|
-
if (isRecord$
|
|
6063
|
+
if (isRecord$5(value.source) && value.source.kind === "menu-role" && value.source.role === "legal-policy") {
|
|
5338
6064
|
return ok({
|
|
5339
6065
|
...base,
|
|
5340
6066
|
kind: "legal-navigation",
|
|
@@ -5347,7 +6073,7 @@ function parseFooterSection(value, index) {
|
|
|
5347
6073
|
}
|
|
5348
6074
|
break;
|
|
5349
6075
|
case "business-address":
|
|
5350
|
-
if (isRecord$
|
|
6076
|
+
if (isRecord$5(value.source) && value.source.kind === "business-address") {
|
|
5351
6077
|
return ok({
|
|
5352
6078
|
...base,
|
|
5353
6079
|
kind: "business-address",
|
|
@@ -5359,7 +6085,7 @@ function parseFooterSection(value, index) {
|
|
|
5359
6085
|
case "contact":
|
|
5360
6086
|
case "opening-hours":
|
|
5361
6087
|
case "social-links":
|
|
5362
|
-
if (isRecord$
|
|
6088
|
+
if (isRecord$5(value.source) && value.source.kind === "public-profile") {
|
|
5363
6089
|
return ok({
|
|
5364
6090
|
...base,
|
|
5365
6091
|
kind: value.kind,
|
|
@@ -5411,7 +6137,7 @@ function validateFooterComposition(composition) {
|
|
|
5411
6137
|
return ok(composition);
|
|
5412
6138
|
}
|
|
5413
6139
|
function parseFooterCompositionV1(value) {
|
|
5414
|
-
if (!isRecord$
|
|
6140
|
+
if (!isRecord$5(value)) {
|
|
5415
6141
|
return err({
|
|
5416
6142
|
code: "invalid_shape",
|
|
5417
6143
|
path: "",
|
|
@@ -5444,7 +6170,7 @@ function parseFooterCompositionV1(value) {
|
|
|
5444
6170
|
});
|
|
5445
6171
|
}
|
|
5446
6172
|
function parseLegacyPlacementSection(value, index, selectedIds) {
|
|
5447
|
-
if (!isRecord$
|
|
6173
|
+
if (!isRecord$5(value)) {
|
|
5448
6174
|
return err({
|
|
5449
6175
|
code: "invalid_section",
|
|
5450
6176
|
path: `modules.${index}`,
|
|
@@ -5452,7 +6178,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
|
|
|
5452
6178
|
});
|
|
5453
6179
|
}
|
|
5454
6180
|
const selected = typeof value.id === "string" && selectedIds.has(value.id);
|
|
5455
|
-
const source = isRecord$
|
|
6181
|
+
const source = isRecord$5(value.source) ? value.source : null;
|
|
5456
6182
|
const mapped = value.kind === "navigation" && source?.kind === "menu-role" && source.role === "footer" ? {
|
|
5457
6183
|
...value,
|
|
5458
6184
|
source: {
|
|
@@ -5471,7 +6197,7 @@ function parseLegacyPlacementSection(value, index, selectedIds) {
|
|
|
5471
6197
|
* It is intentionally not called by the runtime parser.
|
|
5472
6198
|
*/
|
|
5473
6199
|
function upgradePlacementFooterCompositionV1(value) {
|
|
5474
|
-
if (!isRecord$
|
|
6200
|
+
if (!isRecord$5(value) || value.schemaVersion !== 1 || !Array.isArray(value.modules) || !Array.isArray(value.placements)) {
|
|
5475
6201
|
return err({
|
|
5476
6202
|
code: "invalid_shape",
|
|
5477
6203
|
path: "",
|
|
@@ -5480,7 +6206,7 @@ function upgradePlacementFooterCompositionV1(value) {
|
|
|
5480
6206
|
}
|
|
5481
6207
|
const selectedIds = new Set();
|
|
5482
6208
|
for (const [index, placement] of value.placements.entries()) {
|
|
5483
|
-
if (!isRecord$
|
|
6209
|
+
if (!isRecord$5(placement) || typeof placement.moduleId !== "string") {
|
|
5484
6210
|
return err({
|
|
5485
6211
|
code: "invalid_shape",
|
|
5486
6212
|
path: `placements.${index}`,
|
|
@@ -5542,10 +6268,10 @@ function parseFooterEngagementSection(value) {
|
|
|
5542
6268
|
});
|
|
5543
6269
|
}
|
|
5544
6270
|
function upsertFooterEngagementSection(composition, section) {
|
|
5545
|
-
const hasEngagement = composition.sections.some((candidate) => candidate.kind === "engagement");
|
|
6271
|
+
const hasEngagement = composition.sections.some((candidate$1) => candidate$1.kind === "engagement");
|
|
5546
6272
|
return validateFooterComposition({
|
|
5547
6273
|
...composition,
|
|
5548
|
-
sections: hasEngagement ? composition.sections.map((candidate) => candidate.kind === "engagement" ? section : candidate) : [...composition.sections, section]
|
|
6274
|
+
sections: hasEngagement ? composition.sections.map((candidate$1) => candidate$1.kind === "engagement" ? section : candidate$1) : [...composition.sections, section]
|
|
5549
6275
|
});
|
|
5550
6276
|
}
|
|
5551
6277
|
function removeFooterEngagementSection(composition) {
|
|
@@ -5695,7 +6421,7 @@ const defaultSections = (legacy, options) => {
|
|
|
5695
6421
|
];
|
|
5696
6422
|
};
|
|
5697
6423
|
function normalizeLegacyFooterContentToV1(value, options = {}) {
|
|
5698
|
-
if (!isRecord$
|
|
6424
|
+
if (!isRecord$5(value)) {
|
|
5699
6425
|
return err({
|
|
5700
6426
|
code: "invalid_shape",
|
|
5701
6427
|
path: "",
|
|
@@ -5710,7 +6436,7 @@ function normalizeLegacyFooterContentToV1(value, options = {}) {
|
|
|
5710
6436
|
|
|
5711
6437
|
//#endregion
|
|
5712
6438
|
//#region ../core/src/mailing-list-setup.ts
|
|
5713
|
-
function isRecord$
|
|
6439
|
+
function isRecord$4(value) {
|
|
5714
6440
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
5715
6441
|
}
|
|
5716
6442
|
/** The name shown to people for each provider; the native one is the product brand. */
|
|
@@ -5825,7 +6551,7 @@ function mailingListProviderScopeFromConfigured(provider) {
|
|
|
5825
6551
|
}
|
|
5826
6552
|
}
|
|
5827
6553
|
function parseMailingListProviderScope(value) {
|
|
5828
|
-
if (!isRecord$
|
|
6554
|
+
if (!isRecord$4(value)) return err("invalid_mailing_list_provider_scope");
|
|
5829
6555
|
if (value.provider === "native") return ok({ provider: "native" });
|
|
5830
6556
|
if (value.provider === "brevo") {
|
|
5831
6557
|
const accountId = parseBrevoAccountId(value.accountId);
|
|
@@ -5867,7 +6593,7 @@ const newsletterAudienceSchema = z.unknown().transform((value, ctx) => {
|
|
|
5867
6593
|
return z.NEVER;
|
|
5868
6594
|
});
|
|
5869
6595
|
function parseMailingListGlobalForm(value) {
|
|
5870
|
-
if (!isRecord$
|
|
6596
|
+
if (!isRecord$3(value)) return formError("shape");
|
|
5871
6597
|
const fields = parseGlobalFields(value.fields);
|
|
5872
6598
|
if (fields === null) return formError("fields");
|
|
5873
6599
|
if (typeof value.includeOptionalPhone !== "boolean") {
|
|
@@ -5888,7 +6614,7 @@ function parseMailingListGlobalForm(value) {
|
|
|
5888
6614
|
});
|
|
5889
6615
|
}
|
|
5890
6616
|
function parseMailingListSiteSetup(value) {
|
|
5891
|
-
if (!isRecord$
|
|
6617
|
+
if (!isRecord$3(value)) return setupError("shape");
|
|
5892
6618
|
const siteId = parseSiteId(value.siteId);
|
|
5893
6619
|
if (!siteId.ok) return setupError("siteId");
|
|
5894
6620
|
if (value.tag === "not_configured") {
|
|
@@ -5918,7 +6644,7 @@ function parseConfiguredSiteSetup(value, siteId) {
|
|
|
5918
6644
|
});
|
|
5919
6645
|
}
|
|
5920
6646
|
function parseConfiguredMailingListProvider(value) {
|
|
5921
|
-
if (!isRecord$
|
|
6647
|
+
if (!isRecord$3(value) || typeof value.provider !== "string") {
|
|
5922
6648
|
return setupError("provider");
|
|
5923
6649
|
}
|
|
5924
6650
|
switch (value.provider) {
|
|
@@ -5972,7 +6698,7 @@ function parseBrevoSetup(value) {
|
|
|
5972
6698
|
});
|
|
5973
6699
|
}
|
|
5974
6700
|
function parseNewsletterAudience(value) {
|
|
5975
|
-
if (!isRecord$
|
|
6701
|
+
if (!isRecord$3(value)) return err("invalid_newsletter_audience");
|
|
5976
6702
|
if (value.kind === "general" && Object.keys(value).length === 1) {
|
|
5977
6703
|
return ok({ kind: "general" });
|
|
5978
6704
|
}
|
|
@@ -5986,7 +6712,7 @@ function parseNewsletterAudience(value) {
|
|
|
5986
6712
|
});
|
|
5987
6713
|
}
|
|
5988
6714
|
function parseGlobalFields(value) {
|
|
5989
|
-
if (!isRecord$
|
|
6715
|
+
if (!isRecord$3(value) || typeof value.preset !== "string") return null;
|
|
5990
6716
|
switch (value.preset) {
|
|
5991
6717
|
case "emailOnly": return { preset: "emailOnly" };
|
|
5992
6718
|
case "emailName": return isRequirement(value.name) ? {
|
|
@@ -6011,11 +6737,11 @@ function parseGlobalFields(value) {
|
|
|
6011
6737
|
}
|
|
6012
6738
|
}
|
|
6013
6739
|
function parseToggleConfirmation(value) {
|
|
6014
|
-
if (!isRecord$
|
|
6740
|
+
if (!isRecord$3(value)) return null;
|
|
6015
6741
|
return value.tag === "on" || value.tag === "off" ? { tag: value.tag } : null;
|
|
6016
6742
|
}
|
|
6017
6743
|
function parseBrevoConfirmation(value) {
|
|
6018
|
-
if (!isRecord$
|
|
6744
|
+
if (!isRecord$3(value)) return null;
|
|
6019
6745
|
if (value.tag === "off") return { tag: "off" };
|
|
6020
6746
|
if (value.tag !== "on") return null;
|
|
6021
6747
|
const templateId = parseBrevoConfirmationTemplateId(value.templateId);
|
|
@@ -6052,7 +6778,7 @@ function parseOptionalString(value) {
|
|
|
6052
6778
|
function isRequirement(value) {
|
|
6053
6779
|
return value === "required" || value === "optional";
|
|
6054
6780
|
}
|
|
6055
|
-
function isRecord$
|
|
6781
|
+
function isRecord$3(value) {
|
|
6056
6782
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6057
6783
|
}
|
|
6058
6784
|
function formError(field) {
|
|
@@ -6175,7 +6901,7 @@ function assessMatchedChoices(matched, choices) {
|
|
|
6175
6901
|
//#endregion
|
|
6176
6902
|
//#region ../core/src/mailing-list-runtime.ts
|
|
6177
6903
|
function parseMailingListRuntimeContext(value) {
|
|
6178
|
-
if (!isRecord$
|
|
6904
|
+
if (!isRecord$2(value)) return invalidRuntimeContext();
|
|
6179
6905
|
if (value.tag === "not_configured") return ok({ tag: "not_configured" });
|
|
6180
6906
|
if (value.tag !== "configured") return invalidRuntimeContext();
|
|
6181
6907
|
return parseConfiguredRuntimeContext(value);
|
|
@@ -6223,7 +6949,7 @@ function parseBookingSignupOffer(value) {
|
|
|
6223
6949
|
tag: "unavailable",
|
|
6224
6950
|
reason: "disabled"
|
|
6225
6951
|
};
|
|
6226
|
-
if (!isRecord$
|
|
6952
|
+
if (!isRecord$2(value)) return null;
|
|
6227
6953
|
if (value.tag === "offered") return { tag: "offered" };
|
|
6228
6954
|
if (value.tag === "unavailable" && (value.reason === "disabled" || value.reason === "fields" || value.reason === "destination")) {
|
|
6229
6955
|
return {
|
|
@@ -6234,7 +6960,7 @@ function parseBookingSignupOffer(value) {
|
|
|
6234
6960
|
return null;
|
|
6235
6961
|
}
|
|
6236
6962
|
function parseRuntimeProvider(value) {
|
|
6237
|
-
if (!isRecord$
|
|
6963
|
+
if (!isRecord$2(value)) return null;
|
|
6238
6964
|
switch (value.provider) {
|
|
6239
6965
|
case "native": return parseNativeRuntimeProvider(value);
|
|
6240
6966
|
case "brevo": return parseBrevoRuntimeProvider(value);
|
|
@@ -6272,7 +6998,7 @@ function parseMailchimpRuntimeProvider(value) {
|
|
|
6272
6998
|
} : null;
|
|
6273
6999
|
}
|
|
6274
7000
|
function parseMailchimpAudience(value) {
|
|
6275
|
-
if (!isRecord$
|
|
7001
|
+
if (!isRecord$2(value) || !Array.isArray(value.interests)) return null;
|
|
6276
7002
|
const id = parseMailchimpAudienceId(value.id);
|
|
6277
7003
|
const name = parseName(value.name);
|
|
6278
7004
|
const interests = Array.from(value.interests, parseMailchimpInterest);
|
|
@@ -6283,7 +7009,7 @@ function parseMailchimpAudience(value) {
|
|
|
6283
7009
|
} : null;
|
|
6284
7010
|
}
|
|
6285
7011
|
function parseMailchimpInterest(value) {
|
|
6286
|
-
if (!isRecord$
|
|
7012
|
+
if (!isRecord$2(value)) return null;
|
|
6287
7013
|
const id = parseMailchimpInterestId(value.id);
|
|
6288
7014
|
const name = parseName(value.name);
|
|
6289
7015
|
const categoryName = parseName(value.categoryName);
|
|
@@ -6296,7 +7022,7 @@ function parseMailchimpInterest(value) {
|
|
|
6296
7022
|
function parseNamedItems(value, parseId) {
|
|
6297
7023
|
if (!Array.isArray(value)) return null;
|
|
6298
7024
|
const items = Array.from(value, (item) => {
|
|
6299
|
-
if (!isRecord$
|
|
7025
|
+
if (!isRecord$2(item)) return null;
|
|
6300
7026
|
const id = parseId(item.id);
|
|
6301
7027
|
const name = parseName(item.name);
|
|
6302
7028
|
return id !== null && name !== null ? {
|
|
@@ -6311,7 +7037,7 @@ function parseName(value) {
|
|
|
6311
7037
|
const name = value.trim();
|
|
6312
7038
|
return name.length > 0 ? name : null;
|
|
6313
7039
|
}
|
|
6314
|
-
function isRecord$
|
|
7040
|
+
function isRecord$2(value) {
|
|
6315
7041
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6316
7042
|
}
|
|
6317
7043
|
function invalidRuntimeContext() {
|
|
@@ -6585,11 +7311,11 @@ const PRIORITY_IDS = new Set([
|
|
|
6585
7311
|
"emailaddress"
|
|
6586
7312
|
]);
|
|
6587
7313
|
const MAX_COLUMNS = 5;
|
|
6588
|
-
function isRecord(value) {
|
|
7314
|
+
function isRecord$1(value) {
|
|
6589
7315
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6590
7316
|
}
|
|
6591
7317
|
function isSchemaField(value) {
|
|
6592
|
-
return isRecord(value) && typeof value.id === "string";
|
|
7318
|
+
return isRecord$1(value) && typeof value.id === "string";
|
|
6593
7319
|
}
|
|
6594
7320
|
function selectSubmissionPreviewFields(schemaFields, submissions) {
|
|
6595
7321
|
const fields = Array.isArray(schemaFields) ? schemaFields.filter(isSchemaField) : [];
|
|
@@ -6606,9 +7332,9 @@ function selectSubmissionPreviewFields(schemaFields, submissions) {
|
|
|
6606
7332
|
if (id === null) return null;
|
|
6607
7333
|
const type = (field.type ?? "text").toString().toLowerCase();
|
|
6608
7334
|
if (shouldHideInPreview(type)) return null;
|
|
6609
|
-
const label = trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id);
|
|
7335
|
+
const label$1 = trimmedStringOrNull(field.label?.toString()) ?? humanizeSubmissionFieldKey(id);
|
|
6610
7336
|
const normalizedId = id.toLowerCase();
|
|
6611
|
-
const normalizedLabel = label.toLowerCase();
|
|
7337
|
+
const normalizedLabel = label$1.toLowerCase();
|
|
6612
7338
|
let score = 0;
|
|
6613
7339
|
if (PRIORITY_IDS.has(normalizedId)) score += 500;
|
|
6614
7340
|
if (normalizedId.includes("name") || normalizedLabel.includes("name")) score += 550;
|
|
@@ -6631,7 +7357,7 @@ function selectSubmissionPreviewFields(schemaFields, submissions) {
|
|
|
6631
7357
|
}
|
|
6632
7358
|
return {
|
|
6633
7359
|
id,
|
|
6634
|
-
label,
|
|
7360
|
+
label: label$1,
|
|
6635
7361
|
type,
|
|
6636
7362
|
score,
|
|
6637
7363
|
index
|
|
@@ -6682,7 +7408,7 @@ function valueIsMeaningful(value) {
|
|
|
6682
7408
|
if (value === null || value === undefined) return false;
|
|
6683
7409
|
if (typeof value === "string") return value.trim().length > 0;
|
|
6684
7410
|
if (Array.isArray(value)) return value.length > 0;
|
|
6685
|
-
if (isRecord(value)) return Object.keys(value).length > 0;
|
|
7411
|
+
if (isRecord$1(value)) return Object.keys(value).length > 0;
|
|
6686
7412
|
return true;
|
|
6687
7413
|
}
|
|
6688
7414
|
function orderSubmissionFields(schemaFields, data) {
|
|
@@ -7164,33 +7890,6 @@ function generateUuidV4(cryptoSource = globalThis.crypto) {
|
|
|
7164
7890
|
return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
|
|
7165
7891
|
}
|
|
7166
7892
|
|
|
7167
|
-
//#endregion
|
|
7168
|
-
//#region ../core/src/stable-json.ts
|
|
7169
|
-
function stableJsonStringify(value) {
|
|
7170
|
-
return JSON.stringify(sortStableJsonValue(value));
|
|
7171
|
-
}
|
|
7172
|
-
function sortStableJsonValue(value) {
|
|
7173
|
-
if (value === null) return null;
|
|
7174
|
-
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
7175
|
-
if (typeof value === "number") {
|
|
7176
|
-
if (!Number.isFinite(value)) {
|
|
7177
|
-
throw new TypeError("Stable JSON only supports finite numbers");
|
|
7178
|
-
}
|
|
7179
|
-
return value;
|
|
7180
|
-
}
|
|
7181
|
-
if (Array.isArray(value)) {
|
|
7182
|
-
return value.map(sortStableJsonValue);
|
|
7183
|
-
}
|
|
7184
|
-
if (typeof value === "object") {
|
|
7185
|
-
const prototype = Object.getPrototypeOf(value);
|
|
7186
|
-
if (prototype !== Object.prototype && prototype !== null) {
|
|
7187
|
-
throw new TypeError("Stable JSON only supports plain objects");
|
|
7188
|
-
}
|
|
7189
|
-
return Object.fromEntries(Object.entries(value).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).map(([key, child]) => [key, sortStableJsonValue(child)]));
|
|
7190
|
-
}
|
|
7191
|
-
throw new TypeError(`Stable JSON does not support ${typeof value}`);
|
|
7192
|
-
}
|
|
7193
|
-
|
|
7194
7893
|
//#endregion
|
|
7195
7894
|
//#region ../core/src/secret.ts
|
|
7196
7895
|
/**
|
|
@@ -7593,14 +8292,14 @@ const BOOKING_DEFAULTS_BY_BEHAVIOUR = {
|
|
|
7593
8292
|
memberships: "off"
|
|
7594
8293
|
}
|
|
7595
8294
|
};
|
|
7596
|
-
function verticalBookingPreset(id, verticalId, canonicalBehaviour, label, pluralLabel, description, availability$1 = "onboarding") {
|
|
8295
|
+
function verticalBookingPreset(id, verticalId, canonicalBehaviour, label$1, pluralLabel, description, availability$1 = "onboarding") {
|
|
7597
8296
|
return {
|
|
7598
8297
|
id,
|
|
7599
8298
|
verticalId,
|
|
7600
8299
|
canonicalBehaviour,
|
|
7601
8300
|
engine: BOOKING_ENGINE_BY_BEHAVIOUR[canonicalBehaviour],
|
|
7602
8301
|
availability: availability$1,
|
|
7603
|
-
label,
|
|
8302
|
+
label: label$1,
|
|
7604
8303
|
pluralLabel,
|
|
7605
8304
|
description
|
|
7606
8305
|
};
|
|
@@ -8477,4 +9176,642 @@ function resolvePublicSiteVisibility(input) {
|
|
|
8477
9176
|
}
|
|
8478
9177
|
|
|
8479
9178
|
//#endregion
|
|
8480
|
-
|
|
9179
|
+
//#region ../core/src/map-drawing/geometry.ts
|
|
9180
|
+
const VIEW_BOUNDS = {
|
|
9181
|
+
left: 0,
|
|
9182
|
+
top: 0,
|
|
9183
|
+
right: 800,
|
|
9184
|
+
bottom: 800
|
|
9185
|
+
};
|
|
9186
|
+
function samePoint(a, b) {
|
|
9187
|
+
return a[0] === b[0] && a[1] === b[1];
|
|
9188
|
+
}
|
|
9189
|
+
function distance(a, b) {
|
|
9190
|
+
return Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
9191
|
+
}
|
|
9192
|
+
function roundCoordinate(value) {
|
|
9193
|
+
const rounded = Math.round(value * 10) / 10;
|
|
9194
|
+
return rounded === 0 ? 0 : rounded;
|
|
9195
|
+
}
|
|
9196
|
+
function roundPoint(point$1) {
|
|
9197
|
+
return [roundCoordinate(point$1[0]), roundCoordinate(point$1[1])];
|
|
9198
|
+
}
|
|
9199
|
+
/** Liang–Barsky: one bounded segment, no fabricated connecting line across the viewport. */
|
|
9200
|
+
function clipSegment(a, b, bounds) {
|
|
9201
|
+
const dx = b[0] - a[0], dy = b[1] - a[1];
|
|
9202
|
+
const edges = [
|
|
9203
|
+
[-dx, a[0] - bounds.left],
|
|
9204
|
+
[dx, bounds.right - a[0]],
|
|
9205
|
+
[-dy, a[1] - bounds.top],
|
|
9206
|
+
[dy, bounds.bottom - a[1]]
|
|
9207
|
+
];
|
|
9208
|
+
const interval = edges.reduce((range, [p, q]) => {
|
|
9209
|
+
if (range === null || p === 0 && q < 0) return null;
|
|
9210
|
+
if (p === 0) return range;
|
|
9211
|
+
const ratio = q / p;
|
|
9212
|
+
const next = p < 0 ? [Math.max(range[0], ratio), range[1]] : [range[0], Math.min(range[1], ratio)];
|
|
9213
|
+
return next[0] > next[1] ? null : next;
|
|
9214
|
+
}, [0, 1]);
|
|
9215
|
+
return interval === null ? null : [[a[0] + interval[0] * dx, a[1] + interval[0] * dy], [a[0] + interval[1] * dx, a[1] + interval[1] * dy]];
|
|
9216
|
+
}
|
|
9217
|
+
function segments(points, closed = false) {
|
|
9218
|
+
return points.flatMap((start, index) => {
|
|
9219
|
+
const end = points[index + 1] ?? (closed ? points[0] : undefined);
|
|
9220
|
+
return end === undefined ? [] : [{
|
|
9221
|
+
start,
|
|
9222
|
+
end
|
|
9223
|
+
}];
|
|
9224
|
+
});
|
|
9225
|
+
}
|
|
9226
|
+
function clipLine(points, bounds) {
|
|
9227
|
+
return segments(points).reduce((paths$1, { start, end }) => {
|
|
9228
|
+
const segment = clipSegment(start, end, bounds);
|
|
9229
|
+
if (segment === null || samePoint(segment[0], segment[1])) return paths$1;
|
|
9230
|
+
const previous = paths$1[paths$1.length - 1];
|
|
9231
|
+
const last = previous?.[previous.length - 1];
|
|
9232
|
+
return previous !== undefined && last !== undefined && samePoint(last, segment[0]) ? [...paths$1.slice(0, -1), [...previous, segment[1]]] : [...paths$1, segment];
|
|
9233
|
+
}, []);
|
|
9234
|
+
}
|
|
9235
|
+
function polygonEdges(bounds) {
|
|
9236
|
+
const vertical = (x) => (a, b) => [x, a[1] + (b[1] - a[1]) * (x - a[0]) / (b[0] - a[0])];
|
|
9237
|
+
const horizontal = (y) => (a, b) => [a[0] + (b[0] - a[0]) * (y - a[1]) / (b[1] - a[1]), y];
|
|
9238
|
+
return [
|
|
9239
|
+
{
|
|
9240
|
+
inside: (p) => p[0] >= bounds.left,
|
|
9241
|
+
cross: vertical(bounds.left)
|
|
9242
|
+
},
|
|
9243
|
+
{
|
|
9244
|
+
inside: (p) => p[0] <= bounds.right,
|
|
9245
|
+
cross: vertical(bounds.right)
|
|
9246
|
+
},
|
|
9247
|
+
{
|
|
9248
|
+
inside: (p) => p[1] >= bounds.top,
|
|
9249
|
+
cross: horizontal(bounds.top)
|
|
9250
|
+
},
|
|
9251
|
+
{
|
|
9252
|
+
inside: (p) => p[1] <= bounds.bottom,
|
|
9253
|
+
cross: horizontal(bounds.bottom)
|
|
9254
|
+
}
|
|
9255
|
+
];
|
|
9256
|
+
}
|
|
9257
|
+
function clipPolygonEdge(points, edge) {
|
|
9258
|
+
return segments(points, true).flatMap(({ start, end }) => {
|
|
9259
|
+
const wasInside = edge.inside(start), isInside = edge.inside(end);
|
|
9260
|
+
if (wasInside && isInside) return [end];
|
|
9261
|
+
if (wasInside) return [edge.cross(start, end)];
|
|
9262
|
+
return isInside ? [edge.cross(start, end), end] : [];
|
|
9263
|
+
});
|
|
9264
|
+
}
|
|
9265
|
+
function clipPolygon(points, bounds) {
|
|
9266
|
+
const first = points[0], last = points[points.length - 1];
|
|
9267
|
+
const open = first !== undefined && last !== undefined && samePoint(first, last) ? points.slice(0, -1) : points;
|
|
9268
|
+
return polygonEdges(bounds).reduce((ring, edge) => clipPolygonEdge(ring, edge), open);
|
|
9269
|
+
}
|
|
9270
|
+
function deviation(point$1, start, end) {
|
|
9271
|
+
const dx = end[0] - start[0], dy = end[1] - start[1];
|
|
9272
|
+
const fraction = Math.max(0, Math.min(1, ((point$1[0] - start[0]) * dx + (point$1[1] - start[1]) * dy) / (dx * dx + dy * dy || 1)));
|
|
9273
|
+
return distance(point$1, [start[0] + fraction * dx, start[1] + fraction * dy]);
|
|
9274
|
+
}
|
|
9275
|
+
function simplifyChunk(points) {
|
|
9276
|
+
const start = points[0], end = points[points.length - 1];
|
|
9277
|
+
if (start === undefined || end === undefined || points.length <= 2) return points;
|
|
9278
|
+
const furthest = points.slice(1, -1).reduce((best, point$1, index) => {
|
|
9279
|
+
const error = deviation(point$1, start, end);
|
|
9280
|
+
return error > best.error ? {
|
|
9281
|
+
error,
|
|
9282
|
+
index: index + 1
|
|
9283
|
+
} : best;
|
|
9284
|
+
}, {
|
|
9285
|
+
error: .8,
|
|
9286
|
+
index: -1
|
|
9287
|
+
});
|
|
9288
|
+
return furthest.index < 0 ? [start, end] : [...simplifyChunk(points.slice(0, furthest.index + 1)).slice(0, -1), ...simplifyChunk(points.slice(furthest.index))];
|
|
9289
|
+
}
|
|
9290
|
+
/** Douglas–Peucker in overlapping 256-point chunks bounds recursion/work on hostile zigzags. */
|
|
9291
|
+
function simplify(points, closed) {
|
|
9292
|
+
const rounded = points.map(roundPoint);
|
|
9293
|
+
const open = rounded.filter((point$1, index) => rounded[index - 1] === undefined || !samePoint(point$1, rounded[index - 1] ?? point$1));
|
|
9294
|
+
const first = open[0], last = open[open.length - 1];
|
|
9295
|
+
if (first === undefined || last === undefined) return [];
|
|
9296
|
+
const input = closed && !samePoint(first, last) ? [...open, first] : open;
|
|
9297
|
+
const kept = Array.from({ length: Math.ceil((input.length - 1) / 255) }, (_, index) => simplifyChunk(input.slice(index * 255, index * 255 + 256))).flatMap((chunk, index) => index === 0 ? chunk : chunk.slice(1));
|
|
9298
|
+
const start = kept[0], end = kept[kept.length - 1];
|
|
9299
|
+
return closed && start !== undefined && end !== undefined && samePoint(start, end) ? kept.slice(0, -1) : kept;
|
|
9300
|
+
}
|
|
9301
|
+
function expandBounds(bounds, by) {
|
|
9302
|
+
return {
|
|
9303
|
+
left: bounds.left - by,
|
|
9304
|
+
top: bounds.top - by,
|
|
9305
|
+
right: bounds.right + by,
|
|
9306
|
+
bottom: bounds.bottom + by
|
|
9307
|
+
};
|
|
9308
|
+
}
|
|
9309
|
+
function polygonArea(points) {
|
|
9310
|
+
return Math.abs(segments(points, true).reduce((area, { start, end }) => area + start[0] * end[1] - end[0] * start[1], 0)) / 2;
|
|
9311
|
+
}
|
|
9312
|
+
|
|
9313
|
+
//#endregion
|
|
9314
|
+
//#region ../core/src/map-drawing/projection.ts
|
|
9315
|
+
const EARTH_CIRCUMFERENCE = 2 * Math.PI * 6378137;
|
|
9316
|
+
const MERCATOR_LIMIT = 85.0511287798066;
|
|
9317
|
+
function createProjection(pin, radius) {
|
|
9318
|
+
const latitude = pin.lat * Math.PI / 180;
|
|
9319
|
+
const center = [(pin.lng + 180) / 360, (1 - Math.asinh(Math.tan(latitude)) / Math.PI) / 2];
|
|
9320
|
+
const span = 2 * radius / (EARTH_CIRCUMFERENCE * Math.cos(latitude));
|
|
9321
|
+
return {
|
|
9322
|
+
center,
|
|
9323
|
+
span,
|
|
9324
|
+
project: (tile$1, point$1) => {
|
|
9325
|
+
const x = (tile$1.x + point$1[0] / tile$1.extent) / 2 ** tile$1.z;
|
|
9326
|
+
const y = (tile$1.y + point$1[1] / tile$1.extent) / 2 ** tile$1.z;
|
|
9327
|
+
const wrapped = (x - center[0] + 1.5) % 1 - .5;
|
|
9328
|
+
return [400 + wrapped / span * 800, 400 + (y - center[1]) / span * 800];
|
|
9329
|
+
}
|
|
9330
|
+
};
|
|
9331
|
+
}
|
|
9332
|
+
function tileBounds(tile$1, projection) {
|
|
9333
|
+
const a = projection.project(tile$1, [0, 0]), b = projection.project(tile$1, [tile$1.extent, tile$1.extent]);
|
|
9334
|
+
return {
|
|
9335
|
+
left: a[0],
|
|
9336
|
+
top: a[1],
|
|
9337
|
+
right: b[0],
|
|
9338
|
+
bottom: b[1]
|
|
9339
|
+
};
|
|
9340
|
+
}
|
|
9341
|
+
function supportsProjection(pin, projection) {
|
|
9342
|
+
return Math.abs(pin.lat) <= MERCATOR_LIMIT && projection.center[1] - projection.span / 2 >= 0 && projection.center[1] + projection.span / 2 <= 1;
|
|
9343
|
+
}
|
|
9344
|
+
|
|
9345
|
+
//#endregion
|
|
9346
|
+
//#region ../core/src/map-drawing/features.ts
|
|
9347
|
+
const compareKey = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
9348
|
+
const greenKinds = [
|
|
9349
|
+
"park",
|
|
9350
|
+
"forest",
|
|
9351
|
+
"wood",
|
|
9352
|
+
"grass",
|
|
9353
|
+
"meadow",
|
|
9354
|
+
"garden",
|
|
9355
|
+
"nature_reserve",
|
|
9356
|
+
"protected_area",
|
|
9357
|
+
"recreation_ground",
|
|
9358
|
+
"village_green"
|
|
9359
|
+
];
|
|
9360
|
+
function polygonRole(feature$1) {
|
|
9361
|
+
if (feature$1.layer === "earth") return "map-land";
|
|
9362
|
+
if (feature$1.layer === "water") return feature$1.kind === "swimming_pool" ? null : "map-water";
|
|
9363
|
+
if (feature$1.layer === "buildings") return "map-building";
|
|
9364
|
+
return feature$1.layer === "landuse" && greenKinds.includes(feature$1.kind) ? "map-park" : null;
|
|
9365
|
+
}
|
|
9366
|
+
function roadRole(kind) {
|
|
9367
|
+
if (kind === "highway" || kind === "major_road") return "major";
|
|
9368
|
+
if (kind === "minor_road") return "minor";
|
|
9369
|
+
return kind === "path" ? "path" : null;
|
|
9370
|
+
}
|
|
9371
|
+
function intersection(a, b) {
|
|
9372
|
+
const bounds = {
|
|
9373
|
+
left: Math.max(a.left, b.left),
|
|
9374
|
+
top: Math.max(a.top, b.top),
|
|
9375
|
+
right: Math.min(a.right, b.right),
|
|
9376
|
+
bottom: Math.min(a.bottom, b.bottom)
|
|
9377
|
+
};
|
|
9378
|
+
return bounds.left >= bounds.right || bounds.top >= bounds.bottom ? null : bounds;
|
|
9379
|
+
}
|
|
9380
|
+
function canonicalRing(points) {
|
|
9381
|
+
const head = points[0];
|
|
9382
|
+
if (head === undefined) return [];
|
|
9383
|
+
const first = points.reduce((best, point$1, index) => point$1[0] < best.point[0] || point$1[0] === best.point[0] && point$1[1] < best.point[1] ? {
|
|
9384
|
+
point: point$1,
|
|
9385
|
+
index
|
|
9386
|
+
} : best, {
|
|
9387
|
+
point: head,
|
|
9388
|
+
index: 0
|
|
9389
|
+
});
|
|
9390
|
+
const forward = [...points.slice(first.index), ...points.slice(0, first.index)];
|
|
9391
|
+
const backward = [first.point, ...forward.slice(1).reverse()];
|
|
9392
|
+
return compareKey(JSON.stringify(forward), JSON.stringify(backward)) <= 0 ? forward : backward;
|
|
9393
|
+
}
|
|
9394
|
+
function canonicalLine(points) {
|
|
9395
|
+
const reversed = points.slice().reverse();
|
|
9396
|
+
return compareKey(JSON.stringify(points), JSON.stringify(reversed)) <= 0 ? points : reversed;
|
|
9397
|
+
}
|
|
9398
|
+
const CENTRE = [400, 400];
|
|
9399
|
+
function pinDistance(geometry) {
|
|
9400
|
+
if (geometry.kind === "road") return segments(geometry.points).reduce((nearest, { start, end }) => Math.min(nearest, deviation(CENTRE, start, end)), Infinity);
|
|
9401
|
+
return geometry.rings.reduce((best, ring) => ring.reduce((nearest, point$1) => Math.min(nearest, distance(point$1, CENTRE)), best), Infinity);
|
|
9402
|
+
}
|
|
9403
|
+
function candidate(geometry, rank) {
|
|
9404
|
+
const key = JSON.stringify(geometry.kind === "polygon" ? geometry : {
|
|
9405
|
+
kind: "road",
|
|
9406
|
+
role: geometry.role,
|
|
9407
|
+
points: geometry.points
|
|
9408
|
+
});
|
|
9409
|
+
return {
|
|
9410
|
+
geometry,
|
|
9411
|
+
rank,
|
|
9412
|
+
key,
|
|
9413
|
+
distance: pinDistance(geometry)
|
|
9414
|
+
};
|
|
9415
|
+
}
|
|
9416
|
+
/**
|
|
9417
|
+
* Polygons are clipped a fraction past their tile edge so neighbouring tiles' fills overlap; clipped exactly,
|
|
9418
|
+
* anti-aliasing leaves a hairline of background along every tile seam. Fills must be opaque for this to hold.
|
|
9419
|
+
*/
|
|
9420
|
+
const SEAM_OVERLAP = .5;
|
|
9421
|
+
function projectFeature(tile$1, feature$1, projection) {
|
|
9422
|
+
const bounds = intersection(expandBounds(tileBounds(tile$1, projection), feature$1.geometry === "polygon" ? SEAM_OVERLAP : 0), VIEW_BOUNDS);
|
|
9423
|
+
if (bounds === null) return [];
|
|
9424
|
+
const paths$1 = feature$1.paths.map((path) => path.map((point$1) => projection.project(tile$1, point$1)));
|
|
9425
|
+
if (feature$1.geometry === "line") {
|
|
9426
|
+
const role = roadRole(feature$1.kind);
|
|
9427
|
+
return role ? paths$1.flatMap((path) => clipLine(path, bounds)).map((path) => canonicalLine(simplify(canonicalLine(path), false))).filter((path) => path.length >= 2 && segments(path).reduce((sum, { start, end }) => sum + distance(start, end), 0) >= 1).map((points) => candidate({
|
|
9428
|
+
kind: "road",
|
|
9429
|
+
role,
|
|
9430
|
+
points,
|
|
9431
|
+
name: feature$1.name
|
|
9432
|
+
}, feature$1.sortRank)) : [];
|
|
9433
|
+
}
|
|
9434
|
+
const className = polygonRole(feature$1);
|
|
9435
|
+
if (!className) return [];
|
|
9436
|
+
const rings = paths$1.map((path) => simplify(canonicalRing(clipPolygon(path, bounds)), true)).filter((ring) => ring.length >= 3 && polygonArea(ring) >= .3).map(canonicalRing).slice().sort((a, b) => compareKey(JSON.stringify(a), JSON.stringify(b)));
|
|
9437
|
+
return rings.length ? [candidate({
|
|
9438
|
+
kind: "polygon",
|
|
9439
|
+
fillRule: "evenodd",
|
|
9440
|
+
className,
|
|
9441
|
+
rings
|
|
9442
|
+
}, feature$1.sortRank)] : [];
|
|
9443
|
+
}
|
|
9444
|
+
function deduplicate(candidates) {
|
|
9445
|
+
return candidates.slice().sort((a, b) => compareKey(a.key, b.key) || a.rank - b.rank || compareKey(JSON.stringify(a.geometry), JSON.stringify(b.geometry))).filter((item, index, sorted) => item.key !== sorted[index - 1]?.key);
|
|
9446
|
+
}
|
|
9447
|
+
|
|
9448
|
+
//#endregion
|
|
9449
|
+
//#region ../core/src/map-drawing/labels.ts
|
|
9450
|
+
/**
|
|
9451
|
+
* Collision budget for a ~12.5-unit label in an ordinary sans-serif, with room for a heavier site font.
|
|
9452
|
+
* Renderers can set label type smaller than this, never larger.
|
|
9453
|
+
*/
|
|
9454
|
+
const CHARACTER_WIDTH = 7;
|
|
9455
|
+
const HALF_HEIGHT = 8;
|
|
9456
|
+
const PADDING = 5;
|
|
9457
|
+
/** How far a street may bend away from the straight line its label is written along. */
|
|
9458
|
+
const MAX_BEND = 4.5;
|
|
9459
|
+
/** Preferred places along a street for its label, as fractions of its visible length. */
|
|
9460
|
+
const POSITIONS = [
|
|
9461
|
+
.5,
|
|
9462
|
+
.4,
|
|
9463
|
+
.6,
|
|
9464
|
+
.3,
|
|
9465
|
+
.7,
|
|
9466
|
+
.2,
|
|
9467
|
+
.8,
|
|
9468
|
+
.1,
|
|
9469
|
+
.9
|
|
9470
|
+
];
|
|
9471
|
+
/** Street pieces whose ends are this close are one street (OSM ways and tile clipping both split streets). */
|
|
9472
|
+
const JOIN_DISTANCE = 1;
|
|
9473
|
+
/**
|
|
9474
|
+
* Every label lies wholly inside the central 400 × 300 units. Renderers always show at least that much (a phone
|
|
9475
|
+
* at 0.85 px per unit or more, a 1200 × 500 band at 1.5), so a crop never cuts a street name in half.
|
|
9476
|
+
*/
|
|
9477
|
+
const LABEL_WINDOW = {
|
|
9478
|
+
left: 200,
|
|
9479
|
+
right: 600,
|
|
9480
|
+
top: 250,
|
|
9481
|
+
bottom: 550
|
|
9482
|
+
};
|
|
9483
|
+
/** The pin is a teardrop 26 units wide and 38 tall with its tip on the point; labels keep clear of it. */
|
|
9484
|
+
const PIN_KEEP_OUT = {
|
|
9485
|
+
left: 382,
|
|
9486
|
+
right: 418,
|
|
9487
|
+
top: 358,
|
|
9488
|
+
bottom: 406
|
|
9489
|
+
};
|
|
9490
|
+
const LABELLED_ROLES = ["major", "minor"];
|
|
9491
|
+
const textLength = (text) => Array.from(text).length * CHARACTER_WIDTH;
|
|
9492
|
+
/** The label's rotated rectangle, which collision checks treat as solid. */
|
|
9493
|
+
function corners(label$1) {
|
|
9494
|
+
const radians = label$1.angle * Math.PI / 180, cos = Math.cos(radians), sin = Math.sin(radians);
|
|
9495
|
+
const half = textLength(label$1.text) / 2 + PADDING;
|
|
9496
|
+
const at = (x, y) => [label$1.point[0] + x * cos - y * sin, label$1.point[1] + x * sin + y * cos];
|
|
9497
|
+
return [
|
|
9498
|
+
at(-half, -HALF_HEIGHT),
|
|
9499
|
+
at(half, -HALF_HEIGHT),
|
|
9500
|
+
at(half, HALF_HEIGHT),
|
|
9501
|
+
at(-half, HALF_HEIGHT)
|
|
9502
|
+
];
|
|
9503
|
+
}
|
|
9504
|
+
const boxCorners = (box) => [
|
|
9505
|
+
[box.left, box.top],
|
|
9506
|
+
[box.right, box.top],
|
|
9507
|
+
[box.right, box.bottom],
|
|
9508
|
+
[box.left, box.bottom]
|
|
9509
|
+
];
|
|
9510
|
+
/** The label's axis-aligned extent once rotated. */
|
|
9511
|
+
function labelBounds(label$1) {
|
|
9512
|
+
const points = corners(label$1), xs = points.map((point$1) => point$1[0]), ys = points.map((point$1) => point$1[1]);
|
|
9513
|
+
return {
|
|
9514
|
+
left: Math.min(...xs),
|
|
9515
|
+
right: Math.max(...xs),
|
|
9516
|
+
top: Math.min(...ys),
|
|
9517
|
+
bottom: Math.max(...ys)
|
|
9518
|
+
};
|
|
9519
|
+
}
|
|
9520
|
+
function overlaps(a, b) {
|
|
9521
|
+
return a.left <= b.right && a.right >= b.left && a.top <= b.bottom && a.bottom >= b.top;
|
|
9522
|
+
}
|
|
9523
|
+
/** Separating-axis test for two rectangles; diagonal labels would block far too much as bounding boxes. */
|
|
9524
|
+
function rectanglesMeet(a, b) {
|
|
9525
|
+
const axes = [a, b].flatMap((box) => [[box[1][0] - box[0][0], box[1][1] - box[0][1]], [box[3][0] - box[0][0], box[3][1] - box[0][1]]]);
|
|
9526
|
+
return axes.every(([x, y]) => {
|
|
9527
|
+
const project = (box) => box.map((point$1) => point$1[0] * x + point$1[1] * y);
|
|
9528
|
+
const first = project(a), second = project(b);
|
|
9529
|
+
return Math.max(...first) >= Math.min(...second) && Math.max(...second) >= Math.min(...first);
|
|
9530
|
+
});
|
|
9531
|
+
}
|
|
9532
|
+
function labelsCollide(a, b) {
|
|
9533
|
+
return rectanglesMeet(corners(a), corners(b));
|
|
9534
|
+
}
|
|
9535
|
+
function labelCoversPin(label$1) {
|
|
9536
|
+
return rectanglesMeet(corners(label$1), boxCorners(PIN_KEEP_OUT));
|
|
9537
|
+
}
|
|
9538
|
+
const inside = (inner, outer) => inner.left >= outer.left && inner.right <= outer.right && inner.top >= outer.top && inner.bottom <= outer.bottom;
|
|
9539
|
+
function pointAlong(points, at) {
|
|
9540
|
+
return segments(points).reduce((state, { start, end }) => {
|
|
9541
|
+
if (state.point !== null) return state;
|
|
9542
|
+
const size = distance(start, end);
|
|
9543
|
+
if (size === 0 || state.remaining > size) return {
|
|
9544
|
+
remaining: state.remaining - size,
|
|
9545
|
+
point: null
|
|
9546
|
+
};
|
|
9547
|
+
const fraction = state.remaining / size;
|
|
9548
|
+
return {
|
|
9549
|
+
remaining: 0,
|
|
9550
|
+
point: [start[0] + (end[0] - start[0]) * fraction, start[1] + (end[1] - start[1]) * fraction]
|
|
9551
|
+
};
|
|
9552
|
+
}, {
|
|
9553
|
+
remaining: at,
|
|
9554
|
+
point: null
|
|
9555
|
+
}).point;
|
|
9556
|
+
}
|
|
9557
|
+
/** The run of street a label of `size` would cover around `centre`, with the vertices inside it. */
|
|
9558
|
+
function stretch(points, centre, size) {
|
|
9559
|
+
const start = pointAlong(points, centre - size / 2), end = pointAlong(points, centre + size / 2), middle = pointAlong(points, centre);
|
|
9560
|
+
if (start === null || end === null || middle === null) return null;
|
|
9561
|
+
const offsets = points.reduce((sums, point$1, index) => [...sums, index === 0 ? 0 : (sums[index - 1] ?? 0) + distance(points[index - 1] ?? point$1, point$1)], []);
|
|
9562
|
+
return {
|
|
9563
|
+
start,
|
|
9564
|
+
end,
|
|
9565
|
+
middle,
|
|
9566
|
+
vertices: points.filter((_, index) => Math.abs((offsets[index] ?? 0) - centre) < size / 2)
|
|
9567
|
+
};
|
|
9568
|
+
}
|
|
9569
|
+
function uprightAngle(start, end) {
|
|
9570
|
+
const degrees = Math.atan2(end[1] - start[1], end[0] - start[0]) * 180 / Math.PI;
|
|
9571
|
+
const upright = degrees > 90 ? degrees - 180 : degrees <= -90 ? degrees + 180 : degrees;
|
|
9572
|
+
return Math.round(upright * 10) / 10 || 0;
|
|
9573
|
+
}
|
|
9574
|
+
/** Joins `line` onto `chain` when one end of each meets, keeping the chain's direction where it can. */
|
|
9575
|
+
function join(chain, line$1) {
|
|
9576
|
+
const meets = (a, b) => a !== undefined && b !== undefined && distance(a, b) <= JOIN_DISTANCE;
|
|
9577
|
+
const reversed = line$1.slice().reverse();
|
|
9578
|
+
if (meets(chain[chain.length - 1], line$1[0])) return [...chain, ...line$1.slice(1)];
|
|
9579
|
+
if (meets(chain[chain.length - 1], reversed[0])) return [...chain, ...reversed.slice(1)];
|
|
9580
|
+
if (meets(line$1[line$1.length - 1], chain[0])) return [...line$1, ...chain.slice(1)];
|
|
9581
|
+
return meets(reversed[reversed.length - 1], chain[0]) ? [...reversed, ...chain.slice(1)] : null;
|
|
9582
|
+
}
|
|
9583
|
+
function stitchPass(streets) {
|
|
9584
|
+
return streets.reduce((chains, street) => {
|
|
9585
|
+
const index = chains.findIndex((chain$1) => chain$1.name === street.name && chain$1.major === street.major && join(chain$1.points, street.points) !== null);
|
|
9586
|
+
const chain = chains[index];
|
|
9587
|
+
const joined = chain === undefined ? null : join(chain.points, street.points);
|
|
9588
|
+
return chain === undefined || joined === null ? [...chains, street] : chains.map((item, at) => at === index ? {
|
|
9589
|
+
...item,
|
|
9590
|
+
distance: Math.min(item.distance, street.distance),
|
|
9591
|
+
points: joined
|
|
9592
|
+
} : item);
|
|
9593
|
+
}, []);
|
|
9594
|
+
}
|
|
9595
|
+
/** Whole named streets from their pieces, so a name is written along the street rather than one short way of it. */
|
|
9596
|
+
function stitch(streets) {
|
|
9597
|
+
const next = stitchPass(streets);
|
|
9598
|
+
return next.length === streets.length ? next : stitch(next);
|
|
9599
|
+
}
|
|
9600
|
+
function namedStreets(roads) {
|
|
9601
|
+
return roads.slice().sort((a, b) => compareKey(a.key, b.key)).flatMap(({ geometry, distance: distance$1 }) => geometry.kind === "road" && geometry.name !== null && LABELLED_ROLES.includes(geometry.role) ? [{
|
|
9602
|
+
name: geometry.name,
|
|
9603
|
+
major: geometry.role === "major",
|
|
9604
|
+
distance: distance$1,
|
|
9605
|
+
points: geometry.points
|
|
9606
|
+
}] : []);
|
|
9607
|
+
}
|
|
9608
|
+
/** Label options for one street, best first: only where the street runs straight enough to write along. */
|
|
9609
|
+
function streetLabels({ name: text, points }) {
|
|
9610
|
+
const size = textLength(text);
|
|
9611
|
+
const length = segments(points).reduce((sum, { start, end }) => sum + distance(start, end), 0);
|
|
9612
|
+
if (length < size) return [];
|
|
9613
|
+
return POSITIONS.flatMap((position) => {
|
|
9614
|
+
const centre = Math.min(Math.max(length * position, size / 2), length - size / 2);
|
|
9615
|
+
const run = stretch(points, centre, size);
|
|
9616
|
+
if (run === null || run.vertices.some((vertex) => deviation(vertex, run.start, run.end) > MAX_BEND)) return [];
|
|
9617
|
+
return [{
|
|
9618
|
+
className: "map-label",
|
|
9619
|
+
haloClassName: "map-label-halo",
|
|
9620
|
+
text,
|
|
9621
|
+
point: roundPoint(run.middle),
|
|
9622
|
+
angle: uprightAngle(run.start, run.end)
|
|
9623
|
+
}];
|
|
9624
|
+
});
|
|
9625
|
+
}
|
|
9626
|
+
function fits(label$1, placed) {
|
|
9627
|
+
return inside(labelBounds(label$1), LABEL_WINDOW) && !labelCoversPin(label$1) && placed.every((other) => other.text !== label$1.text && !labelsCollide(label$1, other));
|
|
9628
|
+
}
|
|
9629
|
+
/** Nearest streets first, as the framing weighs them: the ones around the door are the names a visitor needs. */
|
|
9630
|
+
function placeLabels(roads, framing) {
|
|
9631
|
+
const { maxLabels, minorLabelDistance } = framing;
|
|
9632
|
+
const weighted = (street) => street.major ? street.distance : street.distance * minorLabelDistance;
|
|
9633
|
+
return stitch(namedStreets(roads)).slice().sort((a, b) => weighted(a) - weighted(b) || compareKey(a.name, b.name)).reduce((placed, street) => {
|
|
9634
|
+
if (placed.length >= maxLabels) return placed;
|
|
9635
|
+
const label$1 = streetLabels(street).find((option) => fits(option, placed));
|
|
9636
|
+
return label$1 === undefined ? placed : [...placed, label$1];
|
|
9637
|
+
}, []);
|
|
9638
|
+
}
|
|
9639
|
+
|
|
9640
|
+
//#endregion
|
|
9641
|
+
//#region ../core/src/map-drawing/coverage.ts
|
|
9642
|
+
/** The bounded mixed-zoom coverage decision shared by extraction and completeness validation. */
|
|
9643
|
+
function drawingTileCoverage(pin) {
|
|
9644
|
+
const contexts = FRAMINGS.map((framing) => ({
|
|
9645
|
+
framing,
|
|
9646
|
+
projection: createProjection(pin, framing.radiusMeters)
|
|
9647
|
+
}));
|
|
9648
|
+
if (contexts.some(({ projection }) => !supportsProjection(pin, projection))) return err("unsupported-latitude");
|
|
9649
|
+
const bounds = contexts.map(({ framing, projection }) => {
|
|
9650
|
+
const n = 2 ** framing.zoom;
|
|
9651
|
+
const left = Math.floor((projection.center[0] - projection.span / 2) * n), right = Math.floor((projection.center[0] + projection.span / 2) * n);
|
|
9652
|
+
const top = Math.floor((projection.center[1] - projection.span / 2) * n), bottom = Math.floor((projection.center[1] + projection.span / 2) * n);
|
|
9653
|
+
return {
|
|
9654
|
+
z: framing.zoom,
|
|
9655
|
+
n,
|
|
9656
|
+
left,
|
|
9657
|
+
top,
|
|
9658
|
+
bottom,
|
|
9659
|
+
width: right - left + 1,
|
|
9660
|
+
count: (right - left + 1) * (bottom - top + 1)
|
|
9661
|
+
};
|
|
9662
|
+
});
|
|
9663
|
+
if (bounds.some((value) => !Number.isFinite(value.count) || value.count < 1 || value.top < 0 || value.bottom >= value.n) || bounds.reduce((sum, value) => sum + value.count, 0) > 96) return err("geometry-limit");
|
|
9664
|
+
return ok(bounds.flatMap(({ z: z$1, n, left, top, width, count }) => Array.from({ length: count }, (_, index) => ({
|
|
9665
|
+
z: z$1,
|
|
9666
|
+
x: ((left + index % width) % n + n) % n,
|
|
9667
|
+
y: top + Math.floor(index / width)
|
|
9668
|
+
}))));
|
|
9669
|
+
}
|
|
9670
|
+
|
|
9671
|
+
//#endregion
|
|
9672
|
+
//#region ../core/src/map-drawing/generate.ts
|
|
9673
|
+
const ROLE_ORDER = [
|
|
9674
|
+
"major",
|
|
9675
|
+
"minor",
|
|
9676
|
+
"path"
|
|
9677
|
+
];
|
|
9678
|
+
const MAX_BUILDINGS = 1e3;
|
|
9679
|
+
const MAX_ROADS = 300;
|
|
9680
|
+
/** What this framing draws: its road roles, buildings or not, and no green patches too small to read. */
|
|
9681
|
+
function belongsIn(framing, item) {
|
|
9682
|
+
const geometry = item.geometry;
|
|
9683
|
+
if (geometry.kind === "road") return framing.roads.some((role) => role === geometry.role) && (geometry.role !== "path" || item.distance <= framing.maxPathDistance);
|
|
9684
|
+
if (geometry.className === "map-building") return framing.buildings;
|
|
9685
|
+
return geometry.className !== "map-park" || geometry.rings.some((ring) => polygonArea(ring) >= framing.minParkArea);
|
|
9686
|
+
}
|
|
9687
|
+
function selectFeatures(framing, candidates) {
|
|
9688
|
+
const ordered = candidates.filter((item) => belongsIn(framing, item)).sort((a, b) => a.distance - b.distance || a.rank - b.rank || compareKey(a.key, b.key));
|
|
9689
|
+
const buildings = ordered.filter((item) => item.geometry.kind === "polygon" && item.geometry.className === "map-building").slice(0, MAX_BUILDINGS);
|
|
9690
|
+
const roleOrder = (item) => item.geometry.kind === "road" ? ROLE_ORDER.indexOf(item.geometry.role) : 0;
|
|
9691
|
+
const roads = ordered.filter((item) => item.geometry.kind === "road").sort((a, b) => roleOrder(a) - roleOrder(b) || a.distance - b.distance || compareKey(a.key, b.key)).slice(0, MAX_ROADS);
|
|
9692
|
+
const ground = ordered.filter((item) => item.geometry.kind === "polygon" && item.geometry.className !== "map-building");
|
|
9693
|
+
return [
|
|
9694
|
+
...ground,
|
|
9695
|
+
...buildings,
|
|
9696
|
+
...roads
|
|
9697
|
+
];
|
|
9698
|
+
}
|
|
9699
|
+
/** Water over green, so a wood or park running to the bank never hides the river. */
|
|
9700
|
+
const POLYGON_ORDER = [
|
|
9701
|
+
"map-land",
|
|
9702
|
+
"map-park",
|
|
9703
|
+
"map-water",
|
|
9704
|
+
"map-building"
|
|
9705
|
+
];
|
|
9706
|
+
function paintingOrder(candidate$1) {
|
|
9707
|
+
const geometry = candidate$1.geometry;
|
|
9708
|
+
return geometry.kind === "road" ? POLYGON_ORDER.length + ROLE_ORDER.length - ROLE_ORDER.indexOf(geometry.role) : POLYGON_ORDER.indexOf(geometry.className);
|
|
9709
|
+
}
|
|
9710
|
+
const casingClass = {
|
|
9711
|
+
major: "map-road-major-casing",
|
|
9712
|
+
minor: "map-road-minor-casing"
|
|
9713
|
+
};
|
|
9714
|
+
const strokeClass = {
|
|
9715
|
+
major: "map-road-major",
|
|
9716
|
+
minor: "map-road-minor",
|
|
9717
|
+
path: "map-road-path"
|
|
9718
|
+
};
|
|
9719
|
+
/** Casings under every stroke; paths are a single thin line with no casing. */
|
|
9720
|
+
function roadPaths(roads) {
|
|
9721
|
+
const lines = roads.flatMap((item) => item.geometry.kind === "road" ? [item.geometry] : []);
|
|
9722
|
+
const casings = lines.flatMap(({ role, points }) => role === "path" ? [] : [{
|
|
9723
|
+
kind: "line",
|
|
9724
|
+
className: casingClass[role],
|
|
9725
|
+
points
|
|
9726
|
+
}]);
|
|
9727
|
+
return [...casings, ...lines.map(({ role, points }) => ({
|
|
9728
|
+
kind: "line",
|
|
9729
|
+
className: strokeClass[role],
|
|
9730
|
+
points
|
|
9731
|
+
}))];
|
|
9732
|
+
}
|
|
9733
|
+
function generateFrame(input, framing, projection) {
|
|
9734
|
+
const candidates = input.tiles.filter((tile$1) => tile$1.z === framing.zoom).flatMap((tile$1) => tile$1.features.flatMap((feature$1) => projectFeature(tile$1, feature$1, projection)));
|
|
9735
|
+
const selected = selectFeatures(framing, deduplicate(candidates)).slice().sort((a, b) => paintingOrder(a) - paintingOrder(b) || a.rank - b.rank || compareKey(a.key, b.key));
|
|
9736
|
+
const polygons = selected.flatMap((item) => item.geometry.kind === "polygon" ? [item.geometry] : []);
|
|
9737
|
+
const roads = selected.filter((item) => item.geometry.kind === "road");
|
|
9738
|
+
return {
|
|
9739
|
+
radiusMeters: framing.radiusMeters,
|
|
9740
|
+
viewBox: [
|
|
9741
|
+
0,
|
|
9742
|
+
0,
|
|
9743
|
+
800,
|
|
9744
|
+
800
|
|
9745
|
+
],
|
|
9746
|
+
backgroundClass: "map-water",
|
|
9747
|
+
paths: [...polygons, ...roadPaths(roads)],
|
|
9748
|
+
labels: placeLabels(roads, framing),
|
|
9749
|
+
pin: {
|
|
9750
|
+
className: "map-pin",
|
|
9751
|
+
point: [400, 400]
|
|
9752
|
+
}
|
|
9753
|
+
};
|
|
9754
|
+
}
|
|
9755
|
+
/** Pure: parse decoded input once, decide both framings, return only closed numeric geometry. */
|
|
9756
|
+
function generateMapDrawing(raw) {
|
|
9757
|
+
const parsed = mapDrawingInputSchema.safeParse(raw);
|
|
9758
|
+
if (!parsed.success) return err("invalid-input");
|
|
9759
|
+
const input = parsed.data;
|
|
9760
|
+
const coverage = drawingTileCoverage(input.pin);
|
|
9761
|
+
if (!coverage.ok) return err(coverage.error);
|
|
9762
|
+
const neighbourhood = {
|
|
9763
|
+
framing: FRAMINGS[0],
|
|
9764
|
+
projection: createProjection(input.pin, FRAMINGS[0].radiusMeters)
|
|
9765
|
+
};
|
|
9766
|
+
const wider = {
|
|
9767
|
+
framing: FRAMINGS[1],
|
|
9768
|
+
projection: createProjection(input.pin, FRAMINGS[1].radiusMeters)
|
|
9769
|
+
};
|
|
9770
|
+
const keys = input.tiles.map((tile$1) => `${tile$1.z}/${tile$1.x}/${tile$1.y}`);
|
|
9771
|
+
const present = new Set(keys);
|
|
9772
|
+
if (present.size !== keys.length) return err("invalid-input");
|
|
9773
|
+
if (coverage.value.some((tile$1) => !present.has(`${tile$1.z}/${tile$1.x}/${tile$1.y}`))) return err("missing-tiles");
|
|
9774
|
+
const drawing = mapDrawingSchema.safeParse({
|
|
9775
|
+
schemaVersion: DRAWING_SCHEMA_VERSION,
|
|
9776
|
+
generatorVersion: DRAWING_GENERATOR_VERSION,
|
|
9777
|
+
buildId: input.buildId,
|
|
9778
|
+
tileSchemaVersion: input.tileSchemaVersion,
|
|
9779
|
+
frames: {
|
|
9780
|
+
neighbourhood: generateFrame(input, neighbourhood.framing, neighbourhood.projection),
|
|
9781
|
+
wider: generateFrame(input, wider.framing, wider.projection)
|
|
9782
|
+
}
|
|
9783
|
+
});
|
|
9784
|
+
if (!drawing.success) return err("geometry-limit");
|
|
9785
|
+
return ok(drawing.data);
|
|
9786
|
+
}
|
|
9787
|
+
|
|
9788
|
+
//#endregion
|
|
9789
|
+
//#region ../core/src/authored-values.ts
|
|
9790
|
+
/** Canonical authored equality: empty storage values and absent optional members agree. */
|
|
9791
|
+
function canonicalAuthoredValue(value) {
|
|
9792
|
+
if (value == null || value === "" || typeof value === "number" && Number.isNaN(value)) return null;
|
|
9793
|
+
if (Array.isArray(value)) return value.map(canonicalAuthoredValue);
|
|
9794
|
+
if (typeof value !== "object") return value;
|
|
9795
|
+
const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b)).map(([key, member]) => [key, canonicalAuthoredValue(member)]).filter(([, member]) => member !== null);
|
|
9796
|
+
return entries.length === 0 ? null : Object.fromEntries(entries);
|
|
9797
|
+
}
|
|
9798
|
+
function authoredValuesEqual(left, right) {
|
|
9799
|
+
return JSON.stringify(canonicalAuthoredValue(left)) === JSON.stringify(canonicalAuthoredValue(right));
|
|
9800
|
+
}
|
|
9801
|
+
|
|
9802
|
+
//#endregion
|
|
9803
|
+
//#region ../core/src/record-guards.ts
|
|
9804
|
+
function isRecord(value) {
|
|
9805
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
9806
|
+
}
|
|
9807
|
+
/**
|
|
9808
|
+
* Type guard for plain object records (excludes class instances like Error/Date).
|
|
9809
|
+
*/
|
|
9810
|
+
function isPlainRecord(value) {
|
|
9811
|
+
if (!isRecord(value)) return false;
|
|
9812
|
+
const proto = Object.getPrototypeOf(value);
|
|
9813
|
+
return proto === Object.prototype || proto === null;
|
|
9814
|
+
}
|
|
9815
|
+
|
|
9816
|
+
//#endregion
|
|
9817
|
+
export { selectCategoryView as $, parseNonNegativeCents as $t, DEFAULT_MAILING_LIST_SUCCESS_MESSAGE as A, unsafeAsEventOfferingId as An, clampConfiguredAmount as At, parseWeeklyOpeningHours as B, unsafeAsProductId as Bn, formatBookingPriceDisplay as Bt, parseManagementKey as C, unsafeAsBookingSessionId as Cn, toUtcIsoDateTimeString as Ct, parseMailingListSignedSourceToken as D, unsafeAsDetectedMimeType as Dn, isSettleableFlexibleBalanceContractStatus as Dt, parseMailingListContentVersionId as E, unsafeAsDemoCapability as En, isLiveOrderInstalmentPlanStatus as Et, DEFAULT_MAILING_LIST_TARGETING as F, unsafeAsOriginalFilename as Fn, splitAmountWithRemainderInFirst as Ft, locationValueSchema as G, unsafeAsStaffMemberId as Gn, publicEventPricingOptions as Gt, decideLocationSave as H, unsafeAsSiteFontId as Hn, isPublicEventPricingOptionDisplayable as Ht, planMailingListChoicePresentation as I, unsafeAsPassId as In, resolveFlexibleBalanceDueAt as It, projectPublicLocation as J, unsafeAsUploadSessionId as Jn, resolvePricingOptionsPriceDisplay as Jt, normalizeLocationAddressText as K, unsafeAsStorageBucketName as Kn, publicEventPricingType as Kt, isCanonicalRichTextDoc as L, unsafeAsPositiveByteCount as Ln, resolveFlexibleBalanceInitialPaymentCents as Lt, parseFooterCompositionV1 as M, unsafeAsFileChecksumSha256 as Mn, getBookingInstalmentPlanIntervalCount as Mt, footerNavigationLayouts as N, unsafeAsMediaAssetId as Nn, getBookingInstalmentProtectedDepositAmountCents as Nt, parseMailingListRuntimeContext as O, unsafeAsEntitlementId as On, isSettleableOrderInstalmentPlanStatus as Ot, parseMailingListPlacementTargeting as P, unsafeAsOccurrenceId as Pn, omitZeroAmountFutureRows as Pt, mapDrawingSchema as Q, isNonNegativeWholeCents as Qt, formatOpeningHoursRows as R, unsafeAsPositiveSeconds as Rn, deriveTotalTicketQuantity as Rt, exposeSecret as S, unsafeAsBookingRequirementId as Sn, toScheduledBookingDateTime as St, parseMailingListBlockId as T, unsafeAsDeclaredMimeType as Tn, validateBookingPaymentOptionKinds as Tt, stableJsonStringify as U, unsafeAsSiteId as Un, publicEventPricingCurrency as Ut, assertAllowedPublicUrl as V, unsafeAsSafeDownloadFilename as Vn, formatEventPricing as Vt, locationMapLinks as W, unsafeAsSiteScopedStoragePath as Wn, publicEventPricingDisplay as Wt, toLocationCoordinates as X, err as Xn, mapNonEmptyArray as Xt, publicLocationSchema as Y, unsafeAsUserId as Yn, fromArray as Yt, isPresent as Z, ok as Zn, mapReadonlyNonEmptyArray as Zt, nextLiveRunSessionStartMs as _, parseRetiredCourseId as _n, parseLocalScheduleTime as _t, areConfiguredBookingVerticalConfigsEqual as a, formatCurrencyAmount as an, formatTimeRange as at, resolveWholeSeriesRunLifecycle as b, parseUserId as bn, resolveBookingTimeZoneId as bt, parseVerticalPresetId as c, formatDurationUnitLabel as cn, buildBookingUtcDateWindow as ct, EVENT_PRESET_VALUES as d, parseDatedOfferingCapacityHoldAuditRef as dn, formatLocalScheduleTime as dt, parsePositiveCents as en, formatDate as et, ORDINARY_EVENT_PRESET_VALUES as f, parseDatedOfferingCapacityHoldId as fn, getBookingLocalDateKey as ft, CANCELLED_SESSION_STATUS as g, parsePageId as gn, parseBookingLocalDate as gt, publicReadFreshnessSeconds as h, parseMembershipId as hn, localScheduleTimePattern as ht, NAVIGATION_LOCATIONS as i, unsafeAsPositiveCents as in, formatTime as it, parseMailingListProviderScope as j, unsafeAsEventSeriesId as jn, getBookingInstalmentDepositRefundability as jt, assessMailingListTargeting as k, unsafeAsEventInstanceId as kn, addIntervalPreservingMonthAnchor as kt, resolveSiteVerticalBookingConfig as l, parseApiKeyId as ln, compareLocalScheduleDates as lt, WHOLE_SERIES_EVENT_PRESET_VALUES as m, parseMediaAssetId as mn, getLocalScheduleTime as mt, resolveEventCatalogue as n, toNonNegativeCents as nn, formatDateTime as nt, canonicalizeConfiguredBookingVerticalConfig as o, parseCurrencyCode as on, inferSiteDateLocale as ot, SURFACE_EVENT_PRESET_VALUES as p, parseEventSeriesId as pn, getLocalScheduleDate as pt, parseLocationValue as q, unsafeAsStoragePath as qn, resolveExactOrFreePriceDisplay as qt, EVENT_PRESET_VOCABULARY as r, unsafeAsNonNegativeCents as rn, formatDateTimeRange as rt, parseVerticalId as s, assertNever as sn, resolveBookingDisplayContext as st, authoredValuesEqual as t, toCents as tn, formatDateRange as tt, DEFAULT_EVENT_TYPE_PRESETS as u, parseCustomerId as un, formatLocalScheduleDate as ut, orderedLiveRunSessions as v, parseSiteId as vn, parseUtcIsoDateTimeString as vt, slugify as w, unsafeAsCheckoutLineId as wn, parseBookingTimeZone as wt, createSecret as x, unsafeAsAppointmentResourceId as xn, toBookingTimeZone as xt, resolveWholeSeriesBookingPhase as y, parseSiteTermsVersionId as yn, resolveBookingBusinessTimeZone as yt, parsePublicContactDetails as z, unsafeAsPricingOptionId as zn, parseEventPageSurfaceSubject as zt };
|