From 62b4087b78d9281f34c9e27ccecb976c3660d133 Mon Sep 17 00:00:00 2001 From: Marwin Hochfelsner <50826859+hlfan@users.noreply.github.com> Date: Sun, 29 Jun 2025 17:55:09 +0200 Subject: [PATCH] Add human-readable wikidata explainations --- app/assets/javascripts/index/element.js | 115 ++++++++++++++++++++++ app/assets/javascripts/osm.js.erb | 2 + app/assets/stylesheets/common.scss | 11 ++- app/controllers/application_controller.rb | 3 +- app/helpers/browse_tags_helper.rb | 8 +- config/locales/en.yml | 5 + config/settings.yml | 3 + test/helpers/browse_tags_helper_test.rb | 14 ++- 8 files changed, 151 insertions(+), 10 deletions(-) diff --git a/app/assets/javascripts/index/element.js b/app/assets/javascripts/index/element.js index 1571c7475..9561a8b14 100644 --- a/app/assets/javascripts/index/element.js +++ b/app/assets/javascripts/index/element.js @@ -1,10 +1,18 @@ (function () { + let abortController = null; + const languagesToRequest = [...new Set([...OSM.preferred_languages.map(l => l.toLowerCase()), "mul", "en"])]; + const wikisToRequest = [...new Set(languagesToRequest.filter(l => l !== "mul").map(l => l.split("-")[0] + "wiki"))]; + const localeName = new Intl.DisplayNames(OSM.preferred_languages, { type: "language" }); + const isOfExpectedLanguage = ({ language }) => languagesToRequest[0].startsWith(language) || language === "mul"; + $(document).on("click", "a[href='#versions-navigation-active-page-item']", function (e) { scrollToActiveVersion(); $("#versions-navigation-active-page-item a.page-link").trigger("focus"); e.preventDefault(); }); + $(document).on("click", "button.wdt-preview", e => previewWikidataValue($(e.currentTarget))); + OSM.Element = function (map, type) { const page = {}; let scrollStartObserver, scrollEndObserver; @@ -13,12 +21,14 @@ OSM.loadSidebarContent(path, function () { initVersionsNavigation(); page._addObject(type, id, version); + abortController = new AbortController(); }); }; page.load = function (path, id, version) { initVersionsNavigation(); page._addObject(type, id, version, true); + abortController = new AbortController(); }; page.unload = function () { @@ -27,6 +37,7 @@ scrollStartObserver = null; scrollEndObserver?.disconnect(); scrollEndObserver = null; + abortController?.abort(); }; page._addObject = function () {}; @@ -101,4 +112,108 @@ scrollableList.scrollLeft = scrollableList.scrollWidth - scrollableList.offsetWidth; } } + + function previewWikidataValue($btn) { + if (!OSM.WIKIDATA_API_URL) return; + const items = $btn.data("qids"); + if (!items?.length) return; + $btn.prop("disabled", true); + fetch(OSM.WIKIDATA_API_URL + "?" + new URLSearchParams({ + action: "wbgetentities", + format: "json", + origin: "*", + ids: items.join("|"), + props: "labels|sitelinks/urls|claims|descriptions", + languages: languagesToRequest.join("|"), + sitefilter: wikisToRequest.join("|") + }), { + headers: { "Api-User-Agent": "OSM-TagPreview (https://github.com/openstreetmap/openstreetmap-website)" }, + signal: abortController?.signal + }) + .then(response => response.ok ? response.json() : Promise.reject(response)) + .then(({ entities }) => { + if (!entities) return Promise.reject(entities); + $btn + .closest("tr") + .after( + items + .filter(qid => entities[qid]) + .map(qid => getLocalizedResponse(entities[qid])) + .filter(data => data.label || data.icon || data.description || data.article) + .map(data => renderWikidataResponse(data, $btn.siblings(`a[href*="wikidata.org/entity/${data.qid}"]`))) + ); + }) + .catch(() => $btn.prop("disabled", false)); + } + + function getLocalizedResponse(entity) { + const rank = ({ rank }) => ({ preferred: 1, normal: 0, deprecated: -1 })[rank] ?? 0; + const toBestClaim = (out, claim) => (rank(claim) > rank(out)) ? claim : out; + const toFirstOf = (property) => (out, localization) => out ?? entity[property][localization]; + const data = { + qid: entity.id, + label: languagesToRequest.reduce(toFirstOf("labels"), null), + icon: [ + "P8972", // small logo or icon + "P154", // logo image + "P14" // traffic sign + ].reduce((out, prop) => out ?? entity.claims[prop]?.reduce(toBestClaim)?.mainsnak?.datavalue?.value, null), + description: languagesToRequest.reduce(toFirstOf("descriptions"), null), + article: wikisToRequest.reduce(toFirstOf("sitelinks"), null) + }; + if (data.article) data.article.language = data.article.site.replace("wiki", ""); + return data; + } + + function renderWikidataResponse({ icon, label, article, description }, $link) { + const cell = $("") + .attr("colspan", 2) + .addClass("bg-body-tertiary"); + + if (icon && OSM.WIKIMEDIA_COMMONS_URL) { + let src = OSM.WIKIMEDIA_COMMONS_URL + "Special:Redirect/file/" + encodeURIComponent(icon); + if (!icon.endsWith(".svg")) src += "?width=128"; + $("") + .attr("href", OSM.WIKIMEDIA_COMMONS_URL + "File:" + encodeURIComponent(icon) + `?uselang=${OSM.i18n.locale}`) + .append($("").attr({ src, height: "32" })) + .addClass("float-end mb-1 ms-2") + .appendTo(cell); + } + if (label) { + const link = $link.clone() + .text(label.value) + .attr("dir", "auto") + .appendTo(cell); + if (!isOfExpectedLanguage(label)) { + link.attr("lang", label.language); + link.after($("").text(" " + localeName.of(label.language))); + } + } + if (article) { + const link = $("") + .attr("href", article.url + `?uselang=${OSM.i18n.locale}`) + .text(label ? OSM.i18n.t("javascripts.element.wikipedia") : article.title) + .attr("dir", "auto") + .appendTo(cell); + if (label) { + link.before(" ("); + link.after(")"); + } + if (!isOfExpectedLanguage(article)) { + link.attr("lang", article.language); + link.after($("").text(" " + localeName.of(article.language))); + } + } + if (description) { + const text = $("
") + .text(description.value) + .addClass("small") + .attr("dir", "auto") + .appendTo(cell); + if (!isOfExpectedLanguage(description)) { + text.attr("lang", description.language); + } + } + return $("").append(cell); + } }()); diff --git a/app/assets/javascripts/osm.js.erb b/app/assets/javascripts/osm.js.erb index bd49050b1..842868ec9 100644 --- a/app/assets/javascripts/osm.js.erb +++ b/app/assets/javascripts/osm.js.erb @@ -19,6 +19,8 @@ OSM = { graphhopper_url fossgis_osrm_url fossgis_valhalla_url + wikidata_api_url + wikimedia_commons_url ] .each_with_object({}) do |key, hash| hash[key.to_s.upcase] = Settings.send(key) if Settings.respond_to?(key) diff --git a/app/assets/stylesheets/common.scss b/app/assets/stylesheets/common.scss index 6a8f90eae..31a4188b8 100644 --- a/app/assets/stylesheets/common.scss +++ b/app/assets/stylesheets/common.scss @@ -633,11 +633,14 @@ tr.turn { #sidebar_content { .browse-tag-list { table-layout: fixed; - white-space: pre-wrap; - word-wrap: break-word; - word-break: break-word; - tr:last-child th, tr:last-child td { + tr > *:not([colspan]) { + white-space: pre-wrap; + word-wrap: break-word; + word-break: break-word; + } + + tr:last-child > * { border-bottom: 0; } } diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 03ea6bedb..fb4da1a21 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -246,8 +246,9 @@ class ApplicationController < ActionController::Base def map_layout policy = request.content_security_policy.clone - policy.connect_src(*policy.connect_src, "http://127.0.0.1:8111", Settings.nominatim_url, Settings.overpass_url, Settings.fossgis_osrm_url, Settings.graphhopper_url, Settings.fossgis_valhalla_url) + policy.connect_src(*policy.connect_src, "http://127.0.0.1:8111", Settings.nominatim_url, Settings.overpass_url, Settings.fossgis_osrm_url, Settings.graphhopper_url, Settings.fossgis_valhalla_url, Settings.wikidata_api_url) policy.form_action(*policy.form_action, "render.openstreetmap.org") + policy.img_src(*policy.img_src, Settings.wikimedia_commons_url, "upload.wikimedia.org") policy.style_src(*policy.style_src, :unsafe_inline) request.content_security_policy = policy diff --git a/app/helpers/browse_tags_helper.rb b/app/helpers/browse_tags_helper.rb index bb79d9eaf..0da8b0905 100644 --- a/app/helpers/browse_tags_helper.rb +++ b/app/helpers/browse_tags_helper.rb @@ -17,10 +17,16 @@ module BrowseTagsHelper elsif wdt = wikidata_links(key, value) # IMPORTANT: Note that wikidata_links() returns an array of hashes, unlike for example wikipedia_link(), # which just returns one such hash. + svg = button_tag :type => "button", :role => "button", :class => "btn btn-link float-end d-flex m-1 mt-0 me-n1 border-0 p-0 wdt-preview", :data => { :qids => wdt.map { |w| w[:title] } } do + tag.svg :width => 27, :height => 16 do + concat tag.title t("browse.tag_details.wikidata_preview", :count => wdt.length) + concat tag.path :fill => "currentColor", :d => "M0 16h1V0h-1Zm2 0h3V0h-3Zm4 0h3V0h-3Zm4 0h1V0h-1Zm2 0h1V0h-1Zm2 0h3V0h-3Zm4 0h1V0h-1Zm2 0h3V0h-3Zm4 0h1V0h-1Zm2 0h1V0h-1Z" + end + end wdt = wdt.map do |w| link_to(w[:title], w[:url], :title => t("browse.tag_details.wikidata_link", :page => w[:title].strip)) end - safe_join(wdt, ";") + svg + safe_join(wdt, ";") elsif wmc = wikimedia_commons_link(key, value) link_to h(wmc[:title]), wmc[:url], :title => t("browse.tag_details.wikimedia_commons_link", :page => wmc[:title]) elsif url = wiki_link("tag", "#{key}=#{value}") diff --git a/config/locales/en.yml b/config/locales/en.yml index 4fc6de6f5..9ef4f018c 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -421,6 +421,9 @@ en: key: "The wiki description page for the %{key} tag" tag: "The wiki description page for the %{key}=%{value} tag" wikidata_link: "The %{page} item on Wikidata" + wikidata_preview: + one: "Wikidata item preview" + other: "Wikidata items preview" wikipedia_link: "The %{page} article on Wikipedia" wikimedia_commons_link: "The %{page} item on Wikimedia Commons" telephone_link: "Call %{phone_number}" @@ -3526,6 +3529,8 @@ en: nothing_found: No features found error: "Error contacting %{server}: %{error}" timeout: "Timeout contacting %{server}" + element: + wikipedia: "Wikipedia" context: directions_from: Directions from here directions_to: Directions to here diff --git a/config/settings.yml b/config/settings.yml index 416fb1931..55fc84447 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -136,6 +136,9 @@ overpass_credentials: false graphhopper_url: "https://graphhopper.com/api/1/route" fossgis_osrm_url: "https://routing.openstreetmap.de/" fossgis_valhalla_url: "https://valhalla1.openstreetmap.de/route" +# Endpoints for Wikimedia integration +wikidata_api_url: "https://www.wikidata.org/w/api.php" +wikimedia_commons_url: "https://commons.wikimedia.org/wiki/" # Main website hosts to match in linkify linkify_hosts: ["www.openstreetmap.org", "www.osm.org", "www.openstreetmap.com", "openstreetmap.org", "osm.org", "openstreetmap.com"] # Shorter host to replace main hosts diff --git a/test/helpers/browse_tags_helper_test.rb b/test/helpers/browse_tags_helper_test.rb index 16351510a..1e9b153fb 100644 --- a/test/helpers/browse_tags_helper_test.rb +++ b/test/helpers/browse_tags_helper_test.rb @@ -41,14 +41,20 @@ class BrowseTagsHelperTest < ActionView::TestCase assert_dom_equal "Test", html html = format_value("wikidata", "Q42") - assert_dom_equal "Q42", html + dom = Rails::Dom::Testing.html_document_fragment.parse html + assert_select dom, "a[title='The Q42 item on Wikidata'][href$='www.wikidata.org/entity/Q42?uselang=en']", :text => "Q42" + assert_select dom, "button.wdt-preview>svg>path[fill]", 1 html = format_value("operator:wikidata", "Q12;Q98") - assert_dom_equal "Q12;" \ - "Q98", html + dom = Rails::Dom::Testing.html_document_fragment.parse html + assert_select dom, "a[title='The Q12 item on Wikidata'][href$='www.wikidata.org/entity/Q12?uselang=en']", :text => "Q12" + assert_select dom, "a[title='The Q98 item on Wikidata'][href$='www.wikidata.org/entity/Q98?uselang=en']", :text => "Q98" + assert_select dom, "button.wdt-preview>svg>path[fill]", 1 html = format_value("name:etymology:wikidata", "Q123") - assert_dom_equal "Q123", html + dom = Rails::Dom::Testing.html_document_fragment.parse html + assert_select dom, "a[title='The Q123 item on Wikidata'][href$='www.wikidata.org/entity/Q123?uselang=en']", :text => "Q123" + assert_select dom, "button.wdt-preview>svg>path[fill]", 1 html = format_value("wikimedia_commons", "File:Test.jpg") assert_dom_equal "File:Test.jpg", html -- 2.39.5