2020-10-07 21:56:58 +00:00
|
|
|
<template>
|
2020-10-08 13:47:38 +00:00
|
|
|
<div class="w-full h-full">
|
2022-03-30 10:08:14 +00:00
|
|
|
<input ref="pacinput" id="pac-input" class="controls" :class="[mapLoaded ? '' : 'hidden']" type="text" placeholder="Search Box">
|
2022-03-30 07:33:56 +00:00
|
|
|
<div class="w-full h-full" ref="mapel"></div>
|
2020-10-08 13:47:38 +00:00
|
|
|
</div>
|
2020-10-07 21:56:58 +00:00
|
|
|
</template>
|
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
<script setup lang="ts">
|
2020-10-07 21:56:58 +00:00
|
|
|
import { Base64 } from 'js-base64'
|
2022-03-28 09:33:51 +00:00
|
|
|
import { onMounted, ref, watch, computed } from 'vue';
|
2022-03-28 15:16:15 +00:00
|
|
|
import { watchDebounced } from '@vueuse/core'
|
2022-03-30 08:28:20 +00:00
|
|
|
import { zlibSync, unzlibSync } from 'fflate';
|
2022-03-28 15:16:15 +00:00
|
|
|
|
2022-10-25 08:59:37 +00:00
|
|
|
import * as GMaps from '@googlemaps/js-api-loader'
|
|
|
|
const { Loader } = GMaps
|
|
|
|
|
2022-03-28 15:16:15 +00:00
|
|
|
const DEFAULT_MAP_POSITION = [48.862895, 2.286978, 18]
|
2022-04-26 07:05:04 +00:00
|
|
|
|
|
|
|
const loader = new Loader({
|
2020-10-07 21:56:58 +00:00
|
|
|
apiKey: "AIzaSyD7Vm3gm4Fm7jSkuIh_yM14GmYhz1P_S4M",
|
2022-03-28 09:33:51 +00:00
|
|
|
version: "3.48",
|
2020-10-07 21:56:58 +00:00
|
|
|
libraries: ["geometry", "places"]
|
|
|
|
});
|
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
const props = defineProps<{
|
|
|
|
density: number,
|
|
|
|
startHash: string
|
|
|
|
}>()
|
|
|
|
|
|
|
|
const emits = defineEmits<{
|
|
|
|
(event: "surfaceUpdate", val: number): void
|
|
|
|
(event: "densityChange", val: number): void
|
|
|
|
(event: "hashChange", val: string): void
|
|
|
|
}>()
|
|
|
|
|
2022-03-28 15:16:15 +00:00
|
|
|
const mapPosition = ref(DEFAULT_MAP_POSITION)
|
2022-03-28 09:33:51 +00:00
|
|
|
const arrPoly = ref<google.maps.LatLng[]>([])
|
|
|
|
const mapLoaded = ref(false);
|
|
|
|
const pacinput = ref()
|
2022-03-30 07:33:56 +00:00
|
|
|
const mapel = ref()
|
2022-03-28 09:33:51 +00:00
|
|
|
|
2022-03-30 07:33:56 +00:00
|
|
|
let currentMap : google.maps.Map;
|
|
|
|
let currentPolygon : google.maps.Polygon;
|
2022-03-28 09:33:51 +00:00
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
loader.loadCallback(e => {
|
|
|
|
if (e) {
|
|
|
|
console.log(e);
|
|
|
|
return;
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 07:33:56 +00:00
|
|
|
currentMap = new google.maps.Map(mapel.value, {
|
2022-03-28 09:33:51 +00:00
|
|
|
zoom: mapPosition.value[2],
|
|
|
|
center: {
|
|
|
|
lat: mapPosition.value[0],
|
|
|
|
lng: mapPosition.value[1]
|
|
|
|
},
|
|
|
|
mapTypeId: 'roadmap',
|
|
|
|
gestureHandling: 'greedy'
|
|
|
|
});
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-04-26 07:05:04 +00:00
|
|
|
const searchBox = new google.maps.places.Autocomplete(pacinput.value, {
|
|
|
|
fields: ['geometry']
|
|
|
|
})
|
|
|
|
// const searchBox = new google.maps.places.SearchBox(pacinput.value);
|
2022-03-28 09:33:51 +00:00
|
|
|
currentMap.controls[google.maps.ControlPosition.LEFT_TOP].push(pacinput.value);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-04-26 07:05:04 +00:00
|
|
|
searchBox.addListener('place_changed', () => {
|
|
|
|
const place = searchBox.getPlace();
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 07:33:56 +00:00
|
|
|
if (place.geometry?.location) {
|
|
|
|
currentMap.setCenter(place.geometry.location);
|
|
|
|
}
|
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
currentMap.setZoom(17);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
reset();
|
2020-10-07 21:56:58 +00:00
|
|
|
});
|
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
currentMap.addListener('bounds_changed', function() {
|
2022-03-30 07:33:56 +00:00
|
|
|
searchBox.setBounds(currentMap.getBounds()!);
|
2022-03-28 09:33:51 +00:00
|
|
|
});
|
|
|
|
currentMap.addListener('center_changed', mapUpdated);
|
|
|
|
currentMap.addListener('zoom_changed', mapUpdated);
|
|
|
|
currentMap.addListener('click', mapClicked);
|
|
|
|
|
|
|
|
currentMap.setOptions({
|
|
|
|
draggableCursor:'crosshair',
|
|
|
|
clickableIcons: false,
|
|
|
|
disableDoubleClickZoom: true,
|
|
|
|
streetViewControl: false
|
|
|
|
});
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
const poly = new google.maps.Polygon({
|
|
|
|
strokeOpacity: 0.8,
|
|
|
|
strokeWeight: 2,
|
|
|
|
fillOpacity: 0.35,
|
|
|
|
editable: true,
|
|
|
|
draggable: true,
|
|
|
|
geodesic: true
|
|
|
|
});
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
poly.setMap(currentMap);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
currentPolygon = poly;
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
if (props.startHash) {
|
|
|
|
loadHash(props.startHash);
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
["insert_at", "remove_at", "set_at"].forEach(ev => google.maps.event.addListener(poly.getPath(), ev, surfaceUpdated));
|
|
|
|
updatePolygonColor();
|
|
|
|
mapLoaded.value = true;
|
|
|
|
});
|
|
|
|
})
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
Get the polygon color from the density value.
|
|
|
|
This is a simple linear interpolation between
|
|
|
|
green and red on the Hue space.
|
|
|
|
*/
|
2022-03-28 09:33:51 +00:00
|
|
|
const getHue = (val: number) => {
|
|
|
|
const min = 0.1;
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
Clamp "max density" because any value
|
|
|
|
above 3.0 should be considered "red"
|
|
|
|
*/
|
2022-03-28 09:33:51 +00:00
|
|
|
const max = 3.0;
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
// inv lerp from the density
|
|
|
|
const t = (val - min) / (max - min);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
// lerp between green and red hue
|
|
|
|
const hue = (1.0 - t) * 110 + 0 * t;
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
return Math.max(0, Math.min(hue, 110));
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
const updatePolygonColor = () => {
|
|
|
|
const hue = getHue(props.density);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
currentPolygon.setOptions({
|
|
|
|
fillColor: `hsl(${hue}, 90%, 50%)`,
|
|
|
|
strokeColor: `hsl(${hue}, 90%, 50%)`
|
|
|
|
});
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
const mapUpdated = () => {
|
|
|
|
const pos = currentMap.getCenter();
|
|
|
|
const zoom = currentMap.getZoom();
|
2020-10-08 15:28:31 +00:00
|
|
|
|
2022-03-30 07:33:56 +00:00
|
|
|
if (!pos || !zoom) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
mapPosition.value = [pos.lat(), pos.lng(), zoom];
|
|
|
|
}
|
2020-10-08 15:28:31 +00:00
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
Add a new point to our polygon
|
|
|
|
using the lat/lng position clicked on the map.
|
|
|
|
*/
|
2022-03-28 09:33:51 +00:00
|
|
|
const mapClicked = (ev: any) => {
|
|
|
|
currentPolygon.getPath().push(ev.latLng);
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
const surfaceUpdated = () => {
|
|
|
|
arrPoly.value = currentPolygon.getPath().getArray().slice();
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
Compute the surface area of our polygon.
|
|
|
|
google.maps.geometry.spherical.computeArea() returns the area in square meters
|
|
|
|
*/
|
2022-03-28 09:33:51 +00:00
|
|
|
emits('surfaceUpdate', google.maps.geometry.spherical.computeArea(currentPolygon.getPath()));
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
const reloadHash = (hash: string) => {
|
|
|
|
loadHash(hash);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
["insert_at", "remove_at", "set_at"].forEach(ev => google.maps.event.addListener(currentPolygon.getPath(), ev, surfaceUpdated));
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
updatePolygonColor();
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
Deserialize out URL hash
|
|
|
|
*/
|
2022-03-28 09:33:51 +00:00
|
|
|
const loadHash = (hash: string) => {
|
2022-03-30 08:28:20 +00:00
|
|
|
if (hash[0] != 'b' && hash[0] != 'c') {
|
2022-03-28 09:33:51 +00:00
|
|
|
return loadLegacyHash(hash);
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 08:28:20 +00:00
|
|
|
const isCompressed = hash[0] == 'c';
|
|
|
|
let buf = Base64.toUint8Array(hash.substr(1));
|
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
if (!buf) {
|
|
|
|
return;
|
|
|
|
}
|
2020-10-08 15:28:31 +00:00
|
|
|
|
2022-03-30 08:28:20 +00:00
|
|
|
if (isCompressed) {
|
|
|
|
buf = unzlibSync(buf)
|
|
|
|
}
|
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/* Extract meta data (density, position & zoom) */
|
2022-03-28 09:33:51 +00:00
|
|
|
const meta = new Float32Array(buf.buffer, 0, 4);
|
2022-03-30 09:17:30 +00:00
|
|
|
/* Extract polygon path */
|
2022-03-28 09:33:51 +00:00
|
|
|
const data = new Float32Array(buf.buffer, 4*4);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
currentMap.setCenter({lat: meta[1], lng: meta[2]});
|
|
|
|
currentMap.setZoom(meta[3]);
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 07:33:56 +00:00
|
|
|
const path : google.maps.LatLngLiteral[] = [];
|
2022-03-28 09:33:51 +00:00
|
|
|
for (let i = 0; i < data.length; i += 2) {
|
|
|
|
path.push({
|
|
|
|
lat: data[i],
|
|
|
|
lng: data[i+1]
|
|
|
|
});
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
if (path.length) {
|
|
|
|
currentPolygon.setPath(path);
|
|
|
|
surfaceUpdated();
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
emits('densityChange', meta[0]);
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
This is the legacy hash decoder.
|
|
|
|
We keep it around so that original
|
|
|
|
links posted online keep working
|
|
|
|
*/
|
2022-03-28 09:33:51 +00:00
|
|
|
const loadLegacyHash = (hash: string) => {
|
2022-03-28 15:16:15 +00:00
|
|
|
const opt = hash.split(';');
|
|
|
|
const curPosition = opt.pop();
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-28 09:33:51 +00:00
|
|
|
if (curPosition) {
|
2022-03-28 15:16:15 +00:00
|
|
|
const cursetting = curPosition.split(',');
|
2022-03-28 09:33:51 +00:00
|
|
|
currentMap.setCenter({lat: parseFloat(cursetting[0]), lng: parseFloat(cursetting[1])});
|
|
|
|
currentMap.setZoom(parseInt(cursetting[2]));
|
|
|
|
}
|
2020-10-07 21:56:58 +00:00
|
|
|
|
2022-03-30 07:33:56 +00:00
|
|
|
const density = parseFloat(opt.pop() ?? '') || 1;
|
|
|
|
const path : google.maps.LatLngLiteral[] = [];
|
2022-03-28 09:33:51 +00:00
|
|
|
|
|
|
|
for (let i = 0; i < opt.length; i++) {
|
2022-03-28 15:16:15 +00:00
|
|
|
const coord = opt[i].split(',');
|
2022-03-28 09:33:51 +00:00
|
|
|
path.push({
|
|
|
|
lat: parseFloat(coord[0]),
|
|
|
|
lng: parseFloat(coord[1])
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
if (path.length) {
|
|
|
|
currentPolygon.setPath(path);
|
|
|
|
surfaceUpdated();
|
2020-10-07 21:56:58 +00:00
|
|
|
}
|
2022-03-28 09:33:51 +00:00
|
|
|
|
|
|
|
emits('densityChange', density);
|
|
|
|
}
|
|
|
|
|
|
|
|
const reset = () => {
|
|
|
|
currentPolygon.getPath().clear();
|
2020-10-07 21:56:58 +00:00
|
|
|
}
|
2022-03-28 09:33:51 +00:00
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
Generate URL hash from various data.
|
|
|
|
|
|
|
|
- density
|
|
|
|
- Map position (center position as lat/lng & zoom value)
|
|
|
|
- Our polygon path as lat/lng points
|
|
|
|
|
|
|
|
The generated buffer is then compressed (if needed) and Base64'd.
|
|
|
|
|
|
|
|
We consider every value to be a float and serialize
|
|
|
|
our data into a binary array with no extra information.
|
|
|
|
|
|
|
|
0 4 8 12 16 ....
|
|
|
|
[density][position lat][position long][zoom][...polygon points]
|
|
|
|
*/
|
2022-03-28 09:33:51 +00:00
|
|
|
const hash = computed(() => {
|
2022-03-28 15:16:15 +00:00
|
|
|
const buf = new Float32Array(arrPoly.value.length*2+4);
|
2022-03-28 09:33:51 +00:00
|
|
|
buf[0] = props.density;
|
|
|
|
buf.set(mapPosition.value, 1);
|
|
|
|
|
|
|
|
for (let i = 0; i < arrPoly.value.length; i++) {
|
|
|
|
buf[4+i*2] = arrPoly.value[i].lat();
|
|
|
|
buf[4+i*2+1] = arrPoly.value[i].lng();
|
|
|
|
}
|
2022-03-30 08:28:20 +00:00
|
|
|
|
|
|
|
let outbuf = new Uint8Array(buf.buffer);
|
|
|
|
const isCompressed = outbuf.byteLength >= 150;
|
|
|
|
|
|
|
|
if (isCompressed) {
|
|
|
|
outbuf = zlibSync(outbuf, { level: 9 });
|
|
|
|
}
|
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
|
|
|
|
*/
|
2022-03-30 08:28:20 +00:00
|
|
|
return (isCompressed ? 'c' : 'b') + Base64.fromUint8Array(outbuf, true);
|
2022-03-28 09:33:51 +00:00
|
|
|
})
|
|
|
|
|
2022-03-28 15:16:15 +00:00
|
|
|
watch(() => props.density, () => updatePolygonColor());
|
2022-03-28 09:33:51 +00:00
|
|
|
|
2022-03-30 09:17:30 +00:00
|
|
|
/*
|
|
|
|
Debounce the URL hash update
|
|
|
|
otherwise it would flood the browser history whenever the user
|
|
|
|
moves the polygon around
|
|
|
|
*/
|
2022-03-28 15:16:15 +00:00
|
|
|
watchDebounced(hash,
|
|
|
|
(hashval: string) => emits('hashChange', hashval),
|
|
|
|
{ debounce: 300 })
|
2022-03-28 09:33:51 +00:00
|
|
|
|
|
|
|
defineExpose({
|
|
|
|
reset,
|
|
|
|
reloadHash
|
|
|
|
})
|
|
|
|
|
2020-10-07 21:56:58 +00:00
|
|
|
</script>
|