diff --git a/.gitignore b/.gitignore index 6c976e9..39dd5af 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .DS_Store node_modules -/dist +frontend/dist # local env files diff --git a/api/api.py b/api/api.py index ac4ece6..80b4455 100644 --- a/api/api.py +++ b/api/api.py @@ -19,8 +19,105 @@ app.config["TEMPLATES_AUTO_RELOAD"] = True CORS(app) +ALLOWED_COMPARISONS = [ + '=', + '!=', + '>', + '<', + '>=', + '<=', + 'starts with', + 'ends with', + 'contains', + 'does not contain', + 'is null', + 'is not null', + ] + +ALLOWED_METHODS = [ + "OR", + "AND" +] + +ALLOWED_CASTS = [ + "integer", + "float", + "cast_to_int", + "cast_to_float" +] + +KEY_COLUMNS = [ + 'access', + 'addr:housename', + 'addr:housenumber', + 'addr:interpolation', + 'admin_level', + 'aerialway', + 'aeroway', + 'amenity', + 'area', + 'barrier', + 'bicycle', + 'brand', + 'bridge', + 'boundary', + 'building', + 'capital', + 'construction', + 'covered', + 'culvert', + 'cutting', + 'denomination', + 'disused', + 'ele', + 'embankment', + 'foot', + 'generator:source', + 'harbour', + 'highway', + 'historic', + 'horse', + 'intermittent', + 'junction', + 'landuse', + 'layer', + 'leisure', + 'lock', + 'man_made', + 'military', + 'motorcar', + 'name', + 'natural', + 'office', + 'oneway', + 'operator', + 'place', + 'population', + 'power', + 'power_source', + 'public_transport', + 'railway', + 'ref', + 'religion', + 'route', + 'service', + 'shop', + 'sport', + 'surface', + 'toll', + 'tourism', + 'tower:type', + 'tracktype', + 'tunnel', + 'water', + 'waterway', + 'wetland', + 'width', + 'wood' +] + def get_db_connection(): - conn = psycopg2.connect(database='osm') + conn = psycopg2.connect(database='mass') return conn def json_query(query, conn=None): @@ -92,6 +189,42 @@ def token_required(f): return decorated +def make_filter_query(filter): + filter_query = sql.SQL("WHERE") + + i = 0 + + for subfilter in filter['filters']: + if subfilter['comparison'] not in ALLOWED_COMPARISONS: + logger.error(f"Invalid comparison {subfilter['comparison']}") + break + + if subfilter['parameter'] not in KEY_COLUMNS: + parameter = sql.SQL("tags->{parameter}").format(parameter=sql.Literal(subfilter['parameter'])) + else: + parameter = sql.SQL("{parameter}").format(parameter=sql.Identifier(subfilter['parameter'])) + + if 'cast' in subfilter and subfilter['cast'] in ALLOWED_CASTS: + if subfilter['cast'] == 'cast_to_float': + parameter = sql.SQL("cast_to_float({parameter}, 0.0)").format(parameter=parameter) + elif subfilter['cast'] == 'cast_to_int': + parameter = sql.SQL("cast_to_int({parameter}, 0)").format(parameter=parameter) + else: + parameter = sql.SQL("CAST({parameter} AS {cast})").format(parameter=parameter, cast=sql.SQL(subfilter['cast'])) + + if 'value' not in subfilter or subfilter['value'] == '': + filter_query = sql.SQL("{filter_query} ({parameter} {comparison})").format(filter_query=filter_query, parameter=parameter, comparison=sql.SQL(subfilter['comparison'])) + else: + filter_query = sql.SQL("{filter_query} ({parameter} {comparison} {value})").format(filter_query=filter_query, parameter=parameter, comparison=sql.SQL(subfilter['comparison']), value=sql.Literal(subfilter['value'])) + + if i != len(filter['filters']) - 1: + filter_query = sql.SQL("{filter_query} {method}").format(filter_query=filter_query, method=sql.SQL(filter['method'])) + + i += 1 + + return filter_query + + @app.route('/intersection') @token_required def get_intersection(): @@ -118,14 +251,14 @@ def get_intersection(): first = filters[0] first_query = sql.SQL("SELECT name, ST_Centroid(way) AS point_geom, way AS geom FROM {table}").format(table=sql.SQL('planet_osm_line') if first['type'] == 'line' else sql.SQL('planet_osm_polygon') if first['type'] == 'polygon' else sql.SQL('planet_osm_point')) - first_filter = sql.SQL("WHERE ({filter})").format(filter=sql.SQL(first['filter'])) + first_filter = make_filter_query(first) first_assembled = sql.SQL("{query} {filter} {bbox}").format(query=first_query, filter=first_filter, bbox=bbox_filter) logger.info(f"Buffer: {buffer}\tFilters: {filters}\tBbox: [{l},{b},{r},{t}]") subqueries = [] for f in filters[1:]: - filter = sql.SQL("WHERE ({filter})").format(filter=sql.SQL(f['filter'])) + filter = make_filter_query(f) if f['type'] == 'point': query = sql.SQL("SELECT way AS geom FROM planet_osm_point") diff --git a/frontend/public/index.html b/frontend/public/index.html index b005db8..402fdb5 100644 --- a/frontend/public/index.html +++ b/frontend/public/index.html @@ -8,7 +8,7 @@ Bellingcat OpenStreetMap search - +