#!/usr/bin/python3 from flask import (Flask, render_template, request, jsonify, redirect, url_for, g, flash, session, Response) from sqlalchemy import func, or_ from sqlalchemy.orm import selectinload from matcher import (nominatim, model, database, commons, wikidata, wikidata_api, osm_oauth, edit, mail) from collections import Counter from time import time, sleep from geoalchemy2 import Geography from requests_oauthlib import OAuth1Session import flask_login import os import json import GeoIP import re srid = 4326 re_point = re.compile(r'^POINT\((.+) (.+)\)$') app = Flask(__name__) app.debug = True app.config.from_object('config.default') login_manager = flask_login.LoginManager(app) login_manager.login_view = 'login_route' osm_api_base = 'https://api.openstreetmap.org/api/0.6' DB_URL = "postgresql:///matcher" database.init_db(DB_URL) entity_keys = {"labels", "sitelinks", "aliases", "claims", "descriptions", "lastrevid"} re_qid = re.compile(r'^Q\d+$') 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() @app.before_request def global_user(): g.user = flask_login.current_user._get_current_object() def cors_jsonify(*args, **kwargs): response = jsonify(*args, **kwargs) response.headers["Access-Control-Allow-Origin"] = "*" return response 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) 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 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_Intersects(db_bbox, cls.way), func.ST_Area(cls.way) < 20 * func.ST_Area(db_bbox), ) for osm in q: if osm.identifier in seen: continue seen.add(osm.identifier) name = osm.name or osm.tags.get("addr:housename") or "[no label]" tagged.append( { "identifier": osm.identifier, "id": osm.id, "type": osm.type, "url": osm.osm_url, "geojson": 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_item_street_addresses(item): street_address = [addr["text"] for addr in item.get_claim("P6375")] if street_address or "P669" not in item.claims: return street_address for claim in item.claims["P669"]: qualifiers = claim.get("qualifiers") if not qualifiers or 'P670' not in qualifiers: continue number = qualifiers["P670"][0]["datavalue"]["value"] street_item = get_item(claim["mainsnak"]["datavalue"]["value"]["numeric-id"]) street = street_item.label() for q in qualifiers["P670"]: number = q["datavalue"]["value"] address = (f"{number} {street}" if g.street_number_first else f"{street} {number}") street_address.append(address) return street_address def get_markers(all_items): items = [] for item in all_items: if not item: continue locations = [list(i.get_lat_lon()) for i in item.locations] image_filenames = item.get_claim("P18") street_address = get_item_street_addresses(item) d = { "qid": item.qid, "label": item.label(), "description": item.description(), "markers": locations, "image_list": image_filenames, "street_address": street_address, "isa_list": [v["id"] for v in item.get_claim("P31") if v], } if aliases := item.get_aliases(): d["aliases"] = aliases items.append(d) return items def geoip_user_record(): gi = GeoIP.open(app.config["GEOIP_DATA"], GeoIP.GEOIP_STANDARD) remote_ip = request.remote_addr return gi.record_by_addr(remote_ip) def get_user_location(): gir = geoip_user_record() return (gir["latitude"], gir["longitude"]) if gir else None @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("/admin/skip_isa") def admin_skip_isa_list(): q = model.Item.query.join(model.SkipIsA).order_by(model.Item.item_id) return render_template("admin/skip_isa.html", q=q) @app.route("/identifier") def identifier_index(): return render_template("identifier_index.html", property_map=property_map) @app.route("/commons/") def get_commons_image(filename): detail = commons.image_detail([filename], thumbheight=1200, thumbwidth=1200) image = detail[filename] return redirect(image["thumburl"]) @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("/old_map///") def old_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(): location = get_user_location() lat, lon = location return redirect(url_for( 'map_location', zoom=16, lat=f'{lat:.5f}', lon=f'{lon:.5f}', )) @app.route("/map///") def map_location(zoom, lat, lon): user = flask_login.current_user username = user.username if user.is_authenticated else None return render_template("map.html", zoom=zoom, lat=lat, lon=lon, username=username) @app.route("/old_map") def old_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: if not item: continue isa_list = item.get_claim("P31") for isa in isa_list: if not isa: print("missing IsA:", item.qid) continue isa_count[isa["id"]] += 1 return isa_count.most_common() def get_and_save_item(qid): entity = wikidata_api.get_entity(qid) entity_qid = entity["id"] if entity_qid != qid: print(f'redirect {qid} -> {entity_qid}') item = model.Item.query.get(entity_qid[1:]) return item coords = wikidata.get_entity_coords(entity["claims"]) item_id = int(qid[1:]) obj = {k: v for k, v in entity.items() if k in entity_keys} try: item = model.Item(item_id=item_id, **obj) except TypeError: print(qid) print(f'{entity["pageid"]=} {entity["ns"]=} {entity["type"]=}') print(entity.keys()) raise item.locations = model.location_objects(coords) database.session.add(item) database.session.commit() return item def get_bbox_centroid(bbox): bbox = make_envelope(bbox) centroid = database.session.query(func.ST_AsText(func.ST_Centroid(bbox))).scalar() lon, lat = re_point.match(centroid).groups() return lat, lon @app.route("/api/1/location") def show_user_location(): return cors_jsonify(get_user_location()) @app.route("/api/1/count") def api_wikidata_items_count(): t0 = time() bounds = request.args.get("bounds") db_bbox = make_envelope(bounds) q = ( model.Item.query.join(model.ItemLocation) .filter(func.ST_Covers(db_bbox, model.ItemLocation.location)) ) t1 = time() - t0 return cors_jsonify(success=True, count=q.count(), duration=t1) @app.route("/api/1/isa") def api_wikidata_isa_counts(): t0 = time() bbox = request.args.get("bounds") bounds = [float(i) for i in bbox.split(",")] db_bbox = make_envelope(bbox) q = ( model.Item.query.join(model.ItemLocation) .filter(func.ST_Covers(db_bbox, model.ItemLocation.location)) ) db_items = q.all() 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) if not item: item = get_and_save_item(qid) label = item.label() if item else "[missing]" isa = { "qid": qid, "count": count, "label": label, } isa_count.append(isa) t1 = time() - t0 return cors_jsonify(success=True, isa_count=isa_count, bounds=bounds, duration=t1) @app.route("/api/1/items") def api_wikidata_items(): bounds = request.args.get("bounds") g.street_number_first = is_street_number_first(*get_bbox_centroid(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) if not item: item = get_and_save_item(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 cors_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 cors_jsonify(success=True, objects=objects, duration=t1) edu = ['Tag:amenity=college', 'Tag:amenity=university', 'Tag:amenity=school', 'Tag:office=educational_institution', 'Tag:building=university'] tall = ['Key:height', 'Key:building:levels'] extra_keys = { 'Q3914': ['Tag:building=school', 'Tag:building=college', 'Tag:amenity=college', 'Tag:office=educational_institution'], # school 'Q322563': edu, # vocational school 'Q383092': edu, # film school 'Q1021290': edu, # music school 'Q1244442': edu, # school building 'Q1469420': edu, # adult education centre 'Q2143781': edu, # drama school 'Q2385804': edu, # educational institution 'Q5167149': edu, # cooking school 'Q7894959': edu, # University Technical College 'Q47530379': edu, # agricultural college 'Q38723': edu, # higher education institution 'Q11303': tall, # skyscraper 'Q18142': tall, # high-rise building 'Q33673393': tall, # multi-storey building 'Q641226': ['Tag:leisure=stadium'], # arena 'Q2301048': ['Tag:aeroway=helipad'], # special airfield 'Q622425': ['Tag:amenity=pub', 'Tag:amenity=music_venue'], # nightclub 'Q187456': ['Tag:amenity=pub', 'Tag:amenity=nightclub'], # bar 'Q16917': ['Tag:amenity=clinic', 'Tag:building=clinic'], # hospital 'Q330284': ['Tag:amenity=market'], # marketplace 'Q5307737': ['Tag:amenity=pub', 'Tag:amenity=bar'], # drinking establishment 'Q875157': ['Tag:tourism=resort'], # resort 'Q174782': ['Tag:leisure=park', 'Tag:highway=pedestrian', 'Tag:foot=yes', 'Tag:area=yes', 'Tag:amenity=market', 'Tag:leisure=common'], # square 'Q34627': ['Tag:religion=jewish'], # synagogue 'Q16970': ['Tag:religion=christian'], # church 'Q32815': ['Tag:religion=islam'], # mosque 'Q811979': ['Key:building'], # architectural structure 'Q11691': ['Key:building'], # stock exchange 'Q1329623': ['Tag:amenity=arts_centre', # cultural centre 'Tag:amenity=community_centre'], 'Q856584': ['Tag:amenity=library'], # library building 'Q11315': ['Tag:landuse=retail'], # shopping mall 'Q39658032': ['Tag:landuse=retail'], # open air shopping centre 'Q277760': ['Tag:historic=folly', 'Tag:historic=city_gate'], # gatehouse 'Q180174': ['Tag:historic=folly'], # folly 'Q15243209': ['Tag:leisure=park', 'Tag:boundary=national_park'], # historic district 'Q3010369': ['Tag:historic=monument'], # opening ceremony 'Q123705': ['Tag:place=suburb'], # neighbourhood 'Q256020': ['Tag:amenity=pub'], # inn 'Q41253': ['Tag:amenity=theatre'], # movie theater 'Q17350442': ['Tag:amenity=theatre'], # venue 'Q156362': ['Tag:amenity=winery'], # winery 'Q14092': ['Tag:leisure=fitness_centre', 'Tag:leisure=sports_centre'], # gymnasium 'Q27686': ['Tag:tourism=hostel', # hotel 'Tag:tourism=guest_house', 'Tag:building=hotel'], 'Q11707': ['Tag:amenity=cafe', 'Tag:amenity=fast_food', 'Tag:shop=deli', 'Tag:shop=bakery', 'Key:cuisine'], # restaurant 'Q2360219': ['Tag:amenity=embassy'], # permanent mission 'Q27995042': ['Tag:protection_title=Wilderness Area'], # wilderness area 'Q838948': ['Tag:historic=memorial', 'Tag:historic=monument'], # work of art 'Q23413': ['Tag:place=locality'], # castle 'Q28045079': ['Tag:historic=archaeological_site', 'Tag:site_type=fortification', 'Tag:embankment=yes'], # contour fort 'Q744099': ['Tag:historic=archaeological_site', 'Tag:site_type=fortification', 'Tag:embankment=yes'], # hillfort 'Q515': ['Tag:border_type=city'], # city 'Q1254933': ['Tag:amenity=university'], # astronomical observatory 'Q1976594': ['Tag:landuse=industrial'], # science park 'Q190928': ['Tag:landuse=industrial'], # shipyard 'Q4663385': ['Tag:historic=train_station', # former railway station 'Tag:railway=historic_station'], 'Q11997323': ['Tag:emergency=lifeboat_station'], # lifeboat station 'Q16884952': ['Tag:castle_type=stately', 'Tag:building=country_house'], # country house 'Q1343246': ['Tag:castle_type=stately', 'Tag:building=country_house'], # English country house 'Q4919932': ['Tag:castle_type=stately'], # stately home 'Q1763828': ['Tag:amenity=community_centre'], # multi-purpose hall 'Q3469910': ['Tag:amenity=community_centre'], # performing arts center 'Q57660343': ['Tag:amenity=community_centre'], # performing arts building 'Q163740': ['Tag:amenity=community_centre', # nonprofit organization 'Tag:amenity=social_facility', 'Key:social_facility'], 'Q41176': ['Key:building:levels'], # building 'Q44494': ['Tag:historic=mill'], # mill 'Q56822897': ['Tag:historic=mill'], # mill building 'Q2175765': ['Tag:public_transport=stop_area'], # tram stop 'Q179700': ['Tag:memorial=statue', # statue 'Tag:memorial:type=statue', 'Tag:historic=memorial'], 'Q1076486': ['Tag:landuse=recreation_ground'], # sports venue 'Q988108': ['Tag:amenity=community_centre', # club 'Tag:community_centre=club_home'], 'Q55004558': ['Tag:service=yard', 'Tag:landuse=railway'], # car barn 'Q19563580': ['Tag:landuse=railway'], # rail yard 'Q134447': ['Tag:generator:source=nuclear'], # nuclear power plant 'Q1258086': ['Tag:leisure=park', 'Tag:boundary=national_park'], # National Historic Site 'Q32350958': ['Tag:leisure=bingo'], # Bingo hall 'Q53060': ['Tag:historic=gate', # gate 'Tag:tourism=attraction'], 'Q3947': ['Tag:tourism=hotel', # house 'Tag:building=hotel', 'Tag:tourism=guest_house'], 'Q847017': ['Tag:leisure=sports_centre'], # sports club 'Q820477': ['Tag:landuse=quarry', 'Tag:gnis:feature_type=Mine'], # mine 'Q77115': ['Tag:leisure=sports_centre'], # community center 'Q35535': ['Tag:amenity=police'], # police 'Q16560': ['Tag:tourism=attraction', # palace 'Tag:historic=yes'], 'Q131734': ['Tag:amenity=pub', # brewery 'Tag:industrial=brewery'], 'Q828909': ['Tag:landuse=commercial', 'Tag:landuse=industrial', 'Tag:historic=dockyard'], # wharf 'Q10283556': ['Tag:landuse=railway'], # motive power depot 'Q18674739': ['Tag:leisure=stadium'], # event venue 'Q20672229': ['Tag:historic=archaeological_site'], # friary 'Q207694': ['Tag:museum=art'], # art museum 'Q22698': ['Tag:leisure=dog_park', 'Tag:amenity=market', 'Tag:place=square', 'Tag:leisure=common'], # park 'Q738570': ['Tag:place=suburb'], # central business district 'Q1133961': ['Tag:place=suburb'], # commercial district 'Q935277': ['Tag:gnis:ftype=Playa', 'Tag:natural=sand'], # salt pan 'Q14253637': ['Tag:gnis:ftype=Playa', 'Tag:natural=sand'], # dry lake 'Q63099748': ['Tag:tourism=hotel', # hotel building 'Tag:building=hotel', 'Tag:tourism=guest_house'], 'Q2997369': ['Tag:leisure=park', 'Tag:highway=pedestrian', 'Tag:foot=yes', 'Tag:area=yes', 'Tag:amenity=market', 'Tag:leisure=common'], # plaza 'Q130003': ['Tag:landuse=winter_sports', # ski resort 'Tag:site=piste', 'Tag:leisure=resort', 'Tag:landuse=recreation_ground'], 'Q4830453': ['Key:office', 'Tag:building=office'], # business 'Q43229': ['Key:office', 'Tag:building=office'], # organization 'Q17084016': ['Tag:office=association', 'Tag:office=ngo'], # nonprofit corporation } skip_tags = {"Key:addr:street"} def get_item(item_id): item = model.Item.query.get(item_id) if item: return item item = get_and_save_item(f"Q{item_id}") database.session.add(item) database.session.commit() return item def get_items(item_ids): items = [] for item_id in item_ids: item = model.Item.query.get(item_id) if not item: if not get_and_save_item(f"Q{item_id}"): continue item = model.Item.query.get(item_id) items.append(item) return items def get_item_tags(item): isa_items = [] isa_list = [v["numeric-id"] for v in item.get_claim("P31")] isa_items = get_items(isa_list) osm_list = set() skip_isa = {row[0] for row in database.session.query(model.SkipIsA.item_id)} seen = set(isa_list) | skip_isa while isa_items: isa = isa_items.pop() if not isa: continue osm = [v for v in isa.get_claim("P1282") if v not in skip_tags] if isa.qid in extra_keys: osm += extra_keys[isa.qid] osm_list.update(osm) subclass_of = [v["numeric-id"] for v in (isa.get_claim("P279") or []) if v] isa_list = [isa_id for isa_id in subclass_of if isa_id not in seen] seen.update(isa_list) isa_items += get_items(isa_list) return sorted(osm_list) @app.route("/api/1/item/Q/tags") def api_get_item_tags(item_id): t0 = time() item = model.Item.query.get(item_id) osm_list = get_item_tags(item) t1 = time() - t0 return cors_jsonify(success=True, qid=item.qid, tag_or_key_list=osm_list, duration=t1) def get_tag_filter(cls, tag_list): tag_filter = [] for tag_or_key in tag_list: if tag_or_key.startswith("Key:"): tag_filter.append(cls.tags.has_key(tag_or_key[4:])) if tag_or_key.startswith("Tag:"): k, _, v = tag_or_key.partition("=") tag_filter.append(cls.tags[k[4:]] == v) return tag_filter def get_country_iso3166_1(lat, lon): 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), model.Polygon.admin_level == "2") for country in q: alpha2 = country.tags.get("ISO3166-1") if not alpha2: continue alpha2_codes.add(alpha2) g.alpha2_codes = alpha2_codes return alpha2_codes def is_street_number_first(lat, lon): if lat is None or lon is None: return True alpha2 = get_country_iso3166_1(lat, lon) alpha2_number_first = {'GB', 'IE', 'US', 'MX', 'CA', 'FR', 'AU', 'NZ', 'ZA'} return bool(alpha2_number_first & alpha2) def get_nearby(bbox, item, max_distance=300): db_bbox = make_envelope(bbox) osm_objects = {} distances = {} tag_list = get_item_tags(item) if not tag_list: return [] item_is_street = item.is_street() for loc in item.locations: lat, lon = loc.get_lat_lon() point = func.ST_SetSRID(func.ST_MakePoint(lon, lat), 4326) for cls in model.Point, model.Line, model.Polygon: if item_is_street and cls == model.Point: continue tag_filter = get_tag_filter(cls, tag_list) dist = func.ST_Distance(point, cls.way.cast(Geography(srid=4326))) q = (cls.query.add_columns(dist.label('distance')) .filter( func.ST_Intersects(db_bbox, cls.way), func.ST_Area(cls.way) < 20 * func.ST_Area(db_bbox), or_(*tag_filter)) .order_by(point.distance_centroid(cls.way))) if item_is_street: q = q.filter(cls.tags.has_key("name"), cls.tags["highway"] != 'bus_stop') if "Key:amenity" in tag_list: q = q.filter(cls.tags["amenity"] != "bicycle_parking", cls.tags["amenity"] != "bicycle_repair_station", cls.tags["amenity"] != "atm", cls.tags["amenity"] != "recycling") q = q.limit(40) # print(q.statement.compile(compile_kwargs={"literal_binds": True})) for i, dist in q: if dist > max_distance: continue osm_objects.setdefault(i.identifier, i) if i.identifier not in distances or dist < distances[i.identifier]: distances[i.identifier] = dist nearby = [(osm_objects[identifier], dist) for identifier, dist in sorted(distances.items(), key=lambda i:i[1])] return nearby[:40] def find_preset_file(k, v, ending): ts_dir = app.config["ID_TAGGING_SCHEMA_DIR"] preset_dir = os.path.join(ts_dir, "data", "presets") filename = os.path.join(preset_dir, k, v + ".json") if os.path.exists(filename): return { "tag_or_key": f"Tag:{k}={v}", "preset": f"{k}/{v}", "filename": filename, } filename = os.path.join(preset_dir, k, f"{v}_{ending}.json") if os.path.exists(filename): return { "tag_or_key": f"Tag:{k}={v}", "preset": f"{k}/{v}", "filename": filename, } filename = os.path.join(preset_dir, k, "_" + v + ".json") if os.path.exists(filename): return { "tag_or_key": f"Tag:{k}={v}", "preset": f"{k}/{v}", "filename": filename, } filename = os.path.join(preset_dir, k + ".json") if os.path.exists(filename): return { "tag_or_key": f"Key:{k}", "preset": k, "filename": filename, } def get_preset_translations(): country_language = { 'AU': 'en-AU', # Australia 'GB': 'en-GB', # United Kingdom 'IE': 'en-GB', # Ireland 'IN': 'en-IN', # India 'NZ': 'en-NZ', # New Zealand } ts_dir = app.config["ID_TAGGING_SCHEMA_DIR"] translation_dir = os.path.join(ts_dir, "dist", "translations") for code in g.alpha2_codes: if code not in country_language: continue filename = os.path.join(translation_dir, country_language[code] + ".json") return json.load(open(filename))["en-GB"]["presets"]["presets"] return {} def get_presets_from_tags(osm): translations = get_preset_translations() found = [] endings = {model.Point: "point", model.Line: "line", model.Polygon: "area"} ending = endings[type(osm)] for k, v in osm.tags.items(): if k == 'amenity' and v == 'clock' and osm.tags.get('display') == 'sundial': tag_or_key = f"Tag:{k}={v}" found.append({"tag_or_key": tag_or_key, "name": "Sundial"}) continue match = find_preset_file(k, v, ending) if not match: continue preset = match["preset"] if preset in translations: match["name"] = translations[preset]["name"] else: match["name"] = json.load(open(match["filename"]))["name"] del match["filename"] found.append(match) return found def get_address_nodes_within_building(building, bbox): db_bbox = make_envelope(bbox) ewkt = building.as_EWKT q = model.Point.query.filter( func.ST_Intersects(db_bbox, model.Point.way), func.ST_Covers(func.ST_GeomFromEWKT(ewkt), model.Point.way), model.Point.tags.has_key("addr:street"), model.Point.tags.has_key("addr:housenumber"), ) return [node.tags for node in q] def get_part_of(thing, bbox): db_bbox = make_envelope(bbox) ewkt = thing.as_EWKT q = model.Polygon.query.filter( func.ST_Intersects(db_bbox, model.Polygon.way), func.ST_Covers(model.Polygon.way, func.ST_GeomFromEWKT(ewkt)), or_( model.Polygon.tags.has_key("landuse"), model.Polygon.tags.has_key("amenity"), ), model.Polygon.tags.has_key("name"), ) return [polygon.tags for polygon in q] def address_from_tags(tags): keys = ["street", "housenumber"] if not all("addr:" + k in tags for k in keys): return if g.street_number_first: keys.reverse() return " ".join(tags["addr:" + k] for k in keys) @app.route("/api/1/item/Q/candidates") def api_find_osm_candidates(item_id): bounds = request.args.get("bounds") g.street_number_first = is_street_number_first(*get_bbox_centroid(bounds)) t0 = time() item = model.Item.query.get(item_id) nearby = [] for osm, dist in get_nearby(bounds, item): tags = osm.tags tags.pop("way_area", None) name = osm.name or tags.get("addr:housename") or tags.get("inscription") if not name and "addr:housenumber" in tags and "addr:street" in tags: name = address_from_tags(tags) if isinstance(osm, model.Polygon) and "building" in osm.tags: address_nodes = get_address_nodes_within_building(osm, bounds) address_list = [address_from_tags(addr) for addr in address_nodes] else: address_list = [] cur = { "identifier": osm.identifier, "distance": dist, "name": name, "tags": tags, "geojson": osm.geojson(), "presets": get_presets_from_tags(osm), "address_list": address_list, } if hasattr(osm, 'area'): cur["area"] = osm.area if address := address_from_tags(tags): cur["address"] = address part_of = [i["name"] for i in get_part_of(osm, bounds) if i["name"] != name] if part_of: cur["part_of"] = part_of nearby.append(cur) t1 = time() - t0 return cors_jsonify(success=True, qid=item.qid, nearby=nearby, duration=t1) @app.route("/api/1/missing") def api_missing_wikidata_items(): qids_arg = request.args.get("qids") if not qids_arg: return cors_jsonify(success=False, error="required parameter 'qids' is missing", items=[], isa_count=[]) qids = [] for qid in qids_arg.upper().split(","): qid = qid.strip() m = re_qid.match(qid) if not m: continue qids.append(qid) if not qids: return jsonify(success=True, items=[], isa_count=[]) lat, lon = request.args.get("lat"), request.args.get("lon") g.street_number_first = is_street_number_first(lat, lon) db_items = [] for qid in qids: item = model.Item.query.get(qid[1:]) if not item: item = get_and_save_item(qid) db_items.append(item) 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) if not item: item = get_and_save_item(qid) label = item.label() if item else "[missing]" isa = { "qid": qid, "count": count, "label": label, } isa_count.append(isa) return cors_jsonify(success=True, items=items, isa_count=isa_count) @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) hit["identifier"] = f"{hit['osm_type']}/{hit['osm_id']}" return cors_jsonify(success=True, hits=hits) @app.route("/refresh/Q") def refresh_item(item_id): assert not model.Item.query.get(item_id) qid = f'Q{item_id}' entity = wikidata_api.get_entity(qid) entity_qid = entity.pop("id") assert qid == entity_qid coords = wikidata.get_entity_coords(entity["claims"]) assert coords obj = {k: v for k, v in entity.items() if k in entity_keys} item = model.Item(item_id=item_id, **obj) print(item) item.locations = model.location_objects(coords) database.session.add(item) database.session.commit() return 'done' @app.route('/login') def login_openstreetmap(): return redirect(url_for('start_oauth', next=request.args.get('next'))) @app.route('/logout') def logout(): next_url = request.args.get('next') or url_for('index') flask_login.logout_user() flash('you are logged out') return redirect(next_url) @app.route('/done/') def done(): flash('login successful') return redirect(url_for('index')) @app.route('/oauth/start') def start_oauth(): next_page = request.args.get('next') if next_page: session['next'] = next_page client_key = app.config['CLIENT_KEY'] client_secret = app.config['CLIENT_SECRET'] request_token_url = 'https://www.openstreetmap.org/oauth/request_token' callback = url_for('oauth_callback', _external=True) oauth = OAuth1Session(client_key, client_secret=client_secret, callback_uri=callback) fetch_response = oauth.fetch_request_token(request_token_url) session['owner_key'] = fetch_response.get('oauth_token') session['owner_secret'] = fetch_response.get('oauth_token_secret') base_authorization_url = 'https://www.openstreetmap.org/oauth/authorize' authorization_url = oauth.authorization_url(base_authorization_url, oauth_consumer_key=client_key) return redirect(authorization_url) @login_manager.user_loader def load_user(user_id): return model.User.query.get(user_id) @app.route("/oauth/callback", methods=["GET"]) def oauth_callback(): client_key = app.config['CLIENT_KEY'] client_secret = app.config['CLIENT_SECRET'] oauth = OAuth1Session(client_key, client_secret=client_secret, resource_owner_key=session['owner_key'], resource_owner_secret=session['owner_secret']) oauth_response = oauth.parse_authorization_response(request.url) verifier = oauth_response.get('oauth_verifier') access_token_url = 'https://www.openstreetmap.org/oauth/access_token' oauth = OAuth1Session(client_key, client_secret=client_secret, resource_owner_key=session['owner_key'], resource_owner_secret=session['owner_secret'], verifier=verifier) oauth_tokens = oauth.fetch_access_token(access_token_url) session['owner_key'] = oauth_tokens.get('oauth_token') session['owner_secret'] = oauth_tokens.get('oauth_token_secret') r = oauth.get(osm_api_base + '/user/details') info = osm_oauth.parse_userinfo_call(r.content) user = model.User.query.filter_by(osm_id=info['id']).one_or_none() if user: user.osm_oauth_token = oauth_tokens.get('oauth_token') user.osm_oauth_token_secret = oauth_tokens.get('oauth_token_secret') else: user = model.User( username=info['username'], description=info['description'], img=info['img'], osm_id=info['id'], osm_account_created=info['account_created'], ) database.session.add(user) database.session.commit() flask_login.login_user(user) next_page = session.get('next') or url_for('index_page') return redirect(next_page) def validate_edit_list(edits): for e in edits: assert model.Item.get_by_qid(e["qid"]) assert e["op"] in {"add", "remove"} osm_type, _, osm_id = e['osm'].partition('/') osm_id = int(osm_id) if osm_type == 'node': assert model.Point.get(osm_id) else: src_id = osm_id if osm_type == "way" else -osm_id assert (model.Line.query.get(src_id) or model.Polygon.query.get(src_id)) @app.route("/api/1/edit", methods=["POST"]) def api_new_edit_session(): user = flask_login.current_user incoming = request.json validate_edit_list(incoming["edit_list"]) es = model.EditSession(user=user, edit_list=incoming['edit_list'], comment=incoming['comment']) database.session.add(es) database.session.commit() session_id = es.id return cors_jsonify(success=True, session_id=session_id) @app.route("/api/1/edit/", methods=["POST"]) def api_edit_session(session_id): es = model.EditSession.query.get(session_id) assert flask_login.current_user.id == es.user_id incoming = request.json for f in 'edit_list', 'comment': if f not in incoming: continue setattr(es, f, incoming[f]) database.session.commit() return cors_jsonify(success=True, session_id=session_id) @app.route("/api/1/real_save/") def api_save_changeset(session_id): es = model.EditSession.query.get(session_id) def send_message(event, **data): data["type"] = event return f"data: {json.dumps(data)}\n\n" def stream(): changeset = edit.new_changeset(es.comment) r = edit.create_changeset(changeset) reply = r.text.strip() if reply == "Couldn't authenticate you": mail.open_changeset_error(session_id, changeset, r) yield send_message("auth-fail", error=reply) return if not reply.isdigit(): mail.open_changeset_error(session_id, changeset, r) yield send_message("changeset-error", error=reply) return changeset_id = int(reply) yield send_message("open", id=changeset_id) update_count = 0 edit.record_changeset( id=changeset_id, comment=es.comment, update_count=update_count ) for e in es.edit_list: pass return Response(stream(), mimetype='text/event-stream') @app.route("/api/1/save/") def mock_api_save_changeset(session_id): es = model.EditSession.query.get(session_id) def send(event, **data): data["type"] = event return f"data: {json.dumps(data)}\n\n" def stream(user): print('stream') changeset_id = database.session.query(func.max(model.Changeset.id) + 1).scalar() sleep(1) yield send("open", id=changeset_id) sleep(1) update_count = 0 print('record_changeset', changeset_id) edit.record_changeset( id=changeset_id, user=user, comment=es.comment, update_count=update_count ) print('edits') for num, e in enumerate(es.edit_list): print(num, e) yield send("progress", edit=e, num=num) sleep(1) yield send("saved", edit=e, num=num) sleep(1) print('closing') yield send("closing") sleep(1) yield send("done") return Response(stream(g.user), mimetype='text/event-stream') if __name__ == "__main__": app.run(host="0.0.0.0")