Revise custom feature selector, API methods

This commit is contained in:
Logan Williams
2023-03-31 09:44:10 +02:00
parent 0d9474bfe0
commit 0cb77b7b18
9 changed files with 743 additions and 116 deletions

View File

@@ -8,7 +8,7 @@
<title>Bellingcat OpenStreetMap search</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@latest/css/materialdesignicons.min.css">
<script defer data-domain="osm.baarle-hertog.xyz" src="https://plausible.io/js/script.js"></script>
<script defer data-domain="osm-search.bellingcat.com" src="https://plausible.io/js/script.js"></script>
</head>
<body>
<noscript>

View File

@@ -31,6 +31,9 @@ export default {
SearchControls,
GoogleLogin,
},
mounted() {
this.$store.dispatch("getKeys");
},
};
</script>

View File

@@ -0,0 +1,124 @@
<template>
<v-card style="margin-top: 1em">
<v-card-title>Custom feature</v-card-title>
<v-card-text>
<v-row v-for="(f, i) in filters" :key="'row' + i">
<v-col cols="3"
><v-select
v-if="i == 0"
label="Feature type"
:items="queryTypes"
v-model="selectedQueryType"
></v-select>
<v-select
v-else-if="i == 1"
label="Condition"
:items="['OR', 'AND']"
v-model="method"
></v-select> </v-col
><v-col>
<!-- Text field for OSM parameter -->
<v-combobox
class="code"
label="OSM key"
v-model="f.parameter"
:items="$store.state.osmKeys"
@input="getValues"
></v-combobox
></v-col>
<v-col>
<!-- Dropdown for type of comparison between parameter and value -->
<v-select
label=""
:items="[
'=',
'!=',
'>',
'<',
'>=',
'<=',
'starts with',
'ends with',
'contains',
'does not contain',
'is null',
'is not null',
]"
v-model="f.comparison"
></v-select>
</v-col>
<v-col>
<!-- Text field for parameter value -->
<v-combobox
class="code"
label="OSM value"
v-model="f.value"
:items="$store.state.selectedKeyValues"
:disabled="
f.comparison == 'is null' || f.comparison == 'is not null'
"
></v-combobox>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn color="secondary" text @click="addFilter">Add condition</v-btn>
<v-btn style="margin-left: auto" color="primary" text @click="addCustom"
>Add custom feature</v-btn
>
</v-card-actions>
</v-card>
</template>
<script>
export default {
name: "FeatureCustom",
data() {
return {
queryTypes: ["point", "line", "polygon"],
selectedQueryType: "point",
method: "OR",
filters: [
{
parameter: "",
comparison: "=",
value: "",
},
],
};
},
methods: {
addCustom() {
this.$store.commit("updateSelected", [
...this.$store.state.selected,
{
name: "Custom filter",
type: this.selectedQueryType,
filters: this.filters.filter((v) => v.parameter != ""),
method: this.method,
},
]);
this.method = "OR";
this.filters = [
{
parameter: "",
comparison: "=",
value: "",
},
];
this.selectedQueryType = "point";
},
addFilter() {
this.filters.push({
parameter: "",
comparison: "=",
value: "",
});
},
getValues(v) {
this.$store.dispatch("getValues", v);
},
},
};
</script>

View File

@@ -11,33 +11,12 @@
<v-card-title>Selected features</v-card-title>
<v-card-text>
<v-col>
<v-card
<FeatureView
v-for="(query, i) in $store.state.selected"
:key="query.name + query.type"
close
style="margin-bottom: 1em"
>
<v-card-title
:color="
query.type == 'point'
? '#8BC34A'
: query.type == 'line'
? '#00BCD4'
: '#FFC107'
"
>
{{ query.name }}&nbsp;
<span class="type">({{ query.type }})</span>
</v-card-title>
<v-card-text>
<span class="code">
{{ query.filter }}
</span>
</v-card-text>
<v-card-actions>
<v-btn color="red" text @click="remove(i)"> Remove </v-btn>
</v-card-actions>
</v-card>
:query="query"
:index="i"
/>
</v-col>
</v-card-text>
</v-card>
@@ -64,44 +43,25 @@
>
</v-card-text>
</v-card>
<v-card style="margin-top: 1em">
<v-card-title>Custom feature</v-card-title>
<v-card-text>
<v-row>
<v-col cols="3"
><v-select
label="Feature type"
:items="queryTypes"
v-model="selectedQueryType"
></v-select> </v-col
><v-col>
<v-text-field
class="code"
label="Filter statement"
v-model="customFilter"
></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-btn color="primary" text @click="addCustom">Add</v-btn>
</v-card-actions>
</v-card>
<FeatureCustom />
</v-col>
</v-row>
</template>
<script>
import queries from "./queries.js";
import FeatureView from "./FeatureView.vue";
import FeatureCustom from "./FeatureCustom.vue";
export default {
name: "FeatureSelector",
components: {
FeatureView,
FeatureCustom,
},
data() {
return {
queries,
queryTypes: ["point", "line", "polygon"],
selectedQueryType: "point",
customFilter: "",
accepting: false,
};
},
@@ -127,28 +87,6 @@ export default {
startDrag(e, item) {
e.dataTransfer.setData("object", JSON.stringify(item));
},
remove(index) {
let value = this.$store.state.selected;
let newValue = [
...value.slice(0, index),
...value.slice(index + 1, value.length),
];
console.log(index, value, newValue);
this.$store.commit("updateSelected", newValue);
},
addCustom() {
this.$store.commit("updateSelected", [
...this.$store.state.selected,
{
name: "Custom filter",
type: this.selectedQueryType,
filter: this.customFilter,
},
]);
this.customFilter = "";
this.selectedQueryType = "point";
},
},
};
</script>

