This commit is contained in:
Edward Betts 2023-05-13 15:01:28 +02:00
parent 47e1280269
commit c607351699
7 changed files with 274 additions and 69 deletions

View File

@ -303,7 +303,7 @@
v-bind:key="isa.qid" v-bind:key="isa.qid"
v-for="isa in item_type_hits" v-for="isa in item_type_hits"
href="#" href="#"
@click.prevent="item_type_filters.includes(isa) || item_type_filters.push(isa)" @click.prevent="add_item_type_filter(isa)"
> >
{{ isa.label }} ({{ isa.qid }}) {{ isa.label }} ({{ isa.qid }})
</a> </a>
@ -458,12 +458,33 @@
<br>{{ wd_item.aliases.join("; ") }} <br>{{ wd_item.aliases.join("; ") }}
</span> </span>
<br><strong>item coordinates</strong>
<span v-for="marker in wd_item.markers">
<br><a target="_blank" :href="marker_osm_url(marker)" @click.stop>
{{ marker[0].toFixed(5) }},
{{ marker[1].toFixed(5) }}
<i class="fa fa-map-o"></i></a>
</span>
<br><strong>item type</strong> <br><strong>item type</strong>
<span v-bind:key="`isa-${wd_item.qid}-${isa.qid}`" v-for="isa in wd_item.isa_list"> <span v-bind:key="`isa-${wd_item.qid}-${isa.qid}`" v-for="isa in wd_item.isa_list">
<br><a :href="qid_url(isa.qid)" target="_blank">{{isa.label}}</a> ({{isa.qid}}) <br><a :href="qid_url(isa.qid)" target="_blank">{{isa.label}}</a> ({{isa.qid}})
<a :href="'/isa/' + isa.qid" target="_blank"><i class="fa fa-pencil-square-o"></i></a> <a :href="'/isa/' + isa.qid" target="_blank"><i class="fa fa-pencil-square-o"></i></a>
</span> </span>
<span v-if="wd_item.wikipedia.length > 0">
<br>
<strong>
Wikipedia
<i class="fa fa-external-link"></i>
</strong>
<br>
<span v-for="wp in wd_item.wikipedia">
<a :href="wikipedia_link(wp.lang, wp.title)" target="_blank">{{wp.lang}}</a>
&nbsp;
</span>
</span>
<span v-if="wd_item.street_address.length"> <span v-if="wd_item.street_address.length">
<br><strong>street address</strong> <br><strong>street address</strong>
<br>{{wd_item.street_address[0]}} <br>{{wd_item.street_address[0]}}
@ -524,6 +545,15 @@
</a> </a>
</span> </span>
<span v-if="wd_item.commons !== undefined">
<br><strong>Images on Commons</strong>
<br><a
:href="`https://commons.wikimedia.org/wiki/${wd_item.commons.replaceAll(' ', '_')}`"
target="_blank">
{{wd_item.commons}} <i class="fa fa-external-link"></i>
</a>
</span>
</div></div> </div></div>
</div> </div>
@ -548,7 +578,7 @@
<div v-if="current_item.nearby && !current_item.nearby.length"> <div v-if="current_item.nearby && !current_item.nearby.length">
<strong>No OSM matches found nearby</strong> <strong>No OSM matches found nearby</strong>
<div class="mt-2" v-if="current_item.tag_or_key_list.length"> <div class="mt-2" v-if="current_item.tag_or_key_list && current_item.tag_or_key_list.length">
<p>The OSM tags/keys used as the search criteria to find matching <p>The OSM tags/keys used as the search criteria to find matching
OSM objects are listed below, along with the Wikidata item that was OSM objects are listed below, along with the Wikidata item that was
the source.</p> the source.</p>
@ -766,6 +796,8 @@ export default {
startLon: Number, startLon: Number,
startZoom: Number, startZoom: Number,
startRadius: Number, startRadius: Number,
startItem: String,
startItemTypeFilter: Array,
username: String, username: String,
startMode: String, startMode: String,
q: String, q: String,
@ -994,10 +1026,26 @@ export default {
} }
}, },
methods: { 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}`; 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) { update_unload_warning(edit_list) {
if (edit_list.length) { if (edit_list.length) {
addEventListener("beforeunload", beforeUnloadListener, {capture: true}); addEventListener("beforeunload", beforeUnloadListener, {capture: true});
@ -1216,14 +1264,19 @@ export default {
this.isa_ticked = Object.keys(this.isa_labels); this.isa_ticked = Object.keys(this.isa_labels);
}, },
build_map_path() { build_map_path() {
if (this.current_item) {
return `/item/${this.current_qid}`;
}
var zoom = this.map.getZoom(); var zoom = this.map.getZoom();
var c = this.map.getCenter(); var c = this.map.getCenter();
var lat = c.lat.toFixed(5); var lat = c.lat.toFixed(5);
var lng = c.lng.toFixed(5); var lng = c.lng.toFixed(5);
var path = `/map/${zoom}/${lat}/${lng}`; 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; return path;
}, },
@ -1755,6 +1808,11 @@ export default {
this.zoom = this.startZoom; this.zoom = this.startZoom;
this.mode = this.startMode; this.mode = this.startMode;
this.changeset_comment = this.defaultComment || '+wikidata'; 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() { mounted() {
@ -1764,7 +1822,6 @@ export default {
zoom: this.zoom || 16, zoom: this.zoom || 16,
}; };
var map = L.map("map", options); var map = L.map("map", options);
var osm_url = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; 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"; 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.search_text = this.q.trim();
this.run_search(); this.run_search();
} else { } else {
this.detail_qid = this.qid_from_url(); this.detail_qid = this.startItem;
if (this.detail_qid) { if (this.detail_qid) {
this.load_wikidata_items(bounds); this.load_wikidata_items(bounds);
} else { } else {
this.auto_load(bounds); this.auto_load(bounds);
this.update_map_path();
} }
this.update_map_path();
} }
window.onpopstate = this.onpopstate; window.onpopstate = this.onpopstate;

View File

@ -38,6 +38,14 @@ skip_tags = {
} }
def get_country_iso3166_1(lat, lon): 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) point = func.ST_SetSRID(func.ST_MakePoint(lon, lat), srid)
alpha2_codes = set() alpha2_codes = set()
q = model.Polygon.query.filter(func.ST_Covers(model.Polygon.way, point), 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 return True
alpha2 = get_country_iso3166_1(lat, lon) 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) 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) return func.ST_MakeEnvelope(west, south, east, north, srid)
def drop_way_area(tags): def drop_way_area(tags):
""" Remove the way_area field from a tags dict. """
if "way_area" in tags: if "way_area" in tags:
del tags["way_area"] del tags["way_area"]
return tags return tags
@ -122,6 +142,8 @@ def get_part_of(table_name, src_id, bbox):
} for osm_id, tags, area in conn.execute(s)] } for osm_id, tags, area in conn.execute(s)]
def get_and_save_item(qid): def get_and_save_item(qid):
""" Download an item from Wikidata and cache it in the database. """
entity = wikidata_api.get_entity(qid) entity = wikidata_api.get_entity(qid)
entity_qid = entity["id"] entity_qid = entity["id"]
if entity_qid != qid: if entity_qid != qid:
@ -396,7 +418,6 @@ def add_isa_filter(q, isa_qids):
) )
subclass_qid = {qid for qid, in q_subclass.all()} subclass_qid = {qid for qid, in q_subclass.all()}
# print(subclass_qid)
isa = func.jsonb_path_query_array( isa = func.jsonb_path_query_array(
model.Item.claims, model.Item.claims,
@ -419,7 +440,7 @@ def wikidata_items_count(bounds, isa_filter=None):
return q.count() return q.count()
def wikidata_isa_counts(bounds): def wikidata_isa_counts(bounds, isa_filter=None):
db_bbox = make_envelope(bounds) db_bbox = make_envelope(bounds)
q = ( q = (
@ -427,6 +448,9 @@ def wikidata_isa_counts(bounds):
.filter(func.ST_Covers(db_bbox, model.ItemLocation.location)) .filter(func.ST_Covers(db_bbox, model.ItemLocation.location))
) )
if isa_filter:
q = add_isa_filter(q, isa_filter)
db_items = q.all() db_items = q.all()
counts = get_isa_count(db_items) 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_id = item.item_id
item_is_linear_feature = item.is_linear_feature() item_is_linear_feature = item.is_linear_feature()
item_is_street = item.is_street() 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()) 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 shape = "area" if table == "polygon" else table
item_identifier_tags = item.get_identifiers_tags()
cur = { cur = {
"identifier": f"{osm_type}/{osm_id}", "identifier": f"{osm_type}/{osm_id}",
"type": osm_type, "type": osm_type,
@ -733,6 +763,8 @@ def find_osm_candidates(item, limit=80, max_distance=450, names=None):
return nearby return nearby
def get_item(item_id): def get_item(item_id):
""" Retrieve a Wikidata item, either from the database or from Wikidata. """
item = model.Item.query.get(item_id) item = model.Item.query.get(item_id)
return item or get_and_save_item(f"Q{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) g.street_number_first = is_street_number_first(*latlng)
def item_detail(item): def item_detail(item):
unsupported_relation_types = {
'Q194356', # wind farm
'Q2175765', # tram stop
}
locations = [list(i.get_lat_lon()) for i in item.locations] locations = [list(i.get_lat_lon()) for i in item.locations]
if not hasattr(g, 'street_number_first'): if not hasattr(g, 'street_number_first'):
g.street_number_first = is_street_number_first(*locations[0]) 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_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 = { d = {
"qid": item.qid, "qid": item.qid,
@ -797,11 +839,21 @@ def item_detail(item):
"p1619": item.time_claim("P1619"), "p1619": item.time_claim("P1619"),
"p576": item.time_claim("P576"), "p576": item.time_claim("P576"),
"heritage_designation": heritage_designation, "heritage_designation": heritage_designation,
"wikipedia": wikipedia_links,
"identifiers": item.get_identifiers(),
} }
if aliases := item.get_aliases(): if aliases := item.get_aliases():
d["aliases"] = 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 return d
@ -892,3 +944,21 @@ def isa_incremental_search(search_terms):
} }
ret.append(cur) ret.append(cur)
return ret 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}

View File

@ -83,8 +83,8 @@ def get_hit_name(hit):
if len(address) == 1: if len(address) == 1:
return n1 return n1
country = address.pop("country") country = address.pop("country", None)
country_code = address.pop("country_code") country_code = address.pop("country_code", None)
if country_code: if country_code:
country_code == country_code.lower() country_code == country_code.lower()

View File

@ -5,7 +5,6 @@ import json
import math import math
import user_agents import user_agents
import re import re
import pattern.en
from datetime import date from datetime import date
from num2words import num2words from num2words import num2words
@ -160,18 +159,6 @@ def is_in_range(address_range, address):
return False 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): def format_wikibase_time(v):
p = v["precision"] p = v["precision"]
t = v["time"] t = v["time"]
@ -180,11 +167,12 @@ def format_wikibase_time(v):
# example: https://www.wikidata.org/wiki/Q108266998 # example: https://www.wikidata.org/wiki/Q108266998
if p == 11: 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: if p == 10:
return date.fromisoformat(t[1:8] + "-01").strftime("%B %Y") return date.fromisoformat(t[1:8] + "-01").strftime("%B %Y")
if p == 9: if p == 9:
return t[1:5] return t[1:5]
if p == 7: if p == 7:
century = ((int(t[:5]) - 1) // 100) + 1 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

View File

@ -4,6 +4,7 @@
{% block content %} {% block content %}
<div class="container my-2"> <div class="container my-2">
{% include "flash_msg.html" %}
<h1>{{ self.title() }}</h1> <h1>{{ self.title() }}</h1>
@ -13,7 +14,7 @@
</a></div> </a></div>
<div class="my-2"> <div class="my-2">
<form method="POST"> <form method="GET" action="{{ url_for("refresh_item", item_id=item.item_id) }}">
<input type="hidden" name="action" value="refresh"> <input type="hidden" name="action" value="refresh">
<input type="submit" value="refresh item" class="btn btn-sm btn-primary"> <input type="submit" value="refresh item" class="btn btn-sm btn-primary">
</form> </form>

View File

@ -4,26 +4,32 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Wikidata items linked to OSM</title> <title>Wikidata items linked to OSM</title>
<link rel="stylesheet" href="https://unpkg.com/bootstrap@5.0.1/dist/css/bootstrap.min.css"> <!--
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"> <link rel="stylesheet" href="https://unpkg.com/bootstrap@5.1.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://unpkg.com/fork-awesome@1.1.7/css/fork-awesome.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.8.0/dist/leaflet.css">
<link rel="stylesheet" href="https://unpkg.com/fork-awesome@1.2.0/css/fork-awesome.min.css">
<link rel="stylesheet" href="https://unpkg.com/leaflet-extra-markers@1.2.1/dist/css/leaflet.extra-markers.min.css"> <link rel="stylesheet" href="https://unpkg.com/leaflet-extra-markers@1.2.1/dist/css/leaflet.extra-markers.min.css">
-->
<link rel="stylesheet" href="{{ url_for("static", filename="frontend/style.css") }}">
</head> </head>
{% from "navbar.html" import navbar with context %} {% from "navbar.html" import navbar with context %}
<body> <body>
{% block nav %}{{ navbar() }}{% endblock %} {% block nav %}{{ navbar() }}{% endblock %}
<div id="app"></div> <div id="app"></div>
<script src="https://unpkg.com/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4" crossorigin="anonymous"></script> <!-- <script src="https://unpkg.com/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script> -->
<script type="module"> <script type="module">
import { createApp } from "https://cdn.skypack.dev/vue@^3.0.11"; import main from {{ url_for('static', filename='frontend/owl.es.js') | tojson }};
import App from {{ url_for('static', filename='snowpack/App.vue.js') | tojson }};
const props = { const props = {
startLat: {{ lat }}, startLat: {{ lat }},
startLon: {{ lon }}, startLon: {{ lon }},
startZoom: {{ zoom }}, startZoom: {{ zoom }},
startRadius: {{ radius | tojson }}, startRadius: {{ (radius or None) | tojson }},
startItem: {{ (qid or None) | tojson }},
startItemTypeFilter: {{ (item_type_filter or []) | tojson }},
defaultComment: {{ config.DEFAULT_COMMENT | tojson }}, defaultComment: {{ config.DEFAULT_COMMENT | tojson }},
username: {{ username | tojson }}, username: {{ username | tojson }},
startMode: {{ mode | tojson }}, startMode: {{ mode | tojson }},
@ -32,7 +38,7 @@
mockUpload: false, mockUpload: false,
}; };
const app = createApp(App, props).mount('#app'); main(props);
</script> </script>
</body> </body>

View File

@ -1,12 +1,12 @@
#!/usr/bin/python3.9 #!/usr/bin/python3.9
from flask import (Flask, render_template, request, jsonify, redirect, url_for, g, from flask import (Flask, render_template, request, jsonify, redirect, url_for, g,
flash, session, Response, stream_with_context) flash, session, Response, stream_with_context, abort, send_file)
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.sql.expression import update from sqlalchemy.sql.expression import update
from matcher import (nominatim, model, database, commons, wikidata, wikidata_api, from matcher import (nominatim, model, database, commons, wikidata, wikidata_api,
osm_oauth, edit, mail, api, error_mail) osm_oauth, edit, mail, api, error_mail)
from werkzeug.debug.tbtools import get_current_traceback # from werkzeug.debug.tbtools import get_current_traceback
from matcher.data import property_map from matcher.data import property_map
from time import time, sleep from time import time, sleep
from requests_oauthlib import OAuth1Session from requests_oauthlib import OAuth1Session
@ -19,6 +19,7 @@ import json
import GeoIP import GeoIP
import re import re
import maxminddb import maxminddb
import sqlalchemy
srid = 4326 srid = 4326
re_point = re.compile(r'^POINT\((.+) (.+)\)$') re_point = re.compile(r'^POINT\((.+) (.+)\)$')
@ -54,27 +55,27 @@ def dict_repr_values(d):
return {key: repr(value) for key, value in d.items()} return {key: repr(value) for key, value in d.items()}
@app.errorhandler(werkzeug.exceptions.InternalServerError) # @app.errorhandler(werkzeug.exceptions.InternalServerError)
def exception_handler(e): # def exception_handler(e):
tb = get_current_traceback() # tb = get_current_traceback()
last_frame = next(frame for frame in reversed(tb.frames) if not frame.is_library) # last_frame = next(frame for frame in reversed(tb.frames) if not frame.is_library)
last_frame_args = inspect.getargs(last_frame.code) # last_frame_args = inspect.getargs(last_frame.code)
if request.path.startswith("/api/"): # if request.path.startswith("/api/"):
return cors_jsonify({ # return cors_jsonify({
"success": False, # "success": False,
"error": tb.exception, # "error": tb.exception,
"traceback": tb.plaintext, # "traceback": tb.plaintext,
"locals": dict_repr_values(last_frame.locals), # "locals": dict_repr_values(last_frame.locals),
"last_function": { # "last_function": {
"name": tb.frames[-1].function_name, # "name": tb.frames[-1].function_name,
"args": repr(last_frame_args), # "args": repr(last_frame_args),
}, # },
}), 500 # }), 500
#
return render_template('show_error.html', # return render_template('show_error.html',
tb=tb, # tb=tb,
last_frame=last_frame, # last_frame=last_frame,
last_frame_args=last_frame_args), 500 # last_frame_args=last_frame_args), 500
def cors_jsonify(*args, **kwargs): def cors_jsonify(*args, **kwargs):
response = jsonify(*args, **kwargs) response = jsonify(*args, **kwargs)
@ -113,8 +114,8 @@ def geoip_user_record():
def get_user_location(): def get_user_location():
remote_ip = request.args.get('ip', request.remote_addr) remote_ip = request.args.get('ip', request.remote_addr)
maxmind = maxminddb_reader.get(remote_ip)["location"] maxmind = maxminddb_reader.get(remote_ip)
return maxmind["location"] if maxmind else None return maxmind.get("location") if maxmind else None
@app.route("/") @app.route("/")
@ -138,6 +139,12 @@ def isa_page(item_id):
item = api.get_item(item_id) item = api.get_item(item_id)
if request.method == "POST": if request.method == "POST":
tag_or_key = request.form["tag_or_key"]
extra = model.ItemExtraKeys(item=item, tag_or_key=tag_or_key)
database.session.add(extra)
database.session.commit()
flash("extra OSM tag/key added")
return redirect(url_for(request.endpoint, item_id=item_id)) return redirect(url_for(request.endpoint, item_id=item_id))
q = model.ItemExtraKeys.query.filter_by(item=item) q = model.ItemExtraKeys.query.filter_by(item=item)
@ -240,12 +247,19 @@ def identifier_page(pid):
def map_start_page(): def map_start_page():
loc = get_user_location() loc = get_user_location()
if loc:
lat, lon = loc["latitude"], loc["longitude"]
radius = loc["accuracy_radius"]
else:
lat, lon = 42.2917, -85.5872
radius = 5
return redirect(url_for( return redirect(url_for(
'map_location', 'map_location',
lat=f'{loc["latitude"]:.5f}', lat=f'{lat:.5f}',
lon=f'{loc["longitude"]:.5f}', lon=f'{lon:.5f}',
zoom=16, zoom=16,
radius=loc["accuracy_radius"], radius=radius,
ip=request.args.get('ip'), ip=request.args.get('ip'),
)) ))
@ -285,9 +299,22 @@ def search_page():
@app.route("/map/<int:zoom>/<float(signed=True):lat>/<float(signed=True):lon>") @app.route("/map/<int:zoom>/<float(signed=True):lat>/<float(signed=True):lon>")
def map_location(zoom, lat, lon): def map_location(zoom, lat, lon):
qid = request.args.get("item") qid = request.args.get("item")
isa_param = request.args.get("isa")
if qid: if qid:
api.get_item(qid[1:]) api.get_item(qid[1:])
isa_list = []
if isa_param:
for isa_qid in isa_param.split(";"):
isa = api.get_item(isa_qid[1:])
if not isa:
continue
cur = {
"qid": isa.qid,
"label": isa.label(),
}
isa_list.append(cur)
return render_template( return render_template(
"map.html", "map.html",
active_tab="map", active_tab="map",
@ -298,9 +325,40 @@ def map_location(zoom, lat, lon):
username=get_username(), username=get_username(),
mode="map", mode="map",
q=None, q=None,
item_type_filter=isa_list,
) )
@app.route("/item/Q<int:item_id>")
def lookup_item(item_id):
item = api.get_item(item_id)
if not item:
# TODO: show nicer page for Wikidata item not found
return abort(404)
try:
lat, lon = item.locations[0].get_lat_lon()
except IndexError:
# TODO: show nicer page for Wikidata item without coordinates
return abort(404)
return render_template(
"map.html",
active_tab="map",
zoom=16,
lat=lat,
lon=lon,
username=get_username(),
mode="map",
q=None,
qid=item.qid,
item_type_filter=[],
)
url = url_for("map_location", zoom=16, lat=lat, lon=lon, item=item.qid)
return redirect(url)
@app.route("/search/map") @app.route("/search/map")
def search_map_page(): def search_map_page():
user_lat, user_lon = get_user_location() or (None, None) user_lat, user_lon = get_user_location() or (None, None)
@ -394,6 +452,15 @@ def api_wikidata_items():
t1 = time() - t0 t1 = time() - t0
return cors_jsonify(success=True, duration=t1, **ret) return cors_jsonify(success=True, duration=t1, **ret)
@app.route("/api/1/place/<osm_type>/<int:osm_id>")
def api_place_items(osm_type, osm_id):
t0 = time()
ret = api.get_place_items(osm_type, osm_id)
t1 = time() - t0
return cors_jsonify(success=True, duration=t1, **ret)
@app.route("/api/1/osm") @app.route("/api/1/osm")
def api_osm_objects(): def api_osm_objects():
@ -540,7 +607,11 @@ def api_search():
hit["name"] = nominatim.get_hit_name(hit) hit["name"] = nominatim.get_hit_name(hit)
hit["label"] = nominatim.get_hit_label(hit) hit["label"] = nominatim.get_hit_label(hit)
hit["address"] = list(hit["address"].items()) hit["address"] = list(hit["address"].items())
hit["identifier"] = f"{hit['osm_type']}/{hit['osm_id']}" if "osm_type" in hit and "osm_id" in hit:
hit["identifier"] = f"{hit['osm_type']}/{hit['osm_id']}"
else:
print(hit)
print(q)
return cors_jsonify(success=True, hits=hits) return cors_jsonify(success=True, hits=hits)
@ -805,6 +876,18 @@ def api_save_changeset(session_id):
return api_call(session_id) return api_call(session_id)
@app.route("/sql", methods=["GET", "POST"])
def run_sql():
if request.method != "POST":
return render_template("run_sql.html")
sql = request.form["sql"]
conn = database.session.connection()
result = conn.execute(sqlalchemy.text(sql))
return render_template("run_sql.html", result=result)
def api_real_save_changeset(session_id): def api_real_save_changeset(session_id):
es = model.EditSession.query.get(session_id) es = model.EditSession.query.get(session_id)