@panoramax/web-viewer
Version:
Panoramax web viewer for geolocated pictures
215 lines (183 loc) • 5.65 kB
JavaScript
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;
}