commit c4aa27e8f47a4942a8a74e2d16cb64dd5de4ab69 Author: Edward Betts Date: Fri May 7 17:46:47 2021 +0200 Initial commit diff --git a/static/css/map.css b/static/css/map.css new file mode 100644 index 0000000..86d6b62 --- /dev/null +++ b/static/css/map.css @@ -0,0 +1,33 @@ +#map { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + z-index: -1; +} + +#search { + position: absolute; + overflow: auto; + top: 20px; + left: 20px; + bottom: 20px; + width: 25%; + background: lightgray; +} + +#load-btn { + position: absolute; + top: 20px; + left: 50%; + transform: translate(-50%, 0); +} + +#sidebar { + position: absolute; + top: 20px; + left: 20px; + bottom: 20px; + overflow: auto; +} diff --git a/static/css/style.css b/static/css/style.css new file mode 100644 index 0000000..e69de29 diff --git a/static/js/app.js b/static/js/app.js new file mode 100644 index 0000000..1dbfebb --- /dev/null +++ b/static/js/app.js @@ -0,0 +1,55 @@ +'use strict'; + +var map = L.map('map'); + +var tiles = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'); +var group = L.featureGroup(); +map.addLayer(group); +tiles.addTo(map); + +var items = {}; +var duration_span = document.getElementById("duration"); + +map.on('moveend', function(e) { + var bounds = map.getBounds(); + console.log("map moved", bounds.toBBoxString()); + + var markers_url = "/api/1/markers.json"; + var params = {bounds: bounds.toBBoxString()}; + axios.get(markers_url, {params: params}).then(response => { + var items = response.data.items; + items.forEach(item => { + if (item.qid in items) + return; + item.markers.forEach(marker => { + var marker = L.marker(marker, {"title": item.label }); + marker.addTo(group); + }); + items[item.qid] = item; + }); + + duration_span.innerText = response.data.duration; + + + }); + +}); + +var hit_links = document.getElementsByClassName("hit-link"); + +// console.log(hit_links); + +for (const link of hit_links) { + link.addEventListener('click', event => { + event.preventDefault(); + var link = event.target; + var id_string = event.target.id; + var re_id = /^hit-link-(\d+)$/; + var hit_index = re_id.exec(id_string)[1]; + var [south, north, west, east] = bbox_list[hit_index]; + var bounds = [[north, west], [south, east]]; + + console.log('click', bounds); + map.fitBounds(bounds); + }); +} diff --git a/static/js/map.js b/static/js/map.js new file mode 100644 index 0000000..154da69 --- /dev/null +++ b/static/js/map.js @@ -0,0 +1,388 @@ +'use strict'; + +// Create a map + +var options = {}; +if (start_lat || start_lng) { + start_lat = start_lat.toFixed(5); + start_lng = start_lng.toFixed(5); + options = { + center: [start_lat, start_lng], + zoom: zoom, + }; + history.replaceState(null, null, `/map/${zoom}/${start_lat}/${start_lng}`); +} + +var map = L.map("map", options); +var group = L.featureGroup(); +var wikidata_items = {}; +var osm_objects = {}; +var wikidata_loaded = false; +var osm_loaded = false; +var loading = document.getElementById("loading"); +var load_text = document.getElementById("load-text"); +var isa_card = document.getElementById("isa-card"); +var isa_labels = {}; +var connections = {}; +map.addLayer(group); +map.zoomControl.setPosition('topright'); + +var blueMarker = L.ExtraMarkers.icon({ + icon: 'fa-wikidata', + markerColor: 'blue', + shape: 'circle', + prefix: 'fa', +}); + +var greenMarker = L.ExtraMarkers.icon({ + icon: 'fa-wikidata', + markerColor: 'green', + shape: 'circle', + prefix: 'fa', +}); + +var redMarker = L.ExtraMarkers.icon({ + icon: 'fa-wikidata', + markerColor: 'red', + shape: 'circle', + prefix: 'fa', +}); + +var osmYellowMarker = L.ExtraMarkers.icon({ + icon: 'fa-map', + markerColor: 'yellow', + shape: 'square', + prefix: 'fa', +}); + +var osmOrangeMarker = L.ExtraMarkers.icon({ + icon: 'fa-map', + markerColor: 'orange', + shape: 'square', + prefix: 'fa', +}); + + +if (!start_lat || !start_lng) { + map.fitBounds([[49.85,-10.5], [58.75, 1.9]]); +} + +// Add OpenStreetMap layer +var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 19 }); + +osm.addTo(map); + +function check_items(items) { + if (items.length === 0) + return; + + var item = items.shift(); + var qid = item.qid; + + var markers_url = `/api/1/item/${qid}`; + axios.get(markers_url).then(response => { + // console.log(response.data, item); + response.data.info.forEach(osm => { + var icon = osmYellowMarker; + var marker = L.marker(osm.centroid, {title: osm.name, icon: icon}); + var popup = ` +

