diff --git a/static/js/map.js b/static/js/map.js index 99f87d7..aa47b20 100644 --- a/static/js/map.js +++ b/static/js/map.js @@ -21,13 +21,20 @@ var loading = document.getElementById("loading"); var load_text = document.getElementById("load-text"); var isa_card = document.getElementById("isa-card"); var detail_card = document.getElementById("detail-card"); +var detail = document.getElementById("detail"); +var detail_image = document.getElementById("detail-image"); +var detail_header = document.getElementById("detail-header"); +var detail_qid; +var candidates = document.getElementById("candidates"); var checkbox_list = document.getElementsByClassName("isa-checkbox"); +var nearby_lookup = {}; var isa_labels = {}; var items_url = "/api/1/items"; var osm_objects_url = "/api/1/osm"; var missing_url = "/api/1/missing"; var hover_circles = []; var selected_circles = []; +var candidate_outline; var isa_count = {}; @@ -267,7 +274,7 @@ function build_item_detail(item, tag_or_key_list) { var popup = "

Wikidata item
"; popup += `${item.label} (${item.qid})`; if (item.description) { - popup += `
description: ${item.description}`; + popup += `
description
${item.description}`; } if (item.isa_list && item.isa_list.length) { popup += "
item type"; @@ -279,17 +286,14 @@ function build_item_detail(item, tag_or_key_list) { } if (tag_or_key_list && tag_or_key_list.length) { - popup += "
OSM tags/keys to search for" + popup += "
OSM tags/keys to search for"; for (const v of tag_or_key_list) { popup += `
${v}`; } } - if (item.image_list && item.image_list.length) { - popup += `
`; - } if (item.street_address && item.street_address.length) { - popup += "
street address" + popup += "
street address"; popup += `
${item.street_address[0]["text"]}`; } @@ -310,7 +314,7 @@ function mouseover(item) { item.markers.forEach((marker) => { var coords = marker.getLatLng(); - var circle = L.circle(coords, {radius: 25}).addTo(map); + var circle = L.circle(coords, { radius: 20 }).addTo(map); hover_circles.push(circle); }); } @@ -336,6 +340,19 @@ function close_item_details() { circle.removeFrom(map); }); selected_circles = []; + + detail_header.innerHTML = ""; + detail.innerHTML = ""; + candidates.innerHTML = ""; + detail_qid = undefined; + + detail_image.setAttribute("src", ""); + detail_image.classList.add("d-none"); + + if (candidate_outline) { + candidate_outline.removeFrom(map); + candidate_outline = undefined; + } } function mouse_events(marker, qid) { @@ -348,6 +365,7 @@ function mouse_events(marker, qid) { mouseout(item); }); marker.on("click", function () { + var wd_item = items[qid].wikidata; isa_card.classList.add("visually-hidden"); detail_card.classList.remove("visually-hidden"); detail_card.classList.add("bg-highlight"); @@ -355,24 +373,77 @@ function mouse_events(marker, qid) { item.markers.forEach((marker) => { var coords = marker.getLatLng(); - var circle = L.circle(coords, {radius: 25, color: "orange"}).addTo(map); + var circle = L.circle(coords, { radius: 20, color: "orange" }).addTo(map); selected_circles.push(circle); }); - window.setTimeout(function() { + window.setTimeout(function () { detail_card.classList.remove("bg-highlight"); }, 500); + detail_qid = qid; + + if (wd_item.image_list && wd_item.image_list.length) { + detail_image.setAttribute("src", `/commons/${wd_item.image_list[0]}`); + detail_image.classList.remove("d-none"); + } else { + detail_image.setAttribute("src", ""); + detail_image.classList.add("d-none"); + } + + var item_label = `${wd_item.label} (${wd_item.qid})`; + detail_header.innerHTML = ""; + detail_header.append(document.createTextNode(item_label)); + var item_tags_url = `/api/1/item/${qid}/tags`; axios.get(item_tags_url).then((response) => { var tag_or_key_list = response.data.tag_or_key_list; - var item_detail = build_item_detail(items[qid].wikidata, tag_or_key_list); + if (response.data.qid != detail_qid) { + tag_or_key_list = []; // different QID + } + var item_detail = build_item_detail(wd_item, tag_or_key_list); detail.innerHTML = item_detail; + if (tag_or_key_list.length == 0) return; + var item_osm_candidates_url = `/api/1/item/${qid}/candidates`; - axios.get(item_osm_candidates_url).then((response) => { - console.log(response.data); - }); + var bounds = map.getBounds(); + + var params = { bounds: bounds.toBBoxString() }; + + axios + .get(item_osm_candidates_url, { params: params }) + .then((response) => { + if (response.data.qid != detail_qid) return; // different QID + var nearby = response.data.nearby; + if (!nearby.length) return; + var osm_html = "Possible OSM matches
"; + for (const osm of nearby) { + var span_id = osm.identifier.replace("/", "_"); + nearby_lookup[span_id] = osm; + osm_html += ` ${osm.distance.toFixed( + 1 + )}m ${osm.name || "no name"}
`; + } + candidates.innerHTML = osm_html; + var span_list = document.getElementsByClassName("osm-candidate"); + for (const osm_span of span_list) { + osm_span.onmouseover = function (e) { + osm = nearby_lookup[e.target.id]; + + if (candidate_outline !== undefined) { + candidate_outline.removeFrom(map); + } + + var mapStyle = { fillOpacity: 0, color: "red" }; + var geojson = L.geoJSON(null, { style: mapStyle }); + geojson.addData(osm.geojson); + geojson.addTo(map); + + candidate_outline = geojson; + }; + } + }); }); }); diff --git a/templates/map.html b/templates/map.html index acd8149..2edecd0 100644 --- a/templates/map.html +++ b/templates/map.html @@ -47,12 +47,20 @@

-
item detail - +
+ item detail +
-
+
+
+
+
+
+
+
+
diff --git a/web_view.py b/web_view.py index 0657014..e7c228d 100755 --- a/web_view.py +++ b/web_view.py @@ -327,6 +327,7 @@ def get_and_save_item(qid): raise item.locations = model.location_objects(coords) database.session.add(item) + database.session.commit() return item @@ -375,23 +376,334 @@ def api_osm_objects(): return jsonify(success=True, objects=objects, duration=t1) -skip_isa = {13226383, 16686448, 2221906} +edu = ['Tag:amenity=college', 'Tag:amenity=university', 'Tag:amenity=school', + 'Tag:office=educational_institution'] +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 + '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 +} + +skip_isa = { + 13226383, + 16686448, + 2221906, + 2133296, # space (architecture) + 56052926, # building division + 15989253, # part + 9350592, # telecommunications infrastructure + 121359, # infrastructure + 28877, # goods + 2897903, # goods and services + 2995644, # result + 733541, # consequence + 408386, # inference + 3249551, # process + 20937557, # series + 16887380, # group + 28813620, # set + 99527517, # collection entity + 1150070, # change + 1190554, # occurrence + 26907166, # temporal entity + 2425052, # electrical appliance + 931447, # electrical load + 210729, # electrical element + 3749263, # electrical device + 16798631, # equipment + 66310125, # nonbiological component + 22811462, # type of manufactured good + 21146257, # type + 16889133, # class + 1310239, # component + 337060, # perceptible object + 581105, # consumer electronics + 2858615, # electronic machine + 1183543, # device + 39546, # tool + 35825432, # converter + 11019, # machine + 8205328, # artificial physical object + 223557, # physical object + 35459920, # three-dimensional object + 488383, # object + 35120, # entity + 1454986, # physical system + 30060700, # scientific object + 58778, # system + 6671777, # structure + 4406616, # concrete object + 2555640, # cell (architecture) + 78642244, # closed space + 1902617, # verblijfsruimte + 180516, # room ['Key:room'] + 17334923, # location + 27096213, # geographic entity + 58416391, # spatial entity + 58415929, # spatio-temporal entity + 811979, # architectural structure + 811430, # human-made geographic feature + 35145743, # human-made landform + 27096235, # artificial geographic entity + 618123, # geographical feature + 386724, # work + 15401930, # product + 102074988, # artificial physical structure + 15710813, # physical structure + 1299240, # interior space + 4830453, # business + 3563237, # economic unit + 2198779, # unit + 7184903, # abstract object + 43229, # organization + 16334295, # group of humans + 16334298, # group of living things + 61961344, # group of physical objects + 98119401, # group or class of physical objects + 106559804, # person or organization + 24229398, # agent + 23958946, # individual entity + 4830453, # business + 2695280, # technique + 21162272, # means + 4026292, # action + 1914636, # activity + 372222, # human-readable medium + 494756, # data + 42848, # data + 1166770, # depiction + 11024, # communication + 6031064, # information exchange + 52948, # interaction + 23009552, # exchange + 23009675, # transfer + 22294683, # biological process involved in intraspecies interaction between organisms + 628858, # workplace + 1228250, # line + 211548, # locus + 36161, # set + 864377, # multiset + 246672, # mathematical object + 5469988, # formalization + 4393498, # representation + 930933, # relation + 217594, # class + 294440, # public space + 7551384, # social space + 83493482, # thanking + 83492918, # acknowledgement + 628523, # message + 11028, # information + 189970, # social status + 11424100, # status + 4897819, # role + 1207505, # quality + 937228, # property + 11862829, # academic discipline + 1047113, # specialty + 9081, # knowledge + 104127086, # memory + 12488383, # content + 2434238, # heritage + 23893363, # heritage + 82821, # tradition + 251777, # custom + 1299714, # habit + 36529775, # habit + 7302601, # recognition +} skip_tags = {"Key:addr:street"} +def get_items(item_ids): + items = [] + for item_id in item_ids: + item = model.Item.query.get(item_id) + if not item: + print(f"get Q{item_id}") + 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 = model.Item.query.filter(model.Item.item_id.in_(isa_list)).all() + isa_items = get_items(isa_list) + osm_list = set() 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] + + print(isa.qid, isa.label(), osm) osm_list.update(osm) - subclass_of = [v["numeric-id"] for v in isa.get_claim("P279")] + 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 += model.Item.query.filter(model.Item.item_id.in_(isa_list)).all() + isa_items += get_items(isa_list) return sorted(osm_list) @@ -404,59 +716,80 @@ def api_get_item_tags(item_id): return jsonify(success=True, qid=item.qid, tag_or_key_list=osm_list, duration=t1) -def get_tag_filter(item): - osm_list = get_item_tags(item) +def get_tag_filter(cls, tag_list): tag_filter = [] - for tag_or_key in osm_list: + for tag_or_key in tag_list: if tag_or_key.startswith("Key:"): - tag_filter.append(model.Polygon.tags.has_key(tag_or_key[4:])) + 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(model.Polygon.tags[k] == v) + tag_filter.append(cls.tags[k[4:]] == v) - return or_(*tag_filter) + return tag_filter + +def get_nearby(bbox, item, max_distance=200): + db_bbox = make_envelope(bbox) -def get_nearby(item, max_distance=100): osm_objects = {} distances = {} - tag_filter = get_tag_filter(item) + tag_list = get_item_tags(item) + if not tag_list: + return [] 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: + tag_filter = get_tag_filter(cls, tag_list) + dist = func.ST_Distance(point, cls.way.cast(Geography(srid=4326))) - dist = func.ST_Distance(point, model.Polygon.way.cast(Geography())) - - q = (model.Polygon.query - .add_columns(dist.label('distance')) - .filter(dist < max_distance, tag_filter) - .order_by(point.distance_centroid(model.Polygon.way)) + 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)) .limit(20)) - for i, dist in q: - osm_objects.setdefault(i.identifier, i) - if i.identifier not in distances or dist < distances[i.identifier]: - distances[i.identifier] = dist + # print(q.statement.compile(compile_kwargs={"literal_binds": True})) - return [(osm_objects[identifier], dist) - for identifier, dist - in sorted(distances.items(), key=lambda i:i[1])] + 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[:10] @app.route("/api/1/item/Q/candidates") def api_find_osm_candidates(item_id): + bounds = request.args.get("bounds") + t0 = time() item = model.Item.query.get(item_id) - max_distance = 100 nearby = [] - for osm, dist in get_nearby(item, max_distance): + for osm, dist in get_nearby(bounds, item): + tags = osm.tags + name = osm.name or tags.get("addr:housename") + if not name and "addr:housenumber" in tags and "addr:street" in tags: + name = tags["addr:housenumber"] + " " + tags["addr:street"] + cur = { "identifier": osm.identifier, "distance": dist, - "tags": osm.tags, - "area": osm.area, + "name": name, + "tags": tags, "geojson": osm.geojson(), } + if hasattr(osm, 'area'): + cur["area"] = osm.area + nearby.append(cur) t1 = time() - t0