Skip to content
Snippets Groups Projects
App.vue 8.39 KiB
Newer Older
Simon Hager's avatar
Simon Hager committed
<template>
    <v-app>
        <v-app-bar class="px-6"
        ><strong class="site-title">OSM Place Search Logger</strong>
            <v-spacer></v-spacer>
Ciro Brodmann's avatar
Ciro Brodmann committed
            <a href="https://wiki.openstreetmap.org/wiki/OSM_Place_Search_Logger">About</a>
        </v-app-bar>
        <v-main fill-height>
            <v-container fluid class="fill-height primary">
                <v-row class="fill-height">
                    <v-col cols="6">
                        <v-card class="px-1 overflow-y-auto" flat>
                            <v-toolbar dense floating flat>
                                <v-text-field
                                        ref="searchField"
                                        hide-details
                                        single-line
                                        variant="outlined"
                                        v-model="search"
                                        label="Search"
                                        v-on:keyup.enter="onEnter"
                                        clearable
                                ></v-text-field>

                                <v-btn icon @click="onSearchClick">
                                    <v-icon>mdi-magnify</v-icon>
                                </v-btn>
                            </v-toolbar>
Ciro Brodmann's avatar
Ciro Brodmann committed
                            This web application stores your search queries in anonymised form and is used for
                            educational and research purposes in order to improve the awesome OpenStreetMap project. By
                            using this web application you agree with these conditions.
                        </v-card>
                        <div style="max-height: 70vh; overflow-y: auto">
                            <result-table
                                    :result-list="listItems"
                                    :on-submit-solution="submitSolution"
                                    :on-select-option="onSelection"
                            ></result-table>
                            <div class="no-entries-label" v-if="searchedWithoutResults">
                                No entries found
                            </div>
Ciro Brodmann's avatar
Ciro Brodmann committed
                        <div v-if="searchedWithoutResults || listItems.length !== 0" width="100%"
                             class="d-flex flex-row my-6 align-center flex-wrap" style="gap: 10px">
                            <v-text-field
                                    hide-details
                                    density="compact"
                                    placeholder="Any other comments?"
                                    v-model="comment"
                            ></v-text-field>
                            <v-btn :disabled="!selected" @click="submitSolution" class="selected-button">
Ciro Brodmann's avatar
Ciro Brodmann committed
                                Submit Solution
                            </v-btn>
                            <v-btn @click="noSolutionFound" class="bg-red-lighten-4"
                            >Entry not present
                            </v-btn>
                        </div>
                    </v-col>
                    <v-col cols="6">
                        <leaflet-map :poi="selectedGeoJson" @on-b-box-changed="onBBoxChanged"></leaflet-map>
                    </v-col>
                </v-row>
            </v-container>
        </v-main>
    </v-app>
Simon Hager's avatar
Simon Hager committed
</template>
<script setup lang="ts">
import {ref, computed, onMounted} from 'vue'
Simon Hager's avatar
Simon Hager committed
import type GeoJsonSearchResult from './types/geojson-search-result'
import LeafletMap from './LeafletMap.vue'
import ResultTable from './ResultTable.vue'
import Feature from './types/feature'
import BoundingBox from "./types/BoundingBox";
Simon Hager's avatar
Simon Hager committed

Simon Hager's avatar
Simon Hager committed
const sessionId = ref<string>()
const userId = ref<string>()
const rawSearchResult = ref<GeoJsonSearchResult>()

Simon Hager's avatar
Simon Hager committed
const search = ref<string>('')
Ciro Brodmann's avatar
Ciro Brodmann committed
const comment = ref<string>('')
Simon Hager's avatar
Simon Hager committed
const searchField = ref<HTMLElement | null>(null)

const boundingBox = ref<BoundingBox>()
const bBoxWhenSearched = ref<BoundingBox>()
Simon Hager's avatar
Simon Hager committed
const baseUrl = 'https://nominatim.openstreetmap.org/search.php'
Simon Hager's avatar
Simon Hager committed

Simon Hager's avatar
Simon Hager committed
function onEnter() {
    searchField.value?.blur()
    onSearchClick()
Simon Hager's avatar
Simon Hager committed
}

Simon Hager's avatar
Simon Hager committed
function onSelection(el: Feature) {
    console.log(el)
    selected.value = el
Simon Hager's avatar
Simon Hager committed
}

Simon Hager's avatar
Simon Hager committed
function onSearchClick() {
    getResults()
Simon Hager's avatar
Simon Hager committed
}

