]> git.openstreetmap.org Git - rails.git/blob - app/assets/javascripts/index_modules/element.js
Merge remote-tracking branch 'upstream/pull/7487'
[rails.git] / app / assets / javascripts / index_modules / element.js
1 let abortController = null;
2 const languagesToRequest = [...new Set(OSM.preferred_languages.map(l => l.toLowerCase()))];
3 const wikisToRequest = [...new Set([...OSM.preferred_languages, "en"].map(l => l.split("-")[0] + "wiki"))];
4 const isOfExpectedLanguage = ({ language }) => languagesToRequest[0].startsWith(language) || language === "mul";
5
6 export function element(type) {
7   return function () {
8     return {
9       load(path, id, version) {
10         OSM.loadSidebarContent(path)
11           .then(() => this.init(path, id, version, true));
12       },
13
14       init(path, id, version, keepViewport) {
15         this._addObject(type, id, version, keepViewport);
16         $(".numbered_pagination").trigger("numbered_pagination:enable");
17         abortController = new AbortController();
18       },
19
20       unload() {
21         this._removeObject();
22         $(".numbered_pagination").trigger("numbered_pagination:disable");
23         abortController?.abort();
24       },
25
26       _addObject() {},
27       _removeObject() {}
28     };
29   };
30 };
31
32 export function mappedElement(type) {
33   return function (map) {
34     return {
35       ...element(type)(map),
36
37       _addObject(type, id, version, keepViewport) {
38         const hashParams = OSM.parseHash();
39         map.addObject({ type: type, id: parseInt(id, 10), version: version && parseInt(version, 10) }, function (bounds) {
40           if (!hashParams.center && bounds.isValid() &&
41               (!keepViewport || !map.getBounds().contains(bounds))) {
42             OSM.router.withoutMoveListener(function () {
43               map.fitBounds(bounds);
44             });
45           }
46         });
47       },
48
49       _removeObject() {
50         map.removeObject();
51       }
52     };
53   };
54 };
55
56 $(document).on("click", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget)));
57
58 function previewWikidataValue($btn) {
59   if (!OSM.WIKIDATA_API_URL) return;
60   const items = $btn.data("qids");
61   if (!items?.length) return;
62   $btn.prop("disabled", true);
63   const fetchOptions = {
64     headers: { "Api-User-Agent": "OSM-TagPreview (https://github.com/openstreetmap/openstreetmap-website)" },
65     signal: abortController?.signal
66   };
67   fetch(OSM.WIKIDATA_API_URL + "?" + new URLSearchParams({
68     action: "wbgetentities",
69     format: "json",
70     origin: "*",
71     ids: items.join("|"),
72     props: "labels|sitelinks/urls|claims|descriptions",
73     languages: languagesToRequest.join("|"),
74     languagefallback: 1,
75     sitefilter: wikisToRequest.join("|")
76   }), fetchOptions)
77     .then(response => response.ok ? response.json() : Promise.reject(response))
78     .then(({ entities }) => {
79       if (!entities) return Promise.reject(entities);
80       $btn
81         .closest("tr")
82         .after(
83           items
84             .filter(qid => entities[qid])
85             .map(qid => getLocalizedResponse(entities[qid]))
86             .filter(data => data.label || data.icon || data.description || data.article)
87             .map(data => renderWikidataResponse(data, $btn.siblings(`a[href*="wikidata.org/entity/${data.qid}"]`), fetchOptions))
88         );
89     })
90     .catch(() => $btn.prop("disabled", false));
91 }
92
93 function getLocalizedResponse(entity) {
94   const siteScheme = OSM.isDark("bs") ? "Q6545942" : "Q101608434";
95   const scheme = ({ qualifiers } = {}) => qualifiers?.P8798?.some(q => q?.datavalue?.value?.id === siteScheme) ?? 0;
96   const rank = ({ rank } = {}) => ({ preferred: 2, normal: 0 })[rank] ?? 0;
97   const toBestClaim = (out, claim) => (!out || rank(claim) + scheme(claim) > rank(out) + scheme(out)) ? claim : out;
98   const toFirstOf = (property) => (out, localization) => out ?? property[localization];
99   const data = {
100     qid: entity.id,
101     label: languagesToRequest.reduce(toFirstOf(entity.labels), null),
102     icon: [
103       "P8972", // small logo or icon
104       "P154", // logo image
105       "P14" // traffic sign
106     ].reduce((out, prop) => out ?? entity.claims[prop]?.filter(claim => claim.rank !== "deprecated").reduce(toBestClaim, null)?.mainsnak?.datavalue?.value, null),
107     description: languagesToRequest.reduce(toFirstOf(entity.descriptions), null),
108     article: wikisToRequest.reduce(toFirstOf(entity.sitelinks), null)
109   };
110   if (data.article) data.article.language = data.article.site.replace("wiki", "");
111   return data;
112 }
113
114 function renderWikidataResponse({ icon, label, article, description }, $link, fetchOptions) {
115   const localeName = new Intl.DisplayNames(OSM.preferred_languages, { type: "language" });
116   const cell = $("<td>")
117     .attr("colspan", 2)
118     .addClass("bg-body-tertiary");
119
120   if (icon && OSM.WIKIMEDIA_COMMONS_URL) {
121     fetchCommonsThumbnail(icon, fetchOptions)
122       .then(imageAttributes => {
123         $("<a>")
124           .attr("href", OSM.WIKIMEDIA_COMMONS_URL + "/wiki/File:" + encodeURIComponent(icon) + `?uselang=${OSM.i18n.locale}`)
125           .append($("<img>").attr(imageAttributes))
126           .addClass("float-end mb-1 ms-2")
127           .prependTo(cell);
128       })
129       .catch(() => {});
130   }
131   if (label) {
132     const link = $link.clone()
133       .text(label.value)
134       .attr("dir", "auto")
135       .appendTo(cell);
136     if (!isOfExpectedLanguage(label)) {
137       link.attr("lang", label.language);
138       link.after($("<sup>").text(" " + localeName.of(label.language)));
139     }
140   }
141   if (article) {
142     const link = $("<a>")
143       .attr("href", article.url + `?uselang=${OSM.i18n.locale}`)
144       .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title)
145       .attr("dir", "auto")
146       .appendTo(cell);
147     if (label) {
148       link.before(" (");
149       link.after(")");
150     }
151     if (!isOfExpectedLanguage(article)) {
152       link.attr("lang", article.language);
153       link.after($("<sup>").text(" " + localeName.of(article.language)));
154     }
155   }
156   if (description) {
157     const text = $("<div>")
158       .text(description.value)
159       .addClass("small")
160       .attr("dir", "auto")
161       .appendTo(cell);
162     if (!isOfExpectedLanguage(description)) {
163       text.attr("lang", description.language);
164     }
165   }
166   return $("<tr>").append(cell);
167 }
168
169 function fetchCommonsThumbnail(filename, fetchOptions) {
170   const isVectorImage = filename.toLowerCase().endsWith(".svg");
171   return fetch(OSM.WIKIMEDIA_COMMONS_URL + "/w/api.php?" + new URLSearchParams({
172     action: "query",
173     format: "json",
174     origin: "*",
175     prop: "imageinfo",
176     titles: "File:" + filename,
177     iiprop: "url|thumburls",
178     iiurlheight: "32"
179   }), fetchOptions)
180     .then(response => response.ok ? response.json() : Promise.reject(response))
181     .then(({ query }) => {
182       const page = Object.values(query.pages)[0];
183       const imageInfo = page.imageinfo?.[0];
184       const imageUrl = isVectorImage ? imageInfo?.url : imageInfo?.thumburl;
185       if (!imageUrl) return Promise.reject(page);
186
187       const imageAttributes = { src: imageUrl, height: "32", alt: filename };
188       if (!isVectorImage) {
189         const thumbUrls = Object.entries(imageInfo.thumburls ?? {});
190         if (thumbUrls.length) {
191           imageAttributes.srcset = thumbUrls
192             .map(([width, thumbnail]) => `${thumbnail.url} ${width}w`)
193             .join(", ");
194           imageAttributes.sizes = `${imageInfo.thumbwidth}px`;
195         }
196       }
197       return imageAttributes;
198     });
199 }