View File

@@ -0,0 +1,49 @@
<template>
<v-card close style="margin-bottom: 1em">
<v-card-title>
{{ query.name }}&nbsp;
<span class="type">({{ query.type }})</span>
</v-card-title>
<v-card-text>
<div v-for="(f, i) in query.filters" :key="i">
<div class="code">
{{ (i == 0 ? "" : query.method + " ") + f.parameter
}}<a
:href="'https://wiki.openstreetmap.org/wiki/Key:' + f.parameter"
target="_blank"
class="super"
>
<v-icon x-small>mdi-open-in-new</v-icon></a
>
{{ f.comparison }} {{ f.value }}
</div>
</div>
</v-card-text>
<v-card-actions>
<v-btn color="red" text @click="remove(index)"> Remove </v-btn>
</v-card-actions>
</v-card>
</template>
<script>
export default {
name: "FeatureView",
props: {
query: Object,
index: Number,
},
methods: {
remove(index) {
this.$store.commit("removeSelected", index);
},
},
};
</script>
<style>
.super {
font-size: 0.8em;
vertical-align: super;
text-decoration: none;
}
</style>

View File

@@ -2,177 +2,503 @@ export default [
{
name: "Power pylon",
type: "point",
filter: "power = 'tower' OR power = 'pole'",
method: "OR",
filters: [
{
parameter: "power",
comparison: "=",
value: "tower",
},
{
parameter: "power",
comparison: "=",
value: "pole",
},
],
},
{
name: "Public transport stop",
type: "point",
filter: "(public_transport IS NOT null OR highway='bus_stop')",
method: "OR",
filters: [
{
parameter: "public_transport",
comparison: "is not null",
},
{
parameter: "highway",
comparison: "=",
value: "bus_stop",
},
],
},
{
name: "Church",
type: "point",
filter: "amenity = 'place_of_worship'",
method: "OR",
filters: [
{
parameter: "amenity",
comparison: "=",
value: "place_of_worship",
},
],
},
{
name: "Hospital",
type: "point",
filter: "amenity = 'hospital'",
method: "OR",
filters: [
{
parameter: "amenity",
comparison: "=",
value: "hospital",
},
],
},
{
name: "Military",
type: "point",
filter: "military IS NOT null OR landuse = 'military'",
method: "OR",
filters: [
{
parameter: "military",
comparison: "is not null",
},
{
parameter: "landuse",
comparison: "=",
value: "military",
},
],
},
{
name: "Restaurant",
type: "point",
filter:
"amenity = 'restaurant' OR amenity = 'cafe' OR amenity = 'pub' OR amenity = 'fast_food'",
method: "OR",
filters: [
{
parameter: "amenity",
comparison: "=",
value: "restaurant",
},
{
parameter: "amenity",
comparison: "=",
value: "cafe",
},
{
parameter: "amenity",
comparison: "=",
value: "pub",
},
{
parameter: "amenity",
comparison: "=",
value: "fast_food",
},
],
},
{
name: "Waterway",
type: "line",
filter: "waterway IS NOT null",
method: "OR",
filters: [
{
parameter: "waterway",
comparison: "is not null",
},
],
},
{
name: "Road",
type: "line",
filter: "highway IS NOT null",
method: "OR",
filters: [
{
parameter: "highway",
comparison: "is not null",
},
],
},
{
name: "Railroad",
type: "line",
filter: "railway IS NOT null",
method: "OR",
filters: [
{
parameter: "railway",
comparison: "is not null",
},
],
},
{
name: "Bridge",
type: "line",
filter: "bridge IS NOT null",
method: "OR",
filters: [
{
parameter: "bridge",
comparison: "is not null",
},
],
},
{
name: "Road (motorway)",
type: "line",
filter: "highway = 'motorway' OR highway = 'motorway_link'",
method: "OR",
filters: [
{
parameter: "highway",
comparison: "=",
value: "motorway",
},
{
parameter: "highway",
comparison: "=",
value: "motorway_link",
},
],
},
{
name: "Road (primary)",
type: "line",
filter: "highway = 'primary' OR highway = 'primary_link'",
method: "OR",
filters: [
{
parameter: "highway",
comparison: "=",
value: "primary",
},
{
parameter: "highway",
comparison: "=",
value: "primary_link",
},
],
},
{
name: "Road (secondary)",
type: "line",
filter: "highway = 'secondary' OR highway = 'secondary_link",
method: "OR",
filters: [
{
parameter: "highway",
comparison: "=",
value: "secondary",
},
{
parameter: "highway",
comparison: "=",
value: "secondary_link",
},
],
},
{
name: "Road (residential)",
type: "line",
filter: "highway = 'residential'",
method: "OR",
filters: [
{
parameter: "highway",
comparison: "=",
value: "residential",
},
],
},
{
name: "Unpaved road",
type: "line",
filter: "surface = 'unpaved'",
method: "OR",
filters: [
{
parameter: "surface",
comparison: "=",
value: "unpaved",
},
],
},
{
name: "1-lane road",
type: "line",
filter: "tags->'lanes' = '1'",
method: "OR",
filters: [
{
parameter: "lanes",
comparison: "=",
value: "1",
},
],
},
{
name: "2-lane road",
type: "line",
filter: "tags->'lanes' = '2'",
method: "OR",
filters: [
{
parameter: "lanes",
comparison: "=",
value: "2",
},
],
},
{
name: "3-lane road",
type: "line",
filter: "tags->'lanes' = '3'",
method: "OR",
filters: [
{
parameter: "lanes",
comparison: "=",
value: "3",
},
],
},
{
name: "4-lane road",
type: "line",
filter: "tags->'lanes' = '4'",
method: "OR",
filters: [
{
parameter: "lanes",
comparison: "=",
value: "4",
},
],
},
{
name: "5-lane road",
type: "line",
filter: "tags->'lanes' = '5'",
method: "OR",
filters: [
{
parameter: "lanes",
comparison: "=",
value: "5",
},
],
},
{
name: "6-lane road",
type: "line",
filter: "tags->'lanes' = '6'",
method: "OR",
filters: [
{
parameter: "lanes",
comparison: "=",
value: "6",
},
],
},
{
name: "Sidewalk",
type: "line",
method: "AND",
filters: [
{
parameter: "sidewalk",
comparison: "is not null",
},
{
parameter: "sidewalk",
comparison: "!=",
value: "no",
},
],
},
{
name: "Cliff",
type: "line",
filter: "planet_osm_line.natural = 'cliff'",
method: "OR",
filters: [
{
parameter: "natural",
comparison: "=",
value: "cliff",
},
],
},
{
name: "Park",
type: "polygon",
filter: "leisure = 'park'",
method: "OR",
filters: [
{
parameter: "leisure",
comparison: "=",
value: "park",
},
],
},
{
name: "Industrial area",
type: "polygon",
filter: "landuse = 'industrial'",
method: "OR",
filters: [
{
parameter: "landuse",
comparison: "=",
value: "industrial",
},
],
},
{
name: "Body of water",
type: "polygon",
filter: "water IS NOT null",
method: "OR",
filters: [
{
parameter: "water",
comparison: "is not null",
},
],
},
{
name: "Forest",
type: "polygon",
filter: "landuse = 'forest' OR planet_osm_polygon.natural = 'forest'",
method: "OR",
filters: [
{
parameter: "landuse",
comparison: "=",
value: "forest",
},
{
parameter: "natural",
comparison: "=",
value: "forest",
},
],
},
{
name: "Farmland",
type: "polygon",
filter: "landuse = 'farmland'",
method: "OR",
filters: [
{
parameter: "landuse",
comparison: "=",
value: "farmland",
},
],
},
{
name: "Building",
type: "polygon",
filter: "building IS NOT null",
method: "OR",
filters: [
{
parameter: "building",
comparison: "is not null",
},
],
},
{
name: "Building (1 story)",
type: "polygon",
filter: "tags->'building:levels' = '1'",
method: "OR",
filters: [
{
parameter: "building:levels",
comparison: "=",
value: "1",
},
],
},
{
name: "Building (2 story)",
type: "polygon",
filter: "tags->'building:levels' = '2'",
method: "OR",
filters: [
{
parameter: "building:levels",
comparison: "=",
value: "2",
},
],
},
{
name: "Building (3 story)",
type: "polygon",
filter: "tags->'building:levels' = '3'",
method: "OR",
filters: [
{
parameter: "building:levels",
comparison: "=",
value: "3",
},
],
},
{
name: "Building (4 story)",
type: "polygon",
filter: "tags->'building:levels' = '4'",
method: "OR",
filters: [
{
parameter: "building:levels",
comparison: "=",
value: "4",
},
],
},
{
name: "Building (5+ stories)",
name: "Building (5-9 stories)",
type: "polygon",
filter: "(tags->'building:levels')::integer >= 5",
method: "AND",
filters: [
{
parameter: "building:levels",
comparison: ">=",
value: "5",
cast: "cast_to_float",
},
{
parameter: "building:levels",
comparison: "<",
value: "10",
cast: "cast_to_float",
},
],
},
{
name: "Building (10+ stories)",
type: "polygon",
method: "OR",
filters: [
{
parameter: "building:levels",
comparison: ">=",
value: "10",
cast: "cast_to_float",
},
],
},
{
name: "Beach",
type: "polygon",
filter: "planet_osm_polygon.natural = 'beach'",
method: "OR",
filters: [
{
parameter: "natural",
comparison: "=",
value: "beach",
},
],
},
{
name: "Military",
type: "polygon",
filter: "military IS NOT null OR landuse = 'military'",
method: "OR",
filters: [
{
parameter: "military",
comparison: "is not null",
},
{
parameter: "landuse",
comparison: "=",
value: "military",
},
],
},
];