function onBBoxChanged(bbox) {
    boundingBox.value = bbox // this step might be unnecessary -> check if this is only a reference to the object from
                             // LeafletMap.vue. if so, we don't need to constantly reassign this value
const viewbox = computed(() => {
    return `${boundingBox.value.lonNE},${boundingBox.value.latNE},${boundingBox.value.lonSW},${boundingBox.value.latSW}`
})

const backendBaseUrl = 'https://osm-place-search-logger.infs.ch/api'
// const backendBaseUrl = 'http://localhost:8080/api'
Simon Hager's avatar
Simon Hager committed
function submitSolution(result: Feature) {
Ciro Brodmann's avatar
Ciro Brodmann committed
    let body = {
        userId: userId.value,
        sessionId: sessionId.value,
Ciro Brodmann's avatar
Ciro Brodmann committed
        rawSearchResult: rawSearchResult.value,
        selection: result,
        success: true,
        bbox: bBoxWhenSearched.value
Ciro Brodmann's avatar
Ciro Brodmann committed
    }

    if (comment.value !== '') {
        body = {...body, comment: comment.value}
    }

    fetch(`${backendBaseUrl}/submit`, {
        method: 'POST',
        mode: 'cors',
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
        },
Ciro Brodmann's avatar
Ciro Brodmann committed
        body: JSON.stringify(body)
    }).then(reset)
Simon Hager's avatar
Simon Hager committed
}
Simon Hager's avatar
Simon Hager committed

Simon Hager's avatar
Simon Hager committed
function noSolutionFound() {
Ciro Brodmann's avatar
Ciro Brodmann committed

    let body = {
        userId: userId.value,
        sessionId: sessionId.value,
Ciro Brodmann's avatar
Ciro Brodmann committed
        rawSearchResult: rawSearchResult.value,
        success: false,
        bbox: bBoxWhenSearched.value
Ciro Brodmann's avatar
Ciro Brodmann committed
    }

    if (comment.value !== '') {
        body = {...body, comment: comment.value}
    }

    fetch(`${backendBaseUrl}/submit`, {
Simon Hager's avatar
Simon Hager committed
        method: 'POST',
        mode: 'cors',
        headers: {
            'Content-Type': 'application/json',
            'Access-Control-Allow-Origin': '*'
Simon Hager's avatar
Simon Hager committed
        },
Ciro Brodmann's avatar
Ciro Brodmann committed
        body: JSON.stringify(body)
    }).then(reset)
}

function reset() {
    rawSearchResult.value = undefined
    search.value = ''
Ciro Brodmann's avatar
Ciro Brodmann committed
    comment.value = ''
    sessionId.value = crypto.randomUUID()
}

function getResults() {
    bBoxWhenSearched.value = {...boundingBox.value}
    selected.value = null;
    const query = queryString.value;
    const requestString = `${baseUrl}?q=${query}&format=geojson&limit=20&viewbox=${viewbox.value}`;

    fetch(requestString, {method: 'GET'})
        .then((res) => res.json())
        .then((json) => (rawSearchResult.value = json))
        .then(() =>
            fetch(`${backendBaseUrl}/search`, {
                method: 'POST',
                mode: 'cors',
                headers: {
                    'Content-Type': 'application/json',
                    'Access-Control-Allow-Origin': '*'
                },
                body: JSON.stringify({
                    userId: userId.value,
                    sessionId: sessionId.value,
                    rawSearchResult: rawSearchResult.value,
                    bbox: boundingBox.value
                })
            })
        )
    //.catch((err) => (error.value = err))
Simon Hager's avatar
Simon Hager committed
}

Simon Hager's avatar
Simon Hager committed
onMounted(() => {
    sessionId.value = crypto.randomUUID()
    userId.value = crypto.randomUUID()
Simon Hager's avatar
Simon Hager committed
})

const queryString = computed(() => {
    const query = search.value ? search.value : ''
    const encodedValue = encodeURIComponent(query)
    return encodedValue.replaceAll('%20', '+')
Simon Hager's avatar
Simon Hager committed
})
Simon Hager's avatar
Simon Hager committed

const listItems = computed(() => {
    return rawSearchResult.value?.features || ([] as Feature[])
Simon Hager's avatar
Simon Hager committed
})

Simon Hager's avatar
Simon Hager committed
const selected = ref<Feature | null>(null)
Simon Hager's avatar
Simon Hager committed

Simon Hager's avatar
Simon Hager committed
const selectedGeoJson = computed(() => {
    if (selected.value === null) {
        return {...rawSearchResult.value, features: []}
    }
Simon Hager's avatar
Simon Hager committed

    return {...rawSearchResult.value, features: [selected.value]}
Simon Hager's avatar
Simon Hager committed
})
Simon Hager's avatar
Simon Hager committed

const searchedWithoutResults = computed(() => {
    return rawSearchResult.value && listItems.value.length === 0
})

Simon Hager's avatar
Simon Hager committed
// https://nominatim.openstreetmap.org/search.php?q=oberer+gubel+48+jona&format=jsonv2
</script>
.site-title {
  color: #8c195f;
  font-size: 22px;
  text-decoration: none;
  font-weight: 500;
  font-size: 12pt;
  color: #717171;
  text-align: center;
  margin: 20px 0;
Ciro Brodmann's avatar
Ciro Brodmann committed

.selected-button {
  background: #7d92f5 !important;
  color: white;

  &.v-btn--disabled {
    background-color: lighten(#7d92f5, 20) !important;
  }
Ciro Brodmann's avatar
Ciro Brodmann committed
}
</style>