+ ${osm.identifier}: ${osm.name} +

` + marker.bindPopup(popup); + marker.addTo(group); + + }); + item.markers.forEach(marker_data => { + var marker = marker_data.marker; + var icon = response.data.tagged ? greenMarker : redMarker; + marker.setIcon(icon); + + response.data.info.forEach(osm => { + var path = [osm.centroid, marker_data]; + var polyline = L.polyline(path, {color: 'green'}).addTo(map); + }); + + }); + if (items.length) + check_items(items); + }); +} + +function update_wikidata() { + if (Object.keys(wikidata_items).length === 0 || Object.keys(osm_objects).length === 0) { + if (wikidata_loaded && osm_loaded) { + loading.classList.add("visually-hidden"); + load_text.classList.remove("visually-hidden"); + } + + return; + } + + for (const qid in osm_objects) { + var osm_list = osm_objects[qid]; + + var item = wikidata_items[qid]; + if (!item) { + osm_list.forEach(osm => { + osm.marker.setIcon(osmOrangeMarker); + }); + continue; + } + + if (item.lines === undefined) + item.lines = []; + item.markers.forEach(marker_data => { + marker_data.marker.setIcon(greenMarker); + osm_list.forEach(osm => { + var path = [osm.centroid, marker_data]; + var polyline = L.polyline(path, {color: 'green'}).addTo(group); + item.lines.push(polyline); + }); + }); + } + for (const qid in wikidata_items) { + if (osm_objects[qid]) + continue; + var item = wikidata_items[qid]; + item.markers.forEach(marker_data => { + marker_data.marker.setIcon(redMarker); + }); + } + + loading.classList.add("visually-hidden"); + load_text.classList.remove("visually-hidden"); +} + +function isa_only(e) { + e.preventDefault(); + + var this_id = e.target.parentNode.childNodes[0].id; + + var checkbox_list = document.getElementsByClassName('isa-checkbox'); + + for (const checkbox of checkbox_list) { + checkbox.checked = checkbox.id == this_id; + } + + checkbox_change(); +} + +function checkbox_change() { + var checkbox_list = document.getElementsByClassName('isa-checkbox'); + var ticked = []; + for (const checkbox of checkbox_list) { + if (checkbox.checked) { + ticked.push(checkbox.id.substr(4)); + } + } + + for (const qid in wikidata_items) { + var item = wikidata_items[qid]; + const item_isa_list = wikidata_items[qid]['isa_list']; + const intersection = ticked.filter(isa_qid => item_isa_list.includes(isa_qid)); + if (item.lines) { + item.lines.forEach(line => { + if (intersection.length) { + line.addTo(group); + } else { + line.removeFrom(group); + } + }); + } + + item.markers.forEach(marker_data => { + var marker = marker_data.marker; + if (intersection.length) { + marker.addTo(group); + } else { + marker.removeFrom(group); + } + }); + + if(osm_objects[qid]) { + osm_objects[qid].forEach(osm => { + if (intersection.length) { + osm.marker.addTo(group); + } else { + osm.marker.removeFrom(group); + } + }); + } + + + + } +} + +function set_isa_list(isa_count) { + isa_card.classList.remove("visually-hidden"); + var isa_list = document.getElementById("isa-list"); + isa_list.innerHTML = ''; + isa_count.forEach(isa => { + isa_labels[isa.qid] = isa.label; + var isa_id = `isa-${isa.qid}`; + var e = document.createElement('div'); + e.setAttribute('class', 'isa-item'); + + var checkbox = document.createElement('input'); + checkbox.setAttribute('type', 'checkbox'); + checkbox.setAttribute('checked', 'checked'); + checkbox.setAttribute('id', isa_id); + checkbox.setAttribute('class', 'isa-checkbox'); + checkbox.onchange = checkbox_change; + e.appendChild(checkbox); + + e.appendChild(document.createTextNode(' ')); + + var label = document.createElement('label'); + label.setAttribute('for', isa_id); + var label_text = document.createTextNode(` ${isa.label} (${isa.qid}): ${isa.count} `); + label.appendChild(label_text); + + e.appendChild(label); + e.appendChild(document.createTextNode(' ')); + + var only = document.createElement('a'); + only.setAttribute('href', '#'); + var only_text = document.createTextNode('only'); + only.appendChild(only_text); + only.onclick = isa_only; + e.appendChild(only); + + isa_list.appendChild(e); + }); +} + +function load_wikidata_items() { + var checkbox_list = document.getElementsByClassName('isa-checkbox'); + + for (const checkbox of checkbox_list) + checkbox.checked = true; + + checkbox_change(); + + loading.classList.remove("visually-hidden"); + load_text.classList.add("visually-hidden"); + + var bounds = map.getBounds(); + console.log("map moved", bounds.toBBoxString()); + + // var items_to_check = []; + var params = {bounds: bounds.toBBoxString()}; + var items_url = "/api/1/items"; + + axios.get(items_url, {params: params}).then(response => { + set_isa_list(response.data.isa_count); + var items = response.data.items; + items.forEach(item => { + if (item.qid in wikidata_items) + return; + item.markers.forEach(marker_data => { + // var icon = marker.tagged ? greenMarker : blueMarker; + var icon = blueMarker; + var label = `${item.label} (${item.qid})` + var marker = L.marker(marker_data, {title: label, icon: icon}); + var wd_url = 'https://www.wikidata.org/wiki/' + item.qid; + var popup = '

Wikidata item
' + popup += `${item.label} (${item.qid})` + if (item.description) { + popup += `
description: ${item.description}` + } + if (item.isa_list) { + popup += '
item type' + for (const [index, isa_qid] of item.isa_list.entries()) { + var isa_url = 'https://www.wikidata.org/wiki/' + isa_qid; + var isa_label = isa_labels[isa_qid]; + popup += `
${isa_label} (${isa_qid})`; + } + } + popup += '

'; + marker.bindPopup(popup); + marker.addTo(group); + marker_data.marker = marker; + }); + wikidata_items[item.qid] = item; + + + // items_to_check.push(item); + }); + + wikidata_loaded = true; + isa_card.classList.remove("visually-hidden"); + update_wikidata(); + + // duration_span.innerText = response.data.duration; + // check_items(items_to_check); + + }); + var osm_objects_url = "/api/1/osm"; + axios.get(osm_objects_url, {params: params}).then(response => { + console.log(`${response.data.duration} seconds`); + response.data.objects.forEach(osm => { + var qid = osm.wikidata; + if (osm_objects[qid] === undefined) + osm_objects[qid] = []; + osm_objects[qid].push(osm); + + var icon = osmYellowMarker; + var marker = L.marker(osm.centroid, {title: osm.name, icon: icon}); + osm.marker = marker; + var wd_url = 'https://www.wikidata.org/wiki/' + qid; + var popup = ` +

