Add a way to share a specific map position
Also rework the way Map component is handled to make it cleaner and more efficient. Fix for issue #23.
This commit is contained in:
parent
3e8ec40330
commit
74a42abe72
28
src/App.vue
28
src/App.vue
@ -7,13 +7,13 @@
|
||||
<v-toolbar-title v-text="title" class="ma-0"></v-toolbar-title>
|
||||
</router-link>
|
||||
<v-spacer></v-spacer>
|
||||
<v-menu offset-y class="menu">
|
||||
<v-menu offset-y class="menu" v-if="$route.name === 'Onboarding' || $route.name === 'Map' || $route.name === 'SharedMap'">
|
||||
<v-btn slot="activator" icon role="button" :aria-label="$t('buttons.menu')">
|
||||
<v-icon>more_vert</v-icon>
|
||||
</v-btn>
|
||||
<v-list>
|
||||
<v-list-tile @click="goToMap">
|
||||
<v-list-tile-title>{{ $t("menu.Map") }}</v-list-tile-title>
|
||||
<v-list-tile @click="isShareMapViewModalShown = true" v-if="isShareMapViewMenuEntryVisible">
|
||||
<v-list-tile-title>{{ $t("menu.shareMapView") }}</v-list-tile-title>
|
||||
</v-list-tile>
|
||||
<v-list-tile @click="goToAbout">
|
||||
<v-list-tile-title>{{ $t("menu.About") }}</v-list-tile-title>
|
||||
@ -23,35 +23,47 @@
|
||||
</v-list-tile>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-btn icon role="button" :aria-label="$t('buttons.back')" v-else @click="goBack">
|
||||
<v-icon>arrow_back</v-icon>
|
||||
</v-btn>
|
||||
<div>
|
||||
<v-progress-linear v-if="isLoading" :indeterminate="true" class="progressBar"></v-progress-linear>
|
||||
</div>
|
||||
</v-toolbar>
|
||||
<v-content>
|
||||
<router-view/>
|
||||
<ShareMapViewModal v-model="isShareMapViewModalShown"></ShareMapViewModal>
|
||||
<router-view></router-view>
|
||||
</v-content>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ShareMapViewModal from '@/components/ShareMapViewModal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ShareMapViewModal,
|
||||
},
|
||||
computed: {
|
||||
isLoading() {
|
||||
return this.$store.state.isLoading;
|
||||
},
|
||||
isShareMapViewMenuEntryVisible() {
|
||||
return this.$store.state.map.center.every(item => item !== null);
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
title: 'Cycl\'Assist',
|
||||
isShareMapViewModalShown: false,
|
||||
title: "Cycl'Assist",
|
||||
};
|
||||
},
|
||||
name: 'App',
|
||||
methods: {
|
||||
goToAbout() {
|
||||
this.$router.push({ name: 'About' });
|
||||
},
|
||||
goToMap() {
|
||||
this.$router.push({ name: 'Map' });
|
||||
goBack() {
|
||||
this.$router.go(-1);
|
||||
},
|
||||
goToSettings() {
|
||||
this.$router.push({ name: 'Settings' });
|
||||
|
37
src/components/LocationError.vue
Normal file
37
src/components/LocationError.vue
Normal file
@ -0,0 +1,37 @@
|
||||
<template>
|
||||
<div>
|
||||
<p class="text-xs-center">{{ error }}</p>
|
||||
<p class="text-xs-center">
|
||||
<v-btn role="button" color="blue" dark @click="retryFunction">{{ $t('misc.retry') }}</v-btn>
|
||||
</p>
|
||||
<p>{{ $t('misc.or') }}</p>
|
||||
<p>
|
||||
<AddressInput
|
||||
:label="$t('locationPicker.pickALocationManually')"
|
||||
:onInput="onManualLocationPicker"
|
||||
></AddressInput>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AddressInput from '@/components/AddressInput.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AddressInput,
|
||||
},
|
||||
methods: {
|
||||
onManualLocationPicker(value) {
|
||||
this.$store.dispatch(
|
||||
'setCurrentPosition',
|
||||
{ latLng: [value.latlng.lat, value.latlng.lng] },
|
||||
);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
error: String,
|
||||
retryFunction: Function,
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,12 +1,33 @@
|
||||
<template>
|
||||
<div class="fill-height fill-width">
|
||||
<v-lmap ref="map" :minZoom="this.minZoom" :maxZoom="this.maxZoom" :options="{ zoomControl: false }" @click="handleClick" @movestart="onMoveStart" @zoomstart="onZoomStart">
|
||||
<v-lmap
|
||||
ref="map"
|
||||
:minZoom="this.minZoom"
|
||||
:maxZoom="this.maxZoom"
|
||||
:options="{ zoomControl: false }"
|
||||
@click="handleClick"
|
||||
@movestart="onMoveStart"
|
||||
@moveend="onMoveEnd"
|
||||
@zoomstart="onZoomStart"
|
||||
>
|
||||
<v-ltilelayer :url="tileServer" :attribution="attribution"></v-ltilelayer>
|
||||
|
||||
<v-lts v-if="heading !== null" :lat-lng="positionLatLng" :options="markerOptions"></v-lts>
|
||||
<v-lcirclemarker v-else :lat-lng="positionLatLng" :color="markerOptions.color" :fillColor="markerOptions.fillColor" :fillOpacity="1.0" :weight="markerOptions.weight" :radius="markerRadius"></v-lcirclemarker>
|
||||
<template v-if="positionLatLng">
|
||||
<v-lts v-if="heading !== null" :lat-lng="positionLatLng" :options="markerOptions"></v-lts>
|
||||
<v-lcirclemarker
|
||||
v-else
|
||||
:lat-lng="positionLatLng"
|
||||
:color="markerOptions.color"
|
||||
:fillColor="markerOptions.fillColor"
|
||||
:fillOpacity="1.0"
|
||||
:weight="markerOptions.weight"
|
||||
:radius="markerRadius"
|
||||
>
|
||||
</v-lcirclemarker>
|
||||
|
||||
<v-lcircle v-if="shouldDisplayAccuracy" :lat-lng="positionLatLng" :radius="radiusFromAccuracy"></v-lcircle>
|
||||
</template>
|
||||
|
||||
<v-lcircle v-if="shouldDisplayAccuracy" :lat-lng="positionLatLng" :radius="radiusFromAccuracy"></v-lcircle>
|
||||
<v-lpolyline :latLngs="polyline" :opacity="0.6" color="#00FF00"></v-lpolyline>
|
||||
|
||||
<v-lmarker v-if="reportLatLng" :lat-lng="reportLatLng" :icon="unknownMarkerIcon"></v-lmarker>
|
||||
@ -21,7 +42,7 @@
|
||||
left
|
||||
color="blue"
|
||||
class="overlayButton"
|
||||
v-if="recenterButton"
|
||||
v-if="isRecenterButtonShown"
|
||||
@click.native.stop="recenterMap"
|
||||
role="button"
|
||||
:aria-label="$t('buttons.recenterMap')"
|
||||
@ -56,21 +77,20 @@ export default {
|
||||
components: {
|
||||
ReportMarker,
|
||||
},
|
||||
props: {
|
||||
accuracy: {
|
||||
type: Number,
|
||||
default: null,
|
||||
},
|
||||
heading: Number, // in degrees, clockwise wrt north
|
||||
markers: Array,
|
||||
onPress: Function,
|
||||
polyline: Array,
|
||||
positionLatLng: Array,
|
||||
reportLatLng: Array,
|
||||
},
|
||||
computed: {
|
||||
markerOptions() {
|
||||
return {
|
||||
fillColor: '#00ff00',
|
||||
color: '#000000',
|
||||
heading: this.heading * (Math.PI / 180), // in radians from North
|
||||
weight: 1,
|
||||
};
|
||||
},
|
||||
radiusFromAccuracy() {
|
||||
if (this.accuracy) {
|
||||
// Compute the radius (in pixels) based on GPS accuracy, taking
|
||||
// into account the current zoom level
|
||||
// Formula coming from https://wiki.openstreetmap.org/wiki/Zoom_levels.
|
||||
return this.accuracy / (
|
||||
(constants.EARTH_RADIUS * 2 * Math.PI * Math.cos(this.positionLatLng[0] *
|
||||
(Math.PI / 180))) /
|
||||
@ -80,19 +100,92 @@ export default {
|
||||
return null;
|
||||
},
|
||||
shouldDisplayAccuracy() {
|
||||
// Only display accuracy if circle is large enough
|
||||
return (
|
||||
this.accuracy &&
|
||||
this.accuracy < 100 &&
|
||||
this.accuracy < constants.ACCURACY_DISPLAY_THRESHOLD &&
|
||||
this.radiusFromAccuracy > this.markerRadius
|
||||
);
|
||||
},
|
||||
markerOptions() {
|
||||
return {
|
||||
fillColor: '#00ff00',
|
||||
color: '#000000',
|
||||
heading: this.heading * (Math.PI / 180),
|
||||
weight: 1,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attribution: this.$t('map.attribution'),
|
||||
isProgrammaticMove: false,
|
||||
isProgrammaticZoom: false,
|
||||
map: null,
|
||||
markerRadius: 10.0,
|
||||
maxZoom: constants.MAX_ZOOM,
|
||||
minZoom: constants.MIN_ZOOM,
|
||||
isRecenterButtonShown: false,
|
||||
tileServer: constants.TILE_SERVERS[this.$store.state.settings.tileServer],
|
||||
unknownMarkerIcon: L.icon({
|
||||
iconAnchor: [20, 40],
|
||||
iconSize: [40, 40],
|
||||
iconUrl: unknownMarkerIcon,
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
if (this.onPress) {
|
||||
this.onPress(event.latlng);
|
||||
}
|
||||
},
|
||||
hideRecenterButton() {
|
||||
if (this.isRecenterButtonShown) {
|
||||
this.isRecenterButtonShown = false;
|
||||
}
|
||||
},
|
||||
onMoveStart() {
|
||||
if (!this.isProgrammaticMove && !this.isProgrammaticZoom) {
|
||||
this.showRecenterButton();
|
||||
}
|
||||
},
|
||||
onMoveEnd() {
|
||||
if (this.onMapCenterUpdate) {
|
||||
const mapCenter = this.map.getCenter();
|
||||
this.onMapCenterUpdate([mapCenter.lat, mapCenter.lng]);
|
||||
}
|
||||
if (this.onMapZoomUpdate) {
|
||||
this.onMapZoomUpdate(this.map.getZoom());
|
||||
}
|
||||
},
|
||||
onZoomStart() {
|
||||
if (!this.isProgrammaticZoom) {
|
||||
this.showRecenterButton();
|
||||
}
|
||||
},
|
||||
recenterMap() {
|
||||
this.hideRecenterButton();
|
||||
if (this.map.getZoom() !== this.zoom) {
|
||||
this.isProgrammaticZoom = true;
|
||||
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
||||
}
|
||||
const mapCenter = this.map.getCenter();
|
||||
if (
|
||||
mapCenter.lat !== this.center[0] &&
|
||||
mapCenter.lng !== this.center[1]
|
||||
) {
|
||||
this.isProgrammaticMove = true;
|
||||
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
||||
}
|
||||
this.map.setView(this.center, this.zoom);
|
||||
},
|
||||
showCompass() {
|
||||
const north = L.control({ position: 'topright' });
|
||||
north.onAdd = () => {
|
||||
const div = L.DomUtil.create('div', 'compassIcon legend');
|
||||
div.innerHTML = `<img src="${compassNorthIcon}">`;
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
return div;
|
||||
};
|
||||
this.map.addControl(north);
|
||||
},
|
||||
showRecenterButton() {
|
||||
if (!this.isRecenterButtonShown) {
|
||||
this.isRecenterButtonShown = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
@ -101,31 +194,66 @@ export default {
|
||||
this.isProgrammaticZoom = true;
|
||||
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
||||
}
|
||||
const mapCenter = this.map.getCenter();
|
||||
if (
|
||||
this.map.getCenter().lat !== this.positionLatLng[0] &&
|
||||
this.map.getCenter().lng !== this.positionLatLng[1]
|
||||
mapCenter.lat !== this.center[0] &&
|
||||
mapCenter.lng !== this.center[1]
|
||||
) {
|
||||
this.isProgrammaticMove = true;
|
||||
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
||||
}
|
||||
this.map.setView(this.positionLatLng, this.zoom);
|
||||
this.map.setView(this.center, this.zoom);
|
||||
this.showCompass();
|
||||
},
|
||||
props: {
|
||||
accuracy: Number,
|
||||
center: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
heading: Number, // in degrees, clockwise wrt north
|
||||
markers: Array,
|
||||
onPress: Function,
|
||||
onMapCenterUpdate: Function,
|
||||
onMapZoomUpdate: Function,
|
||||
polyline: Array,
|
||||
positionLatLng: Array,
|
||||
reportLatLng: Array,
|
||||
zoom: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
positionLatLng(newPositionLatLng) {
|
||||
zoom(newZoom) {
|
||||
if (!this.map) {
|
||||
// Map should have been created
|
||||
return;
|
||||
}
|
||||
if (!this.recenterButton) {
|
||||
if (!this.isRecenterButtonShown) {
|
||||
// Handle programmatic navigation
|
||||
if (this.map.getZoom() !== newZoom) {
|
||||
this.isProgrammaticZoom = true;
|
||||
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
||||
}
|
||||
this.map.setZoom(newZoom);
|
||||
}
|
||||
},
|
||||
center(newCenterLatLng) {
|
||||
if (!this.map) {
|
||||
// Map should have been created
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.isRecenterButtonShown) {
|
||||
// Handle programmatic navigation
|
||||
if (this.map.getZoom() !== this.zoom) {
|
||||
this.isProgrammaticZoom = true;
|
||||
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
||||
}
|
||||
if (
|
||||
this.map.getCenter().lat !== newPositionLatLng[0] &&
|
||||
this.map.getCenter().lng !== newPositionLatLng[1]
|
||||
this.map.getCenter().lat !== newCenterLatLng[0] &&
|
||||
this.map.getCenter().lng !== newCenterLatLng[1]
|
||||
) {
|
||||
this.isProgrammaticMove = true;
|
||||
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
||||
@ -138,7 +266,7 @@ export default {
|
||||
const distances = this.markers.map(
|
||||
marker => ({
|
||||
id: marker.id,
|
||||
distance: distance(newPositionLatLng, marker.latLng),
|
||||
distance: distance(newCenterLatLng, marker.latLng),
|
||||
}),
|
||||
).filter(item => item.distance < constants.MIN_DISTANCE_REPORT_DETAILS);
|
||||
const closestReport = distances.reduce( // Get the closest one
|
||||
@ -155,82 +283,10 @@ export default {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.map.setView(newPositionLatLng, this.zoom);
|
||||
this.map.setView(newCenterLatLng, this.zoom);
|
||||
}
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors',
|
||||
zoom: constants.DEFAULT_ZOOM,
|
||||
markerRadius: 10.0,
|
||||
minZoom: constants.MIN_ZOOM,
|
||||
maxZoom: constants.MAX_ZOOM,
|
||||
tileServer: constants.TILE_SERVERS[this.$store.state.settings.tileServer],
|
||||
isMouseDown: false,
|
||||
isProgrammaticZoom: false,
|
||||
isProgrammaticMove: false,
|
||||
recenterButton: false,
|
||||
map: null,
|
||||
unknownMarkerIcon: L.icon({
|
||||
iconUrl: unknownMarkerIcon,
|
||||
iconSize: [40, 40],
|
||||
iconAnchor: [20, 40],
|
||||
}),
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
if (this.onPress) {
|
||||
this.onPress(event.latlng);
|
||||
}
|
||||
},
|
||||
onMoveStart() {
|
||||
if (!this.isProgrammaticMove) {
|
||||
this.showRecenterButton();
|
||||
}
|
||||
},
|
||||
onZoomStart() {
|
||||
if (!this.isProgrammaticZoom) {
|
||||
this.showRecenterButton();
|
||||
}
|
||||
},
|
||||
showCompass() {
|
||||
const north = L.control({ position: 'topright' });
|
||||
north.onAdd = () => {
|
||||
const div = L.DomUtil.create('div', 'compassIcon legend');
|
||||
div.innerHTML = `<img src="${compassNorthIcon}">`;
|
||||
L.DomEvent.disableClickPropagation(div);
|
||||
return div;
|
||||
};
|
||||
this.map.addControl(north);
|
||||
},
|
||||
showRecenterButton() {
|
||||
if (!this.recenterButton) {
|
||||
this.recenterButton = true;
|
||||
}
|
||||
},
|
||||
hideRecenterButton() {
|
||||
if (this.recenterButton) {
|
||||
this.recenterButton = false;
|
||||
}
|
||||
},
|
||||
recenterMap() {
|
||||
this.hideRecenterButton();
|
||||
if (this.map.getZoom() !== this.zoom) {
|
||||
this.isProgrammaticZoom = true;
|
||||
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
||||
}
|
||||
if (
|
||||
this.map.getCenter().lat !== this.positionLatLng[0] &&
|
||||
this.map.getCenter().lng !== this.positionLatLng[1]
|
||||
) {
|
||||
this.isProgrammaticMove = true;
|
||||
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
||||
}
|
||||
this.map.setView(this.positionLatLng, this.zoom);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
42
src/components/Modal.vue
Normal file
42
src/components/Modal.vue
Normal file
@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<v-dialog v-model="isModalShown" max-width="290">
|
||||
<slot></slot>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('keydown', this.hideModalOnEsc);
|
||||
},
|
||||
computed: {
|
||||
isModalShown: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
hideModalOnEsc(event) {
|
||||
let isEscape = false;
|
||||
if ('key' in event) {
|
||||
isEscape = (event.key === 'Escape' || event.key === 'Esc');
|
||||
} else {
|
||||
isEscape = (event.keyCode === 27);
|
||||
}
|
||||
if (isEscape) {
|
||||
this.isModalShown = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.hideModalOnEsc);
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="error" max-width="290">
|
||||
<Modal v-model="error">
|
||||
<v-card>
|
||||
<v-card-title class="subheading">{{ $t('reportDialog.unableToSendTitle') }}</v-card-title>
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</Modal>
|
||||
<v-bottom-sheet v-model="isActive">
|
||||
<v-card>
|
||||
<v-container fluid>
|
||||
@ -38,16 +38,16 @@
|
||||
<script>
|
||||
import { REPORT_TYPES, REPORT_TYPES_ORDER } from '@/constants';
|
||||
|
||||
import Modal from '@/components/Modal.vue';
|
||||
import ReportTile from './ReportTile.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ReportTile,
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('keydown', this.hideReportDialogOnEsc);
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
lat: Number,
|
||||
lng: Number,
|
||||
components: {
|
||||
Modal,
|
||||
ReportTile,
|
||||
},
|
||||
computed: {
|
||||
isActive: {
|
||||
@ -55,6 +55,9 @@ export default {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
if (val === false) {
|
||||
this.onHide();
|
||||
}
|
||||
this.$emit('input', val);
|
||||
},
|
||||
},
|
||||
@ -67,18 +70,37 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
hideReportDialogOnEsc(event) {
|
||||
let isEscape = false;
|
||||
if ('key' in event) {
|
||||
isEscape = (event.key === 'Escape' || event.key === 'Esc');
|
||||
} else {
|
||||
isEscape = (event.keyCode === 27);
|
||||
}
|
||||
if (isEscape) {
|
||||
this.isActive = false;
|
||||
}
|
||||
},
|
||||
saveReport(type) {
|
||||
this.isActive = !this.isActive;
|
||||
this.isActive = false;
|
||||
return this.$store.dispatch('saveReport', {
|
||||
type,
|
||||
lat: this.lat,
|
||||
lng: this.lng,
|
||||
lat: this.latLng[0],
|
||||
lng: this.latLng[1],
|
||||
}).catch((exc) => {
|
||||
console.error(exc);
|
||||
this.error = exc;
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
window.addEventListener('keydown', this.hideReportDialogOnEsc);
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
latLng: Array,
|
||||
onHide: Function,
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
84
src/components/ShareMapViewModal.vue
Normal file
84
src/components/ShareMapViewModal.vue
Normal file
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<Modal v-model="isActive">
|
||||
<v-card>
|
||||
<v-card-title class="headline">{{ $t('shareMapViewModal.shareCurrentMapView') }}</v-card-title>
|
||||
|
||||
<v-card-text>
|
||||
{{ $t('shareMapViewModal.copyURLToShareCurrentMapView') }}
|
||||
|
||||
<v-text-field ref="shareLinkRef" readonly :hint="shareLinkHint" @click="copyShareLink" v-model="shareMapViewURL" prepend-icon="share"></v-text-field>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
color="green darken-1"
|
||||
@click="isActive = false"
|
||||
dark
|
||||
large
|
||||
role="button"
|
||||
>
|
||||
{{ $t('misc.ok') }}
|
||||
</v-btn>
|
||||
|
||||
<v-spacer></v-spacer>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</Modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DEFAULT_ZOOM } from '@/constants';
|
||||
|
||||
import Modal from '@/components/Modal.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Modal,
|
||||
},
|
||||
computed: {
|
||||
isActive: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val);
|
||||
},
|
||||
},
|
||||
shareMapViewURL() {
|
||||
const currentMapCenter = this.$store.state.map.center;
|
||||
if (!currentMapCenter.every(item => item !== null)) {
|
||||
return null;
|
||||
}
|
||||
const currentMapZoom = this.$store.state.map.zoom || DEFAULT_ZOOM;
|
||||
const path = this.$router.resolve({
|
||||
name: 'SharedMap',
|
||||
params: {
|
||||
lat: currentMapCenter[0],
|
||||
lng: currentMapCenter[1],
|
||||
zoom: currentMapZoom,
|
||||
},
|
||||
}).href;
|
||||
return `${window.location.origin}/${path}`;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
shareLinkHint: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
copyShareLink() {
|
||||
this.$refs.shareLinkRef.$el.querySelector('input').select();
|
||||
if (document.queryCommandSupported && document.queryCommandSupported('copy')) {
|
||||
document.execCommand('copy');
|
||||
this.shareLinkHint = this.$t('shareMapViewModal.copiedToClipboard');
|
||||
}
|
||||
},
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
},
|
||||
};
|
||||
</script>
|
@ -113,19 +113,21 @@ export const REPORT_TYPES_ORDER = ['gcum', 'interrupt', 'obstacle', 'pothole', '
|
||||
export const MIN_DISTANCE_REPORT_DETAILS = 40; // in meters
|
||||
|
||||
export const MOCK_LOCATION = false;
|
||||
export const MOCK_LOCATION_UPDATE_INTERVAL = 5 * 1000;
|
||||
export const MOCK_LOCATION_UPDATE_INTERVAL = 5 * 1000; // in milliseconds
|
||||
|
||||
export const UPDATE_REPORTS_DISTANCE_THRESHOLD = 500;
|
||||
export const UPDATE_REPORTS_DISTANCE_THRESHOLD = 500; // in meters
|
||||
|
||||
// Minimal ratio between upvotes and downvotes needed for a report to be shown
|
||||
export const REPORT_VOTES_THRESHOLD = 0.5;
|
||||
|
||||
export const EARTH_RADIUS = 6378137;
|
||||
export const EARTH_RADIUS = 6378137; // in meters
|
||||
|
||||
export const DEFAULT_ZOOM = 17;
|
||||
export const MIN_ZOOM = 10;
|
||||
export const MAX_ZOOM = 18;
|
||||
|
||||
export const ACCURACY_DISPLAY_THRESHOLD = 100; // in meters
|
||||
|
||||
let opencyclemapURL = 'https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png';
|
||||
if (process.env.THUNDERFOREST_API_KEY) {
|
||||
opencyclemapURL += `?apikey=${process.env.THUNDERFOREST_API_KEY}`;
|
||||
|
@ -8,6 +8,7 @@
|
||||
"usageDescription": "Use the button in the lower right corner to add a new report at your current location. To add a report elsewhere, do a click where you want the report to be shown. Press on a marker on the map to display more informations and report the problem as being still there or solved."
|
||||
},
|
||||
"buttons": {
|
||||
"back": "Back",
|
||||
"close": "Close",
|
||||
"downvote": "Downvote",
|
||||
"menu": "Menu",
|
||||
@ -36,13 +37,17 @@
|
||||
"invalidSelection": "Invalid selection",
|
||||
"pickALocationManually": "pick a location manually"
|
||||
},
|
||||
"map": {
|
||||
"attribution": "Map data © <a href=\"http://openstreetmap.org\">OpenStreetMap</a> contributors"
|
||||
},
|
||||
"menu": {
|
||||
"About": "Help",
|
||||
"Map": "Map",
|
||||
"shareMapView": "Share map view",
|
||||
"Settings": "Settings"
|
||||
},
|
||||
"misc": {
|
||||
"discard": "Discard",
|
||||
"ok": "OK",
|
||||
"or": "or",
|
||||
"retry": "Retry",
|
||||
"spaceBeforeDoublePunctuations": ""
|
||||
@ -74,5 +79,10 @@
|
||||
"save": "Save",
|
||||
"skipOnboarding": "Skip onboarding",
|
||||
"tileServer": "Map tiles server"
|
||||
},
|
||||
"shareMapViewModal": {
|
||||
"copiedToClipboard": "Copied to clipboard!",
|
||||
"copyURLToShareCurrentMapView": "Copy the URL below to share the current map view.",
|
||||
"shareCurrentMapView": "Share current map view"
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import About from '@/components/About.vue';
|
||||
import About from '@/views/About.vue';
|
||||
import Map from '@/views/Map.vue';
|
||||
import Onboarding from '@/views/Onboarding.vue';
|
||||
import Settings from '@/views/Settings.vue';
|
||||
@ -14,6 +14,11 @@ export default new Router({
|
||||
name: 'About',
|
||||
component: About,
|
||||
},
|
||||
{
|
||||
path: '/map=:zoom/:lat/:lng',
|
||||
name: 'SharedMap',
|
||||
component: Map,
|
||||
},
|
||||
{
|
||||
path: '/map',
|
||||
name: 'Map',
|
||||
|
@ -1,13 +1,19 @@
|
||||
import moment from 'moment';
|
||||
|
||||
import * as api from '@/api';
|
||||
import * as constants from '@/constants';
|
||||
import i18n from '@/i18n';
|
||||
|
||||
import {
|
||||
INTRO_WAS_SEEN,
|
||||
IS_LOADING,
|
||||
IS_DONE_LOADING,
|
||||
IS_LOADING,
|
||||
PUSH_REPORT,
|
||||
SET_CURRENT_MAP_CENTER,
|
||||
SET_CURRENT_MAP_ZOOM,
|
||||
SET_CURRENT_POSITION,
|
||||
SET_LOCATION_ERROR,
|
||||
SET_LOCATION_WATCHER_ID,
|
||||
SET_SETTING,
|
||||
SHOW_REPORT_DETAILS,
|
||||
STORE_REPORTS,
|
||||
@ -61,3 +67,49 @@ export function setSetting({ commit }, { setting, value }) {
|
||||
export function markIntroAsSeen({ commit }) {
|
||||
return commit(INTRO_WAS_SEEN);
|
||||
}
|
||||
|
||||
export function setCurrentMapCenter({ commit, state }, { center }) {
|
||||
if (state.map.center.some((item, index) => item !== center[index])) {
|
||||
commit(SET_CURRENT_MAP_CENTER, { center });
|
||||
}
|
||||
}
|
||||
|
||||
export function setCurrentMapZoom({ commit, state }, { zoom }) {
|
||||
if (state.map.zoom !== zoom) {
|
||||
commit(SET_CURRENT_MAP_ZOOM, { zoom });
|
||||
}
|
||||
}
|
||||
|
||||
export function setCurrentPosition(
|
||||
{ commit, state },
|
||||
{ accuracy = null, heading = null, latLng = null },
|
||||
) {
|
||||
const locationState = state.location;
|
||||
if (
|
||||
accuracy !== locationState.accuracy ||
|
||||
heading !== locationState.heading ||
|
||||
locationState.currentLatLng.some((item, index) => item !== latLng[index])
|
||||
) {
|
||||
// Throttle mutations if nothing has changed
|
||||
commit(SET_CURRENT_POSITION, { accuracy, heading, latLng });
|
||||
}
|
||||
}
|
||||
|
||||
export function setLocationWatcherId({ commit }, { id }) {
|
||||
return commit(SET_LOCATION_WATCHER_ID, { id });
|
||||
}
|
||||
|
||||
export function setLocationError({ commit, state }, { error }) {
|
||||
// Unregister location watcher
|
||||
const watcherID = state.location.watcherID;
|
||||
if (watcherID !== null) {
|
||||
if (constants.MOCK_LOCATION) {
|
||||
clearInterval(watcherID);
|
||||
} else {
|
||||
navigator.geolocation.clearWatch(watcherID);
|
||||
}
|
||||
}
|
||||
|
||||
commit(SET_LOCATION_WATCHER_ID, { id: null });
|
||||
commit(SET_LOCATION_ERROR, { error });
|
||||
}
|
||||
|
@ -1,7 +1,12 @@
|
||||
export const INTRO_WAS_SEEN = 'INTRO_WAS_SEEN';
|
||||
export const IS_LOADING = 'IS_LOADING';
|
||||
export const IS_DONE_LOADING = 'IS_DONE_LOADING';
|
||||
export const IS_LOADING = 'IS_LOADING';
|
||||
export const PUSH_REPORT = 'PUSH_REPORT';
|
||||
export const SET_CURRENT_MAP_CENTER = 'SET_CURRENT_MAP_CENTER';
|
||||
export const SET_CURRENT_MAP_ZOOM = 'SET_CURRENT_MAP_ZOOM';
|
||||
export const SET_CURRENT_POSITION = 'SET_CURRENT_POSITION';
|
||||
export const SET_LOCATION_ERROR = 'SET_LOCATION_ERROR';
|
||||
export const SET_LOCATION_WATCHER_ID = 'SET_LOCATION_WATCHER_ID';
|
||||
export const SET_SETTING = 'SET_SETTING';
|
||||
export const SHOW_REPORT_DETAILS = 'SHOW_REPORT_DETAILS';
|
||||
export const STORE_REPORTS = 'STORE_REPORTS';
|
||||
|
@ -54,6 +54,18 @@ if (storageAvailable('localStorage')) {
|
||||
export const initialState = {
|
||||
hasGoneThroughIntro: false,
|
||||
isLoading: false,
|
||||
location: {
|
||||
accuracy: null,
|
||||
currentLatLng: [null, null],
|
||||
error: null,
|
||||
heading: null, // in degrees, clockwise wrt north
|
||||
positionHistory: [],
|
||||
watcherID: null,
|
||||
},
|
||||
map: {
|
||||
center: [null, null],
|
||||
zoom: null,
|
||||
},
|
||||
reportDetails: {
|
||||
id: null,
|
||||
userAsked: null,
|
||||
@ -71,11 +83,37 @@ export const mutations = {
|
||||
[types.INTRO_WAS_SEEN](state) {
|
||||
state.hasGoneThroughIntro = true;
|
||||
},
|
||||
[types.IS_DONE_LOADING](state) {
|
||||
state.isLoading = false;
|
||||
},
|
||||
[types.IS_LOADING](state) {
|
||||
state.isLoading = true;
|
||||
},
|
||||
[types.IS_DONE_LOADING](state) {
|
||||
state.isLoading = false;
|
||||
[types.PUSH_REPORT](state, { report }) {
|
||||
const reportIndex = state.reports.findIndex(item => item.id === report.id);
|
||||
if (reportIndex === -1) {
|
||||
state.reports.push(report);
|
||||
} else {
|
||||
Vue.set(state.reports, reportIndex, report);
|
||||
}
|
||||
},
|
||||
[types.SET_CURRENT_MAP_CENTER](state, { center }) {
|
||||
Vue.set(state.map, 'center', center);
|
||||
},
|
||||
[types.SET_CURRENT_MAP_ZOOM](state, { zoom }) {
|
||||
Vue.set(state.map, 'zoom', zoom);
|
||||
},
|
||||
[types.SET_CURRENT_POSITION](state, { accuracy, heading, latLng }) {
|
||||
Vue.set(state.location, 'accuracy', accuracy);
|
||||
Vue.set(state.location, 'currentLatLng', latLng);
|
||||
Vue.set(state.location, 'heading', heading);
|
||||
state.location.positionHistory.push(latLng);
|
||||
},
|
||||
[types.SET_LOCATION_ERROR](state, { error }) {
|
||||
Vue.set(state.location, 'error', error);
|
||||
},
|
||||
[types.SET_LOCATION_WATCHER_ID](state, { id }) {
|
||||
Vue.set(state.location, 'watcherID', id);
|
||||
},
|
||||
[types.SET_SETTING](state, { setting, value }) {
|
||||
if (storageAvailable('localStorage')) {
|
||||
@ -90,12 +128,4 @@ export const mutations = {
|
||||
[types.STORE_REPORTS](state, { reports }) {
|
||||
state.reports = reports;
|
||||
},
|
||||
[types.PUSH_REPORT](state, { report }) {
|
||||
const reportIndex = state.reports.findIndex(item => item.id === report.id);
|
||||
if (reportIndex === -1) {
|
||||
state.reports.push(report);
|
||||
} else {
|
||||
Vue.set(state.reports, reportIndex, report);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -2,8 +2,20 @@
|
||||
<v-container fluid fill-height class="no-padding">
|
||||
<v-layout row wrap fill-height>
|
||||
<ReportCard></ReportCard>
|
||||
<v-flex xs12 fill-height v-if="latLng">
|
||||
<Map :positionLatLng="latLng" :reportLatLng="reportLatLng" :polyline="positionHistory" :heading="heading" :accuracy="accuracy" :markers="reportsMarkers" :onPress="showReportDialog"></Map>
|
||||
<v-flex xs12 fill-height v-if="mapCenter">
|
||||
<Map
|
||||
:accuracy="accuracy"
|
||||
:center="mapCenter"
|
||||
:heading="heading"
|
||||
:markers="reportsMarkers"
|
||||
:onMapCenterUpdate="onMapCenterUpdate"
|
||||
:onMapZoomUpdate="onMapZoomUpdate"
|
||||
:onPress="showReportDialog"
|
||||
:polyline="positionHistory"
|
||||
:positionLatLng="currentLatLng"
|
||||
:reportLatLng="reportLatLng"
|
||||
:zoom="mapZoom"
|
||||
></Map>
|
||||
<v-btn
|
||||
absolute
|
||||
dark
|
||||
@ -16,25 +28,15 @@
|
||||
@click.native.stop="() => showReportDialog()"
|
||||
role="button"
|
||||
:aria-label="$t('buttons.reportProblem')"
|
||||
v-if="!hasCenterProvidedByRoute"
|
||||
>
|
||||
<v-icon>report_problem</v-icon>
|
||||
</v-btn>
|
||||
<ReportDialog v-model="dialog" :lat="reportLat" :lng="reportLng"></ReportDialog>
|
||||
<ReportDialog v-model="isReportDialogVisible" :latLng="reportLatLng" :onHide="resetReportLatLng"></ReportDialog>
|
||||
</v-flex>
|
||||
<v-flex xs12 sm6 offset-sm3 md4 offset-md4 fill-height v-else class="pa-3">
|
||||
<template v-if="error">
|
||||
<p class="text-xs-center">{{ error }}</p>
|
||||
<p class="text-xs-center">
|
||||
<v-btn role="button" color="blue" dark @click="initializePositionWatching">Retry</v-btn>
|
||||
</p>
|
||||
<p>{{ $t('misc.or') }}</p>
|
||||
<p>
|
||||
<AddressInput :label="$t('locationPicker.pickALocationManually')" :onInput="onManualLocationPicker" v-model="manualLocation"></AddressInput>
|
||||
</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p class="text-xs-center">{{ $t('geolocation.fetching') }}</p>
|
||||
</template>
|
||||
<LocationError :error="error" :retryFunction="initializePositionWatching" v-if="error"></LocationError>
|
||||
<p class="text-xs-center" v-else>{{ $t('geolocation.fetching') }}</p>
|
||||
</v-flex>
|
||||
</v-layout>
|
||||
</v-container>
|
||||
@ -43,35 +45,117 @@
|
||||
<script>
|
||||
import NoSleep from 'nosleep.js';
|
||||
|
||||
import AddressInput from '@/components/AddressInput.vue';
|
||||
import LocationError from '@/components/LocationError.vue';
|
||||
import Map from '@/components/Map.vue';
|
||||
import ReportCard from '@/components/ReportCard.vue';
|
||||
import ReportDialog from '@/components/ReportDialog/index.vue';
|
||||
|
||||
import * as constants from '@/constants';
|
||||
import { distance, mockLocation } from '@/tools';
|
||||
|
||||
import i18n from '@/i18n';
|
||||
import store from '@/store';
|
||||
|
||||
function handlePositionError(error) {
|
||||
// TODO: Not translated when changing locale
|
||||
store.dispatch('setLocationError', { error: error.code });
|
||||
}
|
||||
|
||||
function setPosition(position) {
|
||||
const currentLatLng = store.state.location.currentLatLng;
|
||||
if (currentLatLng) {
|
||||
const distanceFromPreviousPoint = distance(
|
||||
[currentLatLng[0], currentLatLng[1]],
|
||||
[position.coords.latitude, position.coords.longitude],
|
||||
);
|
||||
if (distanceFromPreviousPoint > constants.UPDATE_REPORTS_DISTANCE_THRESHOLD) {
|
||||
store.dispatch('fetchReports');
|
||||
}
|
||||
}
|
||||
store.dispatch(
|
||||
'setCurrentPosition',
|
||||
{
|
||||
accuracy: position.coords.accuracy ? position.coords.accuracy : null,
|
||||
heading: (
|
||||
(position.coords.heading !== null && !isNaN(position.coords.heading))
|
||||
? position.coords.heading
|
||||
: null
|
||||
),
|
||||
latLng: [position.coords.latitude, position.coords.longitude],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
beforeDestroy() {
|
||||
this.disableNoSleep();
|
||||
this.$store.dispatch('hideReportDetails');
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
if (to.name !== 'SharedMap') {
|
||||
// Check that intro was seen except if we are in SharedMap view.
|
||||
// This is required in order to ensure NoSleep works well.
|
||||
if (!store.state.hasGoneThroughIntro) {
|
||||
return next({ name: 'Onboarding', replace: true });
|
||||
}
|
||||
}
|
||||
return next();
|
||||
},
|
||||
components: {
|
||||
AddressInput,
|
||||
LocationError,
|
||||
Map,
|
||||
ReportCard,
|
||||
ReportDialog,
|
||||
},
|
||||
beforeRouteEnter(to, from, next) {
|
||||
if (!store.state.hasGoneThroughIntro) {
|
||||
return next({ name: 'Onboarding', replace: true });
|
||||
}
|
||||
return next();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.disableNoSleep();
|
||||
this.disablePositionWatching();
|
||||
window.removeEventListener('keydown', this.hideReportDialogOnEsc);
|
||||
this.$store.dispatch('hideReportDetails');
|
||||
},
|
||||
computed: {
|
||||
accuracy() {
|
||||
return this.$store.state.location.accuracy;
|
||||
},
|
||||
currentLatLng() {
|
||||
const currentLatLng = this.$store.state.location.currentLatLng;
|
||||
// Check that this is a correct position
|
||||
if (currentLatLng.some(item => item === null)) {
|
||||
return null;
|
||||
}
|
||||
return currentLatLng;
|
||||
},
|
||||
error() {
|
||||
const errorCode = this.$store.state.location.error;
|
||||
if (errorCode) {
|
||||
let errorString = `${i18n.t('geolocation.errorFetchingPosition')} `;
|
||||
if (errorCode === 1) {
|
||||
errorString += i18n.t('geolocation.permissionDenied');
|
||||
} else if (errorCode === 2) {
|
||||
errorString += i18n.t('geolocation.positionUnavailable');
|
||||
} else {
|
||||
errorString += i18n.t('geolocation.timeout');
|
||||
}
|
||||
return errorString;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
hasCenterProvidedByRoute() {
|
||||
return this.$route.params.lat && this.$route.params.lng;
|
||||
},
|
||||
heading() {
|
||||
return this.$store.state.location.heading;
|
||||
},
|
||||
mapCenter() {
|
||||
if (this.hasCenterProvidedByRoute) {
|
||||
return [this.$route.params.lat, this.$route.params.lng];
|
||||
}
|
||||
return this.currentLatLng;
|
||||
},
|
||||
mapZoom() {
|
||||
return (
|
||||
this.$route.params.zoom
|
||||
? parseInt(this.$route.params.zoom, 10)
|
||||
: constants.DEFAULT_ZOOM
|
||||
);
|
||||
},
|
||||
positionHistory() {
|
||||
return this.$store.state.location.positionHistory;
|
||||
},
|
||||
reportsMarkers() {
|
||||
return this.$store.getters.notDismissedReports.map(report => ({
|
||||
id: report.id,
|
||||
@ -79,98 +163,62 @@ export default {
|
||||
latLng: [report.attributes.lat, report.attributes.lng],
|
||||
}));
|
||||
},
|
||||
reportLatLng() {
|
||||
if (this.dialog && this.reportLat && this.reportLng) {
|
||||
return [this.reportLat, this.reportLng];
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
accuracy: null,
|
||||
centering: false,
|
||||
dialog: false,
|
||||
error: null,
|
||||
heading: null,
|
||||
latLng: null,
|
||||
manualLocation: null,
|
||||
isReportDialogVisible: false,
|
||||
noSleep: null,
|
||||
positionHistory: [],
|
||||
reportLat: null,
|
||||
reportLng: null,
|
||||
watchID: null,
|
||||
reportLatLng: null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
initializePositionWatching() {
|
||||
this.error = null; // Reset any error
|
||||
this.disablePositionWatching(); // Ensure at most one at the same time
|
||||
|
||||
if (constants.MOCK_LOCATION) {
|
||||
this.setPosition(mockLocation());
|
||||
this.watchID = setInterval(
|
||||
() => this.setPosition(mockLocation()),
|
||||
constants.MOCK_LOCATION_UPDATE_INTERVAL,
|
||||
);
|
||||
} else {
|
||||
if (!('geolocation' in navigator)) {
|
||||
this.error = this.$t('geolocation.unavailable');
|
||||
}
|
||||
|
||||
this.watchID = navigator.geolocation.watchPosition(
|
||||
this.setPosition,
|
||||
this.handlePositionError,
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 30000,
|
||||
timeout: 27000,
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
disablePositionWatching() {
|
||||
if (this.watchID !== null) {
|
||||
if (constants.MOCK_LOCATION) {
|
||||
clearInterval(this.watchID);
|
||||
} else {
|
||||
navigator.geolocation.clearWatch(this.watchID);
|
||||
}
|
||||
}
|
||||
},
|
||||
handlePositionError(error) {
|
||||
this.error = `${this.$t('geolocation.errorFetchingPosition')} `;
|
||||
if (error.code === 1) {
|
||||
this.error += this.$t('geolocation.permissionDenied');
|
||||
} else if (error.code === 2) {
|
||||
this.error += this.$t('geolocation.positionUnavailable');
|
||||
} else {
|
||||
this.error += this.$t('geolocation.timeout');
|
||||
}
|
||||
},
|
||||
setPosition(position) {
|
||||
if (this.latLng) {
|
||||
const distanceFromPreviousPoint = distance(
|
||||
[this.latLng[0], this.latLng[1]],
|
||||
[position.coords.latitude, position.coords.longitude],
|
||||
);
|
||||
if (distanceFromPreviousPoint > constants.UPDATE_REPORTS_DISTANCE_THRESHOLD) {
|
||||
this.$store.dispatch('fetchReports');
|
||||
}
|
||||
}
|
||||
this.latLng = [position.coords.latitude, position.coords.longitude];
|
||||
this.positionHistory.push(this.latLng);
|
||||
this.heading = null;
|
||||
if (position.coords.heading !== null && !isNaN(position.coords.heading)) {
|
||||
this.heading = position.coords.heading;
|
||||
}
|
||||
this.accuracy = position.coords.accuracy ? position.coords.accuracy : null;
|
||||
},
|
||||
disableNoSleep() {
|
||||
if (this.noSleep) {
|
||||
this.noSleep.disable();
|
||||
}
|
||||
},
|
||||
initializePositionWatching() {
|
||||
if (this.$store.state.location.watcherID !== null) {
|
||||
// Already watching location, no need to add another watcher
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.dispatch('setLocationError', { error: null }); // Reset any error
|
||||
|
||||
// Set up a watcher
|
||||
let watchID = null;
|
||||
if (constants.MOCK_LOCATION) {
|
||||
setPosition(mockLocation());
|
||||
watchID = setInterval(
|
||||
() => setPosition(mockLocation()),
|
||||
constants.MOCK_LOCATION_UPDATE_INTERVAL,
|
||||
);
|
||||
} else {
|
||||
if (!('geolocation' in navigator)) {
|
||||
this.$store.dispatch('setLocationError', { error: this.$t('geolocation.unavailable') });
|
||||
return;
|
||||
}
|
||||
|
||||
watchID = navigator.geolocation.watchPosition(
|
||||
setPosition,
|
||||
handlePositionError,
|
||||
{
|
||||
enableHighAccuracy: true,
|
||||
maximumAge: 30000,
|
||||
},
|
||||
);
|
||||
}
|
||||
this.$store.dispatch('setLocationWatcherId', { id: watchID });
|
||||
},
|
||||
onMapCenterUpdate(center) {
|
||||
this.$store.dispatch('setCurrentMapCenter', { center });
|
||||
},
|
||||
onMapZoomUpdate(zoom) {
|
||||
this.$store.dispatch('setCurrentMapZoom', { zoom });
|
||||
},
|
||||
resetReportLatLng() {
|
||||
this.reportLatLng = null;
|
||||
},
|
||||
setNoSleep() {
|
||||
if (this.$store.state.settings.preventSuspend) {
|
||||
this.noSleep = new NoSleep();
|
||||
@ -179,34 +227,20 @@ export default {
|
||||
},
|
||||
showReportDialog(latlng) {
|
||||
if (latlng) {
|
||||
this.reportLat = latlng.lat;
|
||||
this.reportLng = latlng.lng;
|
||||
this.reportLatLng = [latlng.lat, latlng.lng];
|
||||
} else {
|
||||
this.reportLat = this.latLng[0];
|
||||
this.reportLng = this.latLng[1];
|
||||
this.reportLatLng = this.currentLatLng;
|
||||
}
|
||||
this.dialog = !this.dialog;
|
||||
},
|
||||
hideReportDialogOnEsc(event) {
|
||||
let isEscape = false;
|
||||
if ('key' in event) {
|
||||
isEscape = (event.key === 'Escape' || event.key === 'Esc');
|
||||
} else {
|
||||
isEscape = (event.keyCode === 27);
|
||||
}
|
||||
if (isEscape) {
|
||||
this.dialog = false;
|
||||
}
|
||||
},
|
||||
onManualLocationPicker(value) {
|
||||
this.latLng = [value.latlng.lat, value.latlng.lng];
|
||||
this.isReportDialogVisible = true;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.setNoSleep();
|
||||
this.initializePositionWatching();
|
||||
if (this.$route.name !== 'SharedMap') {
|
||||
// Only enable NoSleep in normal map view (with position tracking).
|
||||
this.setNoSleep();
|
||||
this.initializePositionWatching();
|
||||
}
|
||||
this.$store.dispatch('fetchReports');
|
||||
window.addEventListener('keydown', this.hideReportDialogOnEsc);
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
Loading…
Reference in New Issue
Block a user