View File

@@ -19,6 +19,8 @@ export default new Vuex.Store({
mapCenter: [42.2, -71.7],
mapZoom: 8,
responseTime: null,
osmKeys: [],
selectedKeyValues: [],
},
mutations: {
initializeCredentials(state) {
@@ -49,6 +51,14 @@ export default new Vuex.Store({
updateSelected(state, value) {
state.selected = [...value];
},
removeSelected(state, index) {
let value = state.selected;
let newValue = [
...value.slice(0, index),
...value.slice(index + 1, value.length),
];
state.selected = newValue;
},
setSearchResults(state, data) {
state.searchResults = data;
},
@@ -82,8 +92,51 @@ export default new Vuex.Store({
setResponseTime(state, t) {
state.responseTime = t;
},
setKeys(state, keys) {
state.osmKeys = keys;
},
setSelectedKeyValues(state, values) {
state.selectedKeyValues = values;
},
},
actions: {
getKeys({ commit }) {
fetch(
"https://taginfo.openstreetmap.org/api/4/keys/all?page=1&rp=200&filter=in_wiki&sortname=count_all&sortorder=desc"
)
.then((d) => {
if (d.status != 200) {
return Promise.reject(Error(d.status));
}
return d.json();
})
.then((data) => {
commit(
"setKeys",
data.data.map((d) => d.key)
);
});
},
getValues({ commit }, v) {
commit("setSelectedKeyValues", []);
fetch(
"https://taginfo.openstreetmap.org/api/4/key/values?rp=50&sortname=count_all&sortorder=desc&key=" +
v
)
.then((d) => {
if (d.status != 200) {
return Promise.reject(Error(d.status));
}
return d.json();
})
.then((data) => {
commit(
"setSelectedKeyValues",
data.data.filter((d) => d.fraction > 0.01).map((d) => d.value)
);
});
},
search({ state, commit }) {
let bbox = state.bbox;
let range = state.range;
@@ -94,7 +147,8 @@ export default new Vuex.Store({
let time1 = performance.now();
fetch(
`https://api.baarle-hertog.xyz/intersection?l=${bbox[0][1]}&b=${bbox[0][0]}&r=${bbox[1][1]}&t=${bbox[1][0]}&buffer=${range}&filters=${filters}`,
// `https://api.osm-search.bellingcat.com/intersection?l=${bbox[0][1]}&b=${bbox[0][0]}&r=${bbox[1][1]}&t=${bbox[1][0]}&buffer=${range}&filters=${filters}`,
`http://localhost:5050/intersection?l=${bbox[0][1]}&b=${bbox[0][0]}&r=${bbox[1][1]}&t=${bbox[1][0]}&buffer=${range}&filters=${filters}`,
{
headers: {
Authorization: "Bearer " + state.token,