2018-06-25 18:29:57 +02:00
|
|
|
<template>
|
|
|
|
<div class="fill-height fill-width">
|
2018-07-21 20:00:37 +02:00
|
|
|
<v-lmap
|
|
|
|
ref="map"
|
|
|
|
:minZoom="this.minZoom"
|
|
|
|
:maxZoom="this.maxZoom"
|
|
|
|
:options="{ zoomControl: false }"
|
|
|
|
@click="handleClick"
|
|
|
|
@movestart="onMoveStart"
|
|
|
|
@moveend="onMoveEnd"
|
|
|
|
@zoomstart="onZoomStart"
|
|
|
|
>
|
2018-06-25 18:29:57 +02:00
|
|
|
<v-ltilelayer :url="tileServer" :attribution="attribution"></v-ltilelayer>
|
2018-06-27 11:17:38 +02:00
|
|
|
|
2018-07-21 20:00:37 +02:00
|
|
|
<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>
|
2018-06-27 11:17:38 +02:00
|
|
|
|
2018-07-09 15:25:18 +02:00
|
|
|
<v-lpolyline :latLngs="polyline" :opacity="0.6" color="#00FF00"></v-lpolyline>
|
2018-06-27 11:17:38 +02:00
|
|
|
|
2018-07-13 16:50:34 +02:00
|
|
|
<v-lmarker v-if="reportLatLng" :lat-lng="reportLatLng" :icon="unknownMarkerIcon"></v-lmarker>
|
2018-06-27 14:59:45 +02:00
|
|
|
<ReportMarker v-for="marker in markers" :key="marker.id" :marker="marker"></ReportMarker>
|
2018-06-25 18:29:57 +02:00
|
|
|
</v-lmap>
|
2018-07-03 18:09:56 +02:00
|
|
|
<v-btn
|
|
|
|
absolute
|
|
|
|
dark
|
|
|
|
fab
|
|
|
|
large
|
|
|
|
bottom
|
|
|
|
left
|
|
|
|
color="blue"
|
|
|
|
class="overlayButton"
|
2018-07-21 20:00:37 +02:00
|
|
|
v-if="isRecenterButtonShown"
|
2018-07-03 18:09:56 +02:00
|
|
|
@click.native.stop="recenterMap"
|
2018-07-11 01:14:54 +02:00
|
|
|
role="button"
|
|
|
|
:aria-label="$t('buttons.recenterMap')"
|
2018-07-03 18:09:56 +02:00
|
|
|
>
|
|
|
|
<v-icon>my_location</v-icon>
|
|
|
|
</v-btn>
|
2018-06-25 18:29:57 +02:00
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script>
|
|
|
|
import L from 'leaflet';
|
|
|
|
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
|
|
|
|
import iconUrl from 'leaflet/dist/images/marker-icon.png';
|
|
|
|
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
|
2018-08-05 22:05:56 +02:00
|
|
|
import {
|
|
|
|
LMap, LTileLayer, LMarker, LCircleMarker, LCircle, LPolyline,
|
|
|
|
} from 'vue2-leaflet';
|
|
|
|
import Vue2LeafletTracksymbol from 'vue2-leaflet-tracksymbol';
|
2018-06-25 18:29:57 +02:00
|
|
|
|
2018-07-01 22:27:18 +02:00
|
|
|
import compassNorthIcon from '@/assets/compassNorth.svg';
|
2018-07-13 16:50:34 +02:00
|
|
|
import unknownMarkerIcon from '@/assets/unknownMarker.svg';
|
2018-06-27 14:59:45 +02:00
|
|
|
import * as constants from '@/constants';
|
2018-07-12 17:48:26 +02:00
|
|
|
import { distance } from '@/tools';
|
2018-06-27 14:59:45 +02:00
|
|
|
import ReportMarker from './ReportMarker.vue';
|
2018-06-27 11:17:38 +02:00
|
|
|
|
2018-06-25 18:29:57 +02:00
|
|
|
// Fix for a bug in Leaflet default icon
|
|
|
|
// see https://github.com/PaulLeCam/react-leaflet/issues/255#issuecomment-261904061
|
|
|
|
delete L.Icon.Default.prototype._getIconUrl;
|
|
|
|
L.Icon.Default.mergeOptions({
|
|
|
|
iconRetinaUrl,
|
|
|
|
iconUrl,
|
|
|
|
shadowUrl,
|
|
|
|
});
|
|
|
|
|
|
|
|
export default {
|
2018-06-27 14:59:45 +02:00
|
|
|
components: {
|
2018-08-05 22:05:56 +02:00
|
|
|
'v-lmap': LMap,
|
|
|
|
'v-ltilelayer': LTileLayer,
|
|
|
|
'v-lmarker': LMarker,
|
|
|
|
'v-lcirclemarker': LCircleMarker,
|
|
|
|
'v-lcircle': LCircle,
|
|
|
|
'v-lpolyline': LPolyline,
|
|
|
|
'v-lts': Vue2LeafletTracksymbol,
|
2018-06-27 14:59:45 +02:00
|
|
|
ReportMarker,
|
|
|
|
},
|
2018-06-25 18:29:57 +02:00
|
|
|
computed: {
|
2018-07-21 20:00:37 +02:00
|
|
|
markerOptions() {
|
|
|
|
return {
|
|
|
|
fillColor: '#00ff00',
|
|
|
|
color: '#000000',
|
|
|
|
heading: this.heading * (Math.PI / 180), // in radians from North
|
|
|
|
weight: 1,
|
|
|
|
};
|
|
|
|
},
|
2018-06-27 11:17:38 +02:00
|
|
|
radiusFromAccuracy() {
|
|
|
|
if (this.accuracy) {
|
2018-07-21 20:00:37 +02:00
|
|
|
// 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.
|
2018-06-27 11:17:38 +02:00
|
|
|
return this.accuracy / (
|
2018-08-05 15:47:28 +02:00
|
|
|
(constants.EARTH_RADIUS * 2 * Math.PI * Math.cos(this.positionLatLng[0]
|
|
|
|
* (Math.PI / 180)))
|
|
|
|
/ (2 ** (this.zoom + 8))
|
2018-06-27 11:17:38 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
},
|
|
|
|
shouldDisplayAccuracy() {
|
2018-07-21 20:00:37 +02:00
|
|
|
// Only display accuracy if circle is large enough
|
2018-06-27 11:17:38 +02:00
|
|
|
return (
|
2018-08-05 15:47:28 +02:00
|
|
|
this.accuracy
|
|
|
|
&& this.accuracy < constants.ACCURACY_DISPLAY_THRESHOLD
|
|
|
|
&& this.radiusFromAccuracy > this.markerRadius
|
2018-06-27 11:17:38 +02:00
|
|
|
);
|
|
|
|
},
|
2018-07-27 16:13:16 +02:00
|
|
|
tileServer() {
|
|
|
|
const tileServerSetting = this.$store.state.settings.tileServer;
|
|
|
|
if (tileServerSetting in constants.TILE_SERVERS) {
|
|
|
|
return constants.TILE_SERVERS[tileServerSetting];
|
|
|
|
}
|
|
|
|
const firstColon = tileServerSetting.indexOf(':');
|
|
|
|
return tileServerSetting.substring(firstColon + 1);
|
|
|
|
},
|
2018-07-21 20:00:37 +02:00
|
|
|
},
|
|
|
|
data() {
|
2018-08-05 22:05:56 +02:00
|
|
|
const $t = this.$t.bind(this);
|
2018-07-21 20:00:37 +02:00
|
|
|
return {
|
2018-08-05 22:05:56 +02:00
|
|
|
attribution: $t('map.attribution'),
|
2018-07-21 20:00:37 +02:00
|
|
|
isProgrammaticMove: false,
|
|
|
|
isProgrammaticZoom: false,
|
|
|
|
map: null,
|
|
|
|
markerRadius: 10.0,
|
|
|
|
maxZoom: constants.MAX_ZOOM,
|
|
|
|
minZoom: constants.MIN_ZOOM,
|
|
|
|
isRecenterButtonShown: false,
|
|
|
|
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 (
|
2018-08-05 15:47:28 +02:00
|
|
|
mapCenter.lat !== this.center[0]
|
|
|
|
&& mapCenter.lng !== this.center[1]
|
2018-07-21 20:00:37 +02:00
|
|
|
) {
|
|
|
|
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;
|
2018-06-25 18:29:57 +02:00
|
|
|
};
|
2018-07-21 20:00:37 +02:00
|
|
|
this.map.addControl(north);
|
|
|
|
},
|
|
|
|
showRecenterButton() {
|
|
|
|
if (!this.isRecenterButtonShown) {
|
|
|
|
this.isRecenterButtonShown = true;
|
|
|
|
}
|
2018-06-25 18:29:57 +02:00
|
|
|
},
|
|
|
|
},
|
2018-07-01 22:27:18 +02:00
|
|
|
mounted() {
|
2018-07-02 18:27:35 +02:00
|
|
|
this.map = this.$refs.map.mapObject;
|
|
|
|
if (this.map.getZoom() !== this.zoom) {
|
|
|
|
this.isProgrammaticZoom = true;
|
|
|
|
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
|
|
|
}
|
2018-07-21 20:00:37 +02:00
|
|
|
const mapCenter = this.map.getCenter();
|
2018-07-03 18:09:56 +02:00
|
|
|
if (
|
2018-08-05 15:47:28 +02:00
|
|
|
mapCenter.lat !== this.center[0]
|
|
|
|
&& mapCenter.lng !== this.center[1]
|
2018-07-03 18:09:56 +02:00
|
|
|
) {
|
2018-07-02 19:53:11 +02:00
|
|
|
this.isProgrammaticMove = true;
|
|
|
|
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
|
|
|
}
|
2018-07-21 20:00:37 +02:00
|
|
|
this.map.setView(this.center, this.zoom);
|
2018-07-02 18:27:35 +02:00
|
|
|
this.showCompass();
|
|
|
|
},
|
2018-07-21 20:00:37 +02:00
|
|
|
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,
|
|
|
|
},
|
|
|
|
},
|
2018-07-03 18:09:56 +02:00
|
|
|
watch: {
|
2018-07-21 20:00:37 +02:00
|
|
|
zoom(newZoom) {
|
2018-07-03 18:09:56 +02:00
|
|
|
if (!this.map) {
|
|
|
|
// Map should have been created
|
|
|
|
return;
|
2018-07-02 18:27:35 +02:00
|
|
|
}
|
2018-07-21 20:00:37 +02:00
|
|
|
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) {
|
2018-07-03 18:09:56 +02:00
|
|
|
// Handle programmatic navigation
|
|
|
|
if (this.map.getZoom() !== this.zoom) {
|
|
|
|
this.isProgrammaticZoom = true;
|
|
|
|
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
|
|
|
}
|
|
|
|
if (
|
2018-08-05 15:47:28 +02:00
|
|
|
this.map.getCenter().lat !== newCenterLatLng[0]
|
|
|
|
&& this.map.getCenter().lng !== newCenterLatLng[1]
|
2018-07-03 18:09:56 +02:00
|
|
|
) {
|
|
|
|
this.isProgrammaticMove = true;
|
|
|
|
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
2018-07-12 17:48:26 +02:00
|
|
|
|
|
|
|
// Eventually display closest report
|
|
|
|
const isReportDetailsAlreadyShown = this.$store.state.reportDetails.id;
|
|
|
|
const isReportDetailsOpenedByUser = this.$store.state.reportDetails.userAsked;
|
|
|
|
if (!isReportDetailsAlreadyShown || !isReportDetailsOpenedByUser) {
|
|
|
|
// Compute all markers distance, filter by max distance
|
|
|
|
const distances = this.markers.map(
|
|
|
|
marker => ({
|
|
|
|
id: marker.id,
|
2018-07-21 20:00:37 +02:00
|
|
|
distance: distance(newCenterLatLng, marker.latLng),
|
2018-07-12 17:48:26 +02:00
|
|
|
}),
|
|
|
|
).filter(item => item.distance < constants.MIN_DISTANCE_REPORT_DETAILS);
|
2018-08-05 15:47:28 +02:00
|
|
|
const closestReport = distances.reduce( // Get the closest one
|
2018-07-12 17:48:26 +02:00
|
|
|
(acc, item) => (
|
|
|
|
item.distance < acc.distance ? item : acc
|
|
|
|
),
|
|
|
|
{ distance: Number.MAX_VALUE, id: -1 },
|
|
|
|
);
|
|
|
|
// TODO: Take into account the history of positions for the direction
|
|
|
|
if (closestReport.id !== -1) {
|
|
|
|
this.$store.dispatch('showReportDetails', { id: closestReport.id, userAsked: false });
|
|
|
|
} else {
|
|
|
|
this.$store.dispatch('hideReportDetails');
|
|
|
|
}
|
|
|
|
}
|
2018-07-03 18:09:56 +02:00
|
|
|
}
|
2018-07-21 20:00:37 +02:00
|
|
|
this.map.setView(newCenterLatLng, this.zoom);
|
2018-07-02 19:53:11 +02:00
|
|
|
}
|
2018-07-01 22:55:21 +02:00
|
|
|
},
|
2018-06-28 14:40:56 +02:00
|
|
|
},
|
2018-06-25 18:29:57 +02:00
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<style>
|
|
|
|
.application .leaflet-bar a {
|
|
|
|
color: black;
|
|
|
|
}
|
2018-07-01 22:27:18 +02:00
|
|
|
|
|
|
|
.compassIcon {
|
|
|
|
background-color: white;
|
|
|
|
border-radius: 50%;
|
2018-07-02 18:27:35 +02:00
|
|
|
width: 42px;
|
|
|
|
height: 42px;
|
2018-07-01 22:27:18 +02:00
|
|
|
box-shadow: 0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12);
|
|
|
|
-webkite-box-shadow: 0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12);
|
|
|
|
}
|
2018-07-02 18:27:35 +02:00
|
|
|
|
|
|
|
.compassIcon img {
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
}
|
2018-06-25 18:29:57 +02:00
|
|
|
</style>
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
.fill-width {
|
|
|
|
width: 100%;
|
|
|
|
}
|
|
|
|
</style>
|