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";
6 export function element(type) {
9 load(path, id, version) {
10 OSM.loadSidebarContent(path)
11 .then(() => this.init(path, id, version, true));
14 init(path, id, version, keepViewport) {
15 this._addObject(type, id, version, keepViewport);
16 $(".numbered_pagination").trigger("numbered_pagination:enable");
17 abortController = new AbortController();
22 $(".numbered_pagination").trigger("numbered_pagination:disable");
23 abortController?.abort();
32 export function mappedElement(type) {
33 return function (map) {
35 ...element(type)(map),
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);
56 $(document).on("click", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget)));
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
67 fetch(OSM.WIKIDATA_API_URL + "?" + new URLSearchParams({
68 action: "wbgetentities",
72 props: "labels|sitelinks/urls|claims|descriptions",
73 languages: languagesToRequest.join("|"),
75 sitefilter: wikisToRequest.join("|")
77 .then(response => response.ok ? response.json() : Promise.reject(response))
78 .then(({ entities }) => {
79 if (!entities) return Promise.reject(entities);
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))
90 .catch(() => $btn.prop("disabled", false));
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];
101 label: languagesToRequest.reduce(toFirstOf(entity.labels), null),
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)
110 if (data.article) data.article.language = data.article.site.replace("wiki", "");
114 function renderWikidataResponse({ icon, label, article, description }, $link, fetchOptions) {
115 const localeName = new Intl.DisplayNames(OSM.preferred_languages, { type: "language" });
116 const cell = $("<td>")
118 .addClass("bg-body-tertiary");
120 if (icon && OSM.WIKIMEDIA_COMMONS_URL) {
121 fetchCommonsThumbnail(icon, fetchOptions)
122 .then(imageAttributes => {
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")
132 const link = $link.clone()
136 if (!isOfExpectedLanguage(label)) {
137 link.attr("lang", label.language);
138 link.after($("<sup>").text(" " + localeName.of(label.language)));
142 const link = $("<a>")
143 .attr("href", article.url + `?uselang=${OSM.i18n.locale}`)
144 .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title)
151 if (!isOfExpectedLanguage(article)) {
152 link.attr("lang", article.language);
153 link.after($("<sup>").text(" " + localeName.of(article.language)));
157 const text = $("<div>")
158 .text(description.value)
162 if (!isOfExpectedLanguage(description)) {
163 text.attr("lang", description.language);
166 return $("<tr>").append(cell);
169 function fetchCommonsThumbnail(filename, fetchOptions) {
170 const isVectorImage = filename.toLowerCase().endsWith(".svg");
171 return fetch(OSM.WIKIMEDIA_COMMONS_URL + "/w/api.php?" + new URLSearchParams({
176 titles: "File:" + filename,
177 iiprop: "url|thumburls",
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);
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`)
194 imageAttributes.sizes = `${imageInfo.thumbwidth}px`;
197 return imageAttributes;