NODEDC_DESIGN_GUIDELINE/server/map-presentation-profile.mjs

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 });
}