diff --git a/frontend/App.vue b/frontend/App.vue index 7fee829..3166b5e 100644 --- a/frontend/App.vue +++ b/frontend/App.vue @@ -233,6 +233,7 @@
+

Searching for '{{ recent_search }}', found {{ hits.length }} places.

@@ -256,6 +257,39 @@ Click a result to continue. +
+ + +
+ +
+
+
item type filters
+ + + + +
current item type filters
+ +
no item type filters
+
+
@@ -670,6 +704,10 @@ export default { mode: undefined, current_hit: undefined, recent_search: undefined, + show_item_type_filter: false, + item_type_search: undefined, + item_type_hits: [], + item_type_filters: [], }; }, computed: { @@ -681,7 +719,7 @@ export default { && !this.current_item); }, area_too_big() { - return this.map_area > 1000 * 1000 * 1000; + return !this.item_type_filters.length && this.map_area > 1000 * 1000 * 1000; }, too_many_items() { return this.item_count > 1400; @@ -760,6 +798,19 @@ export default { edits(edit_list) { this.update_unload_warning(edit_list); }, + item_type_search(value) { + if (value.length < 3) { + this.item_type_hits = []; + return; + } + + var params = { q: value }; + var isa_search_url = `${this.api_base_url}/api/1/isa_search`; + + axios.get(isa_search_url, { params: params }).then((response) => { + this.item_type_hits = response.data.items; + }); + }, selected_items(new_items, old_items) { for (const qid of Object.keys(new_items)) { if (!old_items[qid]) @@ -1283,11 +1334,17 @@ export default { var params = { bounds: bounds.toBBoxString() }; + if (this.item_type_filters.length) { + params["isa"] = this.item_type_filters.map(isa => isa.qid).join(","); + } + axios.get(items_url, { params: params }).then((response) => { this.clear_isa(); this.isa_list = response.data.isa_count; this.isa_list.forEach(isa => { - if (this.detail_qid) this.isa_ticked.push(isa.qid); + if (this.detail_qid || this.item_type_filters.length) { + this.isa_ticked.push(isa.qid); + } this.isa_labels[isa.qid] = isa.label; this.isa_lookup[isa.qid] = isa; }); @@ -1329,6 +1386,14 @@ export default { return; } var params = { bounds: bounds.toBBoxString() }; + + console.log(this.item_type_filters.length); + + if (this.item_type_filters.length) { + params["isa"] = this.item_type_filters.map(isa => isa.qid).join(","); + console.log(params.isa); + } + axios.get(count_url, { params: params }).then((response) => { this.item_count = response.data.count; if (!this.too_many_items) this.load_wikidata_items(bounds); @@ -1390,7 +1455,9 @@ export default { if (this.isa_lookup[isa.qid] === undefined) { this.isa_lookup[isa.qid] = isa; this.isa_list.push(isa); - if (this.detail_qid) this.isa_ticked.push(isa.qid); + if (this.detail_qid || this.item_type_filters.length) { + this.isa_ticked.push(isa.qid); + } } else { this.isa_lookup[isa.qid].count += 1; } diff --git a/matcher/api.py b/matcher/api.py index 1edc8bd..86dab52 100644 --- a/matcher/api.py +++ b/matcher/api.py @@ -155,8 +155,22 @@ def get_items_in_bbox(bbox): return q -def get_osm_with_wikidata_tag(bbox): +def get_osm_with_wikidata_tag(bbox, isa_filter=None): bbox_str = ','.join(str(v) for v in bbox) + extra_sql = "" + if isa_filter: + q = ( + model.Item.query.join(model.ItemLocation) + .filter(func.ST_Covers(make_envelope(bbox), + model.ItemLocation.location)) + ) + q = add_isa_filter(q, isa_filter) + qids = [isa.qid for isa in q] + if not qids: + return [] + + qid_list = ",".join(f"'{qid}'" for qid in qids) + extra_sql += f" AND tags -> 'wikidata' in ({qid_list})" # easier than building this query with SQLAlchemy sql = f''' @@ -178,7 +192,7 @@ UNION HAVING st_area(st_collect(way)) < 20 * st_area(ST_MakeEnvelope({bbox_str}, {srid})) ) as anon WHERE tags ? 'wikidata' -''' +''' + extra_sql conn = database.session.connection() result = conn.execute(text(sql)) @@ -263,17 +277,40 @@ def get_item_tags(item): isa_items += [(isa, isa_path) for isa in get_items(isa_list)] return {key: list(values) for key, values in osm_list.items()} +def add_isa_filter(q, isa_qids): + + q_subclass = database.session.query(model.Item.qid).filter( + func.jsonb_path_query_array( + model.Item.claims, + '$.P279[*].mainsnak.datavalue.value.id', + ).bool_op('?|')(list(isa_qids)) + ) + + subclass_qid = {qid for qid, in q_subclass.all()} + # print(subclass_qid) + + isa = func.jsonb_path_query_array( + model.Item.claims, + '$.P31[*].mainsnak.datavalue.value.id', + ).bool_op('?|') + return q.filter(isa(list(isa_qids | subclass_qid))) + + +def wikidata_items_count(bounds, isa_filter=None): -def wikidata_items_count(bounds): q = ( model.Item.query.join(model.ItemLocation) .filter(func.ST_Covers(make_envelope(bounds), model.ItemLocation.location)) ) + if isa_filter: + q = add_isa_filter(q, isa_filter) + + print(q.statement.compile(compile_kwargs={"literal_binds": True})) + return q.count() def wikidata_isa_counts(bounds): - db_bbox = make_envelope(bounds) q = ( @@ -632,9 +669,13 @@ def get_markers(all_items): return [item_detail(item) for item in all_items if item] -def wikidata_items(bounds): +def wikidata_items(bounds, isa_filter=None): check_is_street_number_first(get_bbox_centroid(bounds)) q = get_items_in_bbox(bounds) + + if isa_filter: + q = add_isa_filter(q, isa_filter) + db_items = q.all() items = get_markers(db_items) @@ -692,3 +733,20 @@ def missing_wikidata_items(qids, lat, lon): isa_count.append(isa) return dict(items=items, isa_count=isa_count) + +def isa_incremental_search(search_terms): + en_label = func.jsonb_extract_path_text(model.Item.labels, "en", "value") + q = model.Item.query.filter( + model.Item.claims.has_key("P1282"), + en_label.ilike(f"%{search_terms}%"), + func.length(en_label) < 20, + ) + + ret = [] + for item in q: + cur = { + "qid": item.qid, + "label": item.label(), + } + ret.append(cur) + return ret diff --git a/web_view.py b/web_view.py index 7ff7f61..2bd4add 100755 --- a/web_view.py +++ b/web_view.py @@ -271,6 +271,10 @@ def old_search_page(): def read_bounds_param(): return [float(i) for i in request.args["bounds"].split(",")] +def read_isa_filter_param(): + isa_param = request.args.get('isa') + if isa_param: + return set(qid.strip() for qid in isa_param.upper().split(',')) @app.route("/api/1/location") def show_user_location(): @@ -280,18 +284,30 @@ def show_user_location(): @app.route("/api/1/count") def api_wikidata_items_count(): t0 = time() - count = api.wikidata_items_count(read_bounds_param()) + isa_filter = read_isa_filter_param() + count = api.wikidata_items_count(read_bounds_param(), isa_filter=isa_filter) t1 = time() - t0 return cors_jsonify(success=True, count=count, duration=t1) +@app.route("/api/1/isa_search") +def api_isa_search(): + t0 = time() + search_terms = request.args.get("q") + items = api.isa_incremental_search(search_terms) + t1 = time() - t0 + + return cors_jsonify(success=True, items=items, duration=t1) + @app.route("/api/1/isa") def api_wikidata_isa_counts(): t0 = time() bounds = read_bounds_param() - isa_count = api.wikidata_isa_counts(bounds) + isa_filter = read_isa_filter_param() + + isa_count = api.wikidata_isa_counts(bounds, isa_filter=isa_filter) t1 = time() - t0 return cors_jsonify(success=True, isa_count=isa_count, bounds=bounds, duration=t1) @@ -302,7 +318,9 @@ def api_wikidata_items(): t0 = time() bounds = read_bounds_param() - ret = api.wikidata_items(bounds) + isa_filter = read_isa_filter_param() + + ret = api.wikidata_items(bounds, isa_filter=isa_filter) t1 = time() - t0 return cors_jsonify(success=True, duration=t1, **ret) @@ -311,7 +329,8 @@ def api_wikidata_items(): @app.route("/api/1/osm") def api_osm_objects(): t0 = time() - objects = api.get_osm_with_wikidata_tag(read_bounds_param()) + isa_filter = read_isa_filter_param() + objects = api.get_osm_with_wikidata_tag(read_bounds_param(), isa_filter=isa_filter) t1 = time() - t0 return cors_jsonify(success=True, objects=objects, duration=t1)