Move map from Leaflet to OpenLayers
Also add a way to use a mock GPX trace as geolocation provider.
This commit is contained in:
parent
316527b575
commit
60f041f5a6
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,7 +9,8 @@ yarn-error.log*
|
|||||||
.zanata-cache
|
.zanata-cache
|
||||||
po/*.po
|
po/*.po
|
||||||
po/*.pot
|
po/*.pot
|
||||||
tests/*.gpx
|
tests/gpx/*
|
||||||
|
tests/mock_gpx.json
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.idea
|
.idea
|
||||||
|
@ -172,6 +172,8 @@ Icons are made from the original works:
|
|||||||
licensed under CC BY-SA on Wikimedia.
|
licensed under CC BY-SA on Wikimedia.
|
||||||
* [Pothole icon](https://commons.wikimedia.org/wiki/File:France_road_sign_A2a.svg)
|
* [Pothole icon](https://commons.wikimedia.org/wiki/File:France_road_sign_A2a.svg)
|
||||||
licensed under CC BY-SA on Wikimedia.
|
licensed under CC BY-SA on Wikimedia.
|
||||||
|
* [Compass icon](https://commons.wikimedia.org/wiki/File:Black_and_white_compass.svg)
|
||||||
|
licensed in public domain on Wikimedia.
|
||||||
* [Work icons](https://www.vecteezy.com/vector-art/87351-road-traffic-cartoon-icons-vector)
|
* [Work icons](https://www.vecteezy.com/vector-art/87351-road-traffic-cartoon-icons-vector)
|
||||||
were designed by Vecteezy.
|
were designed by Vecteezy.
|
||||||
* [Trash icon](https://pixabay.com/en/trash-waste-trashcan-garbage-99257/) is
|
* [Trash icon](https://pixabay.com/en/trash-waste-trashcan-garbage-99257/) is
|
||||||
|
@ -44,10 +44,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
extensions: ['.js', '.vue', '.json'],
|
|
||||||
alias: {
|
alias: {
|
||||||
'@': utils.resolve('src'),
|
'@': utils.resolve('src'),
|
||||||
}
|
},
|
||||||
|
extensions: ['.js', '.vue', '.json'],
|
||||||
|
// Load mock_gpx.json from tests first, tests/default then
|
||||||
|
modules: ['tests/', 'tests/default', 'node_modules'],
|
||||||
},
|
},
|
||||||
stats: {
|
stats: {
|
||||||
children: false,
|
children: false,
|
||||||
|
@ -18,16 +18,15 @@
|
|||||||
"file-saver": "^1.3.8",
|
"file-saver": "^1.3.8",
|
||||||
"gps-to-gpx": "^1.4.0",
|
"gps-to-gpx": "^1.4.0",
|
||||||
"howler": "^2.0.14",
|
"howler": "^2.0.14",
|
||||||
"leaflet": "^1.3.1",
|
|
||||||
"leaflet-tracksymbol": "^1.0.8",
|
|
||||||
"material-icons": "^0.2.3",
|
"material-icons": "^0.2.3",
|
||||||
"moment": "^2.22.2",
|
"moment": "^2.22.2",
|
||||||
"nosleep.js": "^0.7.0",
|
"nosleep.js": "^0.7.0",
|
||||||
|
"ol": "^5.1.3",
|
||||||
"roboto-fontface": "^0.9.0",
|
"roboto-fontface": "^0.9.0",
|
||||||
"vue": "^2.5.2",
|
"vue": "^2.5.2",
|
||||||
"vue-i18n": "^8.0.0",
|
"vue-i18n": "^8.0.0",
|
||||||
"vue-router": "^3.0.1",
|
"vue-router": "^3.0.1",
|
||||||
"vue2-leaflet": "^1.0.2",
|
"vuelayers": "^0.10.13",
|
||||||
"vuetify": "^1.1.11",
|
"vuetify": "^1.1.11",
|
||||||
"vuex": "^3.0.1",
|
"vuex": "^3.0.1",
|
||||||
"whatwg-fetch": "^2.0.4"
|
"whatwg-fetch": "^2.0.4"
|
||||||
|
6
scripts/gps_to_js.py
Normal file → Executable file
6
scripts/gps_to_js.py
Normal file → Executable file
@ -1,3 +1,4 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
@ -15,13 +16,12 @@ if __name__ == "__main__":
|
|||||||
for track in gpx.tracks:
|
for track in gpx.tracks:
|
||||||
for segment in track.segments:
|
for segment in track.segments:
|
||||||
for point in segment.points:
|
for point in segment.points:
|
||||||
# TODO: Other fields
|
|
||||||
json_out.append({
|
json_out.append({
|
||||||
'time': point.time.isoformat(),
|
'time': point.time.isoformat(),
|
||||||
'coords': {
|
'coords': {
|
||||||
'accuracy': point.horizontal_dilution,
|
'accuracy': point.horizontal_dilution,
|
||||||
'altitudeAccuracy': point.vertical_dilution,
|
'altitudeAccuracy': point.vertical_dilution,
|
||||||
'heading': None,
|
'heading': point.course,
|
||||||
'latitude': point.latitude,
|
'latitude': point.latitude,
|
||||||
'longitude': point.longitude
|
'longitude': point.longitude
|
||||||
}
|
}
|
||||||
@ -30,5 +30,5 @@ if __name__ == "__main__":
|
|||||||
break
|
break
|
||||||
|
|
||||||
script_dir = os.path.dirname(os.path.realpath(__file__))
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
||||||
with open(os.path.join(script_dir, '../src/tools/mock_gpx.json'), 'w') as fh:
|
with open(os.path.join(script_dir, '../tests/mock_gpx.json'), 'w') as fh:
|
||||||
json.dump(json_out, fh)
|
json.dump(json_out, fh)
|
||||||
|
@ -1,69 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<slot></slot>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Adapted from https://github.com/ais-one/vue2-leaflet-tracksymbol
|
|
||||||
import L from 'leaflet';
|
|
||||||
import 'leaflet-tracksymbol';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
heading: Number,
|
|
||||||
latLng: [Object, Array],
|
|
||||||
options: Object,
|
|
||||||
visible: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
heading(newHeading) {
|
|
||||||
this.mapObject.setHeading(newHeading);
|
|
||||||
},
|
|
||||||
latLng(newLatLng) {
|
|
||||||
this.mapObject.setLatLng(newLatLng);
|
|
||||||
},
|
|
||||||
options(newOptions) {
|
|
||||||
L.setOptions(this.mapObject, newOptions);
|
|
||||||
},
|
|
||||||
visible: 'setVisible',
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const options = Object.assign({}, this.options, { heading: this.heading });
|
|
||||||
this.mapObject = L.trackSymbol(this.latLng, options);
|
|
||||||
if (this.$parent._isMounted) {
|
|
||||||
this.deferredMountedTo(this.$parent.mapObject);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.setVisible(false);
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
deferredMountedTo(parent) {
|
|
||||||
this.parent = parent;
|
|
||||||
const that = this.mapObject;
|
|
||||||
for (let i = 0; i < this.$children.length; i += 1) {
|
|
||||||
this.$children[i].deferredMountedTo(that);
|
|
||||||
}
|
|
||||||
if (this.visible) {
|
|
||||||
this.mapObject.addTo(parent);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setVisible(newVal, oldVal) {
|
|
||||||
if (newVal === oldVal) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (this.mapObject) {
|
|
||||||
if (newVal) {
|
|
||||||
this.mapObject.addTo(this.parent);
|
|
||||||
} else {
|
|
||||||
this.parent.removeLayer(this.mapObject);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -1,38 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="fill-height fill-width">
|
<div class="fill-height fill-width">
|
||||||
<v-lmap
|
<div id="map" class="fill-height fill-width">
|
||||||
ref="map"
|
</div>
|
||||||
: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>
|
|
||||||
|
|
||||||
<template v-if="positionLatLng">
|
|
||||||
<v-lts v-if="heading !== null" :lat-lng="positionLatLng" :heading="headingInRadiansFromNorth" :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-lpolyline :latLngs="polyline" :opacity="0.6" color="#00FF00"></v-lpolyline>
|
|
||||||
|
|
||||||
<v-lmarker v-if="reportLatLng" :lat-lng="reportLatLng" :icon="unknownMarkerIcon"></v-lmarker>
|
|
||||||
<ReportMarker v-for="marker in markers" :key="marker.id" :marker="marker"></ReportMarker>
|
|
||||||
</v-lmap>
|
|
||||||
<v-btn
|
<v-btn
|
||||||
absolute
|
absolute
|
||||||
dark
|
dark
|
||||||
@ -53,74 +23,82 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import L from 'leaflet';
|
// TODO: Map going outside of container + on resize ?
|
||||||
import iconRetinaUrl from 'leaflet/dist/images/marker-icon-2x.png';
|
import 'ol/ol.css';
|
||||||
import iconUrl from 'leaflet/dist/images/marker-icon.png';
|
import Feature from 'ol/Feature';
|
||||||
import shadowUrl from 'leaflet/dist/images/marker-shadow.png';
|
import Map from 'ol/Map';
|
||||||
|
import LineString from 'ol/geom/LineString';
|
||||||
|
import Point from 'ol/geom/Point';
|
||||||
|
import TileLayer from 'ol/layer/Tile';
|
||||||
|
import VectorLayer from 'ol/layer/Vector';
|
||||||
|
import VectorSource from 'ol/source/Vector';
|
||||||
|
import XYZ from 'ol/source/XYZ';
|
||||||
|
import View from 'ol/View';
|
||||||
|
import { defaults as defaultControls } from 'ol/control';
|
||||||
|
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||||
import {
|
import {
|
||||||
LMap, LTileLayer, LMarker, LCircleMarker, LCircle, LPolyline,
|
Circle as CircleStyle, Fill, Icon, Stroke, Style, Text,
|
||||||
} from 'vue2-leaflet';
|
} from 'ol/style';
|
||||||
|
|
||||||
import compassNorthIcon from '@/assets/compassNorth.svg';
|
import compassNorthIcon from '@/assets/compassNorth.svg';
|
||||||
import unknownMarkerIcon from '@/assets/unknownMarker.svg';
|
import unknownMarkerIcon from '@/assets/unknownMarker.svg';
|
||||||
import * as constants from '@/constants';
|
import * as constants from '@/constants';
|
||||||
import { distance } from '@/tools';
|
import { distance } from '@/tools';
|
||||||
import LeafletTracksymbol from './LeafletTrackSymbol.vue';
|
|
||||||
import ReportMarker from './ReportMarker.vue';
|
|
||||||
|
|
||||||
// Fix for a bug in Leaflet default icon
|
const MAIN_VECTOR_LAYER_NAME = 'MAIN';
|
||||||
// see https://github.com/PaulLeCam/react-leaflet/issues/255#issuecomment-261904061
|
const REPORTS_MARKERS_VECTOR_LAYER_NAME = 'REPORTS_MARKERS';
|
||||||
delete L.Icon.Default.prototype._getIconUrl;
|
|
||||||
L.Icon.Default.mergeOptions({
|
|
||||||
iconRetinaUrl,
|
|
||||||
iconUrl,
|
|
||||||
shadowUrl,
|
|
||||||
});
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
|
||||||
'v-lmap': LMap,
|
|
||||||
'v-ltilelayer': LTileLayer,
|
|
||||||
'v-lmarker': LMarker,
|
|
||||||
'v-lcirclemarker': LCircleMarker,
|
|
||||||
'v-lcircle': LCircle,
|
|
||||||
'v-lpolyline': LPolyline,
|
|
||||||
'v-lts': LeafletTracksymbol,
|
|
||||||
ReportMarker,
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
headingInRadiansFromNorth() {
|
headingInRadiansFromNorth() {
|
||||||
if (this.heading !== null) {
|
if (this.heading !== null) {
|
||||||
return this.heading * (Math.PI / 180); // in radians from North
|
// in radians from North
|
||||||
|
return 1.0 * this.heading * (Math.PI / 180);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
olCenter() {
|
||||||
|
// Compute center in OL coordinates system
|
||||||
|
return fromLonLat([this.center[1], this.center[0]]);
|
||||||
|
},
|
||||||
|
olPolyline() {
|
||||||
|
// Compute the polyline in OL coordinates system
|
||||||
|
return this.polyline.map(item => fromLonLat([item[1], item[0]]));
|
||||||
|
},
|
||||||
|
olPosition() {
|
||||||
|
// Compute the current position in OL coordinates system
|
||||||
|
if (this.positionLatLng) {
|
||||||
|
return fromLonLat([this.positionLatLng[1], this.positionLatLng[0]]);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
radiusFromAccuracy() {
|
radiusFromAccuracy() {
|
||||||
if (this.accuracy) {
|
// Compute the radius (in pixels) based on GPS accuracy, taking
|
||||||
// Compute the radius (in pixels) based on GPS accuracy, taking
|
// into account the current zoom level
|
||||||
// into account the current zoom level
|
if (this.accuracy && this.accuracy < constants.ACCURACY_DISPLAY_THRESHOLD) {
|
||||||
// Formula coming from https://wiki.openstreetmap.org/wiki/Zoom_levels.
|
// Formula coming from https://wiki.openstreetmap.org/wiki/Zoom_levels.
|
||||||
return this.accuracy / (
|
const accuracyInPixels = this.accuracy / (
|
||||||
(constants.EARTH_RADIUS * 2 * Math.PI * Math.cos(this.positionLatLng[0]
|
(constants.EARTH_RADIUS * 2 * Math.PI * Math.cos(this.positionLatLng[0]
|
||||||
* (Math.PI / 180)))
|
* (Math.PI / 180)))
|
||||||
/ (2 ** (this.zoom + 8))
|
/ (2 ** (this.zoom + 8))
|
||||||
);
|
);
|
||||||
|
if (accuracyInPixels > constants.POSITION_MARKER_RADIUS) {
|
||||||
|
return accuracyInPixels;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
shouldDisplayAccuracy() {
|
reportDetailsID() {
|
||||||
// Only display accuracy if circle is large enough
|
// Get the currently shown report details ID
|
||||||
return (
|
return this.$store.state.reportDetails.id;
|
||||||
this.accuracy
|
|
||||||
&& this.accuracy < constants.ACCURACY_DISPLAY_THRESHOLD
|
|
||||||
&& this.radiusFromAccuracy > this.markerRadius
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
tileServer() {
|
tileServer() {
|
||||||
const tileServerSetting = this.$store.state.settings.tileServer;
|
const tileServerSetting = this.$store.state.settings.tileServer;
|
||||||
if (tileServerSetting in constants.TILE_SERVERS) {
|
if (tileServerSetting in constants.TILE_SERVERS) {
|
||||||
return constants.TILE_SERVERS[tileServerSetting];
|
return constants.TILE_SERVERS[tileServerSetting];
|
||||||
}
|
}
|
||||||
|
// Remove the protocol part, avoid easily avoidable unsecured
|
||||||
|
// content over HTTPS.
|
||||||
const firstColon = tileServerSetting.indexOf(':');
|
const firstColon = tileServerSetting.indexOf(':');
|
||||||
return tileServerSetting.substring(firstColon + 1);
|
return tileServerSetting.substring(firstColon + 1);
|
||||||
},
|
},
|
||||||
@ -130,32 +108,132 @@ export default {
|
|||||||
return {
|
return {
|
||||||
attribution: $t('map.attribution'),
|
attribution: $t('map.attribution'),
|
||||||
isProgrammaticMove: false,
|
isProgrammaticMove: false,
|
||||||
isProgrammaticZoom: false,
|
|
||||||
map: null,
|
map: null,
|
||||||
markerOptions: {
|
|
||||||
fill: true,
|
|
||||||
fillColor: '#00ff00',
|
|
||||||
fillOpacity: 1.0,
|
|
||||||
color: '#000000',
|
|
||||||
opacity: 1.0,
|
|
||||||
weight: 1,
|
|
||||||
},
|
|
||||||
markerRadius: 10.0,
|
|
||||||
maxZoom: constants.MAX_ZOOM,
|
maxZoom: constants.MAX_ZOOM,
|
||||||
minZoom: constants.MIN_ZOOM,
|
minZoom: constants.MIN_ZOOM,
|
||||||
isRecenterButtonShown: false,
|
isRecenterButtonShown: false,
|
||||||
unknownMarkerIcon: L.icon({
|
// Variables for easy access to map feature and layers
|
||||||
iconAnchor: [20, 40],
|
accuracyFeature: null,
|
||||||
iconSize: [40, 40],
|
reportsMarkersFeatures: {},
|
||||||
iconUrl: unknownMarkerIcon,
|
reportsMarkersVectorSource: null,
|
||||||
}),
|
mainVectorSource: null,
|
||||||
|
polylineFeature: null,
|
||||||
|
positionFeature: null,
|
||||||
|
reportLatLngFeature: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleClick(event) {
|
setPositionFeatureStyle() {
|
||||||
if (this.onPress) {
|
const positionFeatureStyle = this.positionFeature.getStyle();
|
||||||
this.onPress(event.latlng);
|
const rotation = this.headingInRadiansFromNorth - Math.PI / 2;
|
||||||
|
|
||||||
|
// If heading is specified
|
||||||
|
if (this.headingInRadiansFromNorth !== null) {
|
||||||
|
// Check current style and update rotation if an arrow is already drawn
|
||||||
|
if (positionFeatureStyle) {
|
||||||
|
const TextStyle = positionFeatureStyle.getText();
|
||||||
|
if (TextStyle) {
|
||||||
|
TextStyle.setRotation(rotation);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Replace style by an arrow otherwise
|
||||||
|
this.positionFeature.setStyle(new Style({
|
||||||
|
text: new Text({
|
||||||
|
textBaseline: 'middle',
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
rotation,
|
||||||
|
font: '30px sans-serif',
|
||||||
|
text: '►',
|
||||||
|
fill: new Fill({
|
||||||
|
color: '#3399CC',
|
||||||
|
}),
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: '#fff',
|
||||||
|
width: 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No heading specified, force circle if a circle is not already
|
||||||
|
// displayed
|
||||||
|
if (!positionFeatureStyle || !positionFeatureStyle.getImage()) {
|
||||||
|
this.positionFeature.setStyle(new Style({
|
||||||
|
image: new CircleStyle({
|
||||||
|
radius: constants.POSITION_MARKER_RADIUS,
|
||||||
|
fill: new Fill({
|
||||||
|
color: '#3399CC',
|
||||||
|
}),
|
||||||
|
stroke: new Stroke({
|
||||||
|
color: '#fff',
|
||||||
|
width: 2,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deleteReportMarker(markerID) {
|
||||||
|
const feature = this.reportsMarkersFeatures[markerID];
|
||||||
|
if (feature) {
|
||||||
|
this.reportsMarkersVectorSource.removeFeature(feature);
|
||||||
|
delete this.reportsMarkersFeatures[markerID]; // careful to delete the item itself
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawReportMarker(marker, addedMarkersIDs) {
|
||||||
|
if ((addedMarkersIDs && !addedMarkersIDs.has(marker.id))
|
||||||
|
|| this.reportsMarkersFeatures[marker.id]
|
||||||
|
) {
|
||||||
|
// Skip the marker if it was not added or is already on the map
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a Feature for the marker, to add it on the map
|
||||||
|
const reportMarkerFeature = new Feature({
|
||||||
|
geometry: new Point(fromLonLat([marker.latLng[1], marker.latLng[0]])),
|
||||||
|
id: marker.id,
|
||||||
|
});
|
||||||
|
reportMarkerFeature.setStyle(new Style({
|
||||||
|
image: new Icon({
|
||||||
|
anchor: constants.ICON_ANCHOR,
|
||||||
|
scale: (
|
||||||
|
marker.id === this.reportDetailsID
|
||||||
|
? constants.LARGE_ICON_SCALE
|
||||||
|
: constants.NORMAL_ICON_SCALE
|
||||||
|
),
|
||||||
|
src: constants.REPORT_TYPES[marker.type].marker,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
// Add the marker to the map and keep a reference to it
|
||||||
|
this.reportsMarkersFeatures[marker.id] = reportMarkerFeature;
|
||||||
|
this.reportsMarkersVectorSource.addFeature(reportMarkerFeature);
|
||||||
|
},
|
||||||
|
handleClick(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
|
||||||
|
let isClickOnMarker = false;
|
||||||
|
if (this.map) {
|
||||||
|
this.map.forEachFeatureAtPixel(event.pixel, (feature, layer) => {
|
||||||
|
if (layer.get('name') !== REPORTS_MARKERS_VECTOR_LAYER_NAME) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isClickOnMarker = true;
|
||||||
|
this.$store.dispatch(
|
||||||
|
'showReportDetails',
|
||||||
|
{ id: feature.get('id'), userAsked: true },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isClickOnMarker && this.onPress) {
|
||||||
|
// Reverse coordinates as OL uses lng first.
|
||||||
|
const coords = toLonLat(event.coordinate).reverse();
|
||||||
|
this.onPress(coords);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
hideRecenterButton() {
|
hideRecenterButton() {
|
||||||
if (this.isRecenterButtonShown) {
|
if (this.isRecenterButtonShown) {
|
||||||
@ -163,49 +241,38 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMoveStart() {
|
onMoveStart() {
|
||||||
if (!this.isProgrammaticMove && !this.isProgrammaticZoom) {
|
if (!this.isProgrammaticMove) {
|
||||||
this.showRecenterButton();
|
this.showRecenterButton();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onMoveEnd() {
|
onMoveEnd() {
|
||||||
|
const view = this.map.getView();
|
||||||
if (this.onMapCenterUpdate) {
|
if (this.onMapCenterUpdate) {
|
||||||
const mapCenter = this.map.getCenter();
|
const mapCenterLonLat = toLonLat(view.getCenter());
|
||||||
this.onMapCenterUpdate([mapCenter.lat, mapCenter.lng]);
|
this.onMapCenterUpdate([mapCenterLonLat[1], mapCenterLonLat[0]]);
|
||||||
}
|
}
|
||||||
if (this.onMapZoomUpdate) {
|
if (this.onMapZoomUpdate) {
|
||||||
this.onMapZoomUpdate(this.map.getZoom());
|
this.onMapZoomUpdate(view.getZoom());
|
||||||
}
|
|
||||||
},
|
|
||||||
onZoomStart() {
|
|
||||||
if (!this.isProgrammaticZoom) {
|
|
||||||
this.showRecenterButton();
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
recenterMap() {
|
recenterMap() {
|
||||||
|
const view = this.map.getView();
|
||||||
|
const mapCenter = view.getCenter();
|
||||||
|
|
||||||
this.hideRecenterButton();
|
this.hideRecenterButton();
|
||||||
if (this.map.getZoom() !== this.zoom) {
|
|
||||||
this.isProgrammaticZoom = true;
|
|
||||||
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
|
||||||
}
|
|
||||||
const mapCenter = this.map.getCenter();
|
|
||||||
if (
|
if (
|
||||||
mapCenter.lat !== this.center[0]
|
view.getZoom() !== this.zoom
|
||||||
&& mapCenter.lng !== this.center[1]
|
|| mapCenter[0] !== this.olCenter[0]
|
||||||
|
|| mapCenter[1] !== this.olCenter[1]
|
||||||
|
|| view.getRotation() !== 0
|
||||||
) {
|
) {
|
||||||
this.isProgrammaticMove = true;
|
this.isProgrammaticMove = true;
|
||||||
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
||||||
}
|
}
|
||||||
this.map.setView(this.center, this.zoom);
|
view.setCenter(this.olCenter);
|
||||||
},
|
view.setRotation(0);
|
||||||
showCompass() {
|
view.setZoom(this.zoom);
|
||||||
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() {
|
showRecenterButton() {
|
||||||
if (!this.isRecenterButtonShown) {
|
if (!this.isRecenterButtonShown) {
|
||||||
@ -214,21 +281,114 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.map = this.$refs.map.mapObject;
|
// Create accuracy circle feature
|
||||||
if (this.map.getZoom() !== this.zoom) {
|
this.accuracyFeature = new Feature({
|
||||||
this.isProgrammaticZoom = true;
|
geometry: this.olPosition ? new Point(this.olPosition) : null,
|
||||||
this.map.once('zoomend', () => { this.isProgrammaticZoom = false; });
|
});
|
||||||
}
|
this.accuracyFeature.setStyle(new Style({
|
||||||
const mapCenter = this.map.getCenter();
|
image: new CircleStyle({
|
||||||
if (
|
radius: this.radiusFromAccuracy,
|
||||||
mapCenter.lat !== this.center[0]
|
fill: new Fill({
|
||||||
&& mapCenter.lng !== this.center[1]
|
color: 'rgba(51, 153, 204, 0.25)',
|
||||||
) {
|
}),
|
||||||
this.isProgrammaticMove = true;
|
stroke: new Stroke({
|
||||||
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
color: '#3399CC',
|
||||||
}
|
width: 2,
|
||||||
this.map.setView(this.center, this.zoom);
|
}),
|
||||||
this.showCompass();
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
// Create position marker feature
|
||||||
|
this.positionFeature = new Feature({
|
||||||
|
geometry: this.olPosition ? new Point(this.olPosition) : null,
|
||||||
|
});
|
||||||
|
this.setPositionFeatureStyle();
|
||||||
|
|
||||||
|
// Create polyline feature
|
||||||
|
this.polylineFeature = new Feature({
|
||||||
|
geometry: new LineString(this.olPolyline),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the map
|
||||||
|
this.isProgrammaticMove = true;
|
||||||
|
this.mainVectorSource = new VectorSource({
|
||||||
|
features: [
|
||||||
|
this.accuracyFeature,
|
||||||
|
this.positionFeature,
|
||||||
|
this.polylineFeature,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// Initialize markers
|
||||||
|
this.reportsMarkersVectorSource = new VectorSource();
|
||||||
|
this.markers.forEach(marker => this.drawReportMarker(marker, null));
|
||||||
|
// Create the rotate label
|
||||||
|
const rotateLabel = document.createElement('img');
|
||||||
|
rotateLabel.src = compassNorthIcon;
|
||||||
|
rotateLabel.style.width = '100%';
|
||||||
|
rotateLabel.style.height = '100%';
|
||||||
|
// Create the map object
|
||||||
|
this.map = new Map({
|
||||||
|
controls: defaultControls({
|
||||||
|
attributionOptions: {
|
||||||
|
collapsible: false,
|
||||||
|
},
|
||||||
|
rotateOptions: {
|
||||||
|
autoHide: false,
|
||||||
|
label: rotateLabel,
|
||||||
|
},
|
||||||
|
zoom: false,
|
||||||
|
}),
|
||||||
|
layers: [
|
||||||
|
new TileLayer({
|
||||||
|
source: new XYZ({
|
||||||
|
url: this.tileServer,
|
||||||
|
attributions: this.attribution,
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
new VectorLayer({
|
||||||
|
name: MAIN_VECTOR_LAYER_NAME,
|
||||||
|
source: this.mainVectorSource,
|
||||||
|
}),
|
||||||
|
new VectorLayer({
|
||||||
|
name: REPORTS_MARKERS_VECTOR_LAYER_NAME,
|
||||||
|
source: this.reportsMarkersVectorSource,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
target: 'map',
|
||||||
|
view: new View({
|
||||||
|
center: this.olCenter,
|
||||||
|
maxZoom: this.maxZoom,
|
||||||
|
minZoom: this.minZoom,
|
||||||
|
rotation: 0,
|
||||||
|
zoom: this.zoom,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
this.map.once('moveend', () => {
|
||||||
|
this.isProgrammaticMove = false;
|
||||||
|
|
||||||
|
this.map.on('click', this.handleClick);
|
||||||
|
// Take care that OpenLayer map actually catches "pointerdown"
|
||||||
|
// events and not "click" events. Then, we need an explicit event
|
||||||
|
// handler for "click" to stop propagation to ReportCard component.
|
||||||
|
document.querySelector('#map').addEventListener(
|
||||||
|
'click',
|
||||||
|
event => event.stopPropagation(),
|
||||||
|
);
|
||||||
|
this.map.on('movestart', this.onMoveStart);
|
||||||
|
this.map.on('moveend', this.onMoveEnd);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set pointer to hover when hovering a report marker
|
||||||
|
this.map.on('pointermove', (event) => {
|
||||||
|
if (event.dragging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hit = this.map.hasFeatureAtPixel(event.pixel, {
|
||||||
|
layerFilter: layer => layer.get('name') === REPORTS_MARKERS_VECTOR_LAYER_NAME,
|
||||||
|
});
|
||||||
|
this.map.getTargetElement().style.cursor = hit ? 'pointer' : '';
|
||||||
|
});
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
accuracy: Number,
|
accuracy: Number,
|
||||||
@ -250,93 +410,169 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
|
reportDetailsID(newID, oldID) {
|
||||||
|
[oldID, newID].forEach((id) => {
|
||||||
|
// We actually have to delete and recreate the feature,
|
||||||
|
// OpenLayer won't refresh the view if we only change the
|
||||||
|
// size. :/
|
||||||
|
this.deleteReportMarker(id);
|
||||||
|
const marker = this.markers.find(item => item.id === id);
|
||||||
|
if (marker) {
|
||||||
|
this.drawReportMarker(marker, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
markers(newMarkers, oldMarkers) {
|
||||||
|
// Map should have been created
|
||||||
|
if (this.reportsMarkersVectorSource === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute the diff between old and new marker arrays, to determine
|
||||||
|
// added and removed markers
|
||||||
|
const oldIDs = new Set(oldMarkers.map(item => item.id));
|
||||||
|
const newIDs = new Set(newMarkers.map(item => item.id));
|
||||||
|
|
||||||
|
// Add new markers to the map
|
||||||
|
const addedMarkersIDs = new Set([...newIDs].filter(x => !oldIDs.has(x)));
|
||||||
|
this.markers.forEach(marker => this.drawReportMarker(marker, addedMarkersIDs));
|
||||||
|
|
||||||
|
// Remove removed markers from the map
|
||||||
|
const removedMarkersIDs = [...oldIDs].filter(x => !newIDs.has(x));
|
||||||
|
removedMarkersIDs.forEach(id => this.deleteReportMarker(id));
|
||||||
|
},
|
||||||
|
olCenter(newOlCenter) {
|
||||||
|
if (!this.map) {
|
||||||
|
// Map should have been created
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = this.map.getView();
|
||||||
|
if (!this.isRecenterButtonShown) {
|
||||||
|
const currentCenter = view.getCenter();
|
||||||
|
// Handle programmatic navigation
|
||||||
|
if (
|
||||||
|
view.getZoom() !== this.zoom
|
||||||
|
|| currentCenter[0] !== newOlCenter[0]
|
||||||
|
|| currentCenter[1] !== newOlCenter[1]
|
||||||
|
) {
|
||||||
|
this.isProgrammaticMove = true;
|
||||||
|
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
distance: distance(this.center, marker.latLng),
|
||||||
|
}),
|
||||||
|
).filter(item => item.distance < constants.MIN_DISTANCE_REPORT_DETAILS);
|
||||||
|
const closestReport = distances.reduce( // Get the closest one
|
||||||
|
(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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update view
|
||||||
|
view.setCenter(newOlCenter);
|
||||||
|
view.setRotation(0);
|
||||||
|
view.setZoom(this.zoom);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
olPolyline(newOlPolyline) {
|
||||||
|
if (this.polylineFeature) {
|
||||||
|
// Update polyline trace
|
||||||
|
this.polylineFeature.setGeometry(new LineString(newOlPolyline));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
olPosition(newOlPosition) {
|
||||||
|
if (this.positionFeature) {
|
||||||
|
// Update position marker position
|
||||||
|
this.positionFeature.setGeometry(
|
||||||
|
newOlPosition ? new Point(newOlPosition) : null,
|
||||||
|
);
|
||||||
|
this.setPositionFeatureStyle();
|
||||||
|
}
|
||||||
|
if (this.accuracyFeature) {
|
||||||
|
// Update accuracy circle position and radius
|
||||||
|
this.accuracyFeature.setGeometry(
|
||||||
|
newOlPosition ? new Point(newOlPosition) : null,
|
||||||
|
);
|
||||||
|
this.accuracyFeature.getStyle().getImage().setRadius(this.radiusFromAccuracy);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reportLatLng(newReportLatLng) {
|
||||||
|
// Eventually remove old marker
|
||||||
|
if (this.reportLatLngFeature && this.mainVectorSource) {
|
||||||
|
this.mainVectorSource.removeFeature(this.reportLatLngFeature);
|
||||||
|
this.reportLatLngFeature = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create unknown report marker if needed
|
||||||
|
if (newReportLatLng && this.mainVectorSource) {
|
||||||
|
this.reportLatLngFeature = new Feature({
|
||||||
|
geometry: new Point(fromLonLat([newReportLatLng[1], newReportLatLng[0]])),
|
||||||
|
});
|
||||||
|
this.reportLatLngFeature.setStyle(new Style({
|
||||||
|
image: new Icon({
|
||||||
|
anchor: constants.ICON_ANCHOR,
|
||||||
|
scale: constants.NORMAL_ICON_SCALE,
|
||||||
|
src: unknownMarkerIcon,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
this.mainVectorSource.addFeature(this.reportLatLngFeature);
|
||||||
|
}
|
||||||
|
},
|
||||||
zoom(newZoom) {
|
zoom(newZoom) {
|
||||||
if (!this.map) {
|
if (!this.map) {
|
||||||
// Map should have been created
|
// Map should have been created
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const view = this.map.getView();
|
||||||
if (!this.isRecenterButtonShown) {
|
if (!this.isRecenterButtonShown) {
|
||||||
// Handle programmatic navigation
|
// Handle programmatic navigation
|
||||||
if (this.map.getZoom() !== newZoom) {
|
if (view.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 !== newCenterLatLng[0]
|
|
||||||
&& this.map.getCenter().lng !== newCenterLatLng[1]
|
|
||||||
) {
|
|
||||||
this.isProgrammaticMove = true;
|
this.isProgrammaticMove = true;
|
||||||
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
this.map.once('moveend', () => { this.isProgrammaticMove = false; });
|
||||||
|
|
||||||
// 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,
|
|
||||||
distance: distance(newCenterLatLng, marker.latLng),
|
|
||||||
}),
|
|
||||||
).filter(item => item.distance < constants.MIN_DISTANCE_REPORT_DETAILS);
|
|
||||||
const closestReport = distances.reduce( // Get the closest one
|
|
||||||
(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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.map.setView(newCenterLatLng, this.zoom);
|
view.setZoom(newZoom);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
.application .leaflet-bar a {
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
.compassIcon {
|
|
||||||
background-color: white;
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 42px;
|
|
||||||
height: 42px;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.compassIcon img {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fill-width {
|
.fill-width {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#map .ol-control button {
|
||||||
|
height: 3em !important;
|
||||||
|
width: 3em !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map .ol-rotate {
|
||||||
|
background: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map .ol-rotate button {
|
||||||
|
background-color: white !important;
|
||||||
|
border: 1px solid rgba(0, 0, 0, .87);
|
||||||
|
border-radius: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ReportErrorModal v-model="hasError"></ReportErrorModal>
|
<ReportErrorModal v-model="hasError"></ReportErrorModal>
|
||||||
<v-bottom-sheet v-model="isActive">
|
<v-bottom-sheet v-model="isActive" persistent id="reportCardSheet">
|
||||||
<v-card>
|
<v-card>
|
||||||
<v-container fluid>
|
<v-container fluid>
|
||||||
<v-layout row wrap>
|
<v-layout row wrap>
|
||||||
@ -84,6 +84,19 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('keydown', this.hideReportDialogOnEsc);
|
window.addEventListener('keydown', this.hideReportDialogOnEsc);
|
||||||
|
|
||||||
|
// Use persistent mode and recreate the clicking outside event handler
|
||||||
|
// here as Vuetify uses capture mode which has some issues with
|
||||||
|
// OpenLayers events.
|
||||||
|
const app = document.querySelector('[data-app]') || document.body;
|
||||||
|
app.addEventListener(
|
||||||
|
'click',
|
||||||
|
(event) => {
|
||||||
|
if (this.isActive && event.target.closest('#reportCardSheet') === null) {
|
||||||
|
this.isActive = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
value: Boolean,
|
value: Boolean,
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
<template>
|
|
||||||
<v-lmarker :lat-lng="marker.latLng" :icon="icon" @click="onClick"></v-lmarker>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import L from 'leaflet';
|
|
||||||
import { LMarker } from 'vue2-leaflet';
|
|
||||||
|
|
||||||
import { REPORT_TYPES } from '@/constants';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
'v-lmarker': LMarker,
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
marker: Object,
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
icon() {
|
|
||||||
if (this.$store.state.reportDetails.id === this.marker.id) {
|
|
||||||
return L.icon(REPORT_TYPES[this.marker.type].markerLarge);
|
|
||||||
}
|
|
||||||
return L.icon(REPORT_TYPES[this.marker.type].marker);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
showCard: false,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
onClick() {
|
|
||||||
this.$store.dispatch('showReportDetails', { id: this.marker.id, userAsked: true });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
@ -13,98 +13,52 @@ import potholeIcon from '@/assets/pothole.svg';
|
|||||||
|
|
||||||
export const VERSION = '0.1';
|
export const VERSION = '0.1';
|
||||||
|
|
||||||
|
export const NORMAL_ICON_SCALE = 0.625;
|
||||||
|
export const LARGE_ICON_SCALE = 1.0;
|
||||||
|
export const ICON_ANCHOR = [0, 0.5];
|
||||||
export const REPORT_TYPES = {
|
export const REPORT_TYPES = {
|
||||||
accident: {
|
accident: {
|
||||||
description: 'reportLabels.accidentDescription',
|
description: 'reportLabels.accidentDescription',
|
||||||
label: 'reportLabels.accident',
|
label: 'reportLabels.accident',
|
||||||
image: accidentIcon,
|
image: accidentIcon,
|
||||||
marker: {
|
marker: accidentMarker,
|
||||||
iconUrl: accidentMarker,
|
markerLarge: accidentMarker,
|
||||||
iconSize: [40, 40],
|
|
||||||
iconAnchor: [20, 40],
|
|
||||||
},
|
|
||||||
markerLarge: {
|
|
||||||
iconUrl: accidentMarker,
|
|
||||||
iconSize: [60, 60],
|
|
||||||
iconAnchor: [30, 60],
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
},
|
||||||
gcum: {
|
gcum: {
|
||||||
description: 'reportLabels.gcumDescription',
|
description: 'reportLabels.gcumDescription',
|
||||||
label: 'reportLabels.gcum',
|
label: 'reportLabels.gcum',
|
||||||
image: gcumIcon,
|
image: gcumIcon,
|
||||||
marker: {
|
marker: gcumMarker,
|
||||||
iconUrl: gcumMarker,
|
markerLarge: gcumMarker,
|
||||||
iconSize: [40, 40],
|
|
||||||
iconAnchor: [20, 40],
|
|
||||||
},
|
|
||||||
markerLarge: {
|
|
||||||
iconUrl: gcumMarker,
|
|
||||||
iconSize: [60, 60],
|
|
||||||
iconAnchor: [30, 60],
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
},
|
||||||
interrupt: {
|
interrupt: {
|
||||||
description: 'reportLabels.interruptDescription',
|
description: 'reportLabels.interruptDescription',
|
||||||
label: 'reportLabels.interrupt',
|
label: 'reportLabels.interrupt',
|
||||||
image: interruptIcon,
|
image: interruptIcon,
|
||||||
marker: {
|
marker: interruptMarker,
|
||||||
iconUrl: interruptMarker,
|
markerLarge: interruptMarker,
|
||||||
iconSize: [40, 40],
|
|
||||||
iconAnchor: [20, 40],
|
|
||||||
},
|
|
||||||
markerLarge: {
|
|
||||||
iconUrl: interruptMarker,
|
|
||||||
iconSize: [60, 60],
|
|
||||||
iconAnchor: [30, 60],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
misc: {
|
misc: {
|
||||||
description: 'reportLabels.miscDescription',
|
description: 'reportLabels.miscDescription',
|
||||||
label: 'reportLabels.misc',
|
label: 'reportLabels.misc',
|
||||||
image: miscIcon,
|
image: miscIcon,
|
||||||
marker: {
|
marker: miscMarker,
|
||||||
iconUrl: miscMarker,
|
markerLarge: miscMarker,
|
||||||
iconSize: [40, 40],
|
|
||||||
iconAnchor: [20, 40],
|
|
||||||
},
|
|
||||||
markerLarge: {
|
|
||||||
iconUrl: miscMarker,
|
|
||||||
iconSize: [60, 60],
|
|
||||||
iconAnchor: [30, 60],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
obstacle: {
|
obstacle: {
|
||||||
description: 'reportLabels.obstacleDescription',
|
description: 'reportLabels.obstacleDescription',
|
||||||
label: 'reportLabels.obstacle',
|
label: 'reportLabels.obstacle',
|
||||||
image: obstacleIcon,
|
image: obstacleIcon,
|
||||||
marker: {
|
marker: obstacleMarker,
|
||||||
iconUrl: obstacleMarker,
|
markerLarge: obstacleMarker,
|
||||||
iconSize: [40, 40],
|
|
||||||
iconAnchor: [20, 40],
|
|
||||||
},
|
|
||||||
markerLarge: {
|
|
||||||
iconUrl: obstacleMarker,
|
|
||||||
iconSize: [60, 60],
|
|
||||||
iconAnchor: [30, 60],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
pothole: {
|
pothole: {
|
||||||
description: 'reportLabels.potholeDescription',
|
description: 'reportLabels.potholeDescription',
|
||||||
label: 'reportLabels.pothole',
|
label: 'reportLabels.pothole',
|
||||||
image: potholeIcon,
|
image: potholeIcon,
|
||||||
marker: {
|
marker: potholeMarker,
|
||||||
iconUrl: potholeMarker,
|
markerLarge: potholeMarker,
|
||||||
iconSize: [40, 40],
|
|
||||||
iconAnchor: [20, 40],
|
|
||||||
},
|
|
||||||
markerLarge: {
|
|
||||||
iconUrl: potholeMarker,
|
|
||||||
iconSize: [60, 60],
|
|
||||||
iconAnchor: [30, 60],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
// Display order of the report types
|
// Display order of the report types
|
||||||
@ -112,7 +66,7 @@ export const REPORT_TYPES_ORDER = ['gcum', 'interrupt', 'obstacle', 'pothole', '
|
|||||||
|
|
||||||
export const MIN_DISTANCE_REPORT_DETAILS = 40; // in meters
|
export const MIN_DISTANCE_REPORT_DETAILS = 40; // in meters
|
||||||
|
|
||||||
export const MOCK_LOCATION = false;
|
export const MOCK_LOCATION = true;
|
||||||
export const MOCK_LOCATION_USE_GPX = true;
|
export const MOCK_LOCATION_USE_GPX = true;
|
||||||
export const MOCK_LOCATION_GPX_PLAYBACK_SPEED = 2.0;
|
export const MOCK_LOCATION_GPX_PLAYBACK_SPEED = 2.0;
|
||||||
export const MOCK_LOCATION_UPDATE_INTERVAL = 5 * 1000; // in milliseconds
|
export const MOCK_LOCATION_UPDATE_INTERVAL = 5 * 1000; // in milliseconds
|
||||||
@ -141,13 +95,14 @@ export const MIN_ZOOM = 10;
|
|||||||
export const MAX_ZOOM = 18;
|
export const MAX_ZOOM = 18;
|
||||||
|
|
||||||
export const ACCURACY_DISPLAY_THRESHOLD = 100; // in meters
|
export const ACCURACY_DISPLAY_THRESHOLD = 100; // in meters
|
||||||
|
export const POSITION_MARKER_RADIUS = 10; // in pixels
|
||||||
|
|
||||||
let opencyclemapURL = 'https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png';
|
let opencyclemapURL = 'https://tile.thunderforest.com/cycle/{z}/{x}/{y}.png';
|
||||||
if (process.env.THUNDERFOREST_API_KEY) {
|
if (process.env.THUNDERFOREST_API_KEY) {
|
||||||
opencyclemapURL += `?apikey=${process.env.THUNDERFOREST_API_KEY}`;
|
opencyclemapURL += `?apikey=${process.env.THUNDERFOREST_API_KEY}`;
|
||||||
}
|
}
|
||||||
export const TILE_SERVERS = {
|
export const TILE_SERVERS = {
|
||||||
'cartodb-voyager': 'https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png',
|
'cartodb-voyager': 'https://{a-c}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png',
|
||||||
opencyclemap: opencyclemapURL,
|
opencyclemap: opencyclemapURL,
|
||||||
};
|
};
|
||||||
export const DEFAULT_TILE_SERVER = 'cartodb-voyager';
|
export const DEFAULT_TILE_SERVER = 'cartodb-voyager';
|
||||||
|
@ -9,9 +9,11 @@ import {
|
|||||||
MOCK_LOCATION_LNG_MIN, MOCK_LOCATION_LNG_MAX,
|
MOCK_LOCATION_LNG_MIN, MOCK_LOCATION_LNG_MAX,
|
||||||
} from '@/constants';
|
} from '@/constants';
|
||||||
|
|
||||||
let mockGPX = null;
|
let mockGPX = [];
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
mockGPX = require('@/tools/mock_gpx.json'); // eslint-disable-line global-require
|
// Use a node_modules require here, this is handled by Webpack to fetch either
|
||||||
|
// a custom mock_gpx.json or a default empty one.
|
||||||
|
mockGPX = require('mock_gpx.json'); // eslint-disable-line global-require
|
||||||
}
|
}
|
||||||
|
|
||||||
export function distance(latLng1, latLng2) {
|
export function distance(latLng1, latLng2) {
|
||||||
@ -37,7 +39,7 @@ export function mockLocationRandom() {
|
|||||||
}
|
}
|
||||||
const newLocation = {
|
const newLocation = {
|
||||||
coords: {
|
coords: {
|
||||||
accuracy: 10, // In meters
|
accuracy: Math.random() * 100, // In meters
|
||||||
latitude: (
|
latitude: (
|
||||||
(Math.random() * (MOCK_LOCATION_LAT_MAX - MOCK_LOCATION_LAT_MIN))
|
(Math.random() * (MOCK_LOCATION_LAT_MAX - MOCK_LOCATION_LAT_MIN))
|
||||||
+ MOCK_LOCATION_LAT_MIN
|
+ MOCK_LOCATION_LAT_MIN
|
||||||
@ -55,23 +57,25 @@ export function mockLocationRandom() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function mockLocationWithGPX(index, setPosition) {
|
export function mockLocationWithGPX(index, setPosition) {
|
||||||
setPosition(mockGPX[index]);
|
if (mockGPX[index]) {
|
||||||
if (index < mockGPX.length) {
|
setPosition(mockGPX[index]);
|
||||||
const delay = (
|
if (mockGPX[index + 1]) {
|
||||||
moment(mockGPX[index + 1].time).valueOf()
|
const delay = (
|
||||||
- moment(mockGPX[index].time).valueOf()
|
moment(mockGPX[index + 1].time).valueOf()
|
||||||
);
|
- moment(mockGPX[index].time).valueOf()
|
||||||
setTimeout(
|
);
|
||||||
() => mockLocationWithGPX(index + 1, setPosition),
|
setTimeout(
|
||||||
delay / MOCK_LOCATION_GPX_PLAYBACK_SPEED,
|
() => mockLocationWithGPX(index + 1, setPosition),
|
||||||
);
|
delay / MOCK_LOCATION_GPX_PLAYBACK_SPEED,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mockLocation(setPosition) {
|
export function mockLocation(setPosition) {
|
||||||
if (MOCK_LOCATION_USE_GPX) {
|
if (MOCK_LOCATION_USE_GPX) {
|
||||||
mockLocationWithGPX(0, setPosition);
|
mockLocationWithGPX(0, setPosition);
|
||||||
return null;
|
return -1; // Return a fake setInterval id
|
||||||
}
|
}
|
||||||
setPosition(mockLocationRandom());
|
setPosition(mockLocationRandom());
|
||||||
return setInterval(
|
return setInterval(
|
||||||
|
@ -63,6 +63,7 @@ function handlePositionError(error) {
|
|||||||
function setPosition(position) {
|
function setPosition(position) {
|
||||||
const lastLocation = store.getters.getLastLocation;
|
const lastLocation = store.getters.getLastLocation;
|
||||||
if (lastLocation !== null) {
|
if (lastLocation !== null) {
|
||||||
|
// TODO: Should not be lastLocation
|
||||||
const distanceFromPreviousPoint = distance(
|
const distanceFromPreviousPoint = distance(
|
||||||
[lastLocation.latitude, lastLocation.longitude],
|
[lastLocation.latitude, lastLocation.longitude],
|
||||||
[position.coords.latitude, position.coords.longitude],
|
[position.coords.latitude, position.coords.longitude],
|
||||||
@ -90,12 +91,11 @@ export default {
|
|||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
currentLatLng() {
|
currentLatLng() {
|
||||||
const currentLocation = this.$store.getters.getLastLocation;
|
|
||||||
// Check that this is a correct position
|
// Check that this is a correct position
|
||||||
if (currentLocation === null) {
|
if (this.currentLocation === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return [currentLocation.latitude, currentLocation.longitude];
|
return [this.currentLocation.latitude, this.currentLocation.longitude];
|
||||||
},
|
},
|
||||||
currentLocation() {
|
currentLocation() {
|
||||||
return this.$store.getters.getLastLocation || {};
|
return this.$store.getters.getLastLocation || {};
|
||||||
@ -202,7 +202,7 @@ export default {
|
|||||||
},
|
},
|
||||||
showReportDialog(latlng) {
|
showReportDialog(latlng) {
|
||||||
if (latlng) {
|
if (latlng) {
|
||||||
this.reportLatLng = [latlng.lat, latlng.lng];
|
this.reportLatLng = [latlng[0], latlng[1]];
|
||||||
} else {
|
} else {
|
||||||
this.reportLatLng = this.currentLatLng;
|
this.reportLatLng = this.currentLatLng;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user