const IDENTIFIER = /^[a-z][a-z0-9._:-]{1,159}$/; const FIELD = /^[a-z][a-z0-9_.-]{0,127}$/; const SEMVER = /^\d+\.\d+\.\d+$/; const PROFILE_KEYS = new Set([ "id", "version", "title", "semanticTypes", "label", "target", "pin", "facets", "styles", "classes", "defaultClassId", "sort", ]); const LABEL_KEYS = new Set([ "mode", "fields", "fontWeight", "sizePx", "color", "outlineColor", "outlineWidthPx", "backgroundColor", "backgroundOpacity", "paddingX", "paddingY", "maxLength", "offsetX", "offsetY", "hideCameraHeightMeters", ]); const TARGET_KEYS = new Set([ "variant", "stemHeightMeters", "headSizePx", "stemWidthPx", "outlineColor", "outlineOpacity", "outlineWidthPx", "hideCameraHeightMeters", ]); const FACET_KEYS = new Set(["id", "field", "label", "filterable", "counter", "values"]); const FACET_VALUE_KEYS = new Set(["value", "label", "order"]); const STYLE_KEYS = new Set(["id", "color", "opacity"]); const CLASS_KEYS = new Set(["id", "label", "priority", "match", "styleId", "renderable"]); const MATCH_KEYS = new Set(["field", "equals"]); const SORT_KEYS = new Set(["field", "order"]); export function normalizeMapPresentationProfile(value) { object(value, "invalid_map_presentation_profile"); onlyKeys(value, PROFILE_KEYS, "invalid_map_presentation_profile_fields"); const id = identifier(value.id, "invalid_map_presentation_profile_id"); const version = text(value.version, 32, "invalid_map_presentation_profile_version"); if (!SEMVER.test(version)) fail("invalid_map_presentation_profile_version"); const title = text(value.title, 120, "invalid_map_presentation_profile_title"); const semanticTypes = identifiers(value.semanticTypes, 1, 8, "invalid_map_presentation_profile_semantic_types"); const label = normalizeLabel(value.label); if (value.target !== undefined && value.pin !== undefined) fail("duplicate_map_presentation_profile_target"); const target = normalizeTarget(value.target ?? value.pin); const facets = normalizeFacets(value.facets); const facetByField = new Map(facets.map((facet) => [facet.field, facet])); const styles = normalizeStyles(value.styles); const styleIds = new Set(styles.map((style) => style.id)); const classes = normalizeClasses(value.classes, facetByField, styleIds); const classIds = new Set(classes.map((item) => item.id)); const defaultClassId = identifier(value.defaultClassId, "invalid_map_presentation_profile_default_class"); if (!classIds.has(defaultClassId)) fail("map_presentation_profile_default_class_not_found"); const sort = normalizeSort(value.sort, facetByField); return { id, version, title, semanticTypes, label, target, facets, styles, classes: [...classes].sort((left, right) => right.priority - left.priority || left.id.localeCompare(right.id)), defaultClassId, sort, }; } export function normalizeMapPresentationProfiles(value) { if (!Array.isArray(value) || value.length > 32) fail("invalid_map_presentation_profiles"); const profiles = value.map(normalizeMapPresentationProfile); unique(profiles.map((profile) => profile.id), "duplicate_map_presentation_profile_id"); return profiles; } function normalizeLabel(value) { object(value, "invalid_map_presentation_profile_label"); onlyKeys(value, LABEL_KEYS, "invalid_map_presentation_profile_label_fields"); const mode = value.mode === undefined ? "attributes" : value.mode; if (!["subject_id", "attributes", "none"].includes(mode)) fail("invalid_map_presentation_profile_label_mode"); return { mode, fields: fields(value.fields, 1, 16, "invalid_map_presentation_profile_label_fields"), fontWeight: integer(value.fontWeight, 400, 700, "invalid_map_presentation_profile_label_weight"), sizePx: number(value.sizePx, 8, 32, "invalid_map_presentation_profile_label_size"), color: hex(value.color, "invalid_map_presentation_profile_label_color"), outlineColor: hex(value.outlineColor, "invalid_map_presentation_profile_label_outline"), outlineWidthPx: number(value.outlineWidthPx, 0, 6, "invalid_map_presentation_profile_label_outline_width"), backgroundColor: hex(value.backgroundColor ?? "#0c0d12", "invalid_map_presentation_profile_label_background"), backgroundOpacity: number(value.backgroundOpacity ?? 0.86, 0, 1, "invalid_map_presentation_profile_label_background_opacity"), paddingX: number(value.paddingX ?? 8, 0, 40, "invalid_map_presentation_profile_label_padding"), paddingY: number(value.paddingY ?? 5, 0, 40, "invalid_map_presentation_profile_label_padding"), maxLength: integer(value.maxLength, 8, 240, "invalid_map_presentation_profile_label_max_length"), offsetX: number(value.offsetX, -100, 100, "invalid_map_presentation_profile_label_offset"), offsetY: number(value.offsetY, -100, 100, "invalid_map_presentation_profile_label_offset"), hideCameraHeightMeters: number(value.hideCameraHeightMeters, 1, 100_000_000, "invalid_map_presentation_profile_label_lod"), }; } function normalizeTarget(value) { object(value, "invalid_map_presentation_profile_target"); onlyKeys(value, TARGET_KEYS, "invalid_map_presentation_profile_target_fields"); if (value.variant !== "elevated-spike") fail("invalid_map_presentation_profile_target_variant"); return { variant: "elevated-spike", stemHeightMeters: number(value.stemHeightMeters, 1, 100_000, "invalid_map_presentation_profile_target_stem_height"), headSizePx: number(value.headSizePx, 1, 64, "invalid_map_presentation_profile_target_head_size"), stemWidthPx: number(value.stemWidthPx, 0.25, 16, "invalid_map_presentation_profile_target_stem_width"), outlineColor: hex(value.outlineColor, "invalid_map_presentation_profile_target_outline"), outlineOpacity: number(value.outlineOpacity, 0, 1, "invalid_map_presentation_profile_target_outline_opacity"), outlineWidthPx: number(value.outlineWidthPx, 0, 8, "invalid_map_presentation_profile_target_outline_width"), hideCameraHeightMeters: number(value.hideCameraHeightMeters, 1, 100_000_000, "invalid_map_presentation_profile_target_lod"), }; } function normalizeFacets(value) { if (!Array.isArray(value) || value.length < 1 || value.length > 16) fail("invalid_map_presentation_profile_facets"); const facets = value.map((facet) => { object(facet, "invalid_map_presentation_profile_facet"); onlyKeys(facet, FACET_KEYS, "invalid_map_presentation_profile_facet_fields"); const values = Array.isArray(facet.values) ? facet.values.map((item) => { object(item, "invalid_map_presentation_profile_facet_value"); onlyKeys(item, FACET_VALUE_KEYS, "invalid_map_presentation_profile_facet_value_fields"); return { value: identifier(item.value, "invalid_map_presentation_profile_facet_value"), label: text(item.label, 80, "invalid_map_presentation_profile_facet_value_label"), order: integer(item.order, 0, 1000, "invalid_map_presentation_profile_facet_value_order"), }; }) : fail("invalid_map_presentation_profile_facet_values"); if (values.length < 1 || values.length > 32) fail("invalid_map_presentation_profile_facet_values"); unique(values.map((item) => item.value), "duplicate_map_presentation_profile_facet_value"); return { id: identifier(facet.id, "invalid_map_presentation_profile_facet_id"), field: field(facet.field, "invalid_map_presentation_profile_facet_field"), label: text(facet.label, 80, "invalid_map_presentation_profile_facet_label"), filterable: boolean(facet.filterable, "invalid_map_presentation_profile_facet_filterable"), counter: boolean(facet.counter, "invalid_map_presentation_profile_facet_counter"), values: [...values].sort((left, right) => left.order - right.order || left.value.localeCompare(right.value)), }; }); unique(facets.map((facet) => facet.id), "duplicate_map_presentation_profile_facet_id"); unique(facets.map((facet) => facet.field), "duplicate_map_presentation_profile_facet_field"); return facets; } function normalizeStyles(value) { if (!Array.isArray(value) || value.length < 1 || value.length > 32) fail("invalid_map_presentation_profile_styles"); const styles = value.map((style) => { object(style, "invalid_map_presentation_profile_style"); onlyKeys(style, STYLE_KEYS, "invalid_map_presentation_profile_style_fields"); return { id: identifier(style.id, "invalid_map_presentation_profile_style_id"), color: hex(style.color, "invalid_map_presentation_profile_style_color"), opacity: number(style.opacity, 0, 1, "invalid_map_presentation_profile_style_opacity"), }; }); unique(styles.map((style) => style.id), "duplicate_map_presentation_profile_style_id"); return styles; } function normalizeClasses(value, facetByField, styleIds) { if (!Array.isArray(value) || value.length < 1 || value.length > 64) fail("invalid_map_presentation_profile_classes"); const classes = value.map((item) => { object(item, "invalid_map_presentation_profile_class"); onlyKeys(item, CLASS_KEYS, "invalid_map_presentation_profile_class_fields"); if (!Array.isArray(item.match) || item.match.length > 8) fail("invalid_map_presentation_profile_class_match"); const match = item.match.map((condition) => { object(condition, "invalid_map_presentation_profile_class_condition"); onlyKeys(condition, MATCH_KEYS, "invalid_map_presentation_profile_class_condition_fields"); const matchField = field(condition.field, "invalid_map_presentation_profile_class_condition_field"); const equals = identifier(condition.equals, "invalid_map_presentation_profile_class_condition_value"); const facet = facetByField.get(matchField); if (!facet || !facet.values.some((item) => item.value === equals)) { fail("map_presentation_profile_class_condition_not_declared"); } return { field: matchField, equals }; }); const styleId = identifier(item.styleId, "invalid_map_presentation_profile_class_style"); if (!styleIds.has(styleId)) fail("map_presentation_profile_class_style_not_found"); return { id: identifier(item.id, "invalid_map_presentation_profile_class_id"), label: text(item.label, 80, "invalid_map_presentation_profile_class_label"), priority: integer(item.priority, -10000, 10000, "invalid_map_presentation_profile_class_priority"), match, styleId, renderable: boolean(item.renderable, "invalid_map_presentation_profile_class_renderable"), }; }); unique(classes.map((item) => item.id), "duplicate_map_presentation_profile_class_id"); return classes; } function normalizeSort(value, facetByField) { if (!Array.isArray(value) || value.length > 16) fail("invalid_map_presentation_profile_sort"); const sort = value.map((item) => { object(item, "invalid_map_presentation_profile_sort_item"); onlyKeys(item, SORT_KEYS, "invalid_map_presentation_profile_sort_fields"); const sortField = field(item.field, "invalid_map_presentation_profile_sort_field"); const facet = facetByField.get(sortField); if (!facet || !Array.isArray(item.order) || item.order.length !== facet.values.length) { fail("map_presentation_profile_sort_not_declared"); } const order = item.order.map((entry) => identifier(entry, "invalid_map_presentation_profile_sort_order")); unique(order, "duplicate_map_presentation_profile_sort_order"); if (order.some((entry) => !facet.values.some((item) => item.value === entry))) { fail("map_presentation_profile_sort_not_declared"); } return { field: sortField, order }; }); unique(sort.map((item) => item.field), "duplicate_map_presentation_profile_sort_field"); return sort; } function onlyKeys(value, allowed, code) { if (Object.keys(value).some((key) => !allowed.has(key))) fail(code); } function object(value, code) { if (!value || typeof value !== "object" || Array.isArray(value)) fail(code); } function identifier(value, code) { const normalized = text(value, 160, code); if (!IDENTIFIER.test(normalized)) fail(code); return normalized; } function field(value, code) { const normalized = text(value, 128, code); if (!FIELD.test(normalized)) fail(code); return normalized; } function identifiers(value, min, max, code) { if (!Array.isArray(value) || value.length < min || value.length > max) fail(code); const normalized = value.map((item) => identifier(item, code)); unique(normalized, code); return normalized; } function fields(value, min, max, code) { if (!Array.isArray(value) || value.length < min || value.length > max) fail(code); const normalized = value.map((item) => field(item, code)); unique(normalized, code); return normalized; } function text(value, max, code) { if (typeof value !== "string") fail(code); const normalized = value.trim(); if (!normalized || normalized.length > max) fail(code); return normalized; } function hex(value, code) { const normalized = text(value, 7, code).toLowerCase(); if (!/^#[0-9a-f]{6}$/.test(normalized)) fail(code); return normalized; } function number(value, min, max, code) { if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) fail(code); return value; } function integer(value, min, max, code) { const normalized = number(value, min, max, code); if (!Number.isInteger(normalized)) fail(code); return normalized; } function boolean(value, code) { if (typeof value !== "boolean") fail(code); return value; } function unique(values, code) { if (new Set(values).size !== values.length) fail(code); } function fail(code) { throw Object.assign(new Error(code), { statusCode: 400 }); }