@panoramax/web-viewer 4.0.2-develop-9b499e28 → 4.0.2-develop-e389d775
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/build/index.css +1 -1
- package/build/index.css.map +1 -1
- package/build/index.js +274 -59
- package/build/index.js.map +1 -1
- package/config/jest/mocks.js +5 -0
- package/docs/reference/components/core/PhotoViewer.md +1 -0
- package/docs/reference/components/core/Viewer.md +1 -0
- package/docs/reference/components/menus/AnnotationsList.md +16 -0
- package/docs/reference/components/ui/HashTags.md +15 -0
- package/docs/reference/components/ui/ListItem.md +38 -0
- package/docs/reference/components/ui/Photo.md +53 -1
- package/docs/reference/components/ui/SemanticsTable.md +32 -0
- package/docs/reference/utils/PresetsManager.md +35 -0
- package/docs/reference.md +4 -0
- package/mkdocs.yml +4 -0
- package/package.json +2 -1
- package/src/components/core/Basic.css +2 -0
- package/src/components/core/PhotoViewer.js +11 -0
- package/src/components/core/Viewer.js +7 -0
- package/src/components/layout/Tabs.js +1 -1
- package/src/components/menus/AnnotationsList.js +146 -0
- package/src/components/menus/PictureLegend.js +2 -4
- package/src/components/menus/PictureMetadata.js +66 -2
- package/src/components/menus/index.js +1 -0
- package/src/components/styles.js +34 -0
- package/src/components/ui/HashTags.js +98 -0
- package/src/components/ui/ListItem.js +83 -0
- package/src/components/ui/Photo.js +137 -0
- package/src/components/ui/SemanticsTable.js +87 -0
- package/src/components/ui/index.js +3 -0
- package/src/img/osm.svg +49 -0
- package/src/img/wd.svg +1 -0
- package/src/translations/en.json +22 -0
- package/src/translations/fr.json +20 -0
- package/src/utils/PresetsManager.js +137 -0
- package/src/utils/index.js +3 -1
- package/src/utils/picture.js +28 -0
- package/src/utils/semantics.js +162 -0
- package/src/utils/services.js +38 -0
- package/src/utils/widgets.js +18 -1
- package/tests/components/core/__snapshots__/PhotoViewer.test.js.snap +10 -0
- package/tests/components/core/__snapshots__/Viewer.test.js.snap +10 -0
- package/tests/utils/PresetsManager.test.js +123 -0
- package/tests/utils/semantics.test.js +125 -0
package/src/translations/en.json
CHANGED
|
@@ -173,6 +173,28 @@
|
|
|
173
173
|
"metadata_exif_value": "Value",
|
|
174
174
|
"metadata_exif_doc": "Docs for EXIF tags",
|
|
175
175
|
"metadata_exif_doc_title": "Go to Exiv2 documentation to have full details on EXIF and XMP tags definitions",
|
|
176
|
+
"semantics_title": "Tags",
|
|
177
|
+
"semantics_tags": "{nb} tags",
|
|
178
|
+
"semantics_features": "Detected features",
|
|
179
|
+
"semantics_features_none": "No feature has been detected or annotated in this picture yet.",
|
|
180
|
+
"semantics_features_default_title": "Feature #{nb}",
|
|
181
|
+
"semantics_features_subtitle": "{nb} associated properties",
|
|
182
|
+
"semantics_show_all_tags": "Show all tags",
|
|
183
|
+
"semantics_hide_all_tags": "Hide all tags",
|
|
184
|
+
"semantics_key": "Key",
|
|
185
|
+
"semantics_value": "Value",
|
|
186
|
+
"semantics_key_doc": "Know more about this key",
|
|
187
|
+
"semantics_value_doc": "Know more about this tag",
|
|
188
|
+
"semantics_tags_picture": "Picture tags",
|
|
189
|
+
"semantics_tags_picture_none": "No tags has been set for this picture yet.",
|
|
190
|
+
"semantics_qualifiers": "Qualifiers",
|
|
191
|
+
"semantics_show_annotations": "Display picture annotations",
|
|
192
|
+
"semantics_hide_annotations": "Hide picture annotations",
|
|
193
|
+
"semantics_wikidata_properties": {
|
|
194
|
+
"P31": "instance of",
|
|
195
|
+
"P279": "subclass of",
|
|
196
|
+
"P361": "part of"
|
|
197
|
+
},
|
|
176
198
|
"report": "Report",
|
|
177
199
|
"report_auth": "This report will be sent using your account \"{a}\"",
|
|
178
200
|
"report_nature_label": "Nature of the issue",
|
package/src/translations/fr.json
CHANGED
|
@@ -173,6 +173,26 @@
|
|
|
173
173
|
"metadata_exif_value": "Valeur",
|
|
174
174
|
"metadata_exif_doc": "Doc des attributs EXIF",
|
|
175
175
|
"metadata_exif_doc_title": "Acééder à la doc Exiv2 pour en savoir plus sur les attributs EXIF et XMP",
|
|
176
|
+
"semantics_title": "Tags",
|
|
177
|
+
"semantics_tags": "{nb} attributs",
|
|
178
|
+
"semantics_features": "Objets détectés",
|
|
179
|
+
"semantics_features_none": "Aucun objet n'a été détecté ou annoté dans cette photo pour l'instant.",
|
|
180
|
+
"semantics_features_default_title": "Objet #{nb}",
|
|
181
|
+
"semantics_features_subtitle": "{nb} attributs associés",
|
|
182
|
+
"semantics_show_all_tags": "Voir tous les attributs",
|
|
183
|
+
"semantics_hide_all_tags": "Masquer les attributs",
|
|
184
|
+
"semantics_key": "Clé",
|
|
185
|
+
"semantics_value": "Valeur",
|
|
186
|
+
"semantics_key_doc": "En savoir plus à propos de cette clé",
|
|
187
|
+
"semantics_value_doc": "En savoir plus à propos de cet attribut",
|
|
188
|
+
"semantics_tags_picture": "Attributs de la photo",
|
|
189
|
+
"semantics_tags_picture_none": "Aucun attribut n'a été défini pour cette photo pour l'instant.",
|
|
190
|
+
"semantics_qualifiers": "Qualificateurs",
|
|
191
|
+
"semantics_wikidata_properties": {
|
|
192
|
+
"P31": "instance de",
|
|
193
|
+
"P279": "sous-classe de",
|
|
194
|
+
"P361": "partie de"
|
|
195
|
+
},
|
|
176
196
|
"report": "Signaler",
|
|
177
197
|
"report_auth": "Ce signalement sera envoyé en utilisant votre compte \"{a}\"",
|
|
178
198
|
"report_nature_label": "Nature du problème",
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { PanoramaxPresetsURL } from "./services";
|
|
2
|
+
|
|
3
|
+
// Iconify aliases for whole collections
|
|
4
|
+
const ICON_COLLECTIONS_SUB = {
|
|
5
|
+
"fas": "fa6-solid"
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
// Iconify aliases for specific icons
|
|
9
|
+
const ICON_SUBS = {
|
|
10
|
+
"fa6-solid:directions": "fa6-solid:diamond-turn-right"
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Presets Manager handle retrieval of presets against dedicated endpoint.
|
|
15
|
+
* It allows an easily search between all presets, and handles translations.
|
|
16
|
+
*
|
|
17
|
+
* @class Panoramax.utils.PresetsManager
|
|
18
|
+
* @typicalname presetsManager
|
|
19
|
+
* @param {string} [lang] The user prefered language. Defaults to web browser preferences.
|
|
20
|
+
* @param {boolean} [skipLoad=false] Set to true to disable automatic load of API data
|
|
21
|
+
*/
|
|
22
|
+
export default class PresetManager {
|
|
23
|
+
constructor(lang = null, skipLoad = false) {
|
|
24
|
+
this._ready = false;
|
|
25
|
+
this._presets = null;
|
|
26
|
+
this._translations = {};
|
|
27
|
+
if(!skipLoad) { this._load(lang); }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Downloads various JSON file against presets API.
|
|
32
|
+
* @private
|
|
33
|
+
*/
|
|
34
|
+
async _load(lang) {
|
|
35
|
+
lang = lang || window.navigator.languages[0]?.substring(0, 2);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const [translationsENRes, translationsLangRes, presetsRes] = await Promise.all([
|
|
39
|
+
fetch(`${PanoramaxPresetsURL()}/translations/en.min.json`),
|
|
40
|
+
lang ? fetch(`${PanoramaxPresetsURL()}/translations/${lang}.min.json`) : Promise.resolve({ok: true}),
|
|
41
|
+
fetch(`${PanoramaxPresetsURL()}/presets.min.json`),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
!translationsENRes || !translationsLangRes || !presetsRes
|
|
46
|
+
|| !translationsENRes.ok || !translationsLangRes.ok || !presetsRes.ok
|
|
47
|
+
) {
|
|
48
|
+
this._ready = -1;
|
|
49
|
+
throw new Error("Presets service is not available");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
this._presets = await presetsRes.json();
|
|
53
|
+
this._translations.en = (await translationsENRes.json())?.en?.presets;
|
|
54
|
+
if(lang) {
|
|
55
|
+
this._translations[lang] = (await translationsLangRes.json())?.[lang]?.presets;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Post-process presets
|
|
59
|
+
Object.entries(this._presets).forEach(([pid,p]) => {
|
|
60
|
+
// Add labels
|
|
61
|
+
if(this._translations[lang]?.presets?.[pid]?.name) {
|
|
62
|
+
p.name = this._translations[lang].presets[pid].name;
|
|
63
|
+
}
|
|
64
|
+
else if(this._translations.en?.presets?.[pid]?.name) {
|
|
65
|
+
p.name = this._translations.en.presets[pid].name;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Create iconify-compatible icon
|
|
69
|
+
if(p.icon) {
|
|
70
|
+
const iconColl = p.icon.split("-")[0];
|
|
71
|
+
const iconCollSub = ICON_COLLECTIONS_SUB[iconColl];
|
|
72
|
+
p.iconify = iconCollSub ? p.icon.replace(iconColl+"-", iconCollSub+":") : p.icon.split("-")[0]+":"+p.icon.split("-").slice(1).join("-");
|
|
73
|
+
if(ICON_SUBS[p.iconify]) { p.iconify = ICON_SUBS[p.iconify]; }
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
this._ready = true;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("Presets service throws an error:", error);
|
|
80
|
+
this._ready = -1;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolves when the all necessary data was downloaded.
|
|
86
|
+
* @memberOf Panoramax.utils.PresetsManager#
|
|
87
|
+
* @returns {Promise}
|
|
88
|
+
*/
|
|
89
|
+
async onceReady() {
|
|
90
|
+
if(!this._ready) {
|
|
91
|
+
await new Promise(resolve => setTimeout(resolve, 250));
|
|
92
|
+
return this.onceReady();
|
|
93
|
+
}
|
|
94
|
+
else if(this._ready === -1) { return Promise.reject(); }
|
|
95
|
+
else { return Promise.resolve(); }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Find the best matching preset for a given STAC feature.
|
|
100
|
+
* @memberOf Panoramax.utils.PresetsManager#
|
|
101
|
+
* @returns {Promise}
|
|
102
|
+
* @fulfil {object|null} The preset JSON that best matches, or null if no matches
|
|
103
|
+
*/
|
|
104
|
+
async getPreset(f) {
|
|
105
|
+
return this.onceReady().then(() => {
|
|
106
|
+
const candidatePresets = Object.values(this._presets).filter(p => {
|
|
107
|
+
if(Object.keys(p.tags).length === 0) { return false; }
|
|
108
|
+
|
|
109
|
+
// Check if every tag on preset exists in feature
|
|
110
|
+
for(let pk in p.tags) {
|
|
111
|
+
const pv = p.tags[pk];
|
|
112
|
+
if(!f.semantics.find(s => s.key === pk && (pv === "*" || pv === s.value))) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Find best matching preset
|
|
120
|
+
candidatePresets.sort((a,b) => {
|
|
121
|
+
const nbTagsA = Object.keys(a.tags).length;
|
|
122
|
+
const nbTagsB = Object.keys(b.tags).length;
|
|
123
|
+
|
|
124
|
+
if(nbTagsA > nbTagsB) { return -1; }
|
|
125
|
+
else if(nbTagsA === nbTagsB) {
|
|
126
|
+
const nbTagsStarA = Object.values(a.tags).filter(v => v === "*").length;
|
|
127
|
+
const nbTagsStarB = Object.values(b.tags).filter(v => v === "*").length;
|
|
128
|
+
if(nbTagsStarA < nbTagsStarB) { return -1; }
|
|
129
|
+
else if(nbTagsStarA > nbTagsStarB) { return 1; }
|
|
130
|
+
else { return 0; }
|
|
131
|
+
}
|
|
132
|
+
else { return 1; }
|
|
133
|
+
});
|
|
134
|
+
return candidatePresets.shift();
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
package/src/utils/index.js
CHANGED
|
@@ -2,11 +2,13 @@ import * as geocoder from "./geocoder";
|
|
|
2
2
|
import * as i18n from "./i18n";
|
|
3
3
|
import * as map from "./map";
|
|
4
4
|
import * as picture from "./picture";
|
|
5
|
+
import * as semantics from "./semantics";
|
|
5
6
|
import * as services from "./services";
|
|
6
7
|
import * as utils from "./utils";
|
|
7
8
|
import * as widgets from "./widgets";
|
|
8
9
|
|
|
9
|
-
export { geocoder, i18n, map, picture, services, utils, widgets };
|
|
10
|
+
export { geocoder, i18n, map, picture, semantics, services, utils, widgets };
|
|
10
11
|
export {default as API} from "./API";
|
|
11
12
|
export {default as PhotoAdapter} from "./PhotoAdapter";
|
|
13
|
+
export {default as PresetsManager} from "./PresetsManager";
|
|
12
14
|
export {default as URLHandler} from "./URLHandler";
|
package/src/utils/picture.js
CHANGED
|
@@ -377,6 +377,11 @@ export function apiFeatureToPSVNode(f, t, fastInternet=false, customLinkFilter=n
|
|
|
377
377
|
};
|
|
378
378
|
}
|
|
379
379
|
|
|
380
|
+
// Cleanup empty semantics feature (metacatalog bug)
|
|
381
|
+
if(f.properties?.semantics) {
|
|
382
|
+
f.properties.semantics = f.properties.semantics.filter(o => Object.keys(o).length > 0);
|
|
383
|
+
}
|
|
384
|
+
|
|
380
385
|
const node = {
|
|
381
386
|
id: f.id,
|
|
382
387
|
caption: getNodeCaption(f, t),
|
|
@@ -444,4 +449,27 @@ export function filterRelatedPicsLinks(metadata, customFilter = null) {
|
|
|
444
449
|
arrowStyle: arrowStyle(l),
|
|
445
450
|
linkOffset: { yaw: rectifiedYaw }
|
|
446
451
|
}));
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Read structured hashtags from picture metadata
|
|
456
|
+
* @private
|
|
457
|
+
*/
|
|
458
|
+
export function getHashTags(metadata) {
|
|
459
|
+
if(!metadata?.properties?.semantics) { return []; }
|
|
460
|
+
|
|
461
|
+
// Find hashtag entry in semantics
|
|
462
|
+
const hashtagsTags = metadata.properties.semantics.filter(kv => kv.key === "hashtags");
|
|
463
|
+
const readHashTags = [];
|
|
464
|
+
hashtagsTags.forEach(htt => htt.value.split(";").forEach(v => readHashTags.push(v.trim())));
|
|
465
|
+
|
|
466
|
+
return readHashTags;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Check if a given picture has associated annotations.
|
|
471
|
+
* @private
|
|
472
|
+
*/
|
|
473
|
+
export function hasAnnotations(metadata) {
|
|
474
|
+
return metadata?.properties?.annotations?.length > 0;
|
|
447
475
|
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { html } from "lit";
|
|
2
|
+
import { fa } from "./widgets";
|
|
3
|
+
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
|
4
|
+
import logoOsm from "../img/osm.svg";
|
|
5
|
+
import logoWd from "../img/wd.svg";
|
|
6
|
+
import { OSMWikiURL, WikidataURL } from "./services";
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
// General helpers
|
|
10
|
+
function valToValLink(val, url, title) {
|
|
11
|
+
return html`
|
|
12
|
+
${val}
|
|
13
|
+
<pnx-link-button
|
|
14
|
+
kind="superinline"
|
|
15
|
+
size="sm"
|
|
16
|
+
href=${url}
|
|
17
|
+
target="_blank"
|
|
18
|
+
style="vertical-align: middle"
|
|
19
|
+
title=${title}
|
|
20
|
+
>${fa(faInfoCircle)}</pnx-link-button>`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// OSM helpers
|
|
24
|
+
const OSM_MAIN_KEYS = [
|
|
25
|
+
"highway", "building", "amenity", "landuse", "natural", "waterway", "leisure",
|
|
26
|
+
"shop", "railway", "tourism", "barrier", "boundary", "place", "power"
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Supported prefixes for nice semantic display
|
|
30
|
+
const KNOWN_PREFIXES = {
|
|
31
|
+
"": { title: "Panoramax" },
|
|
32
|
+
|
|
33
|
+
osm: {
|
|
34
|
+
title: "OpenStreetMap",
|
|
35
|
+
logo: logoOsm,
|
|
36
|
+
key_transform: (tag, _t) => valToValLink(
|
|
37
|
+
tag.key,
|
|
38
|
+
`${OSMWikiURL()}/Key:${tag.key}`,
|
|
39
|
+
_t.pnx.semantics_key_doc
|
|
40
|
+
),
|
|
41
|
+
value_transform: (tag, _t) => {
|
|
42
|
+
if(OSM_MAIN_KEYS.includes(tag.key)) {
|
|
43
|
+
return valToValLink(
|
|
44
|
+
tag.value,
|
|
45
|
+
`${OSMWikiURL()}/Tag:${tag.key}=${tag.value}`,
|
|
46
|
+
_t.pnx.semantics_value_doc
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
else { return tag.value; }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
wd: {
|
|
54
|
+
title: "Wikidata",
|
|
55
|
+
logo: logoWd,
|
|
56
|
+
key_transform: (tag, _t) => {
|
|
57
|
+
const url = `${WikidataURL()}/Property:${tag.key}`;
|
|
58
|
+
return _t.pnx.semantics_wikidata_properties[tag.key]
|
|
59
|
+
? valToValLink(
|
|
60
|
+
`${_t.pnx.semantics_wikidata_properties[tag.key]} (${tag.key})`,
|
|
61
|
+
url,
|
|
62
|
+
_t.pnx.semantics_key_doc
|
|
63
|
+
)
|
|
64
|
+
: valToValLink(tag.key, url, _t.pnx.semantics_key_doc);
|
|
65
|
+
},
|
|
66
|
+
value_transform: (tag, _t) => valToValLink(
|
|
67
|
+
tag.value,
|
|
68
|
+
`${WikidataURL()}/${tag.value}`, _t.pnx.semantics_value_doc
|
|
69
|
+
)
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
exif: { title: "EXIF" },
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Transform a prefix|key=value into parsed object.
|
|
78
|
+
* Does not handle qualifiers tags.
|
|
79
|
+
* @private
|
|
80
|
+
*/
|
|
81
|
+
export function decodeBasicTag(tag) {
|
|
82
|
+
const firstEqual = (tag || "").indexOf("=");
|
|
83
|
+
if(firstEqual < 0) { return null; }
|
|
84
|
+
return {
|
|
85
|
+
key: decodeKey(tag.substring(0, firstEqual)),
|
|
86
|
+
value: tag.substring(firstEqual+1),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
/** @private */
|
|
92
|
+
export function decodeKey(key = "") {
|
|
93
|
+
const regex = /^(?:([a-z_]+)\|)?([^[]+)(?:\[(.*)\])?$/;
|
|
94
|
+
const match = key.match(regex);
|
|
95
|
+
|
|
96
|
+
if (!match) {
|
|
97
|
+
return { prefix: "", subkey: key, qualifies: null };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const [, prefix, subkey, qualifies ] = match;
|
|
101
|
+
return {
|
|
102
|
+
prefix: prefix || "",
|
|
103
|
+
subkey,
|
|
104
|
+
qualifies: decodeBasicTag(qualifies),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Transforms raw API semantics properties into ready-to-display container.
|
|
111
|
+
* @param {object[]} tags The API semantics tags
|
|
112
|
+
* @returns {object[]} A list of groups (by prefix), with {title, tags} information.
|
|
113
|
+
*/
|
|
114
|
+
export function groupByPrefix(tags) {
|
|
115
|
+
// Create raw groups by prefix
|
|
116
|
+
const byPrefix = {};
|
|
117
|
+
const qualifiers = [];
|
|
118
|
+
|
|
119
|
+
// First pass: analyze tags, separate by prefix
|
|
120
|
+
tags.forEach(tag => {
|
|
121
|
+
const decodedKey = decodeKey(tag.key);
|
|
122
|
+
|
|
123
|
+
// Put apart qualifiers, to later insert on tags themselves
|
|
124
|
+
if(decodedKey.qualifies) {
|
|
125
|
+
qualifiers.push(Object.assign({}, tag, decodedKey));
|
|
126
|
+
}
|
|
127
|
+
// Process classic tag
|
|
128
|
+
else {
|
|
129
|
+
if (!byPrefix[decodedKey.prefix]) { byPrefix[decodedKey.prefix] = []; }
|
|
130
|
+
|
|
131
|
+
byPrefix[decodedKey.prefix].push(decodedKey.prefix.length > 0 ? {
|
|
132
|
+
key: decodedKey.subkey,
|
|
133
|
+
value: tag.value
|
|
134
|
+
} : tag);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Second pass: add qualifiers on concerned tags
|
|
139
|
+
qualifiers.forEach(({key, prefix, subkey, qualifies, value}) => {
|
|
140
|
+
const concernedTag = byPrefix[qualifies.key.prefix]?.find(t => (
|
|
141
|
+
t.key === qualifies.key.subkey
|
|
142
|
+
&& (!qualifies.value || qualifies.value === t.value)
|
|
143
|
+
));
|
|
144
|
+
|
|
145
|
+
if(concernedTag) {
|
|
146
|
+
if(!concernedTag.qualifiers) { concernedTag.qualifiers = []; }
|
|
147
|
+
concernedTag.qualifiers.push({key, prefix, subkey, value});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Append known prefixes information
|
|
152
|
+
let groups = Object.entries(byPrefix).map(([prefix, prefixTags]) => {
|
|
153
|
+
if(KNOWN_PREFIXES[prefix]) {
|
|
154
|
+
return Object.assign({ prefix, tags: prefixTags }, KNOWN_PREFIXES[prefix]);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
return { prefix, title: prefix, tags: prefixTags };
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
return groups;
|
|
162
|
+
}
|
package/src/utils/services.js
CHANGED
|
@@ -4,6 +4,36 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Panoramax Presets URL
|
|
9
|
+
* @returns {string} The presets URL
|
|
10
|
+
*/
|
|
11
|
+
export function PanoramaxPresetsURL() {
|
|
12
|
+
return "https://tagging-presets-22605e.gitlab.io";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Temaki icons URL
|
|
17
|
+
* @param {string} [iconName] The icon name to insert in URL
|
|
18
|
+
* @returns {string} The SVG icon URL
|
|
19
|
+
*/
|
|
20
|
+
export function TemakiIconURL(iconName) {
|
|
21
|
+
return `https://raw.githubusercontent.com/rapideditor/temaki/refs/heads/main/icons/${iconName}.svg`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Wikidata URL
|
|
26
|
+
* @returns {string} The Wikidata URL
|
|
27
|
+
*/
|
|
28
|
+
export function WikidataURL() {
|
|
29
|
+
return "https://www.wikidata.org/wiki";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
/* -----------------------------------------------------
|
|
34
|
+
* OpenStreetMap docs & tools
|
|
35
|
+
*/
|
|
36
|
+
|
|
7
37
|
/**
|
|
8
38
|
* OpenStreetMap iD editor URL
|
|
9
39
|
* @returns {string} The editor URL
|
|
@@ -12,6 +42,14 @@ export function IdEditorURL() {
|
|
|
12
42
|
return "https://www.openstreetmap.org/edit?editor=id";
|
|
13
43
|
}
|
|
14
44
|
|
|
45
|
+
/**
|
|
46
|
+
* OpenStreetMap wiki URL
|
|
47
|
+
* @returns {string} The wiki URL
|
|
48
|
+
*/
|
|
49
|
+
export function OSMWikiURL() {
|
|
50
|
+
return "https://wiki.openstreetmap.org/wiki";
|
|
51
|
+
}
|
|
52
|
+
|
|
15
53
|
|
|
16
54
|
/* -----------------------------------------------------
|
|
17
55
|
* Internet speed tests
|
package/src/utils/widgets.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
// Every single icon imported separately to reduce bundle size
|
|
2
1
|
import { icon } from "@fortawesome/fontawesome-svg-core";
|
|
2
|
+
import { setCustomIconLoader } from "iconify-icon";
|
|
3
|
+
import { TemakiIconURL } from "./services";
|
|
3
4
|
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -13,6 +14,22 @@ export function fa(i, o) {
|
|
|
13
14
|
return icon(i, o).node[0];
|
|
14
15
|
}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Register more icons providers for Iconify
|
|
19
|
+
* @private
|
|
20
|
+
*/
|
|
21
|
+
export function moreIcons() {
|
|
22
|
+
setCustomIconLoader(async (name) => {
|
|
23
|
+
const response = await fetch(TemakiIconURL(name));
|
|
24
|
+
if (!response.ok) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
body: await response.text()
|
|
29
|
+
};
|
|
30
|
+
}, "temaki");
|
|
31
|
+
}
|
|
32
|
+
|
|
16
33
|
/**
|
|
17
34
|
* Create a web component with its initial properties
|
|
18
35
|
* @private
|
|
@@ -23,6 +23,12 @@ exports[`_initWidgets should handle widgets if width is not small 1`] = `
|
|
|
23
23
|
slot="top-left"
|
|
24
24
|
/>,
|
|
25
25
|
],
|
|
26
|
+
Array [
|
|
27
|
+
<pnx-hashtags
|
|
28
|
+
class="pnx-only-psv pnx-print-hidden"
|
|
29
|
+
slot="top-right"
|
|
30
|
+
/>,
|
|
31
|
+
],
|
|
26
32
|
],
|
|
27
33
|
"results": Array [
|
|
28
34
|
Object {
|
|
@@ -37,6 +43,10 @@ exports[`_initWidgets should handle widgets if width is not small 1`] = `
|
|
|
37
43
|
"type": "return",
|
|
38
44
|
"value": undefined,
|
|
39
45
|
},
|
|
46
|
+
Object {
|
|
47
|
+
"type": "return",
|
|
48
|
+
"value": undefined,
|
|
49
|
+
},
|
|
40
50
|
],
|
|
41
51
|
}
|
|
42
52
|
`;
|
|
@@ -16,6 +16,12 @@ exports[`_initWidgets should handle widgets if width is not small 1`] = `
|
|
|
16
16
|
slot="top-left"
|
|
17
17
|
/>,
|
|
18
18
|
],
|
|
19
|
+
Array [
|
|
20
|
+
<pnx-hashtags
|
|
21
|
+
class="pnx-only-psv pnx-print-hidden"
|
|
22
|
+
slot="top-right"
|
|
23
|
+
/>,
|
|
24
|
+
],
|
|
19
25
|
Array [
|
|
20
26
|
<pnx-widget-player
|
|
21
27
|
class="pnx-only-psv pnx-print-hidden"
|
|
@@ -68,6 +74,10 @@ exports[`_initWidgets should handle widgets if width is not small 1`] = `
|
|
|
68
74
|
"type": "return",
|
|
69
75
|
"value": undefined,
|
|
70
76
|
},
|
|
77
|
+
Object {
|
|
78
|
+
"type": "return",
|
|
79
|
+
"value": undefined,
|
|
80
|
+
},
|
|
71
81
|
],
|
|
72
82
|
}
|
|
73
83
|
`;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import PresetsManager from "../../src/utils/PresetsManager";
|
|
2
|
+
|
|
3
|
+
const mockPresets = {
|
|
4
|
+
"osm_traffic_sign": {
|
|
5
|
+
"icon": "fas-directions",
|
|
6
|
+
"tags": {
|
|
7
|
+
"osm|traffic_sign": "*"
|
|
8
|
+
},
|
|
9
|
+
"geometry": [
|
|
10
|
+
"point",
|
|
11
|
+
"vertex"
|
|
12
|
+
]
|
|
13
|
+
},
|
|
14
|
+
"osm_traffic_calming": {
|
|
15
|
+
"icon": "temaki-diamond",
|
|
16
|
+
"tags": {
|
|
17
|
+
"osm|traffic_calming": "*"
|
|
18
|
+
},
|
|
19
|
+
"geometry": [
|
|
20
|
+
"vertex"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
"osm_shop": {
|
|
24
|
+
"icon": "maki-shop",
|
|
25
|
+
"tags": {
|
|
26
|
+
"osm|shop": "*"
|
|
27
|
+
},
|
|
28
|
+
"geometry": [
|
|
29
|
+
"point",
|
|
30
|
+
"area"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// Mock des appels fetch
|
|
36
|
+
global.fetch = jest.fn(() =>
|
|
37
|
+
Promise.resolve({
|
|
38
|
+
ok: true,
|
|
39
|
+
json: () => Promise.resolve({}),
|
|
40
|
+
})
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
fetch.mockClear();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should initialize with default values", () => {
|
|
48
|
+
const presetsManager = new PresetsManager(null, true);
|
|
49
|
+
expect(presetsManager._ready).toBe(false);
|
|
50
|
+
expect(presetsManager._presets).toBe(null);
|
|
51
|
+
expect(presetsManager._translations).toEqual({});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("should load translations and presets", async () => {
|
|
55
|
+
const mockTranslationsEN = { en: { presets: { preset1: { name: "Preset 1" } } } };
|
|
56
|
+
const mockTranslationsFR = { fr: { presets: { preset1: { name: "Préréglage 1" } } } };
|
|
57
|
+
|
|
58
|
+
fetch.mockImplementationOnce(() =>
|
|
59
|
+
Promise.resolve({
|
|
60
|
+
ok: true,
|
|
61
|
+
json: () => Promise.resolve(mockTranslationsEN),
|
|
62
|
+
})
|
|
63
|
+
).mockImplementationOnce(() =>
|
|
64
|
+
Promise.resolve({
|
|
65
|
+
ok: true,
|
|
66
|
+
json: () => Promise.resolve(mockTranslationsFR),
|
|
67
|
+
})
|
|
68
|
+
).mockImplementationOnce(() =>
|
|
69
|
+
Promise.resolve({
|
|
70
|
+
ok: true,
|
|
71
|
+
json: () => Promise.resolve(mockPresets),
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const presetsManager = new PresetsManager("fr");
|
|
76
|
+
await presetsManager.onceReady();
|
|
77
|
+
|
|
78
|
+
expect(presetsManager._translations.en).toEqual(mockTranslationsEN.en.presets);
|
|
79
|
+
expect(presetsManager._translations.fr).toEqual(mockTranslationsFR.fr.presets);
|
|
80
|
+
expect(presetsManager._presets).toEqual(mockPresets);
|
|
81
|
+
expect(presetsManager._ready).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("should handle errors during load", async () => {
|
|
85
|
+
global.console = { error: jest.fn() };
|
|
86
|
+
|
|
87
|
+
fetch.mockImplementation(() =>
|
|
88
|
+
Promise.resolve({
|
|
89
|
+
ok: false,
|
|
90
|
+
})
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const presetsManager = new PresetsManager();
|
|
94
|
+
await expect(presetsManager.onceReady()).rejects.toBeUndefined();
|
|
95
|
+
expect(presetsManager._ready).toBe(-1);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("should find the best matching preset", async () => {
|
|
99
|
+
fetch.mockImplementationOnce(() =>
|
|
100
|
+
Promise.resolve({
|
|
101
|
+
ok: true,
|
|
102
|
+
json: () => Promise.resolve({}),
|
|
103
|
+
})
|
|
104
|
+
).mockImplementationOnce(() =>
|
|
105
|
+
Promise.resolve({
|
|
106
|
+
ok: true,
|
|
107
|
+
json: () => Promise.resolve({}),
|
|
108
|
+
})
|
|
109
|
+
).mockImplementationOnce(() =>
|
|
110
|
+
Promise.resolve({
|
|
111
|
+
ok: true,
|
|
112
|
+
json: () => Promise.resolve(mockPresets),
|
|
113
|
+
})
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const presetsManager = new PresetsManager();
|
|
117
|
+
await presetsManager.onceReady();
|
|
118
|
+
|
|
119
|
+
const feature = { semantics: [{ key: "key1", value: "value1" }, { key: "key2", value: "value2" }] };
|
|
120
|
+
const bestPreset = await presetsManager.getPreset(feature);
|
|
121
|
+
|
|
122
|
+
expect(bestPreset).toEqual(mockPresets.preset2);
|
|
123
|
+
});
|