+ ${osm.name}: + ${osm.identifier} +
+ Wikidata tag: ${qid} +

` + marker.bindPopup(popup); + marker.addTo(group); + }); + + osm_loaded = true; + update_wikidata(); + }); + + +}; + +var load_btn = document.getElementById('load-btn'); +load_btn.onclick = load_wikidata_items; + +var search_btn = document.getElementById('search-btn'); +var search_form = document.getElementById('search-form'); +search_form.onsubmit = function(e) { + e.preventDefault(); + var search_text = document.getElementById('search-text').value.trim(); + if (!search_text) + return; + var params = {q: search_text}; + var search_url = "/api/1/search"; + var search_results = document.getElementById('search-results'); + axios.get(search_url, {params: params}).then(response => { + search_results.innerHTML = ''; + response.data.hits.forEach(hit => { + var e = document.createElement('div'); + var category = document.createTextNode(hit.category + ' '); + e.appendChild(category); + var a = document.createElement('a'); + var lat = parseFloat(hit.lat).toFixed(5); + var lon = parseFloat(hit.lon).toFixed(5); + a.setAttribute('href', `/map/15/${lat}/${lon}`); + var link_text = document.createTextNode(hit.name); + a.appendChild(link_text); + e.appendChild(a); + search_results.appendChild(e); + }); + console.log(response.data); + }); +} + +map.on('moveend', function (e) { + var zoom = map.getZoom(); + var c = map.getCenter(); + var lat = c.lat.toFixed(5); + var lng = c.lng.toFixed(5); + history.replaceState(null, null, `/map/${zoom}/${lat}/${lng}`); +}); diff --git a/static/js/search_map.js b/static/js/search_map.js new file mode 100644 index 0000000..3dbd082 --- /dev/null +++ b/static/js/search_map.js @@ -0,0 +1,40 @@ +'use strict'; + +// Create a map + +var options = {}; +if (user_lat && user_lon) { + options = { + center: [user_lat, user_lon], + zoom: 15, + }; +} + +var map = L.map("map", options); +map.zoomControl.setPosition('topright'); + +if (!user_lat || !user_lon) { + map.fitBounds([[49.85,-10.5], [58.75, 1.9]]); +} + +// Add OpenStreetMap layer +var osm = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { maxZoom: 18 }); + +osm.addTo(map); + +var hits = document.getElementsByClassName("hit-card"); +var current_hit = null; + +for (const card of hits) { + card.addEventListener('mouseover', event => { + var id_string = card.id; + if (current_hit == id_string) return; + current_hit = id_string; + var re_id = /^hit-card-(\d+)$/; + var hit_index = re_id.exec(id_string)[1]; + var [south, north, west, east] = bbox_list[hit_index]; + var bounds = [[north, west], [south, east]]; + + map.fitBounds(bounds); + }); +} diff --git a/templates/base.html b/templates/base.html new file mode 100644 index 0000000..52682ad --- /dev/null +++ b/templates/base.html @@ -0,0 +1,24 @@ + + + + + + {% block title %}{% endblock %} OSM ↔ Wikidata + + + + + + + {% block style %} + {% endblock %} + + + +
+ {% block content %}{% endblock %} +
+ + {% block script %}{% endblock %} + + diff --git a/templates/empty_page.html b/templates/empty_page.html new file mode 100644 index 0000000..1336c30 --- /dev/null +++ b/templates/empty_page.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block title %}{% endblock %} + +{% block content %} +
+ +
+{% endblock %} diff --git a/templates/identifier_index.html b/templates/identifier_index.html new file mode 100644 index 0000000..d3b88f5 --- /dev/null +++ b/templates/identifier_index.html @@ -0,0 +1,25 @@ +{% extends "base.html" %} + +{% block title %}{% endblock %} + +{% block content %} +
+ + + {% for pid, osm_keys, label in property_map %} + + + + + {% endfor %} +
+ {{ label }} + ({{ pid }}) + + + + + {{ '/'.join(osm_keys) }}
+ +
+{% endblock %} diff --git a/templates/identifier_page.html b/templates/identifier_page.html new file mode 100644 index 0000000..a0eecf4 --- /dev/null +++ b/templates/identifier_page.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}{% endblock %} + +{% block content %} +
+ +

{{ label }} ({{ pid }})

+

Back to identifier index

+ +

Total number of items: {{ total }}

+

OSM total: {{ osm_total }}

+ + + {% for item in items %} + + + + + + {% endfor %} +
{{ item.label() }} ({{ item.qid }}){{ ' / '.join(item.get_claim(pid)) }} + {% for point in osm_points[item.qid] %} + {{ point.identifier }}: {{ point.tags.name }}
+ {% endfor %} +
+ +
+{% endblock %} diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..ee014fd --- /dev/null +++ b/templates/index.html @@ -0,0 +1,16 @@ +{% extends "base.html" %} + +{% block title %}{% endblock %} + +{% block content %} +
+

OSM/Wikidata

+ + +
+{% endblock %} diff --git a/templates/map.html b/templates/map.html new file mode 100644 index 0000000..e538b78 --- /dev/null +++ b/templates/map.html @@ -0,0 +1,62 @@ + + + + + Map showing Wikidata items linked to OSM + + + + + + + +
+ + + + + + + + + + + + + + diff --git a/templates/search.html b/templates/search.html new file mode 100644 index 0000000..a6a298c --- /dev/null +++ b/templates/search.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block title %}{% endblock %} +{% block style %} + + + +{% endblock %} + +{% block script %} + + + + + + +{% endblock %} + +{% block content %} +
+
+ +
+ + +
+ + {% if hits %} + {% for hit in hits %} +
+
+

{{ hit.display_name }}

+

+ {{ hit.category }} + {{ hit.osm_type }} +

+
+
+ {% endfor %} + +
+
+ +
+
+ +
+
+

Duration: seconds

+ +
+
+ + {% endif %} + + +{% endblock %} diff --git a/templates/search_map.html b/templates/search_map.html new file mode 100644 index 0000000..80f591b --- /dev/null +++ b/templates/search_map.html @@ -0,0 +1,51 @@ + + + + + Map + + + + + + +
+ + + + + + + + diff --git a/web_view.py b/web_view.py new file mode 100755 index 0000000..fb6fd8f --- /dev/null +++ b/web_view.py @@ -0,0 +1,378 @@ +#!/usr/bin/python3 + +from flask import Flask, render_template, request, jsonify, redirect, url_for +from sqlalchemy import func +from sqlalchemy.orm import selectinload +from matcher import nominatim, model, database +from collections import Counter +from time import time +import GeoIP + +srid = 4326 + +app = Flask(__name__) +app.debug = True + +DB_URL = "postgresql:///matcher" +database.init_db(DB_URL) + +property_map = [ + ("P238", ["iata"], "IATA airport code"), + ("P239", ["icao"], "ICAO airport code"), + ("P240", ["faa", "ref"], "FAA airport code"), + # ('P281', ['addr:postcode', 'postal_code'], 'postal code'), + ("P296", ["ref", "ref:train", "railway:ref"], "station code"), + ("P300", ["ISO3166-2"], "ISO 3166-2 code"), + ("P359", ["ref:rce"], "Rijksmonument ID"), + ("P590", ["ref:gnis", "GNISID", "gnis:id", "gnis:feature_id"], "USGS GNIS ID"), + ("P649", ["ref:nrhp"], "NRHP reference number"), + ("P722", ["uic_ref"], "UIC station code"), + ("P782", ["ref"], "LAU (local administrative unit)"), + ("P836", ["ref:gss"], "UK Government Statistical Service code"), + ("P856", ["website", "contact:website", "url"], "website"), + ("P882", ["nist:fips_code"], "FIPS 6-4 (US counties)"), + ("P901", ["ref:fips"], "FIPS 10-4 (countries and regions)"), + # A UIC id can be a IBNR, but not every IBNR is an UIC id + ("P954", ["uic_ref"], "IBNR ID"), + ("P981", ["ref:woonplaatscode"], "BAG code for Dutch residencies"), + ("P1216", ["HE_ref"], "National Heritage List for England number"), + ("P2253", ["ref:edubase"], "EDUBase URN"), + ("P2815", ["esr:user", "ref", "ref:train"], "ESR station code"), + ("P3425", ["ref", "ref:SIC"], "Natura 2000 site ID"), + ("P3562", ["seamark:light:reference"], "Admiralty number"), + ( + "P4755", + ["ref", "ref:train", "ref:crs", "crs", "nat_ref"], + "UK railway station code", + ), + ("P4803", ["ref", "ref:train"], "Amtrak station code"), + ("P6082", ["nycdoitt:bin"], "NYC Building Identification Number"), + ("P5086", ["ref"], "FIPS 5-2 alpha code (US states)"), + ("P5087", ["ref:fips"], "FIPS 5-2 numeric code (US states)"), + ("P5208", ["ref:bag"], "BAG building ID for Dutch buildings"), +] + + +@app.teardown_appcontext +def shutdown_session(exception=None): + database.session.remove() + + +def check_for_tagged_qids(qids): + tagged = set() + for qid in qids: + for cls in model.Point, model.Polygon, model.Line: + q = cls.query.filter(cls.tags["wikidata"] == qid) + print(q) + if q.count(): + tagged.add(qid) + break + + return tagged + + +def check_for_tagged_qid(qid): + return any( + database.session.query( + cls.query.filter( + cls.tags.has_key("wikidata"), cls.tags["wikidata"] == qid + ).exists() + ).scalar() + for cls in (model.Point, model.Polygon, model.Line) + ) + + +def get_tagged_qid(qid): + tagged = [] + seen = set() + for cls in (model.Point, model.Polygon, model.Line): + q = cls.query.filter(cls.tags.has_key("wikidata"), cls.tags["wikidata"] == qid) + for osm in q: + if osm.identifier in seen: + continue + seen.add(osm.identifier) + tagged.append( + { + "identifier": osm.identifier, + "url": osm.osm_url, + # 'geoson': osm.geojson(), + "centroid": list(osm.get_centroid()), + "name": osm.name or "[no label]", + } + ) + + return tagged + + +def make_envelope(bbox): + west, south, east, north = [float(i) for i in bbox.split(",")] + return func.ST_MakeEnvelope(west, south, east, north, srid) + + +def get_osm_with_wikidata_tag(bbox): + db_bbox = make_envelope(bbox) + + tagged = [] + + seen = set() + for cls in (model.Point, model.Polygon, model.Line): + q = cls.query.filter( + cls.tags.has_key("wikidata"), func.ST_Covers(db_bbox, cls.way) + ) + for osm in q: + if osm.identifier in seen: + continue + seen.add(osm.identifier) + name = osm.name + if not name: + if "addr:housename" in osm.tags: + name = osm.tags["addr:housename"] + else: + name = "[no label]" + + tagged.append( + { + "identifier": osm.identifier, + "url": osm.osm_url, + # 'geoson': osm.geojson(), + "centroid": list(osm.get_centroid()), + "name": name, + "wikidata": osm.tags["wikidata"], + } + ) + + return tagged + + +def get_items_in_bbox(bbox): + db_bbox = make_envelope(bbox) + + q = ( + model.Item.query.join(model.ItemLocation) + .filter(func.ST_Covers(db_bbox, model.ItemLocation.location)) + .options(selectinload(model.Item.locations)) + ) + + return q + + +def get_markers(all_items): + items = [] + for item in all_items: + if "en" not in item.labels: + continue + locations = [list(i.get_lat_lon()) for i in item.locations] + item = { + "qid": item.qid, + "label": item.label(), + "description": item.description(), + "markers": locations, + "isa_list": [v["id"] for v in item.get_claim("P31")], + } + items.append(item) + + return items + + +def get_user_location(): + gi = GeoIP.open("/home/edward/lib/data/GeoIPCity.dat", GeoIP.GEOIP_STANDARD) + + remote_ip = request.remote_addr + gir = gi.record_by_addr(remote_ip) + if not gir: + return + lat, lon = gir["latitude"], gir["longitude"] + return (lat, lon) + + +@app.route("/") +def redirect_from_root(): + return redirect(url_for("map_start_page")) + + +@app.route("/index") +def index_page(): + return render_template("index.html") + + +@app.route("/identifier") +def identifier_index(): + return render_template("identifier_index.html", property_map=property_map) + + +@app.route("/identifier/") +def identifier_page(pid): + per_page = 10 + page = int(request.args.get("page", 1)) + property_dict = {pid: (osm_keys, label) for pid, osm_keys, label in property_map} + osm_keys, label = property_dict[pid] + + wd = model.Item.query.filter(model.Item.claims.has_key(pid)) + total = wd.count() + + start = per_page * (page - 1) + items = wd.all()[start : per_page * page] + + qids = [item.qid for item in items] + print(qids) + + # pred = None + # values = set() + # for item in items: + # values |= set(item.get_claim(pid)) + # + # for key in osm_keys: + # if key == 'ref': + # continue + # if pred is None: + # pred = model.Point.tags[key].in_(values) + # else: + # pred |= model.Point.tags[key].in_(values) + # + + osm_points = {} + + for qid in qids: + osm_points[qid] = model.Point.query.filter( + model.Point.tags["wikidata"] == qid + ).all() + + osm_total = len(osm_points) + + return render_template( + "identifier_page.html", + pid=pid, + osm_keys=osm_keys, + label=label, + items=items, + total=total, + osm_total=osm_total, + osm_points=osm_points, + ) + + +@app.route("/map///") +def map_location(zoom, lat, lng): + t = int(time()) + return render_template("map.html", zoom=zoom, lat=lat, lng=lng, time=t) + + +@app.route("/map") +def map_start_page(): + t = int(time()) + location = get_user_location() + if not location: + return render_template("map.html", zoom=16, lat=None, lng=None, time=t) + + lat, lng = location + return render_template("map.html", zoom=16, lat=lat, lng=lng, time=t) + + +@app.route("/search/map") +def search_map_page(): + user_lat, user_lon = get_user_location() or (None, None) + + q = request.args.get("q") + if not q: + return render_template("map.html", user_lat=user_lat, user_lon=user_lon) + + hits = nominatim.lookup(q) + for hit in hits: + if "geotext" in hit: + del hit["geotext"] + bbox = [hit["boundingbox"] for hit in hits] + + return render_template( + "search_map.html", + hits=hits, + bbox_list=bbox, + user_lat=user_lat, + user_lon=user_lon, + ) + + +@app.route("/search") +def search_page(): + q = request.args.get("q") + if not q: + return render_template("search.html", hits=None, bbox_list=None) + hits = nominatim.lookup(q) + for hit in hits: + if "geotext" in hit: + del hit["geotext"] + bbox = [hit["boundingbox"] for hit in hits] + return render_template("search.html", hits=hits, bbox_list=bbox) + + +def get_isa_count(items): + isa_count = Counter() + for item in items: + isa_list = item.get_claim("P31") + for isa in isa_list: + isa_count[isa["id"]] += 1 + + return isa_count.most_common() + + +@app.route("/api/1/items") +def api_wikidata_items(): + bounds = request.args.get("bounds") + t0 = time() + q = get_items_in_bbox(bounds) + db_items = q.all() + items = get_markers(db_items) + + counts = get_isa_count(db_items) + isa_ids = [qid[1:] for qid, count in counts] + isa_items = { + isa.qid: isa for isa in model.Item.query.filter(model.Item.item_id.in_(isa_ids)) + } + + isa_count = [] + for qid, count in counts: + item = isa_items.get(qid) + label = item.label() if item else "[missing]" + isa = { + "qid": qid, + "count": count, + "label": label, + } + isa_count.append(isa) + + t1 = time() - t0 + print(f"wikidata: {t1} seconds") + + return jsonify(success=True, items=items, isa_count=isa_count, duration=t1) + + +@app.route("/api/1/osm") +def api_osm_objects(): + bounds = request.args.get("bounds") + t0 = time() + objects = get_osm_with_wikidata_tag(bounds) + t1 = time() - t0 + print(f"OSM: {t1} seconds") + return jsonify(success=True, objects=objects, duration=t1) + + +@app.route("/api/1/item/Q") +def api_item_detail(item_id): + qid = f"Q{item_id}" + tagged = get_tagged_qid(qid) + return jsonify(qid=qid, tagged=bool(tagged), info=tagged) + + +@app.route("/api/1/search") +def api_search(): + q = request.args["q"] + hits = nominatim.lookup(q) + for hit in hits: + if "geotext" in hit: + del hit["geotext"] + hit["name"] = nominatim.get_hit_name(hit) + + return jsonify(hits=hits) + + +if __name__ == "__main__": + app.run(host="0.0.0.0")