Display reports on the map

This commit is contained in:
Lucas Verney 2018-06-26 11:04:23 +02:00
parent dd4075b18c
commit e961a8dbb1
9 changed files with 988 additions and 128 deletions

View File

@ -3,7 +3,7 @@ root = true
[*] [*]
charset = utf-8 charset = utf-8
indent_style = space indent_style = space
indent_size = 2 indent_size = 4
end_of_line = lf end_of_line = lf
insert_final_newline = true insert_final_newline = false
trim_trailing_whitespace = true trim_trailing_whitespace = true

View File

@ -1,12 +1,12 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>cyclassist</title> <title>cyclassist</title>
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
</body> </body>
</html> </html>

View File

@ -1,82 +1,82 @@
{ {
"name": "cyclassist", "name": "cyclassist",
"version": "1.0.0", "version": "1.0.0",
"description": "A Vue.js project", "description": "A Vue.js project",
"author": "Phyks (Lucas Verney) <phyks@phyks.me>", "author": "Phyks (Lucas Verney) <phyks@phyks.me>",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js", "dev": "webpack-dev-server --inline --progress --config build/webpack.dev.conf.js",
"start": "npm run dev", "start": "npm run dev",
"lint": "eslint --ext .js,.vue src", "lint": "eslint --ext .js,.vue src",
"build": "node build/build.js" "build": "node build/build.js"
}, },
"dependencies": { "dependencies": {
"es6-promise": "^4.2.4", "es6-promise": "^4.2.4",
"isomorphic-fetch": "^2.2.1", "isomorphic-fetch": "^2.2.1",
"leaflet": "^1.3.1", "leaflet": "^1.3.1",
"leaflet-tracksymbol": "^1.0.8", "leaflet-tracksymbol": "^1.0.8",
"material-icons": "^0.2.3", "material-icons": "^0.2.3",
"nosleep.js": "^0.7.0", "nosleep.js": "^0.7.0",
"roboto-fontface": "^0.9.0", "roboto-fontface": "^0.9.0",
"vue": "^2.5.2", "vue": "^2.5.2",
"vue-i18n": "^7.8.1", "vue-i18n": "^7.8.1",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",
"vue2-leaflet": "^1.0.2", "vue2-leaflet": "^1.0.2",
"vue2-leaflet-tracksymbol": "^1.0.10", "vue2-leaflet-tracksymbol": "^1.0.10",
"vuetify": "^1.0.0" "vuetify": "^1.0.0"
}, },
"devDependencies": { "devDependencies": {
"autoprefixer": "^7.1.2", "autoprefixer": "^7.1.2",
"babel-core": "^6.22.1", "babel-core": "^6.22.1",
"babel-eslint": "^7.1.1", "babel-eslint": "^7.1.1",
"babel-helper-vue-jsx-merge-props": "^2.0.3", "babel-helper-vue-jsx-merge-props": "^2.0.3",
"babel-loader": "^7.1.1", "babel-loader": "^7.1.1",
"babel-plugin-syntax-jsx": "^6.18.0", "babel-plugin-syntax-jsx": "^6.18.0",
"babel-plugin-transform-runtime": "^6.22.0", "babel-plugin-transform-runtime": "^6.22.0",
"babel-plugin-transform-vue-jsx": "^3.5.0", "babel-plugin-transform-vue-jsx": "^3.5.0",
"babel-preset-env": "^1.3.2", "babel-preset-env": "^1.3.2",
"babel-preset-stage-2": "^6.22.0", "babel-preset-stage-2": "^6.22.0",
"chalk": "^2.0.1", "chalk": "^2.0.1",
"copy-webpack-plugin": "^4.0.1", "copy-webpack-plugin": "^4.0.1",
"css-loader": "^0.28.0", "css-loader": "^0.28.0",
"eslint": "^3.19.0", "eslint": "^3.19.0",
"eslint-config-airbnb-base": "^11.3.0", "eslint-config-airbnb-base": "^11.3.0",
"eslint-friendly-formatter": "^3.0.0", "eslint-friendly-formatter": "^3.0.0",
"eslint-import-resolver-webpack": "^0.8.3", "eslint-import-resolver-webpack": "^0.8.3",
"eslint-loader": "^1.7.1", "eslint-loader": "^1.7.1",
"eslint-plugin-html": "^3.0.0", "eslint-plugin-html": "^3.0.0",
"eslint-plugin-import": "^2.7.0", "eslint-plugin-import": "^2.7.0",
"extract-text-webpack-plugin": "^3.0.0", "extract-text-webpack-plugin": "^3.0.0",
"file-loader": "^1.1.4", "file-loader": "^1.1.4",
"friendly-errors-webpack-plugin": "^1.6.1", "friendly-errors-webpack-plugin": "^1.6.1",
"html-webpack-plugin": "^2.30.1", "html-webpack-plugin": "^2.30.1",
"node-notifier": "^5.1.2", "node-notifier": "^5.1.2",
"optimize-css-assets-webpack-plugin": "^3.2.0", "optimize-css-assets-webpack-plugin": "^3.2.0",
"ora": "^1.2.0", "ora": "^1.2.0",
"portfinder": "^1.0.13", "portfinder": "^1.0.13",
"postcss-import": "^11.0.0", "postcss-import": "^11.0.0",
"postcss-loader": "^2.0.8", "postcss-loader": "^2.0.8",
"postcss-url": "^7.2.1", "postcss-url": "^7.2.1",
"rimraf": "^2.6.0", "rimraf": "^2.6.0",
"semver": "^5.3.0", "semver": "^5.3.0",
"shelljs": "^0.7.6", "shelljs": "^0.7.6",
"uglifyjs-webpack-plugin": "^1.1.1", "uglifyjs-webpack-plugin": "^1.1.1",
"url-loader": "^0.5.8", "url-loader": "^0.5.8",
"vue-loader": "^13.3.0", "vue-loader": "^13.3.0",
"vue-style-loader": "^3.0.1", "vue-style-loader": "^3.0.1",
"vue-template-compiler": "^2.5.2", "vue-template-compiler": "^2.5.2",
"webpack": "^3.6.0", "webpack": "^3.6.0",
"webpack-bundle-analyzer": "^2.9.0", "webpack-bundle-analyzer": "^2.9.0",
"webpack-dev-server": "^2.9.1", "webpack-dev-server": "^2.9.1",
"webpack-merge": "^4.1.0" "webpack-merge": "^4.1.0"
}, },
"engines": { "engines": {
"node": ">= 6.0.0", "node": ">= 6.0.0",
"npm": ">= 3.0.0" "npm": ">= 3.0.0"
}, },
"browserslist": [ "browserslist": [
"> 1%", "> 1%",
"last 2 versions", "last 2 versions",
"not ie <= 8" "not ie <= 8"
] ]
} }

