Introduce Mimirsbrunn geocoder and improve addresses & maps
Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
@@ -1,125 +1,242 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-field :label="$t('Find an address')">
|
||||
<b-field expanded>
|
||||
<template slot="label">
|
||||
{{ $t('Find an address') }}
|
||||
<b-button v-if="!gettingLocation" size="is-small" icon-right="map-marker" @click="locateMe" />
|
||||
<span v-else>{{ $t('Getting location') }}</span>
|
||||
</template>
|
||||
<b-autocomplete
|
||||
:data="data"
|
||||
v-model="queryText"
|
||||
:placeholder="$t('e.g. 10 Rue Jangot')"
|
||||
field="description"
|
||||
field="fullName"
|
||||
:loading="isFetching"
|
||||
@typing="getAsyncData"
|
||||
icon="map-marker"
|
||||
@select="option => selected = option">
|
||||
expanded
|
||||
@select="updateSelected">
|
||||
|
||||
<template slot-scope="{option}">
|
||||
<b>{{ option.description }}</b><br />
|
||||
<i v-if="option.url != null">Local</i>
|
||||
<p>
|
||||
<small>{{ option.street }},  {{ option.postalCode }} {{ option.locality }}</small>
|
||||
</p>
|
||||
<b-icon :icon="option.poiInfos.poiIcon.icon" />
|
||||
<b>{{ option.poiInfos.name }}</b><br />
|
||||
<small>{{ option.poiInfos.alternativeName }}</small>
|
||||
</template>
|
||||
<template slot="empty">
|
||||
<span v-if="queryText.length < 5">{{ $t('Please type at least 5 characters') }}</span>
|
||||
<span v-else-if="isFetching">{{ $t('Searching…') }}</span>
|
||||
<span v-if="isFetching">{{ $t('Searching…') }}</span>
|
||||
<div v-else class="is-enabled">
|
||||
<span>{{ $t('No results for "{queryText}"', { queryText }) }}</span>
|
||||
<p class="control" @click="addressModalActive = true">
|
||||
<button type="button" class="button is-primary">{{ $t('Add') }}</button>
|
||||
</p>
|
||||
<span>{{ $t('No results for "{queryText}". You can try another search term or drag and drop the marker on the map', { queryText }) }}</span>
|
||||
<!-- <p class="control" @click="openNewAddressModal">-->
|
||||
<!-- <button type="button" class="button is-primary">{{ $t('Add') }}</button>-->
|
||||
<!-- </p>-->
|
||||
</div>
|
||||
</template>
|
||||
</b-autocomplete>
|
||||
</b-field>
|
||||
<b-modal :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">
|
||||
<div class="modal-card" style="width: auto">
|
||||
<header class="modal-card-head">
|
||||
<p class="modal-card-title">{{ $t('Add an address') }}</p>
|
||||
</header>
|
||||
<section class="modal-card-body">
|
||||
<form>
|
||||
<b-field :label="$t('Name')">
|
||||
<b-input aria-required="true" required v-model="selected.description" />
|
||||
</b-field>
|
||||
<div class="map" v-if="selected && selected.geom">
|
||||
<map-leaflet
|
||||
:coords="selected.geom"
|
||||
:marker="{ text: [selected.poiInfos.name, selected.poiInfos.alternativeName], icon: selected.poiInfos.poiIcon.icon}"
|
||||
:updateDraggableMarkerCallback="reverseGeoCode"
|
||||
:options="{ zoom: mapDefaultZoom }"
|
||||
:readOnly="false"
|
||||
/>
|
||||
</div>
|
||||
<!-- <b-modal v-if="selected" :active.sync="addressModalActive" :width="640" has-modal-card scroll="keep">-->
|
||||
<!-- <div class="modal-card" style="width: auto">-->
|
||||
<!-- <header class="modal-card-head">-->
|
||||
<!-- <p class="modal-card-title">{{ $t('Add an address') }}</p>-->
|
||||
<!-- </header>-->
|
||||
<!-- <section class="modal-card-body">-->
|
||||
<!-- <form>-->
|
||||
<!-- <b-field :label="$t('Name')">-->
|
||||
<!-- <b-input aria-required="true" required v-model="selected.description" />-->
|
||||
<!-- </b-field>-->
|
||||
|
||||
<b-field :label="$t('Street')">
|
||||
<b-input v-model="selected.street" />
|
||||
</b-field>
|
||||
<!-- <b-field :label="$t('Street')">-->
|
||||
<!-- <b-input v-model="selected.street" />-->
|
||||
<!-- </b-field>-->
|
||||
|
||||
<b-field :label="$t('Postal Code')">
|
||||
<b-input v-model="selected.postalCode" />
|
||||
</b-field>
|
||||
<!-- <b-field grouped>-->
|
||||
<!-- <b-field :label="$t('Postal Code')">-->
|
||||
<!-- <b-input v-model="selected.postalCode" />-->
|
||||
<!-- </b-field>-->
|
||||
|
||||
<b-field :label="$t('Locality')">
|
||||
<b-input v-model="selected.locality" />
|
||||
</b-field>
|
||||
<!-- <b-field :label="$t('Locality')">-->
|
||||
<!-- <b-input v-model="selected.locality" />-->
|
||||
<!-- </b-field>-->
|
||||
<!-- </b-field>-->
|
||||
|
||||
<b-field :label="$t('Region')">
|
||||
<b-input v-model="selected.region" />
|
||||
</b-field>
|
||||
<!-- <b-field grouped>-->
|
||||
<!-- <b-field :label="$t('Region')">-->
|
||||
<!-- <b-input v-model="selected.region" />-->
|
||||
<!-- </b-field>-->
|
||||
|
||||
<b-field :label="$t('Country')">
|
||||
<b-input v-model="selected.country" />
|
||||
</b-field>
|
||||
</form>
|
||||
</section>
|
||||
<footer class="modal-card-foot">
|
||||
<button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>
|
||||
</footer>
|
||||
</div>
|
||||
</b-modal>
|
||||
<!-- <b-field :label="$t('Country')">-->
|
||||
<!-- <b-input v-model="selected.country" />-->
|
||||
<!-- </b-field>-->
|
||||
<!-- </b-field>-->
|
||||
<!-- </form>-->
|
||||
<!-- </section>-->
|
||||
<!-- <footer class="modal-card-foot">-->
|
||||
<!-- <button class="button" type="button" @click="resetPopup()">{{ $t('Clear') }}</button>-->
|
||||
<!-- </footer>-->
|
||||
<!-- </div>-->
|
||||
<!-- </b-modal>-->
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
|
||||
import { Address, IAddress } from '@/types/address.model';
|
||||
import { ADDRESS } from '@/graphql/address';
|
||||
import { ADDRESS, REVERSE_GEOCODE } from '@/graphql/address';
|
||||
import { Modal } from 'buefy/dist/components/dialog';
|
||||
import { LatLng } from 'leaflet';
|
||||
|
||||
@Component({
|
||||
components: {
|
||||
'map-leaflet': () => import(/* webpackChunkName: "map" */ '@/components/Map.vue'),
|
||||
Modal,
|
||||
},
|
||||
})
|
||||
export default class AddressAutoComplete extends Vue {
|
||||
|
||||
@Prop({ required: false, default: () => [] }) initialData!: IAddress[];
|
||||
@Prop({ required: false }) value!: IAddress;
|
||||
@Prop({ required: true }) value!: IAddress;
|
||||
|
||||
data: IAddress[] = this.initialData;
|
||||
selected: IAddress|null = new Address();
|
||||
data: IAddress[] = [];
|
||||
selected!: IAddress;
|
||||
isFetching: boolean = false;
|
||||
queryText: string = this.value && this.value.description || '';
|
||||
queryText: string = this.value && (new Address(this.value)).fullName || '';
|
||||
addressModalActive: boolean = false;
|
||||
private gettingLocation: boolean = false;
|
||||
private location!: Position;
|
||||
private gettingLocationError: any;
|
||||
private mapDefaultZoom: number = 15;
|
||||
|
||||
@Watch('value')
|
||||
updateEditing() {
|
||||
this.selected = this.value;
|
||||
const address = new Address(this.selected);
|
||||
this.queryText = `${address.poiInfos.name} ${address.poiInfos.alternativeName}`;
|
||||
}
|
||||
|
||||
async getAsyncData(query) {
|
||||
if (query.length < 5) {
|
||||
if (!query.length) {
|
||||
this.data = [];
|
||||
this.selected = new Address();
|
||||
return;
|
||||
}
|
||||
|
||||
if (query.length < 3) {
|
||||
this.data = [];
|
||||
return;
|
||||
}
|
||||
this.isFetching = true;
|
||||
const result = await this.$apollo.query({
|
||||
query: ADDRESS,
|
||||
fetchPolicy: 'no-cache',
|
||||
variables: { query },
|
||||
fetchPolicy: 'network-only',
|
||||
variables: {
|
||||
query,
|
||||
locale: this.$i18n.locale,
|
||||
},
|
||||
});
|
||||
|
||||
this.data = result.data.searchAddress as IAddress[];
|
||||
this.data = result.data.searchAddress.map(address => new Address(address));
|
||||
this.isFetching = false;
|
||||
}
|
||||
|
||||
// Watch deep because of subproperties
|
||||
@Watch('selected', { deep: true })
|
||||
updateSelected() {
|
||||
updateSelected(option) {
|
||||
if (option == null) return;
|
||||
this.selected = option;
|
||||
console.log('update selected', this.selected);
|
||||
this.$emit('input', this.selected);
|
||||
}
|
||||
|
||||
resetPopup() {
|
||||
this.selected = new Address();
|
||||
}
|
||||
|
||||
openNewAddressModal() {
|
||||
this.resetPopup();
|
||||
this.addressModalActive = true;
|
||||
}
|
||||
|
||||
async reverseGeoCode(e: LatLng, zoom: Number) {
|
||||
// If the position has been updated through autocomplete selection, no need to geocode it !
|
||||
if (this.checkCurrentPosition(e)) return;
|
||||
const result = await this.$apollo.query({
|
||||
query: REVERSE_GEOCODE,
|
||||
variables: {
|
||||
latitude: e.lat,
|
||||
longitude: e.lng,
|
||||
zoom,
|
||||
locale: this.$i18n.locale,
|
||||
},
|
||||
});
|
||||
|
||||
this.data = result.data.reverseGeocode.map(address => new Address(address));
|
||||
const defaultAddress = new Address(this.data[0]);
|
||||
this.selected = defaultAddress;
|
||||
this.$emit('input', this.selected);
|
||||
this.queryText = `${defaultAddress.poiInfos.name} ${defaultAddress.poiInfos.alternativeName}`;
|
||||
}
|
||||
|
||||
checkCurrentPosition(e: LatLng) {
|
||||
if (!this.selected || !this.selected.geom) return false;
|
||||
const lat = parseFloat(this.selected.geom.split(';')[1]);
|
||||
const lon = parseFloat(this.selected.geom.split(';')[0]);
|
||||
|
||||
return e.lat === lat && e.lng === lon;
|
||||
}
|
||||
|
||||
async locateMe(): Promise<void> {
|
||||
|
||||
this.gettingLocation = true;
|
||||
try {
|
||||
this.gettingLocation = false;
|
||||
this.location = await this.getLocation();
|
||||
this.mapDefaultZoom = 12;
|
||||
this.reverseGeoCode(new LatLng(this.location.coords.latitude, this.location.coords.longitude), 12);
|
||||
} catch (e) {
|
||||
this.gettingLocation = false;
|
||||
this.gettingLocationError = e.message;
|
||||
}
|
||||
}
|
||||
|
||||
async getLocation(): Promise<Position> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
if (!('geolocation' in navigator)) {
|
||||
reject(new Error('Geolocation is not available.'));
|
||||
}
|
||||
|
||||
navigator.geolocation.getCurrentPosition(pos => {
|
||||
resolve(pos);
|
||||
}, err => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.autocomplete .dropdown-item.is-disabled .is-enabled {
|
||||
opacity: 1 !important;
|
||||
cursor: auto;
|
||||
.autocomplete {
|
||||
.dropdown-menu {
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.dropdown-item.is-disabled {
|
||||
opacity: 1 !important;
|
||||
cursor: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.read-only {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.map {
|
||||
height: 400px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user