276 lines
13 KiB
JavaScript
276 lines
13 KiB
JavaScript
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 });
|
|
}
|