View File

@ -12,9 +12,13 @@ export function saveReport(type, lat, lng) {
lat, lat,
lng, lng,
}), }),
}); })
.catch(exc => console.error(`Unable to post report: ${exc}.`));
} }
export function getReports() { export function getReports() {
// TODO return fetch(`${BASE_URL}api/v1/reports`)
.then(response => response.json())
.then(response => response.data)
.catch(exc => console.error(`Unable to fetch reports: ${exc}.`));
} }

View File

@ -3,6 +3,7 @@
<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 :lat-lng="latlng" :options="markerOptions"></v-lts>
<v-lmarker v-for="marker in markers" :key="marker.id" :lat-lng="marker.latLng"></v-lmarker>
</v-lmap> </v-lmap>
</div> </div>
</template> </template>
@ -33,6 +34,7 @@ export default {
heading: Number, heading: Number,
lat: Number, lat: Number,
lng: Number, lng: Number,
markers: Array,
}, },
computed: { computed: {
latlng() { latlng() {

View File

@ -1,2 +0,0 @@
export const MOCK_LOCATION = { lat: 48.866667, lng: 2.333333, heading: 20 * (Math.PI / 180) };
// export const MOCK_LOCATION = false;

30
src/tools/index.js Normal file
View File

@ -0,0 +1,30 @@
export function distance(latLng1, latLng2) {
const lat1 = (latLng1[0] * Math.PI) / 180;
const lng1 = (latLng1[1] * Math.PI) / 180;
const lat2 = (latLng2[0] * Math.PI) / 180;
const lng2 = (latLng2[1] * Math.PI) / 180;
const a = (
(Math.sin((lat2 - lat1) / 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 EARTH_RADIUS = 6371000;
return EARTH_RADIUS * c;
}
export function mockLocation() {
const LAT_MIN = 48.854031;
const LNG_MIN = 2.281279;
const LAT_MAX = 48.886123;
const LNG_MAX = 2.392742;
return {
coords: {
latitude: (Math.random() * (LAT_MAX - LAT_MIN)) + LAT_MIN,
longitude: (Math.random() * (LNG_MAX - LNG_MIN)) + LNG_MIN,
heading: 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"></Map> <Map :lat="lat" :lng="lng" :heading="heading" :markers="reportsMarkers"></Map>
<v-btn <v-btn
fixed fixed
dark dark
@ -30,9 +30,14 @@
<script> <script>
import NoSleep from 'nosleep.js'; import NoSleep from 'nosleep.js';
import { MOCK_LOCATION } from '@/constants'; import * as api from '@/api';
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 { 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: {
@ -42,11 +47,23 @@ export default {
created() { created() {
this.initializePositionWatching(); this.initializePositionWatching();
this.setNoSleep(); this.setNoSleep();
this.fetchReports();
}, },
beforeDestroy() { beforeDestroy() {
this.disableNoSleep(); this.disableNoSleep();
this.disablePositionWatching(); this.disablePositionWatching();
}, },
computed: {
reportsMarkers() {
if (!this.reports) {
return [];
}
return this.reports.map(report => ({
id: report.id,
latLng: [report.attributes.lat, report.attributes.lng],
}));
},
},
data() { data() {
return { return {
dialog: false, dialog: false,
@ -55,6 +72,7 @@ export default {
lat: null, lat: null,
lng: null, lng: null,
noSleep: null, noSleep: null,
reports: null,
watchID: null, watchID: null,
}; };
}, },
@ -62,15 +80,17 @@ 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 (!('geolocation' in navigator)) {
this.error = this.$t('geolocation.unavailable');
}
if (MOCK_LOCATION) { if (MOCK_LOCATION) {
this.lat = MOCK_LOCATION.lat; this.setPosition(mockLocation());
this.lng = MOCK_LOCATION.lng; this.watchID = setInterval(
this.heading = MOCK_LOCATION.heading; () => this.setPosition(mockLocation()),
MOCK_LOCATION_UPDATE_INTERVAL,
);
} else { } else {
if (!('geolocation' in navigator)) {
this.error = this.$t('geolocation.unavailable');
}
this.watchID = navigator.geolocation.watchPosition( this.watchID = navigator.geolocation.watchPosition(
this.setPosition, this.setPosition,
this.handlePositionError, this.handlePositionError,
@ -84,13 +104,26 @@ export default {
}, },
disablePositionWatching() { disablePositionWatching() {
if (this.watchID !== null) { if (this.watchID !== null) {
navigator.geolocation.clearWatch(this.watchID); if (MOCK_LOCATION) {
clearInterval(this.watchID);
} else {
navigator.geolocation.clearWatch(this.watchID);
}
} }
}, },
handlePositionError(error) { handlePositionError(error) {
this.error = `Error ${error.code}: ${error.message}`; this.error = `Error ${error.code}: ${error.message}`;
}, },
setPosition(position) { setPosition(position) {
if (this.lat && this.lng) {
const distanceFromPreviousPoint = distance(
[this.lat, this.lng],
[position.coords.latitude, position.coords.longitude],
);
if (distanceFromPreviousPoint > UPDATE_REPORTS_DISTANCE_THRESHOLD) {
this.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) { if (position.coords.heading) {
@ -101,7 +134,6 @@ export default {
}, },
setNoSleep() { setNoSleep() {
this.noSleep = new NoSleep(); this.noSleep = new NoSleep();
console.log(this.noSleep);
this.noSleep.enable(); this.noSleep.enable();
}, },
disableNoSleep() { disableNoSleep() {
@ -109,6 +141,11 @@ export default {
this.noSleep.disable(); this.noSleep.disable();
} }
}, },
fetchReports() {
api.getReports().then((reports) => {
this.reports = reports;
});
},
}, },
}; };
</script> </script>

833
yarn.lock

File diff suppressed because it is too large Load Diff