kopia lustrzana https://github.com/paraboul/mapchecking
New version wip
commit
36437131ac
|
@ -0,0 +1,18 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<meta name="viewport" content="initial-scale=1.0, user-scalable=no">
|
||||||
|
<meta name="author" content="Anthony Catel"/>
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:url" content="https://www.mapchecking.com/" />
|
||||||
|
<meta property="og:image" content="https://www.mapchecking.com/img/socialimage.png" />
|
||||||
|
<meta property="og:site_name" content="MapChecking" />
|
||||||
|
<title>MapChecking - Crowd size estimator</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app" class="antialiased text-gray-800"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
Plik diff jest za duży
Load Diff
|
@ -0,0 +1,22 @@
|
||||||
|
{
|
||||||
|
"name": "mapchecking",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@googlemaps/js-api-loader": "^1.4.0",
|
||||||
|
"between": "git+https://github.com/paraboul/between.git",
|
||||||
|
"js-base64": "^3.5.2",
|
||||||
|
"tailwindcss": "^1.8.11",
|
||||||
|
"vue": "^3.0.0-rc.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vue/compiler-sfc": "^3.0.0-rc.1",
|
||||||
|
"autoprefixer": "^9.7.5",
|
||||||
|
"postcss": "^8.1.1",
|
||||||
|
"postcss-cli": "^8.0.0",
|
||||||
|
"vite": "^1.0.0-rc.1"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: [
|
||||||
|
require("tailwindcss"),
|
||||||
|
require("autoprefixer")
|
||||||
|
]
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-stretch md:flex-row h-screen">
|
||||||
|
<div class="md:h-full w-full">
|
||||||
|
<Map :density="density" :startHash="startHash" ref="map" @densityChange="densityUpdate" @hashChange="hashUpdate" @surfaceUpdate="surfaceUpdate" />
|
||||||
|
</div>
|
||||||
|
<div class="relative w-full lg:w-2/3 px-4 py-2 font-sans border-l border-gray-500 bg-gray-100">
|
||||||
|
<h1 class="text-2xl">MapChecking • Crowd size estimation</h1>
|
||||||
|
<span class="text-gray-800 leading-tight">This tool helps you estimate and fact-check the maximum number of people standing in a given area.</span>
|
||||||
|
|
||||||
|
<div class="shadow-md rounded-md px-4 py-3 bg-white mt-4">
|
||||||
|
<div v-if="surface !== 0" class="relative">
|
||||||
|
<span class="text-sm text-gray-700">Surface area <span class="font-semibold">{{ formatArea(surface) }}sqm</span> • <span class="font-semibold">{{ formatArea(surface_feet) }}sqft</span></span>
|
||||||
|
|
||||||
|
<button @click="$refs.map.reset()" class="rounded absolute right-0 px-2 py-1 text-xs inline-block bg-red-400 shadow-md text-white font-bold hover:shadow-none focus:outline-none">Reset the area</button>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="font-semibold">Crowd density <span class="text-xs text-gray-700"><a class="underline hover:no-underline" target="_blank" href="http://www.gkstill.com/Support/crowd-density/625sm/Density6.html">What does it look like?</a></span></span>
|
||||||
|
<input class="block w-full" type="range" min="0.1" max="5.0" step="0.05" :value="density" v-model.number="density" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-around pt-2">
|
||||||
|
<button @click="setDensity(0.5)" class="btn">Light</button>
|
||||||
|
<button @click="setDensity(2)" class="btn">Crowded</button>
|
||||||
|
<button @click="setDensity(4)" class="btn">Packed</button>
|
||||||
|
</div>
|
||||||
|
<div class="text-center mt-1">
|
||||||
|
<span class="block font-semibold text-teal-600">{{ density.toFixed(2) }} people per sqm <small>(~10 ftqm)</small></span>
|
||||||
|
<span class="inline-block mt-2 text-xl font-bold text-gray-800">{{ estimated }} estimated</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="text-center font-bold" v-else>
|
||||||
|
Start by delimiting an area on the map
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="bottom-0 left-0 absolute h-8 bg-white border-t border-gray-300 w-full text-xs tracking-tight text-center py-2">Created by Anthony Catel</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Map from './components/Map.vue'
|
||||||
|
import Between from 'between'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
Map
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
surfaceUpdate(data) {
|
||||||
|
this.surface = data;
|
||||||
|
},
|
||||||
|
|
||||||
|
hashUpdate(hash) {
|
||||||
|
window.location.hash = hash;
|
||||||
|
},
|
||||||
|
|
||||||
|
densityUpdate(val) {
|
||||||
|
this.density = val;
|
||||||
|
},
|
||||||
|
|
||||||
|
formatArea(val) {
|
||||||
|
return Number.parseFloat(val).toFixed(0);
|
||||||
|
},
|
||||||
|
|
||||||
|
setDensity(val) {
|
||||||
|
Between.block(800, Between.easing.Exponential.Out, (obj) => {
|
||||||
|
obj.density = val;
|
||||||
|
}, this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
surface: 0,
|
||||||
|
density: 1.5,
|
||||||
|
startHash: window.location.hash && window.location.hash.length > 3 ?
|
||||||
|
window.location.hash.substring(1) : ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
surface_feet() {
|
||||||
|
return (this.surface * 10.764).toFixed(2);
|
||||||
|
},
|
||||||
|
|
||||||
|
estimated() {
|
||||||
|
return parseInt(this.surface * this.density);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,227 @@
|
||||||
|
<template>
|
||||||
|
|
||||||
|
<div class="w-full h-full" id="map"></div>
|
||||||
|
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Loader as MapLoader } from '@googlemaps/js-api-loader';
|
||||||
|
import { Base64 } from 'js-base64'
|
||||||
|
|
||||||
|
const loader = new MapLoader({
|
||||||
|
apiKey: "AIzaSyD7Vm3gm4Fm7jSkuIh_yM14GmYhz1P_S4M",
|
||||||
|
version: "weekly",
|
||||||
|
libraries: ["geometry", "places"]
|
||||||
|
});
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "Map",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
density: Number,
|
||||||
|
startHash: String
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
|
||||||
|
loader.loadCallback(e => {
|
||||||
|
if (e) {
|
||||||
|
console.log(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const map = new google.maps.Map(document.getElementById("map"), {
|
||||||
|
zoom: this.mapPosition[2],
|
||||||
|
center: {
|
||||||
|
lat: this.mapPosition[0],
|
||||||
|
lng: this.mapPosition[1]
|
||||||
|
},
|
||||||
|
mapTypeId: 'roadmap'
|
||||||
|
});
|
||||||
|
|
||||||
|
map.addListener('center_changed', this.mapUpdated);
|
||||||
|
map.addListener('zoom_changed', this.mapUpdated);
|
||||||
|
map.addListener('click', this.mapClicked);
|
||||||
|
|
||||||
|
map.setOptions({
|
||||||
|
draggableCursor:'crosshair',
|
||||||
|
clickableIcons: false,
|
||||||
|
disableDoubleClickZoom: true
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$map = map;
|
||||||
|
|
||||||
|
const poly = new google.maps.Polygon({
|
||||||
|
strokeOpacity: 0.8,
|
||||||
|
strokeWeight: 2,
|
||||||
|
fillOpacity: 0.35,
|
||||||
|
editable: true,
|
||||||
|
draggable: true,
|
||||||
|
geodesic: true
|
||||||
|
});
|
||||||
|
|
||||||
|
poly.setMap(map);
|
||||||
|
|
||||||
|
this.$poly = poly;
|
||||||
|
|
||||||
|
if (this.startHash) {
|
||||||
|
this.loadHash(this.startHash);
|
||||||
|
}
|
||||||
|
|
||||||
|
["insert_at", "remove_at", "set_at"].forEach(ev => google.maps.event.addListener(poly.getPath(), ev, this.surfaceUpdated));
|
||||||
|
this.updatePolygonColor();
|
||||||
|
|
||||||
|
this.$updateHashTimer = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
watch: {
|
||||||
|
density(val) {
|
||||||
|
this.updatePolygonColor();
|
||||||
|
},
|
||||||
|
|
||||||
|
hash(hashval) {
|
||||||
|
if (this.$updateHashTimer) {
|
||||||
|
clearTimeout(this.$updateHashTimer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$updateHashTimer = setTimeout(() => {
|
||||||
|
this.$emit('hashChange', hashval);
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getHue(val) {
|
||||||
|
const min = 0.1;
|
||||||
|
const max = 3.0;
|
||||||
|
|
||||||
|
// inv lerp from the density
|
||||||
|
const t = (val - min) / (max - min);
|
||||||
|
|
||||||
|
// lerp between green and red hue
|
||||||
|
const hue = (1.0 - t) * 110 + 0 * t;
|
||||||
|
|
||||||
|
return Math.max(0, Math.min(hue, 110));
|
||||||
|
},
|
||||||
|
|
||||||
|
updatePolygonColor() {
|
||||||
|
const hue = this.getHue(this.density);
|
||||||
|
|
||||||
|
this.$poly.setOptions({
|
||||||
|
fillColor: `hsl(${hue}, 90%, 50%)`,
|
||||||
|
strokeColor: `hsl(${hue}, 90%, 50%)`
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
mapUpdated() {
|
||||||
|
const pos = this.$map.getCenter();
|
||||||
|
const zoom = this.$map.getZoom();
|
||||||
|
|
||||||
|
this.mapPosition = [pos.lat().toFixed(7), pos.lng().toFixed(7), zoom];
|
||||||
|
},
|
||||||
|
|
||||||
|
mapClicked(ev) {
|
||||||
|
const path = this.$poly.getPath();
|
||||||
|
|
||||||
|
path.push(ev.latLng);
|
||||||
|
},
|
||||||
|
|
||||||
|
surfaceUpdated() {
|
||||||
|
this.surface = google.maps.geometry.spherical.computeArea(this.$poly.getPath()).toFixed(2);
|
||||||
|
this.arrPoly = this.$poly.getPath().getArray().slice();
|
||||||
|
|
||||||
|
this.$emit('surfaceUpdate', this.surface);
|
||||||
|
},
|
||||||
|
|
||||||
|
loadHash(hash) {
|
||||||
|
if (hash[0] != 'b') {
|
||||||
|
return this.loadLegacyHash(hash);
|
||||||
|
}
|
||||||
|
|
||||||
|
const buf = Base64.toUint8Array(hash.substr(1));
|
||||||
|
if (!buf) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = new Float32Array(buf.buffer, 0, 4);
|
||||||
|
const data = new Float32Array(buf.buffer, 4*4);
|
||||||
|
|
||||||
|
this.$map.setCenter({lat: meta[1], lng: meta[2]});
|
||||||
|
this.$map.setZoom(parseInt(meta[3]));
|
||||||
|
|
||||||
|
let path = [];
|
||||||
|
for (let i = 0; i < data.length; i += 2) {
|
||||||
|
path.push({
|
||||||
|
lat: data[i],
|
||||||
|
lng: data[i+1]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.length) {
|
||||||
|
this.$poly.setPath(path);
|
||||||
|
this.surfaceUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('densityChange', parseInt(meta[0]));
|
||||||
|
},
|
||||||
|
|
||||||
|
loadLegacyHash(hash) {
|
||||||
|
let opt = hash.split(';');
|
||||||
|
console.log(opt);
|
||||||
|
let curPosition = opt.pop();
|
||||||
|
|
||||||
|
if (curPosition) {
|
||||||
|
let cursetting = curPosition.split(',');
|
||||||
|
this.$map.setCenter({lat: parseFloat(cursetting[0]), lng: parseFloat(cursetting[1])});
|
||||||
|
this.$map.setZoom(parseInt(cursetting[2]));
|
||||||
|
}
|
||||||
|
|
||||||
|
let density = parseFloat(opt.pop()) || 1;
|
||||||
|
|
||||||
|
let path = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < opt.length; i++) {
|
||||||
|
let coord = opt[i].split(',');
|
||||||
|
path.push({
|
||||||
|
lat: parseFloat(coord[0]),
|
||||||
|
lng: parseFloat(coord[1])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.length) {
|
||||||
|
this.$poly.setPath(path);
|
||||||
|
this.surfaceUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('densityChange', density);
|
||||||
|
},
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.$poly.getPath().clear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
hash() {
|
||||||
|
let buf = new Float32Array(this.arrPoly.length*2+4);
|
||||||
|
buf[0] = this.density;
|
||||||
|
buf.set(this.mapPosition, 1);
|
||||||
|
|
||||||
|
for (let i = 0; i < this.arrPoly.length; i++) {
|
||||||
|
buf[4+i*2] = this.arrPoly[i].lat();
|
||||||
|
buf[4+i*2+1] = this.arrPoly[i].lng();
|
||||||
|
}
|
||||||
|
return 'b' + Base64.fromUint8Array(new Uint8Array(buf.buffer), true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mapPosition: [48.862895, 2.286978, 18],
|
||||||
|
surface: 0,
|
||||||
|
arrPoly: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,105 @@
|
||||||
|
@tailwind base;
|
||||||
|
|
||||||
|
@tailwind components;
|
||||||
|
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
@apply bg-white border border-teal-500 text-teal-500 rounded-lg font-semibold py-1 px-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:focus {
|
||||||
|
@apply outline-none font-bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=range] {
|
||||||
|
width: 100%;
|
||||||
|
margin: 2.6px 0;
|
||||||
|
background-color: transparent;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
input[type=range]:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
input[type=range]::-webkit-slider-runnable-track {
|
||||||
|
@apply bg-teal-500;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 3.7px;
|
||||||
|
width: 100%;
|
||||||
|
height: 12.8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type=range]::-webkit-slider-thumb {
|
||||||
|
margin-top: -2.6px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2.5px solid #000000;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
}
|
||||||
|
input[type=range]:focus::-webkit-slider-runnable-track {
|
||||||
|
@apply bg-teal-500;
|
||||||
|
/*background: linear-gradient(to right, rgb(56, 178, 172), rgb(156, 80, 172));*/
|
||||||
|
}
|
||||||
|
input[type=range]::-moz-range-track {
|
||||||
|
@apply bg-teal-500;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 3.7px;
|
||||||
|
width: 100%;
|
||||||
|
height: 12.8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type=range]::-moz-range-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2.5px solid #000000;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-track {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
border-width: 3.6px 0;
|
||||||
|
color: transparent;
|
||||||
|
width: 100%;
|
||||||
|
height: 12.8px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-fill-lower {
|
||||||
|
background: #ff6ca6;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7.4px;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-fill-upper {
|
||||||
|
@apply bg-teal-500;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 7.4px;
|
||||||
|
}
|
||||||
|
input[type=range]::-ms-thumb {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #ffffff;
|
||||||
|
border: 2.5px solid #000000;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 0px;
|
||||||
|
/*Needed to keep the Edge thumb centred*/
|
||||||
|
}
|
||||||
|
input[type=range]:focus::-ms-fill-lower {
|
||||||
|
@apply bg-teal-500;
|
||||||
|
}
|
||||||
|
input[type=range]:focus::-ms-fill-upper {
|
||||||
|
@apply bg-teal-500;
|
||||||
|
}
|
||||||
|
/*TODO: Use one of the selectors from https://stackoverflow.com/a/20541859/7077589 and figure out
|
||||||
|
how to remove the virtical space around the range input in IE*/
|
||||||
|
@supports (-ms-ime-align:auto) {
|
||||||
|
/* Pre-Chromium Edge only styles, selector taken from hhttps://stackoverflow.com/a/32202953/7077589 */
|
||||||
|
input[type=range] {
|
||||||
|
margin: 0;
|
||||||
|
/*Edge starts the margin from the thumb, not the track as other browsers do*/
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { createApp } from 'vue'
|
||||||
|
|
||||||
|
import App from './App.vue'
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
createApp(App).mount('#app')
|
|
@ -0,0 +1,18 @@
|
||||||
|
module.exports = {
|
||||||
|
future: {
|
||||||
|
// removeDeprecatedGapUtilities: true,
|
||||||
|
// purgeLayersByDefault: true,
|
||||||
|
},
|
||||||
|
purge: [],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
indigo: '#5c6ac4',
|
||||||
|
blue: '#007ace',
|
||||||
|
nidium: '#3eb7b3'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {},
|
||||||
|
plugins: [],
|
||||||
|
}
|
Ładowanie…
Reference in New Issue