diff --git a/frontend/App.vue b/frontend/App.vue
index ba87f42..77b1964 100644
--- a/frontend/App.vue
+++ b/frontend/App.vue
@@ -303,7 +303,7 @@
v-bind:key="isa.qid"
v-for="isa in item_type_hits"
href="#"
- @click.prevent="item_type_filters.includes(isa) || item_type_filters.push(isa)"
+ @click.prevent="add_item_type_filter(isa)"
>
{{ isa.label }} ({{ isa.qid }})
@@ -458,12 +458,33 @@
{{ wd_item.aliases.join("; ") }}
+
item coordinates
+
+
+ {{ marker[0].toFixed(5) }},
+ {{ marker[1].toFixed(5) }}
+
+
+
item type
{{isa.label}} ({{isa.qid}})
+
+
+
+ Wikipedia
+
+
+
+
+ {{wp.lang}}
+
+
+
+
street address
{{wd_item.street_address[0]}}
@@ -524,6 +545,15 @@
+
+
Images on Commons
+
+ {{wd_item.commons}}
+
+
+
@@ -548,7 +578,7 @@
The OSM tags/keys used as the search criteria to find matching OSM objects are listed below, along with the Wikidata item that was the source.
@@ -766,6 +796,8 @@ export default { startLon: Number, startZoom: Number, startRadius: Number, + startItem: String, + startItemTypeFilter: Array, username: String, startMode: String, q: String, @@ -994,10 +1026,26 @@ export default { } }, methods: { - api_call(endpoint, options) { + wikipedia_link(lang, title) { + var norm_title = title.replaceAll(" ", "_"); + return `https://${lang}.wikipedia.org/wiki/${norm_title}`; + }, + marker_osm_url(marker) { + var lat = marker[0].toFixed(5); + var lon = marker[1].toFixed(5); + return `https://www.openstreetmap.org/?mlat=${lat}&mlon=${lon}#map=18/${lat}/${lon}` + }, + add_item_type_filter(isa) { + if (this.item_type_filters.includes(isa)) { + return; + } + this.item_type_filters.push(isa); + this.update_map_path(); + }, + api_call(endpoint, options) { var url = `${this.api_base_url}/api/1/${endpoint}`; - return axios.get(url, options).catch(this.show_api_error_modal); - }, + return axios.get(url, options).catch(this.show_api_error_modal); + }, update_unload_warning(edit_list) { if (edit_list.length) { addEventListener("beforeunload", beforeUnloadListener, {capture: true}); @@ -1216,14 +1264,19 @@ export default { this.isa_ticked = Object.keys(this.isa_labels); }, build_map_path() { + if (this.current_item) { + return `/item/${this.current_qid}`; + } var zoom = this.map.getZoom(); var c = this.map.getCenter(); var lat = c.lat.toFixed(5); var lng = c.lng.toFixed(5); var path = `/map/${zoom}/${lat}/${lng}`; - if (this.current_item) { - path += `?item=${this.current_qid}`; + + if (this.item_type_filters.length) { + path += "?isa=" + this.item_type_filters.map((t) => t.qid).join(";"); } + return path; }, @@ -1755,6 +1808,11 @@ export default { this.zoom = this.startZoom; this.mode = this.startMode; this.changeset_comment = this.defaultComment || '+wikidata'; + console.log(this.startItemTypeFilter); + if (this.startItemTypeFilter.length) { + this.show_item_type_filter = true; + } + this.item_type_filters = this.startItemTypeFilter; }, mounted() { @@ -1764,7 +1822,6 @@ export default { zoom: this.zoom || 16, }; - var map = L.map("map", options); var osm_url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; var tile_url = "https://tile-c.openstreetmap.fr/hot/{z}/{x}/{y}.png"; @@ -1788,13 +1845,13 @@ export default { this.search_text = this.q.trim(); this.run_search(); } else { - this.detail_qid = this.qid_from_url(); + this.detail_qid = this.startItem; if (this.detail_qid) { this.load_wikidata_items(bounds); } else { this.auto_load(bounds); + this.update_map_path(); } - this.update_map_path(); } window.onpopstate = this.onpopstate; diff --git a/matcher/api.py b/matcher/api.py index bb84aca..2199311 100644 --- a/matcher/api.py +++ b/matcher/api.py @@ -38,6 +38,14 @@ skip_tags = { } def get_country_iso3166_1(lat, lon): + """ + For a given lat/lon return a set of ISO country codes. + + Also cache the country code in the global object. + + Normally there should be only one country. + """ + point = func.ST_SetSRID(func.ST_MakePoint(lon, lat), srid) alpha2_codes = set() q = model.Polygon.query.filter(func.ST_Covers(model.Polygon.way, point), @@ -57,7 +65,18 @@ def is_street_number_first(lat, lon): return True alpha2 = get_country_iso3166_1(lat, lon) - alpha2_number_first = {'GB', 'IE', 'US', 'MX', 'CA', 'FR', 'AU', 'NZ', 'ZA'} + # Incomplete list of countries that put street number first. + alpha2_number_first = { + 'GB', # United Kingdom + 'IE', # Ireland + 'US', # United States + 'MX', # Mexico + 'CA', # Canada + 'FR', # France + 'AU', # Australia + 'NZ', # New Zealand + 'ZA', # South Africa + } return bool(alpha2_number_first & alpha2) @@ -92,6 +111,7 @@ def make_envelope_around_point(lat, lon, distance): return func.ST_MakeEnvelope(west, south, east, north, srid) def drop_way_area(tags): + """ Remove the way_area field from a tags dict. """ if "way_area" in tags: del tags["way_area"] return tags @@ -122,6 +142,8 @@ def get_part_of(table_name, src_id, bbox): } for osm_id, tags, area in conn.execute(s)] def get_and_save_item(qid): + """ Download an item from Wikidata and cache it in the database. """ + entity = wikidata_api.get_entity(qid) entity_qid = entity["id"] if entity_qid != qid: @@ -396,7 +418,6 @@ def add_isa_filter(q, isa_qids): ) subclass_qid = {qid for qid, in q_subclass.all()} - # print(subclass_qid) isa = func.jsonb_path_query_array( model.Item.claims, @@ -419,7 +440,7 @@ def wikidata_items_count(bounds, isa_filter=None): return q.count() -def wikidata_isa_counts(bounds): +def wikidata_isa_counts(bounds, isa_filter=None): db_bbox = make_envelope(bounds) q = ( @@ -427,6 +448,9 @@ def wikidata_isa_counts(bounds): .filter(func.ST_Covers(db_bbox, model.ItemLocation.location)) ) + if isa_filter: + q = add_isa_filter(q, isa_filter) + db_items = q.all() counts = get_isa_count(db_items) @@ -605,7 +629,11 @@ def find_osm_candidates(item, limit=80, max_distance=450, names=None): item_id = item.item_id item_is_linear_feature = item.is_linear_feature() item_is_street = item.is_street() - item_names = {n.lower() for n in item.names().keys()} + item_names_dict = item.names() + if item_names_dict: + item_names = {n.lower() for n in item_names_dict.keys()} + else: + item_names = set() check_is_street_number_first(item.locations[0].get_lat_lon()) @@ -702,6 +730,8 @@ def find_osm_candidates(item, limit=80, max_distance=450, names=None): shape = "area" if table == "polygon" else table + item_identifier_tags = item.get_identifiers_tags() + cur = { "identifier": f"{osm_type}/{osm_id}", "type": osm_type, @@ -733,6 +763,8 @@ def find_osm_candidates(item, limit=80, max_distance=450, names=None): return nearby def get_item(item_id): + """ Retrieve a Wikidata item, either from the database or from Wikidata. """ + item = model.Item.query.get(item_id) return item or get_and_save_item(f"Q{item_id}") @@ -763,6 +795,11 @@ def check_is_street_number_first(latlng): g.street_number_first = is_street_number_first(*latlng) def item_detail(item): + unsupported_relation_types = { + 'Q194356', # wind farm + 'Q2175765', # tram stop + } + locations = [list(i.get_lat_lon()) for i in item.locations] if not hasattr(g, 'street_number_first'): g.street_number_first = is_street_number_first(*locations[0]) @@ -783,6 +820,11 @@ def item_detail(item): }) isa_items = [get_item(isa["numeric-id"]) for isa in item.get_isa()] + isa_lookup = {isa.qid: isa for isa in isa_items} + + wikipedia_links = [{"lang": site[:-4], "title": link["title"]} + for site, link in sorted(item.sitelinks.items()) + if site.endswith("wiki") and len(site) < 8] d = { "qid": item.qid, @@ -797,11 +839,21 @@ def item_detail(item): "p1619": item.time_claim("P1619"), "p576": item.time_claim("P576"), "heritage_designation": heritage_designation, + "wikipedia": wikipedia_links, + "identifiers": item.get_identifiers(), } if aliases := item.get_aliases(): d["aliases"] = aliases + if "commonswiki" in item.sitelinks: + d["commons"] = item.sitelinks["commonswiki"]["title"] + + unsupported = isa_lookup.keys() & unsupported_relation_types + if unsupported: + d["unsupported_relation_types"] = [isa for isa in d["isa_list"] + if isa["qid"] in isa_lookup] + return d @@ -892,3 +944,21 @@ def isa_incremental_search(search_terms): } ret.append(cur) return ret + +def get_place_items(osm_type, osm_id): + src_id = osm_id * {'way': 1, 'relation': -1}[osm_type] + + q = (model.Item.query + .join(model.ItemLocation) + .join(model.Polygon, func.ST_Covers(model.Polygon.way, model.ItemLocation.location)) + .filter(model.Polygon.src_id == src_id)) + # sql = q.statement.compile(compile_kwargs={"literal_binds": True}) + + item_count = q.count() + items = [] + for item in q: + keys = ["item_id", "labels", "descriptions", "aliases", "sitelinks", "claims"] + item_dict = {key: getattr(item, key) for key in keys} + items.append(item_dict) + + return {"count": item_count, "items": items} diff --git a/matcher/nominatim.py b/matcher/nominatim.py index 5e70793..cfee85f 100644 --- a/matcher/nominatim.py +++ b/matcher/nominatim.py @@ -83,8 +83,8 @@ def get_hit_name(hit): if len(address) == 1: return n1 - country = address.pop("country") - country_code = address.pop("country_code") + country = address.pop("country", None) + country_code = address.pop("country_code", None) if country_code: country_code == country_code.lower() diff --git a/matcher/utils.py b/matcher/utils.py index 81335a0..0cace13 100644 --- a/matcher/utils.py +++ b/matcher/utils.py @@ -5,7 +5,6 @@ import json import math import user_agents import re -import pattern.en from datetime import date from num2words import num2words @@ -160,18 +159,6 @@ def is_in_range(address_range, address): return False -def pluralize_label(label): - text = label["value"] - if label["language"] != "en": - return text - - # pattern.en.pluralize has the plural of 'mine' as 'ours' - if text == "mine": - return "mines" - - return pattern.en.pluralize(text) - - def format_wikibase_time(v): p = v["precision"] t = v["time"] @@ -180,11 +167,12 @@ def format_wikibase_time(v): # example: https://www.wikidata.org/wiki/Q108266998 if p == 11: - return date.fromisoformat(t[1:11]).strftime("%d %B %Y") + return date.fromisoformat(t[1:11]).strftime("%-d %B %Y") if p == 10: return date.fromisoformat(t[1:8] + "-01").strftime("%B %Y") if p == 9: return t[1:5] if p == 7: century = ((int(t[:5]) - 1) // 100) + 1 - return num2words(century, to="ordinal_num") + " century" + end = " BC" if century < 0 else "" + return num2words(abs(century), to="ordinal_num") + " century" + end diff --git a/templates/isa.html b/templates/isa.html index 0b0ac24..edb2950 100644 --- a/templates/isa.html +++ b/templates/isa.html @@ -4,6 +4,7 @@ {% block content %}