UNPKG

@panoramax/web-viewer

Version:

Panoramax web viewer for geolocated pictures

215 lines (183 loc) 5.65 kB
import { html } from "lit"; import { fa } from "./widgets"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import logoOsm from "../img/osm.svg"; import logoWd from "../img/wd.svg"; import { OSMWikiURL, WikidataURL } from "./services"; // General helpers function valToValLink(val, url, title) { return html` ${val} <pnx-link-button kind="superinline" size="sm" href=${url} target="_blank" style="vertical-align: middle" title=${title} >${fa(faInfoCircle)}</pnx-link-button>`; } // OSM helpers const OSM_MAIN_KEYS = [ "highway", "building", "amenity", "landuse", "natural", "waterway", "leisure", "shop", "railway", "tourism", "barrier", "boundary", "place", "power" ]; // Supported prefixes for nice semantic display const KNOWN_PREFIXES = { "": { title: "Panoramax" }, osm: { title: "OpenStreetMap", logo: logoOsm, key_transform: (tag, _t) => valToValLink( tag.key, `${OSMWikiURL()}/Key:${tag.key}`, _t.pnx.semantics_key_doc ), value_transform: (tag, _t) => { if(OSM_MAIN_KEYS.includes(tag.key)) { return valToValLink( tag.value, `${OSMWikiURL()}/Tag:${tag.key}=${tag.value}`, _t.pnx.semantics_value_doc ); } else { return tag.value; } } }, wd: { title: "Wikidata", logo: logoWd, key_transform: (tag, _t) => { const url = `${WikidataURL()}/Property:${tag.key}`; return _t.pnx.semantics_wikidata_properties[tag.key] ? valToValLink( `${_t.pnx.semantics_wikidata_properties[tag.key]} (${tag.key})`, url, _t.pnx.semantics_key_doc ) : valToValLink(tag.key, url, _t.pnx.semantics_key_doc); }, value_transform: (tag, _t) => valToValLink( tag.value, `${WikidataURL()}/${tag.value}`, _t.pnx.semantics_value_doc ) }, exif: { title: "EXIF" }, }; /** * Transform a prefix|key=value into parsed object. * Does not handle qualifiers tags. * @private */ export function decodeBasicTag(tag) { const firstEqual = (tag || "").lastIndexOf("="); if(firstEqual < 0) { return null; } return { key: decodeKey(tag.substring(0, firstEqual)), value: tag.substring(firstEqual+1), }; } /** @private */ export function decodeKey(key = "") { const regex = /^(?:([a-z_]+)\|)?([^[]+)(?:\[(.*)\])?$/; const match = key.match(regex); if (!match) { return { prefix: "", subkey: key, qualifies: null }; } const [, prefix, subkey, qualifies ] = match; return { key: key, prefix: prefix || "", subkey, qualifies: decodeBasicTag(qualifies), }; } /** * Transforms a string containing a list of tags in a ready-to-use parsed object list. * @param {string} str The string to read (each tag separated by newline `\n`) * @returns {object[]} List of API-formatted tags */ export function parseSemanticsString(str) { const parsedTags = str.split("\n").map(t => { const parts = decodeBasicTag(t); if( parts && parts.key.key.length <= 256 && parts.value.length <= 2048 ) { return { key: parts.key.key, value: parts.value }; } else { return null; } }); if(parsedTags.findIndex(v => !v) >= 0) { if(str.trim().length === 0 && parsedTags.length === 1) { return []; } throw new Error("Invalid tags"); } return parsedTags; } /** * Computes the difference between two set of API tags. * API expects a delta between old & new, so result contains only * added and deleted tags. * @param {object[]} prev The previous set of tags * @param {object[]} next The new set of tags * @returns {object[]} The new set of tags, with only added/deleted tags */ export function computeDiffTags(prev = [], next = []) { const res = []; // Look for new values (next || []) .filter(nt => prev.find(pt => pt.key == nt.key && pt.value == nt.value) === undefined) .forEach(t => res.push({key: t.key, value: t.value, action: "add"})); // Look for prev values missing in next (prev || []) .filter(pt => next.find(nt => pt.key == nt.key && pt.value == nt.value) === undefined) .forEach(t => res.push({key: t.key, value: t.value, action: "delete"})); return res; } /** * Transforms raw API semantics properties into ready-to-display container. * @param {object[]} tags The API semantics tags * @returns {object[]} A list of groups (by prefix), with {title, tags} information. */ export function groupByPrefix(tags) { // Create raw groups by prefix const byPrefix = {}; const qualifiers = []; // First pass: analyze tags, separate by prefix tags.forEach(tag => { const decodedKey = decodeKey(tag.key); // Put apart qualifiers, to later insert on tags themselves if(decodedKey.qualifies) { qualifiers.push(Object.assign({}, tag, decodedKey)); } // Process classic tag else { if (!byPrefix[decodedKey.prefix]) { byPrefix[decodedKey.prefix] = []; } byPrefix[decodedKey.prefix].push(decodedKey.prefix.length > 0 ? { key: decodedKey.subkey, value: tag.value } : tag); } }); // Second pass: add qualifiers on concerned tags qualifiers.forEach(({key, prefix, subkey, qualifies, value}) => { const concernedTag = byPrefix[qualifies.key.prefix]?.find(t => ( t.key === qualifies.key.subkey && (!qualifies.value || qualifies.value === t.value) )); if(concernedTag) { if(!concernedTag.qualifiers) { concernedTag.qualifiers = []; } concernedTag.qualifiers.push({key, prefix, subkey, value}); } }); // Append known prefixes information let groups = Object.entries(byPrefix).map(([prefix, prefixTags]) => { if(KNOWN_PREFIXES[prefix]) { return Object.assign({ prefix, tags: prefixTags }, KNOWN_PREFIXES[prefix]); } else { return { prefix, title: prefix, tags: prefixTags }; } }); return groups; }