Few UI improvements

* Better info message when fetching geolocation
* No longer displaying a marker with heading if heading is unknown. Use
a circle marker instead.
* Display the accuracy area
This commit is contained in:
Lucas Verney 2018-06-27 11:17:38 +02:00
parent 0d54fa29a9
commit 989db3b787
6 changed files with 64 additions and 24 deletions

View File

@ -2,7 +2,12 @@
<div class="fill-height fill-width"> <div class="fill-height fill-width">
<v-lmap :center="latlng" :zoom="this.zoom" :minZoom="this.minZoom" :maxZoom="this.maxZoom" :options="{ zoomControl: false }"> <v-lmap :center="latlng" :zoom="this.zoom" :minZoom="this.minZoom" :maxZoom="this.maxZoom" :options="{ zoomControl: false }">
<v-ltilelayer :url="tileServer" :attribution="attribution"></v-ltilelayer> <v-ltilelayer :url="tileServer" :attribution="attribution"></v-ltilelayer>
<v-lts :lat-lng="latlng" :options="markerOptions"></v-lts>
<v-lts v-if="heading" :lat-lng="latlng" :options="markerOptions"></v-lts>
<v-lcirclemarker v-else :lat-lng="latlng" :color="markerOptions.color" :fillColor="markerOptions.fillColor" :fillOpacity="1.0" :weight="markerOptions.weight" :radius="markerRadius"></v-lcirclemarker>
<v-lcircle v-if="shouldDisplayAccuracy" :lat-lng="latlng" :radius="radiusFromAccuracy"></v-lcircle>
<v-lmarker v-for="marker in markers" :key="marker.id" :lat-lng="marker.latLng"></v-lmarker> <v-lmarker v-for="marker in markers" :key="marker.id" :lat-lng="marker.latLng"></v-lmarker>
</v-lmap> </v-lmap>
</div> </div>
@ -14,6 +19,8 @@ import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
import iconUrl from 'leaflet/dist/images/marker-icon.png'; import iconUrl from 'leaflet/dist/images/marker-icon.png';
import shadowUrl from 'leaflet/dist/images/marker-shadow.png'; import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
import { EARTH_RADIUS } from '@/constants';
// Fix for a bug in Leaflet default icon // Fix for a bug in Leaflet default icon
// see https://github.com/PaulLeCam/react-leaflet/issues/255#issuecomment-261904061 // see https://github.com/PaulLeCam/react-leaflet/issues/255#issuecomment-261904061
delete L.Icon.Default.prototype._getIconUrl; delete L.Icon.Default.prototype._getIconUrl;
@ -26,24 +33,46 @@ L.Icon.Default.mergeOptions({
export const DEFAULT_ZOOM = 17; export const DEFAULT_ZOOM = 17;
export const MIN_ZOOM = 15; export const MIN_ZOOM = 15;
export const MAX_ZOOM = 18; export const MAX_ZOOM = 18;
export const TILE_SERVER = process.env.TILE_SERVER || 'https://a.tile.thunderforest.com/cycle/{z}/{x}/{y}.png'; export const TILE_SERVER = process.env.TILE_SERVER || 'https://a.tile.thunderforest.com/cycle/{z}/{x}/{y}.png';
export default { export default {
props: { props: {
accuracy: {
type: Number,
default: null,
},
heading: Number, heading: Number,
lat: Number, lat: Number,
lng: Number, lng: Number,
markers: Array, markers: Array,
}, },
computed: { computed: {
radiusFromAccuracy() {
if (this.accuracy) {
return this.accuracy / (
(EARTH_RADIUS * 2 * Math.PI * Math.cos(this.lat)) /
(2 ** (this.zoom + 8))
);
}
return null;
},
shouldDisplayAccuracy() {
return (
this.accuracy &&
this.accuracy < 100 &&
this.radiusFromAccuracy > this.markerRadius
);
},
latlng() { latlng() {
return [this.lat, this.lng]; return [this.lat, this.lng];
}, },
markerOptions() { markerOptions() {
return { return {
fillColor: '#00ff00', fillColor: '#00ff00',
color: '#000000',
heading: this.heading, heading: this.heading,
weight: 1,
}; };
}, },
}, },
@ -51,6 +80,7 @@ export default {
return { return {
attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors', attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors',
zoom: DEFAULT_ZOOM, zoom: DEFAULT_ZOOM,
markerRadius: 10.0,
minZoom: MIN_ZOOM, minZoom: MIN_ZOOM,
maxZoom: MAX_ZOOM, maxZoom: MAX_ZOOM,
tileServer: TILE_SERVER, tileServer: TILE_SERVER,

6
src/constants.js Normal file
View File

@ -0,0 +1,6 @@
export const MOCK_LOCATION = false;
export const MOCK_LOCATION_UPDATE_INTERVAL = 30 * 1000;
export const UPDATE_REPORTS_DISTANCE_THRESHOLD = 500;
export const EARTH_RADIUS = 6378137;

View File

@ -1,7 +1,7 @@
// Keys should be sorted alphabetically // Keys should be sorted alphabetically
export default { export default {
geolocation: { geolocation: {
enable: 'Please accept the geolocation permission request to use the app.', fetching: 'Fetching current position…',
unavailable: 'Sorry, geolocation is not available in your browser.', unavailable: 'Sorry, geolocation is not available in your browser.',
}, },
menu: { menu: {

View File

@ -19,6 +19,8 @@ Vue.use(Vuetify);
Vue.component('v-lmap', Vue2Leaflet.LMap); Vue.component('v-lmap', Vue2Leaflet.LMap);
Vue.component('v-ltilelayer', Vue2Leaflet.LTileLayer); Vue.component('v-ltilelayer', Vue2Leaflet.LTileLayer);
Vue.component('v-lmarker', Vue2Leaflet.LMarker); Vue.component('v-lmarker', Vue2Leaflet.LMarker);
Vue.component('v-lcirclemarker', Vue2Leaflet.LCircleMarker);
Vue.component('v-lcircle', Vue2Leaflet.LCircle);
Vue.component('v-lpolyline', Vue2Leaflet.LPolyline); Vue.component('v-lpolyline', Vue2Leaflet.LPolyline);
Vue.component('v-lts', Vue2LeafletTracksymbol); Vue.component('v-lts', Vue2LeafletTracksymbol);

View File

@ -1,3 +1,5 @@
import { EARTH_RADIUS } from '@/constants';
export function distance(latLng1, latLng2) { export function distance(latLng1, latLng2) {
const lat1 = (latLng1[0] * Math.PI) / 180; const lat1 = (latLng1[0] * Math.PI) / 180;
const lng1 = (latLng1[1] * Math.PI) / 180; const lng1 = (latLng1[1] * Math.PI) / 180;
@ -10,7 +12,6 @@ export function distance(latLng1, latLng2) {
(Math.cos(lat1) * Math.cos(lat2) * (Math.sin((lng2 - lng1) / 2.0) ** 2)) (Math.cos(lat1) * Math.cos(lat2) * (Math.sin((lng2 - lng1) / 2.0) ** 2))
); );
const c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); const c = 2.0 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const EARTH_RADIUS = 6371000;
return EARTH_RADIUS * c; return EARTH_RADIUS * c;
} }
@ -22,9 +23,10 @@ export function mockLocation() {
const LNG_MAX = 2.392742; const LNG_MAX = 2.392742;
return { return {
coords: { coords: {
accuracy: 10, // In meters
latitude: (Math.random() * (LAT_MAX - LAT_MIN)) + LAT_MIN, latitude: (Math.random() * (LAT_MAX - LAT_MIN)) + LAT_MIN,
longitude: (Math.random() * (LNG_MAX - LNG_MIN)) + LNG_MIN, longitude: (Math.random() * (LNG_MAX - LNG_MIN)) + LNG_MIN,
heading: 20 * (Math.PI / 180), heading: null, // 20 * (Math.PI / 180),
}, },
}; };
} }

View File

@ -2,7 +2,7 @@
<v-container fluid fill-height class="no-padding"> <v-container fluid fill-height class="no-padding">
<v-layout row wrap fill-height> <v-layout row wrap fill-height>
<v-flex xs12 fill-height v-if="lat && lng"> <v-flex xs12 fill-height v-if="lat && lng">
<Map :lat="lat" :lng="lng" :heading="heading" :markers="reportsMarkers"></Map> <Map :lat="lat" :lng="lng" :heading="heading" :accuracy="accuracy" :markers="reportsMarkers"></Map>
<v-btn <v-btn
fixed fixed
dark dark
@ -18,10 +18,15 @@
<ReportDialog v-model="dialog" :lat="lat" :lng="lng"></ReportDialog> <ReportDialog v-model="dialog" :lat="lat" :lng="lng"></ReportDialog>
</v-flex> </v-flex>
<v-flex xs12 fill-height v-else class="pa-3"> <v-flex xs12 fill-height v-else class="pa-3">
<p>{{ error }}</p> <template v-if="error">
<p class="text-xs-center">{{ error }}</p>
<p class="text-xs-center"> <p class="text-xs-center">
<v-btn color="blue" dark @click="initializePositionWatching">Retry</v-btn> <v-btn color="blue" dark @click="initializePositionWatching">Retry</v-btn>
</p> </p>
</template>
<template v-else>
<p class="text-xs-center">{{ $t('geolocation.fetching') }}</p>
</template>
</v-flex> </v-flex>
</v-layout> </v-layout>
</v-container> </v-container>
@ -32,12 +37,9 @@ import NoSleep from 'nosleep.js';
import Map from '@/components/Map.vue'; import Map from '@/components/Map.vue';
import ReportDialog from '@/components/ReportDialog/index.vue'; import ReportDialog from '@/components/ReportDialog/index.vue';
import * as constants from '@/constants';
import { distance, mockLocation } from '@/tools'; import { distance, mockLocation } from '@/tools';
const MOCK_LOCATION = false;
const MOCK_LOCATION_UPDATE_INTERVAL = 30 * 1000;
const UPDATE_REPORTS_DISTANCE_THRESHOLD = 500;
export default { export default {
components: { components: {
Map, Map,
@ -62,8 +64,9 @@ export default {
}, },
data() { data() {
return { return {
accuracy: null,
dialog: false, dialog: false,
error: this.$t('geolocation.enable'), error: null,
heading: null, heading: null,
lat: null, lat: null,
lng: null, lng: null,
@ -75,11 +78,11 @@ export default {
initializePositionWatching() { initializePositionWatching() {
this.disablePositionWatching(); // Ensure at most one at the same time this.disablePositionWatching(); // Ensure at most one at the same time
if (MOCK_LOCATION) { if (constants.MOCK_LOCATION) {
this.setPosition(mockLocation()); this.setPosition(mockLocation());
this.watchID = setInterval( this.watchID = setInterval(
() => this.setPosition(mockLocation()), () => this.setPosition(mockLocation()),
MOCK_LOCATION_UPDATE_INTERVAL, constants.MOCK_LOCATION_UPDATE_INTERVAL,
); );
} else { } else {
if (!('geolocation' in navigator)) { if (!('geolocation' in navigator)) {
@ -99,7 +102,7 @@ export default {
}, },
disablePositionWatching() { disablePositionWatching() {
if (this.watchID !== null) { if (this.watchID !== null) {
if (MOCK_LOCATION) { if (constants.MOCK_LOCATION) {
clearInterval(this.watchID); clearInterval(this.watchID);
} else { } else {
navigator.geolocation.clearWatch(this.watchID); navigator.geolocation.clearWatch(this.watchID);
@ -115,17 +118,14 @@ export default {
[this.lat, this.lng], [this.lat, this.lng],
[position.coords.latitude, position.coords.longitude], [position.coords.latitude, position.coords.longitude],
); );
if (distanceFromPreviousPoint > UPDATE_REPORTS_DISTANCE_THRESHOLD) { if (distanceFromPreviousPoint > constants.UPDATE_REPORTS_DISTANCE_THRESHOLD) {
this.$store.dispatch('fetchReports'); this.$store.dispatch('fetchReports');
} }
} }
this.lat = position.coords.latitude; this.lat = position.coords.latitude;
this.lng = position.coords.longitude; this.lng = position.coords.longitude;
if (position.coords.heading) { this.heading = position.coords.heading ? position.coords.heading : null;
this.heading = position.coords.heading; this.accuracy = position.coords.accuracy ? position.coords.accuracy : null;
} else {
this.heading = null;
}
}, },
setNoSleep() { setNoSleep() {
this.noSleep = new NoSleep(); this.noSleep = new NoSleep();