VueJS rewrite;
This commit is contained in:
parent
97b62ca02c
commit
fd4ba2a8bb
12
.editorconfig
Normal file
12
.editorconfig
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
end_of_line = lf
|
||||||
|
charset = utf-8
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.md]
|
||||||
|
trim_trailing_whitespace = false
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
node_modules
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# poi dist
|
||||||
|
dist
|
47
README.md
Normal file
47
README.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# app
|
||||||
|
|
||||||
|
> My posh Vue project
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
You can replace `yarn` with `npm run` here.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# build for production
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# development mode
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# run unit tests
|
||||||
|
yarn test
|
||||||
|
|
||||||
|
# serve the bundled dist folder in production mode
|
||||||
|
yarn serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Polyfills
|
||||||
|
|
||||||
|
By default we only polyfill `window.Promise` and `Object.assign`. You can add more polyfills in `./src/polyfills.js`.
|
||||||
|
|
||||||
|
## Analyze bundle size
|
||||||
|
|
||||||
|
Run `yarn report` to get a report of bundle size which helps you:
|
||||||
|
|
||||||
|
- Realize what's really inside your bundle
|
||||||
|
- Find out what modules make up the most of it's size
|
||||||
|
- Find modules that got there by mistake
|
||||||
|
- Optimize it!
|
||||||
|
|
||||||
|
## Progress Web App
|
||||||
|
|
||||||
|
Your app is now offline-ready (only in production bundle), which means you can visit it without network.
|
||||||
|
|
||||||
|
Here we use a default [manifest.json](./static/manifest.json) to configurure your pwa, for example, to enable *Add to Home Screen* feature on Android. It will be copied directly to `./dist/manifest.json`.
|
||||||
|
|
||||||
|
|
||||||
|
For all the available options, please head to [poi-preset-offline](https://github.com/egoist/poi/tree/master/packages/poi-preset-offline#api).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
This project is generated by [create-vue-app](https://github.com/vue-land/create-vue-app).
|
29
index.ejs
Normal file
29
index.ejs
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-type" content="text/html; charset=utf-8"/>
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
|
|
||||||
|
|
||||||
|
<link rel="manifest" href="/manifest.json">
|
||||||
|
<meta name="theme-color" content="#4DBA87">
|
||||||
|
|
||||||
|
|
||||||
|
<% if (htmlWebpackPlugin.options.description) { %>
|
||||||
|
<meta name="description" content="<%= htmlWebpackPlugin.options.description %>"/>
|
||||||
|
<% } %>
|
||||||
|
|
||||||
|
<% for (var chunk of webpack.chunks) {
|
||||||
|
for (var file of chunk.files) {
|
||||||
|
if (file.match(/\.(js|css)$/)) { %>
|
||||||
|
<link rel="<%= chunk.initial?'preload':'prefetch' %>" href="<%= htmlWebpackPlugin.files.publicPath + file %>" as="<%= file.match(/\.css$/)?'style':'script' %>"><% }}} %>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
You need to enable JavaScript to run this app.
|
||||||
|
</noscript>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
37
index.html
37
index.html
@ -1,37 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>Cool places in Lyon 7</title>
|
|
||||||
|
|
||||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css">
|
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.4.0/dist/leaflet.css"
|
|
||||||
integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=="
|
|
||||||
crossorigin=""/>
|
|
||||||
<link href='https://api.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.css' rel='stylesheet' />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Magellan</h1>
|
|
||||||
<form class="form" id="form">
|
|
||||||
<div class="form-group">
|
|
||||||
<input id="address" class="form-control" type="text" name="title" placeholder="Adresse">
|
|
||||||
<input id="submit" class="btn btn-primary" type="submit" value="Search">
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<ul id="results"></ul>
|
|
||||||
<hr>
|
|
||||||
<div id="the-map" style="width: 100%; height: 800px"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="//unpkg.com/kinto/dist/kinto.min.js"></script>
|
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.4.0/dist/leaflet.js"
|
|
||||||
integrity="sha512-QVftwZFqvtRNi0ZyCtsznlKSWOStnDORoefr1enyq5mVL4tmKB3S/EnC3rRJcxCPavG10IcrVGSmPh6Qw5lwrg=="
|
|
||||||
crossorigin=""></script>
|
|
||||||
|
|
||||||
<script src='https://api.mapbox.com/mapbox-gl-js/v0.53.0/mapbox-gl.js'></script>
|
|
||||||
|
|
||||||
<script src="magellan.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
151
magellan.js
151
magellan.js
@ -1,151 +0,0 @@
|
|||||||
(function() {
|
|
||||||
const MIN_ZOOM = 6;
|
|
||||||
const MAX_ZOOM = 18;
|
|
||||||
const INITIAL_ZOOM = 14;
|
|
||||||
|
|
||||||
const TILE_URL = 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png';
|
|
||||||
|
|
||||||
const STATE = {
|
|
||||||
locations: new Map(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let db = new Kinto({
|
|
||||||
bucket: "magellan"
|
|
||||||
});
|
|
||||||
let kintoLocations = db.collection("locations");
|
|
||||||
let syncOptions = {
|
|
||||||
remote: "https://kinto.b.delire.party/v1",
|
|
||||||
//headers: {Authorization: "Basic " + btoa('user:pass')}
|
|
||||||
};
|
|
||||||
|
|
||||||
async function sync() {
|
|
||||||
try {
|
|
||||||
await kintoLocations.sync(syncOptions);
|
|
||||||
} catch (err){
|
|
||||||
alert(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sync();
|
|
||||||
|
|
||||||
function addLocation(loc) {
|
|
||||||
if (!STATE.locations.has(loc.label)) {
|
|
||||||
let marker = L.marker(loc.coordinates)
|
|
||||||
.addTo(map)
|
|
||||||
.bindPopup(loc.label);
|
|
||||||
STATE.locations.set(loc.label, marker);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
try {
|
|
||||||
let locations = await kintoLocations.list();
|
|
||||||
locations = locations.data;
|
|
||||||
console.log(locations)
|
|
||||||
locations.forEach(addLocation);
|
|
||||||
} catch (err) {
|
|
||||||
alert(`on init: ${err.toString()}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
init();
|
|
||||||
|
|
||||||
async function addNewLocation(loc) {
|
|
||||||
if (addLocation(loc)) {
|
|
||||||
let marker = STATE.locations.get(loc.label);
|
|
||||||
marker.openPopup();
|
|
||||||
|
|
||||||
// Add it to kinto.
|
|
||||||
try {
|
|
||||||
await kintoLocations.create({
|
|
||||||
label: loc.label,
|
|
||||||
coordinates: loc.coordinates
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
alert(`Error when creating new location: ${err.toString()}`);
|
|
||||||
};
|
|
||||||
|
|
||||||
sync();
|
|
||||||
} else {
|
|
||||||
STATE.locations.get(loc.label).openPopup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Location {
|
|
||||||
constructor({ id, label, coordinates}) {
|
|
||||||
this.id = id || null;
|
|
||||||
this.label = label;
|
|
||||||
this.coordinates = coordinates;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchLocation(address) {
|
|
||||||
// Alternatively, search only for addresses (specialized for Lyon 7).
|
|
||||||
//const GEOCODING_URL = 'https://api-adresse.data.gouv.fr/search/?q={query}&postcode=69007';
|
|
||||||
|
|
||||||
const GEOCODING_URL = 'https://demo.addok.xyz/search/?q={query}&limit=20';
|
|
||||||
let resp = await fetch(GEOCODING_URL.replace('{query}', address));
|
|
||||||
let results = await resp.json();
|
|
||||||
|
|
||||||
results = results.features.map(feature => {
|
|
||||||
let { properties: {label: label}, geometry: { coordinates: coordinates }} = feature;
|
|
||||||
coordinates.unshift(coordinates.pop());
|
|
||||||
return new Location({label, coordinates});
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(results);
|
|
||||||
return results;
|
|
||||||
}
|
|
||||||
|
|
||||||
let map = L.map('the-map').setView([45.751591, 4.845695], INITIAL_ZOOM);
|
|
||||||
|
|
||||||
L.tileLayer(TILE_URL, {minZoom: MIN_ZOOM, maxZoom: MAX_ZOOM, attribution: 'Carte données © <a href="https://osm.org/copyright/">OpenStreetMap (ODbL)</a> / fond OSM-FR (CC-by-SA), <a href="https://www.data.gouv.fr/fr/datasets/points-dinterets-openstreetmap/">POI: OpenStreetMap (ODbL)</a>, <a href="https://www.data.gouv.fr/fr/datasets/base-d-adresses-nationale-ouverte-bano/">adresses BANO (ODbL)</a>, <a href="https://github.com/addok/addok">geocodeur addok</a>'}).addTo(map);
|
|
||||||
|
|
||||||
let $ = document.querySelector.bind(document);
|
|
||||||
let $address = $('#address');
|
|
||||||
let $submit = $('#submit');
|
|
||||||
let $results = window.results = $('#results');
|
|
||||||
|
|
||||||
$('#form').onsubmit = function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$submit.onclick = async function() {
|
|
||||||
let address = $address.value.trim();
|
|
||||||
|
|
||||||
$results.innerHTML = `searching for ${address}...`;
|
|
||||||
let firstResult = true;
|
|
||||||
|
|
||||||
let locations = await fetchLocation(address);
|
|
||||||
locations = locations.map(loc => {
|
|
||||||
let li = document.createElement('li');
|
|
||||||
|
|
||||||
let a = document.createElement('a');
|
|
||||||
a.href = '#';
|
|
||||||
a.onclick = function() {
|
|
||||||
L.popup()
|
|
||||||
.setLatLng(loc.coordinates)
|
|
||||||
.setContent(loc.label)
|
|
||||||
.openOn(map);
|
|
||||||
}
|
|
||||||
a.appendChild(document.createTextNode(loc.label))
|
|
||||||
li.appendChild(a);
|
|
||||||
|
|
||||||
li.appendChild(document.createTextNode(' '));
|
|
||||||
|
|
||||||
let button = document.createElement('button');
|
|
||||||
button.appendChild(document.createTextNode('Save'));
|
|
||||||
button.onclick = () => { addNewLocation(loc) };
|
|
||||||
li.appendChild(button);
|
|
||||||
|
|
||||||
if (firstResult) {
|
|
||||||
firstResult = false;
|
|
||||||
$results.innerHTML = '';
|
|
||||||
}
|
|
||||||
$results.appendChild(li);
|
|
||||||
return li;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})();
|
|
37
package.json
Normal file
37
package.json
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"private": true,
|
||||||
|
"name": "app",
|
||||||
|
"productName": "app",
|
||||||
|
"description": "My posh Vue project",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "poi",
|
||||||
|
"build": "poi build",
|
||||||
|
"report": "poi build --bundle-report",
|
||||||
|
"serve": "serve dist --single"
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"name": "bnjbvr",
|
||||||
|
"email": "public@benj.me"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"kinto": "^12.3.0",
|
||||||
|
"leaflet": "^1.4.0",
|
||||||
|
"leaflet-sidebar-v2": "^3.1.1",
|
||||||
|
"lodash.debounce": "^4.0.8",
|
||||||
|
"normalize.css": "^7.0.0",
|
||||||
|
"object-assign": "^4.1.1",
|
||||||
|
"offline-plugin": "^4.8.0",
|
||||||
|
"promise-polyfill": "^6.0.2",
|
||||||
|
"vue-router": "^3.0.2",
|
||||||
|
"vue2-leaflet": "^2.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@fortawesome/fontawesome-free": "^5.8.1",
|
||||||
|
"poi": "^9.0.0",
|
||||||
|
"poi-preset-bundle-report": "^2.0.0",
|
||||||
|
"poi-preset-offline": "^9.0.0",
|
||||||
|
"serve": "^6.1.0"
|
||||||
|
}
|
||||||
|
}
|
29
poi.config.js
Normal file
29
poi.config.js
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
const path = require('path');
|
||||||
|
const pkg = require('./package');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
entry: [
|
||||||
|
'src/polyfills.js',
|
||||||
|
'src/index.js'
|
||||||
|
],
|
||||||
|
html: {
|
||||||
|
title: pkg.productName,
|
||||||
|
description: pkg.description,
|
||||||
|
template: path.join(__dirname, 'index.ejs')
|
||||||
|
},
|
||||||
|
postcss: {
|
||||||
|
plugins: [
|
||||||
|
// Your postcss plugins
|
||||||
|
]
|
||||||
|
},
|
||||||
|
presets: [
|
||||||
|
require('poi-preset-bundle-report')(),
|
||||||
|
require('poi-preset-offline')({
|
||||||
|
pwa: './src/pwa.js', // Path to pwa runtime entry
|
||||||
|
pluginOptions: {} // Additional options for offline-plugin
|
||||||
|
})
|
||||||
|
],
|
||||||
|
output: {
|
||||||
|
publicUrl: '/magellan',
|
||||||
|
}
|
||||||
|
};
|
35
src/backend.js
Normal file
35
src/backend.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import * as Kinto from 'kinto';
|
||||||
|
|
||||||
|
const DB = new Kinto({
|
||||||
|
bucket: 'magellan'
|
||||||
|
});
|
||||||
|
|
||||||
|
const LOCATIONS = DB.collection('locations');
|
||||||
|
|
||||||
|
const syncOptions = { remote: 'https://kinto.b.delire.party/v1' };
|
||||||
|
|
||||||
|
export async function getAll() {
|
||||||
|
let locations = await LOCATIONS.list();
|
||||||
|
return locations.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkNotMissing(obj, field) {
|
||||||
|
if (typeof obj[field] === 'undefined') {
|
||||||
|
console.warn(`missing field ${field} on object in ${new Error().stack}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function create(loc) {
|
||||||
|
checkNotMissing(loc, 'label');
|
||||||
|
checkNotMissing(loc, 'coordinates');
|
||||||
|
|
||||||
|
let result = await LOCATIONS.create({
|
||||||
|
label: loc.label,
|
||||||
|
coordinates: loc.coordinates,
|
||||||
|
});
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sync() {
|
||||||
|
await LOCATIONS.sync(syncOptions);
|
||||||
|
}
|
58
src/components/App.vue
Normal file
58
src/components/App.vue
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<div id="app">
|
||||||
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from '../store';
|
||||||
|
import * as backend from '../backend';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'app',
|
||||||
|
created() {
|
||||||
|
backend.sync().then(() => {
|
||||||
|
return backend.getAll();
|
||||||
|
}).then(locations => {
|
||||||
|
store.setLocations(locations);
|
||||||
|
}).catch(err => {
|
||||||
|
alert(`at app startup: ${err.toString()}`);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style src="normalize.css/normalize.css"></style>
|
||||||
|
<style src="@fortawesome/fontawesome-free/css/all.css"></style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
code {
|
||||||
|
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif;
|
||||||
|
font-size: 0.9em;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
code::before, code::after {
|
||||||
|
content: '`';
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bm-burger-button {
|
||||||
|
top: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#app {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bm-menu a,
|
||||||
|
.bm-menu a:visited {
|
||||||
|
color: white
|
||||||
|
}
|
||||||
|
</style>
|
53
src/components/ListAll.vue
Normal file
53
src/components/ListAll.vue
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div class="container">
|
||||||
|
<ul class="locations">
|
||||||
|
<li
|
||||||
|
v-for="location in store.locations"
|
||||||
|
@click="showDetails(location)"
|
||||||
|
>
|
||||||
|
<router-link :to="createLink(location)">
|
||||||
|
{{location.label}}
|
||||||
|
</router-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
store
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
createLink(loc) {
|
||||||
|
let lat = loc.coordinates[0];
|
||||||
|
let lng = loc.coordinates[1];
|
||||||
|
return `/${lat}/${lng}`;
|
||||||
|
},
|
||||||
|
|
||||||
|
showDetails(loc) {
|
||||||
|
let lat = loc.coordinates[0];
|
||||||
|
let lng = loc.coordinates[1];
|
||||||
|
store.openPopup(loc.label, lat, lng);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
li {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.leaflet-sidebar-content ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 5px;
|
||||||
|
}
|
||||||
|
</style>
|
105
src/components/Map.vue
Normal file
105
src/components/Map.vue
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<l-map
|
||||||
|
id="the-map"
|
||||||
|
ref="myMap"
|
||||||
|
:zoom="zoom"
|
||||||
|
:min-zoom="min_zoom"
|
||||||
|
:max-zoom="max_zoom"
|
||||||
|
:center="center"
|
||||||
|
:options="{zoomControl: false}"
|
||||||
|
@click="onClick"
|
||||||
|
>
|
||||||
|
<l-tile-layer
|
||||||
|
:url="url"
|
||||||
|
:attribution="attribution"
|
||||||
|
></l-tile-layer>
|
||||||
|
|
||||||
|
<l-marker
|
||||||
|
v-for="loc in store.locations"
|
||||||
|
:lat-lng="loc.coordinates"
|
||||||
|
>
|
||||||
|
<l-popup>
|
||||||
|
{{ loc.label }}
|
||||||
|
</l-popup>
|
||||||
|
</l-marker>
|
||||||
|
|
||||||
|
<l-control-zoom position="topright"></l-control-zoom>
|
||||||
|
|
||||||
|
<Sidebar></Sidebar>
|
||||||
|
</l-map>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { LPopup, LControl, LControlZoom, LMap, LTileLayer, LMarker } from 'vue2-leaflet';
|
||||||
|
import * as L from 'leaflet';
|
||||||
|
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
import Sidebar from './Sidebar';
|
||||||
|
|
||||||
|
const TILE_URL = 'https://{s}.tile.openstreetmap.fr/osmfr/{z}/{x}/{y}.png';
|
||||||
|
const ATTRIBUTION = 'Carte données © <a href="https://osm.org/copyright/">OpenStreetMap (ODbL)</a> / fond OSM-FR (CC-by-SA), <a href="https://www.data.gouv.fr/fr/datasets/points-dinterets-openstreetmap/">POI: OpenStreetMap (ODbL)</a>, <a href="https://www.data.gouv.fr/fr/datasets/base-d-adresses-nationale-ouverte-bano/">adresses BANO (ODbL)</a>, <a href="https://github.com/addok/addok">geocodeur addok</a>';
|
||||||
|
|
||||||
|
const MIN_ZOOM = 6;
|
||||||
|
const MAX_ZOOM = 18;
|
||||||
|
const INITIAL_ZOOM = 14;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'fullmap',
|
||||||
|
|
||||||
|
components: {
|
||||||
|
LMap,
|
||||||
|
LTileLayer,
|
||||||
|
LMarker,
|
||||||
|
LControlZoom,
|
||||||
|
LControl,
|
||||||
|
LPopup,
|
||||||
|
Sidebar,
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
let lat = this.$route.params.lat;
|
||||||
|
let lng = this.$route.params.lng;
|
||||||
|
|
||||||
|
return {
|
||||||
|
center: L.latLng(lat, lng),
|
||||||
|
zoom: INITIAL_ZOOM,
|
||||||
|
min_zoom: MIN_ZOOM,
|
||||||
|
max_zoom: MAX_ZOOM,
|
||||||
|
url: TILE_URL,
|
||||||
|
attribution: ATTRIBUTION,
|
||||||
|
store,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onClick(event) {
|
||||||
|
let {lat, lng} = event.latlng;
|
||||||
|
this.$router.replace(`/${lat}/${lng}`);
|
||||||
|
this.$refs.myMap.mapObject.panTo(event.latlng);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.store.setMapObject(this.$refs.myMap.mapObject);
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
$route(to, from) {
|
||||||
|
if (to.name === 'position') {
|
||||||
|
let {lat, lng} = to.params;
|
||||||
|
this.$refs.myMap.mapObject.panTo({ lat, lng });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style src="leaflet/dist/leaflet.css"></style>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
#the-map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
</style>
|
119
src/components/Search.vue
Normal file
119
src/components/Search.vue
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<span class="form">
|
||||||
|
<input type="text" placeholder="place" v-model="searchInput" @keydown="doSearch" />
|
||||||
|
<button @click="clearSearch()" :disabled="searchInput.length === 0">
|
||||||
|
<i class="fa fa-trash-alt"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<p v-if="searchStatus">{{ searchStatus }}</p>
|
||||||
|
<ul class="results" v-if="results.length > 0">
|
||||||
|
<li
|
||||||
|
v-for="result in results"
|
||||||
|
@click="openPopup(result)"
|
||||||
|
>
|
||||||
|
<router-link :to="{ name: 'position', params: {
|
||||||
|
lat: result.coordinates[0],
|
||||||
|
lng: result.coordinates[1] } }">{{result.label}}</router-link>
|
||||||
|
|
||||||
|
<i class="fa fa-save" @click="save(result)"></i>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<p v-if="searchInput.length > 0 && searchStatus === null && results.length == 0">
|
||||||
|
No results found.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import debounce from 'lodash.debounce';
|
||||||
|
|
||||||
|
import store from '../store';
|
||||||
|
|
||||||
|
const GEOCODING_URL = 'https://demo.addok.xyz/search/?q={query}&limit=20';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Menu',
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
searchInput: '',
|
||||||
|
searchStatus: null,
|
||||||
|
results: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
debouncedSearch: debounce(async function() {
|
||||||
|
if (this.searchInput.length === 0) {
|
||||||
|
// Just cleared the input.
|
||||||
|
this.searchStatus = null;
|
||||||
|
this.results = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch search.
|
||||||
|
this.searchStatus = "Searching...";
|
||||||
|
|
||||||
|
let resp = await fetch(GEOCODING_URL.replace('{query}', this.searchInput));
|
||||||
|
let results = await resp.json();
|
||||||
|
this.searchStatus = null;
|
||||||
|
|
||||||
|
this.results = (results && results.features && results.features.map(feature => {
|
||||||
|
let { properties: {label: label}, geometry: { coordinates: coordinates }} = feature;
|
||||||
|
coordinates.unshift(coordinates.pop());
|
||||||
|
return {label, coordinates}
|
||||||
|
})) || [];
|
||||||
|
}, 300),
|
||||||
|
|
||||||
|
doSearch() {
|
||||||
|
if (this.searchInput.length > 0) {
|
||||||
|
this.searchStatus = "Waiting for more characters...";
|
||||||
|
this.results = [];
|
||||||
|
}
|
||||||
|
this.debouncedSearch();
|
||||||
|
},
|
||||||
|
|
||||||
|
openPopup(loc) {
|
||||||
|
this.store.openPopup(loc.label, loc.coordinates[0], loc.coordinates[1]);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
this.searchInput = '';
|
||||||
|
this.searchStatus = null;
|
||||||
|
this.results = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
async save(loc) {
|
||||||
|
await store.addLocation(loc);
|
||||||
|
this.clearSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
flex-grow: 2;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: block;
|
||||||
|
padding: 10px;
|
||||||
|
background-color: white;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-save, .fa-trash-alt {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
66
src/components/Sidebar.vue
Normal file
66
src/components/Sidebar.vue
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div id="sidebar" class="leaflet-sidebar collapsed">
|
||||||
|
<div class="leaflet-sidebar-tabs">
|
||||||
|
<ul role="tablist"> <!-- top aligned tabs -->
|
||||||
|
<li><a href="#search" role="tab"><i class="fa fa-search"></i></a></li>
|
||||||
|
<li><a href="#all" role="tab"><i class="fa fa-bars"></i></a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="leaflet-sidebar-content">
|
||||||
|
<div class="leaflet-sidebar-pane" id="search">
|
||||||
|
<h1 class="leaflet-sidebar-header">Search<div class="leaflet-sidebar-close"><i class="fa fa-caret-left"></i></div></h1>
|
||||||
|
<Search/>
|
||||||
|
</div>
|
||||||
|
<div class="leaflet-sidebar-pane" id="all">
|
||||||
|
<h1 class="leaflet-sidebar-header">All locations<div class="leaflet-sidebar-close"><i class="fa fa-caret-left"></i></div></h1>
|
||||||
|
<ListAll></ListAll>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as _ from 'leaflet-sidebar-v2';
|
||||||
|
|
||||||
|
import Search from './Search';
|
||||||
|
import ListAll from './ListAll';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "sidebar",
|
||||||
|
components: {
|
||||||
|
Search,
|
||||||
|
ListAll,
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.mapObject = L.control.sidebar({
|
||||||
|
autopan: true,
|
||||||
|
closeButton: true,
|
||||||
|
container: 'sidebar',
|
||||||
|
position: 'left',
|
||||||
|
});
|
||||||
|
|
||||||
|
let parentContainer = this.$parent;
|
||||||
|
while (typeof parentContainer.mapObject === 'undefined') {
|
||||||
|
parentContainer = parentContainer.$parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.mapObject.addTo(parentContainer.mapObject);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
this.mapObject.remove();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style src="leaflet-sidebar-v2/css/leaflet-sidebar.min.css"></style>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.leaflet-sidebar-content {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-sidebar-pane h1 + * {
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
</style>
|
34
src/index.js
Normal file
34
src/index.js
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import Vue from 'vue';
|
||||||
|
import VueRouter from 'vue-router';
|
||||||
|
|
||||||
|
import { Icon } from 'leaflet';
|
||||||
|
|
||||||
|
import App from './components/App.vue';
|
||||||
|
import Map from './components/Map.vue';
|
||||||
|
|
||||||
|
delete Icon.Default.prototype._getIconUrl;
|
||||||
|
|
||||||
|
Icon.Default.mergeOptions({
|
||||||
|
iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
|
||||||
|
iconUrl: require('leaflet/dist/images/marker-icon.png'),
|
||||||
|
shadowUrl: require('leaflet/dist/images/marker-shadow.png')
|
||||||
|
});
|
||||||
|
|
||||||
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
|
Vue.use(VueRouter);
|
||||||
|
|
||||||
|
const routes = [
|
||||||
|
{ name: "position", path: "/:lat/:lng", component: Map },
|
||||||
|
{ path: "/", redirect: '/45.751591/4.845695/' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const router = new VueRouter({
|
||||||
|
routes
|
||||||
|
});
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: '#app',
|
||||||
|
render: h => h(App),
|
||||||
|
router,
|
||||||
|
});
|
5
src/polyfills.js
Normal file
5
src/polyfills.js
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
if (!window.Promise) {
|
||||||
|
window.Promise = require('promise-polyfill');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign = require('object-assign');
|
7
src/pwa.js
Normal file
7
src/pwa.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import runtime from 'offline-plugin/runtime';
|
||||||
|
|
||||||
|
runtime.install({
|
||||||
|
onUpdateReady() {
|
||||||
|
runtime.applyUpdate();
|
||||||
|
}
|
||||||
|
});
|
42
src/store.js
Normal file
42
src/store.js
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import * as backend from './backend';
|
||||||
|
|
||||||
|
const store = {
|
||||||
|
locations: [],
|
||||||
|
locationLabels: new Set(),
|
||||||
|
setLocations(newLocations) {
|
||||||
|
this.locations = newLocations;
|
||||||
|
this.locations.sort((a, b) => a.label > b.label);
|
||||||
|
for (let loc of newLocations) {
|
||||||
|
this.locationLabels.add(loc.label);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async addLocation(loc) {
|
||||||
|
if (this.locationLabels.has(loc.label)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newLoc = null;
|
||||||
|
try {
|
||||||
|
newLoc = await backend.create(loc);
|
||||||
|
} catch (err) {
|
||||||
|
alert(`Error when saving a new place: ${err.toString()}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.locations.push(newLoc);
|
||||||
|
this.locations.sort((a, b) => a.label > b.label);
|
||||||
|
this.locationLabels.add(newLoc.label);
|
||||||
|
},
|
||||||
|
|
||||||
|
mapObject: null,
|
||||||
|
setMapObject(mapObject) {
|
||||||
|
this.mapObject = mapObject;
|
||||||
|
},
|
||||||
|
openPopup(label, lat, lng) {
|
||||||
|
L.popup()
|
||||||
|
.setLatLng([lat, lng])
|
||||||
|
.setContent(label)
|
||||||
|
.openOn(this.mapObject);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default store;
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
BIN
static/icons/android-chrome-192x192.png
Normal file
BIN
static/icons/android-chrome-192x192.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
BIN
static/icons/android-chrome-512x512.png
Normal file
BIN
static/icons/android-chrome-512x512.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 29 KiB |
20
static/manifest.json
Normal file
20
static/manifest.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "app",
|
||||||
|
"short_name": "app",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/icons/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/icons/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"start_url": "/",
|
||||||
|
"display": "standalone",
|
||||||
|
"background_color": "#000000",
|
||||||
|
"theme_color": "#4DBA87"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user