2023-09-12 09:15:28 +00:00
|
|
|
<script setup lang="ts">
|
|
|
|
import WithRender from "./overpass-form.vue";
|
|
|
|
import Vue from "vue";
|
|
|
|
import { Component, Watch } from "vue-property-decorator";
|
|
|
|
import { InjectMapComponents, InjectMapContext } from "../../utils/decorators";
|
|
|
|
import { getOverpassPreset, OverpassPreset, overpassPresets, validateOverpassQuery } from "facilmap-leaflet";
|
|
|
|
import { MapComponents, MapContext } from "../leaflet-map/leaflet-map";
|
|
|
|
import "./overpass-form.scss";
|
|
|
|
import { debounce } from "lodash-es";
|
|
|
|
|
|
|
|
@WithRender
|
|
|
|
@Component({ })
|
|
|
|
export default class OverpassForm extends Vue {
|
|
|
|
|
2023-10-23 21:05:19 +00:00
|
|
|
const mapComponents = injectMapComponentsRequired();
|
|
|
|
const mapContext = injectMapContextRequired();
|
2023-09-12 09:15:28 +00:00
|
|
|
|
|
|
|
activeTab = 0;
|
|
|
|
searchTerm = "";
|
|
|
|
customQuery = "";
|
|
|
|
customQueryValidationState: boolean | null = null;
|
|
|
|
customQueryValidationError: string | null = null;
|
|
|
|
customQueryAbortController?: AbortController;
|
|
|
|
|
|
|
|
created(): void {
|
|
|
|
this.validateCustomQueryDebounced = debounce(this.validateCustomQuery, 500);
|
|
|
|
}
|
|
|
|
|
|
|
|
get categories(): (typeof overpassPresets) {
|
|
|
|
return overpassPresets.map((cat) => {
|
|
|
|
const presets = cat.presets.map((presets) => presets.map((preset) => ({ ...preset, isChecked: this.mapContext.overpassPresets.includes(preset) })));
|
|
|
|
return {
|
|
|
|
...cat,
|
|
|
|
presets,
|
|
|
|
checked: presets.flat().filter((preset) => preset.isChecked).length
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
get filteredPresets(): OverpassPreset[] {
|
|
|
|
if (!this.searchTerm)
|
|
|
|
return [];
|
|
|
|
|
|
|
|
const lowerTerm = this.searchTerm.toLowerCase();
|
|
|
|
return this.categories.map((cat) => cat.presets).flat().flat().filter((preset) => preset.label.toLowerCase().includes(lowerTerm));
|
|
|
|
}
|
|
|
|
|
|
|
|
@Watch("mapContext.overpassCustom", { immediate: true })
|
|
|
|
handleCustomQueryChange(customQuery: string): void {
|
|
|
|
this.customQuery = customQuery;
|
|
|
|
}
|
|
|
|
|
|
|
|
togglePreset(key: string, enable: boolean): void {
|
|
|
|
const without = this.mapContext.overpassPresets.filter((p) => p.key != key);
|
|
|
|
this.mapComponents.overpassLayer.setQuery([
|
|
|
|
...without,
|
|
|
|
...(enable ? [getOverpassPreset(key)!] : [])
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleIsCustom(): void {
|
|
|
|
if (this.mapContext.overpassIsCustom) {
|
|
|
|
this.mapComponents.overpassLayer.setQuery(this.mapContext.overpassPresets);
|
|
|
|
if (this.customQueryAbortController)
|
|
|
|
this.customQueryAbortController.abort();
|
|
|
|
} else {
|
|
|
|
this.mapComponents.overpassLayer.setQuery(this.mapContext.overpassCustom);
|
|
|
|
this.validateCustomQuery();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleCustomQueryInput(): void {
|
|
|
|
if (this.customQueryAbortController)
|
|
|
|
this.customQueryAbortController.abort();
|
|
|
|
this.validateCustomQueryDebounced();
|
|
|
|
}
|
|
|
|
|
|
|
|
async validateCustomQuery(): Promise<void> {
|
|
|
|
const query = this.customQuery;
|
2021-05-15 20:05:07 +00:00
|
|
|
|
2023-09-12 09:15:28 +00:00
|
|
|
if (!query) {
|
|
|
|
this.customQueryValidationState = null;
|
|
|
|
this.customQueryValidationError = null;
|
|
|
|
} else {
|
|
|
|
const abortController = new AbortController();
|
|
|
|
this.customQueryAbortController = abortController;
|
|
|
|
const result = await validateOverpassQuery(query, this.customQueryAbortController.signal);
|
|
|
|
if (!abortController.signal.aborted) {
|
|
|
|
this.customQueryValidationState = !result;
|
|
|
|
this.customQueryValidationError = result ?? null;
|
|
|
|
} else
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.customQueryValidationState !== false)
|
|
|
|
this.mapComponents.overpassLayer.setQuery(query);
|
|
|
|
}
|
|
|
|
|
|
|
|
validateCustomQueryDebounced!: () => void;
|
|
|
|
|
|
|
|
}
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<div class="fm-overpass-form">
|
|
|
|
<template v-if="!mapContext.overpassIsCustom">
|
2023-10-24 00:08:47 +00:00
|
|
|
<input class="form-control" type="search" v-model="searchTerm" placeholder="Filter…" autofocus />
|
2023-09-12 09:15:28 +00:00
|
|
|
<hr />
|
|
|
|
|
|
|
|
<div v-if="searchTerm" class="checkbox-grid">
|
|
|
|
<b-form-checkbox
|
|
|
|
v-for="preset in filteredPresets"
|
|
|
|
:checked="preset.isChecked"
|
|
|
|
@input="togglePreset(preset.key, $event)"
|
|
|
|
>{{preset.label}}</b-form-checkbox>
|
|
|
|
</div>
|
|
|
|
|
2023-10-25 02:09:33 +00:00
|
|
|
<ul class="nav nav-pills">
|
|
|
|
<template v-for="(category, idx) in categories">
|
|
|
|
<li class="nav-item">
|
|
|
|
<a
|
|
|
|
href="javascript:"
|
|
|
|
class="nav-link"
|
|
|
|
:class="{ active: activeTab === idx }"
|
|
|
|
@click="activeTab = idx"
|
|
|
|
>
|
|
|
|
{{category.label}}
|
|
|
|
<b-badge v-if="category.checked > 0" :variant="activeTab == idx ? 'secondary' : 'primary'">{{category.checked}}</b-badge>
|
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</template>
|
|
|
|
</ul>
|
|
|
|
|
|
|
|
<template v-for="presets in categories[activeTab].presets">
|
|
|
|
<hr />
|
|
|
|
<div class="checkbox-grid">
|
|
|
|
<b-form-checkbox
|
|
|
|
v-for="preset in presets"
|
|
|
|
:checked="preset.isChecked"
|
|
|
|
@input="togglePreset(preset.key, $event)"
|
|
|
|
>{{preset.label}}</b-form-checkbox>
|
|
|
|
</div>
|
|
|
|
</template>
|
2023-09-12 09:15:28 +00:00
|
|
|
</template>
|
|
|
|
<template v-else>
|
|
|
|
<b-form-group :state="customQueryValidationState">
|
2023-10-25 02:09:33 +00:00
|
|
|
<textarea v-model="customQuery" rows="5" :state="customQueryValidationState" class="form-control text-monospace" @input="handleCustomQueryInput"></textarea>
|
2023-09-12 09:15:28 +00:00
|
|
|
<template #invalid-feedback><pre>{{customQueryValidationError}}</pre></template>
|
|
|
|
</b-form-group>
|
|
|
|
|
|
|
|
<hr />
|
|
|
|
|
|
|
|
<p>
|
|
|
|
Enter an <a href="https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#The_Query_Statement" target="_blank">Overpass query statement</a>
|
|
|
|
here. Settings and an <code>out</code> statement are added automatically in the background. For ways and relations, a marker will be shown at
|
|
|
|
the geometric centre, no lines or polygons are drawn.
|
|
|
|
</p>
|
|
|
|
<p>
|
|
|
|
Example queries are <code>nwr[amenity=parking]</code> to get parking places or
|
|
|
|
<code>(nwr[amenity=atm];nwr[amenity=bank][atm][atm!=no];)</code> for ATMs.
|
|
|
|
</p>
|
|
|
|
</template>
|
2021-05-15 16:26:45 +00:00
|
|
|
|
|
|
|
<hr />
|
|
|
|
|
2023-09-12 09:15:28 +00:00
|
|
|
<b-button-toolbar>
|
2023-10-24 00:08:47 +00:00
|
|
|
<button
|
|
|
|
type="button"
|
|
|
|
class="btn btn-light"
|
|
|
|
:class="{ active: mapContext.overpassIsCustom }"
|
2023-09-12 09:15:28 +00:00
|
|
|
@click="toggleIsCustom()"
|
2023-10-24 00:08:47 +00:00
|
|
|
>Custom query</button>
|
2023-09-12 09:15:28 +00:00
|
|
|
</b-button-toolbar>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
.fm-overpass-form {
|
|
|
|
display: flex;
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
|
|
.checkbox-grid {
|
|
|
|
column-width: 160px;
|
|
|
|
padding: 0 .25rem;
|
|
|
|
}
|
|
|
|
|
|
|
|
pre {
|
|
|
|
color: inherit;
|
|
|
|
font-size: inherit;
|
|
|
|
}
|
|
|
|
|
|
|
|
fieldset + hr, p + hr {
|
|
|
|
margin-top: 0;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|