WIP: Migrate Map to Vue
This commit is contained in:
parent
2d9233fabb
commit
87b8f9e9ba
203
package-lock.json
generated
203
package-lock.json
generated
@ -1015,6 +1015,22 @@
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"@babel/runtime-corejs2": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.5.4.tgz",
|
||||
"integrity": "sha512-sHv74OzyZ18d6tjHU0HmlVES3+l+lydkOMTiKsJSTGWcTBpIMfXLEgduahlJrQjknW9RCQAqLIEdLOHjBmq/hg==",
|
||||
"requires": {
|
||||
"core-js": "^2.6.5",
|
||||
"regenerator-runtime": "^0.13.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"regenerator-runtime": {
|
||||
"version": "0.13.2",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.2.tgz",
|
||||
"integrity": "sha512-S/TQAZJO+D3m9xeN1WTI8dLKBBiRgXBlTJvbWjCThHWZj9EvHK70Ff50/tYj2J/fvBY6JtFVwRuazHN2E7M9BA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@babel/template": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
|
||||
@ -1126,6 +1142,103 @@
|
||||
"physical-cpu-count": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@turf/bbox": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/bbox/-/bbox-5.1.5.tgz",
|
||||
"integrity": "sha1-MFHfUUrUxQ9KT5uKLRX9i2hA7aM=",
|
||||
"requires": {
|
||||
"@turf/helpers": "^5.1.5",
|
||||
"@turf/meta": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/boolean-point-in-polygon": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/boolean-point-in-polygon/-/boolean-point-in-polygon-5.1.5.tgz",
|
||||
"integrity": "sha1-8BzBlNHgMKVIv9qYHLpDz9YpQbc=",
|
||||
"requires": {
|
||||
"@turf/helpers": "^5.1.5",
|
||||
"@turf/invariant": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/center": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/center/-/center-5.1.5.tgz",
|
||||
"integrity": "sha1-RKss2VT2PA03dX9xWKmcPvURS4A=",
|
||||
"requires": {
|
||||
"@turf/bbox": "^5.1.5",
|
||||
"@turf/helpers": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/clone": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/clone/-/clone-5.1.5.tgz",
|
||||
"integrity": "sha1-JT6NNUdxgZduM636tQoPAqfw42c=",
|
||||
"requires": {
|
||||
"@turf/helpers": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/distance": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/distance/-/distance-5.1.5.tgz",
|
||||
"integrity": "sha1-Oc8YIEu/h1h9cH5gmmARiQkVZAk=",
|
||||
"requires": {
|
||||
"@turf/helpers": "^5.1.5",
|
||||
"@turf/invariant": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/explode": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/explode/-/explode-5.1.5.tgz",
|
||||
"integrity": "sha1-sSsvd0AEobSPYrqVsgocZVo94Rg=",
|
||||
"requires": {
|
||||
"@turf/helpers": "^5.1.5",
|
||||
"@turf/meta": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/helpers": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/helpers/-/helpers-5.1.5.tgz",
|
||||
"integrity": "sha1-FTQFInq5M9AEpbuWQantmZ/L4M8="
|
||||
},
|
||||
"@turf/invariant": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/invariant/-/invariant-5.2.0.tgz",
|
||||
"integrity": "sha1-8BUP9ykLOFd7c9CIt5MsHuCqkKc=",
|
||||
"requires": {
|
||||
"@turf/helpers": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/meta": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@turf/meta/-/meta-5.2.0.tgz",
|
||||
"integrity": "sha1-OxrUhe4MOwsXdRMqMsOE1T5LpT0=",
|
||||
"requires": {
|
||||
"@turf/helpers": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/nearest-point": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/nearest-point/-/nearest-point-5.1.5.tgz",
|
||||
"integrity": "sha1-EgUN5Bw5hEMiTHl43g9iE5ANNPs=",
|
||||
"requires": {
|
||||
"@turf/clone": "^5.1.5",
|
||||
"@turf/distance": "^5.1.5",
|
||||
"@turf/helpers": "^5.1.5",
|
||||
"@turf/meta": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@turf/point-on-feature": {
|
||||
"version": "5.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@turf/point-on-feature/-/point-on-feature-5.1.5.tgz",
|
||||
"integrity": "sha1-MMfwMkMCd8ZBjZbSieRba/shP+c=",
|
||||
"requires": {
|
||||
"@turf/boolean-point-in-polygon": "^5.1.5",
|
||||
"@turf/center": "^5.1.5",
|
||||
"@turf/explode": "^5.1.5",
|
||||
"@turf/helpers": "^5.1.5",
|
||||
"@turf/nearest-point": "^5.1.5"
|
||||
}
|
||||
},
|
||||
"@types/q": {
|
||||
"version": "1.5.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
|
||||
@ -1929,6 +2042,11 @@
|
||||
"upath": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"chroma-js": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-1.4.1.tgz",
|
||||
"integrity": "sha512-jTwQiT859RTFN/vIf7s+Vl/Z2LcMrvMv3WUFmd/4u76AdlFC0NTNgqEEFPcRiHmAswPsMiQEDZLM8vX8qXpZNQ=="
|
||||
},
|
||||
"cipher-base": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
|
||||
@ -2142,8 +2260,7 @@
|
||||
"core-js": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
|
||||
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
@ -2623,6 +2740,11 @@
|
||||
"node-addon-api": "^1.6.0"
|
||||
}
|
||||
},
|
||||
"debounce-promise": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/debounce-promise/-/debounce-promise-3.1.2.tgz",
|
||||
"integrity": "sha512-rZHcgBkbYavBeD9ej6sP56XfG53d51CD4dnaw989YX/nZ/ZJfgRx/9ePKmTNiUiyQvh4mtrMoS3OAWW+yoYtpg=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
|
||||
@ -2750,6 +2872,15 @@
|
||||
"randombytes": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"distinct-colors": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/distinct-colors/-/distinct-colors-1.0.4.tgz",
|
||||
"integrity": "sha1-Z9/KV5S8qVDAHSUaUqozWG5a97Q=",
|
||||
"requires": {
|
||||
"chroma-js": "^1.1.1",
|
||||
"mout": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"dom-serializer": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
|
||||
@ -4740,6 +4871,11 @@
|
||||
"integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
|
||||
"dev": true
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E="
|
||||
},
|
||||
"merge-source-map": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz",
|
||||
@ -4886,6 +5022,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mout": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/mout/-/mout-0.11.1.tgz",
|
||||
"integrity": "sha1-ujYR318OWx/7/QEWa48C0fX6K5k="
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
@ -5144,6 +5285,11 @@
|
||||
"rbush": "2.0.2"
|
||||
}
|
||||
},
|
||||
"ol-tilecache": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/ol-tilecache/-/ol-tilecache-3.0.2.tgz",
|
||||
"integrity": "sha512-oA+UM8juw775gVm0ZgH5WIV9DnDhfLYWnY7zxkpIR8FDZaoFiPa0idG+GFRdRokeCqMoeoNMSD8aRguFbDE9XA=="
|
||||
},
|
||||
"on-finished": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
|
||||
@ -5321,6 +5467,21 @@
|
||||
"safe-buffer": "^5.1.1"
|
||||
}
|
||||
},
|
||||
"parse-color": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz",
|
||||
"integrity": "sha1-e3SLlag/A/FqlPU15S1/PZRlhhk=",
|
||||
"requires": {
|
||||
"color-convert": "~0.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"color-convert": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz",
|
||||
"integrity": "sha1-vbbGnOZg+t/+CwAHzER+G59ygr0="
|
||||
}
|
||||
}
|
||||
},
|
||||
"parse-json": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
|
||||
@ -6329,6 +6490,14 @@
|
||||
"inherits": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "6.5.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.2.tgz",
|
||||
"integrity": "sha512-HUb7j3kvb7p7eCUHE3FqjoDsC1xfZQ4AHFWfTKSpZ+sAhhz5X1WX0ZuUqWbzB2QhSLp3DoLUG+hMdEDKqWo2Zg==",
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
|
||||
@ -7028,6 +7197,11 @@
|
||||
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
|
||||
"dev": true
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz",
|
||||
"integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ=="
|
||||
},
|
||||
"tty-browserify": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
|
||||
@ -7294,8 +7468,7 @@
|
||||
"uuid": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
|
||||
"dev": true
|
||||
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
|
||||
},
|
||||
"v8-compile-cache": {
|
||||
"version": "2.0.3",
|
||||
@ -7358,6 +7531,23 @@
|
||||
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
|
||||
"dev": true
|
||||
},
|
||||
"vuelayers": {
|
||||
"version": "0.11.4",
|
||||
"resolved": "https://registry.npmjs.org/vuelayers/-/vuelayers-0.11.4.tgz",
|
||||
"integrity": "sha512-6asCq2vDCPM4mS60NGeEBGKiVVJZ/GACYtu7UUVPkhtdmvI7bSzKjKuv0cTQkwcgh6YRtqzAIzML0l9fwb4AQw==",
|
||||
"requires": {
|
||||
"@babel/runtime-corejs2": "^7.1.2",
|
||||
"@turf/point-on-feature": "^5.1.5",
|
||||
"debounce-promise": "^3.1.0",
|
||||
"merge-descriptors": "^1.0.1",
|
||||
"ol": "^5.3.1",
|
||||
"ol-tilecache": "^3.0.1",
|
||||
"parse-color": "^1.0.0",
|
||||
"rxjs": "^6.3.3",
|
||||
"uuid": "^3.3.2",
|
||||
"whatwg-fetch": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"w3c-hr-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",
|
||||
@ -7391,6 +7581,11 @@
|
||||
"iconv-lite": "0.4.24"
|
||||
}
|
||||
},
|
||||
"whatwg-fetch": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
|
||||
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
|
||||
},
|
||||
"whatwg-mimetype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
|
||||
|
@ -5,9 +5,11 @@
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"aprs-parser": "^1.0.4",
|
||||
"distinct-colors": "^1.0.4",
|
||||
"ol": "^5.3.3",
|
||||
"vue": "^2.6.10",
|
||||
"vue-hot-reload-api": "^2.3.3",
|
||||
"vuelayers": "^0.11.4",
|
||||
"ws": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
203
src/Map.vue
Normal file
203
src/Map.vue
Normal file
@ -0,0 +1,203 @@
|
||||
<template>
|
||||
<vl-map data-projection="EPSG:4326">
|
||||
<vl-view :zoom="10" :center="[-72.15, 43.9]">
|
||||
<vl-layer-tile>
|
||||
<vl-source-osm> </vl-source-osm>
|
||||
</vl-layer-tile>
|
||||
<vl-layer-group>
|
||||
<vl-layer-vector v-for="(gpxURL, name) in routes" :key="name">
|
||||
<vl-source-vector :url="gpxURL" :format-factory="gpxFormatFactory">
|
||||
</vl-source-vector>
|
||||
</vl-layer-vector>
|
||||
<vl-style-stroke color="hsl(200, 90%, 30%)" :width="5">
|
||||
</vl-style-stroke>
|
||||
</vl-layer-group>
|
||||
|
||||
<vl-layer-group>
|
||||
<vl-layer-group
|
||||
v-for="(packets, callsign, idx) in paths"
|
||||
:key="callsign"
|
||||
>
|
||||
<!--Paths -->
|
||||
<vl-layer-vector>
|
||||
<vl-source-vector>
|
||||
<vl-feature>
|
||||
<vl-geom-line-string :coordinates="packetsToPoints(packets)">
|
||||
</vl-geom-line-string>
|
||||
</vl-feature>
|
||||
</vl-source-vector>
|
||||
<vl-style-box>
|
||||
<vl-style-stroke :color="stationColors[idx].hex()" :width="2">
|
||||
</vl-style-stroke>
|
||||
</vl-style-box>
|
||||
</vl-layer-vector>
|
||||
|
||||
<!-- Points -->
|
||||
<vl-layer-vector>
|
||||
<vl-source-vector>
|
||||
<vl-feature>
|
||||
<vl-geom-multi-point :coordinates="packetsToPoints(packets)">
|
||||
</vl-geom-multi-point>
|
||||
</vl-feature>
|
||||
</vl-source-vector>
|
||||
<vl-style-box>
|
||||
<vl-style-circle :radius="3">
|
||||
<vl-style-fill :color="stationColors[idx].hex()">
|
||||
</vl-style-fill>
|
||||
</vl-style-circle>
|
||||
</vl-style-box>
|
||||
</vl-layer-vector>
|
||||
</vl-layer-group>
|
||||
</vl-layer-group>
|
||||
</vl-view>
|
||||
</vl-map>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { APRSParser } from "aprs-parser";
|
||||
import distinctColors from "distinct-colors";
|
||||
|
||||
import { Control } from "ol/control";
|
||||
import { GPX } from "ol/format";
|
||||
|
||||
import Vue from "vue";
|
||||
import VueLayers from "vuelayers";
|
||||
import "vuelayers/lib/style.css";
|
||||
|
||||
Vue.use(VueLayers);
|
||||
|
||||
import route_data from "gpx/*.gpx";
|
||||
import { readFileSync } from "fs";
|
||||
const packetLog = readFileSync(__dirname + "/../IS_packets.txt", "utf-8");
|
||||
|
||||
function parsePackets(packetLog) {
|
||||
let parser = new APRSParser();
|
||||
return (
|
||||
packetLog
|
||||
.trim()
|
||||
.split("\n")
|
||||
// parse to Date and APRS packet
|
||||
.map(line => {
|
||||
let packet = parser.parse(line.slice(29));
|
||||
packet.date = new Date(line.slice(0, 18));
|
||||
return packet;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
packets: parsePackets(packetLog),
|
||||
routes: route_data
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
gpxFormatFactory(options) {
|
||||
return new GPX(options);
|
||||
},
|
||||
|
||||
packetsToPoints(packets) {
|
||||
return packets.map(packet => [
|
||||
packet.data.longitude,
|
||||
packet.data.latitude
|
||||
]);
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
paths() {
|
||||
return (
|
||||
this.packets
|
||||
.filter(
|
||||
packet =>
|
||||
packet.date > new Date("2018-07-13") &&
|
||||
packet.date < new Date("2018-07-14")
|
||||
)
|
||||
// filter to just positional data
|
||||
.filter(packet => "data" in packet && "latitude" in packet.data)
|
||||
// join into Arrays of points
|
||||
.reduce((acc, packet) => {
|
||||
let callsign = packet.from.toString().trim();
|
||||
if (!(callsign in acc)) acc[callsign] = [];
|
||||
acc[callsign].push(packet);
|
||||
return acc;
|
||||
}, {})
|
||||
);
|
||||
},
|
||||
|
||||
stationColors() {
|
||||
return distinctColors({
|
||||
count: Object.keys(this.paths).length,
|
||||
lightMin: 20,
|
||||
lightMax: 80
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ol-control.layer-toggles {
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
background-color: rgba(70, 115, 164, 0.7);
|
||||
color: #eee;
|
||||
white-space: nowrap;
|
||||
max-height: calc(100vh - 1em);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.layer-toggles > div {
|
||||
margin: 0.5em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.ol-control.layer-toggles:hover {
|
||||
background-color: rgba(0, 60, 136, 0.7);
|
||||
}
|
||||
|
||||
.layer-toggles > div > label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.expand {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expand + span::before {
|
||||
content: "\25B6";
|
||||
}
|
||||
|
||||
.expand:checked + span::before {
|
||||
content: "\25BC";
|
||||
}
|
||||
|
||||
.expand ~ .collapsible-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.expand:checked ~ .collapsible-content {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.collapsible-content {
|
||||
margin-left: 0.8em;
|
||||
}
|
||||
|
||||
.collapsible-content > label {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
48
src/map.css
48
src/map.css
@ -1,48 +0,0 @@
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.map {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
.ol-control.layer-toggles {
|
||||
top: 0.5em;
|
||||
right: 0.5em;
|
||||
background-color: rgba(70, 115, 164, 0.7);
|
||||
color: #eee;
|
||||
white-space: nowrap;
|
||||
max-height: calc(100vh - 1em);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.layer-toggles > div {
|
||||
margin: 0.5em;
|
||||
margin-right: 1em;
|
||||
}
|
||||
.ol-control.layer-toggles:hover {
|
||||
background-color: rgba(0,60,136,0.7);
|
||||
}
|
||||
.layer-toggles > div > label {
|
||||
display: block;
|
||||
}
|
||||
.expand {
|
||||
display: none;
|
||||
}
|
||||
.expand + span::before {
|
||||
content: '\25B6';
|
||||
}
|
||||
.expand:checked + span::before {
|
||||
content: '\25BC';
|
||||
}
|
||||
.expand ~ .collapsible-content {
|
||||
display: none;
|
||||
}
|
||||
.expand:checked ~ .collapsible-content {
|
||||
display: block;
|
||||
}
|
||||
.collapsible-content {
|
||||
margin-left: 0.8em;
|
||||
}
|
||||
.collapsible-content > label {
|
||||
display: block;
|
||||
}
|
11
src/map.html
11
src/map.html
@ -1,11 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<link rel="stylesheet" type="text/css" href="map.css">
|
||||
<title>OpenLayers example</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map" class="map"></div>
|
||||
<script src="./map.js"></script>
|
||||
</body>
|
||||
</html>
|
314
src/map.js
314
src/map.js
@ -1,314 +0,0 @@
|
||||
import 'ol/ol.css';
|
||||
import {Map as olMap, View} from 'ol';
|
||||
import {Control} from 'ol/control';
|
||||
import {Group as LayerGroup, Tile as TileLayer, Vector as VectorLayer} from 'ol/layer';
|
||||
import {OSM, Vector as VectorSource} from 'ol/source';
|
||||
import {GPX} from 'ol/format';
|
||||
import Feature from 'ol/Feature';
|
||||
import {fromLonLat} from 'ol/proj';
|
||||
import {Circle as CircleStyle, Icon, Fill, Stroke, Style, Text} from 'ol/style';
|
||||
import Projection from 'ol/proj/Projection';
|
||||
import {LineString, Point} from 'ol/geom';
|
||||
|
||||
import {readFileSync} from 'fs';
|
||||
const packetLog = readFileSync(__dirname + '/../IS_packets.txt', 'utf-8');
|
||||
|
||||
import {APRSParser} from 'aprs-parser';
|
||||
|
||||
let tile_layer = new TileLayer({source: new OSM()});
|
||||
|
||||
import route_data from 'gpx/*.gpx';
|
||||
|
||||
let routes = new LayerGroup({
|
||||
title: "Routes",
|
||||
layers: Object.keys(route_data).map(name => new VectorLayer({
|
||||
title: name.replace(/_/g, " "),
|
||||
source: new VectorSource({
|
||||
url: route_data[name],
|
||||
format: new GPX()
|
||||
}),
|
||||
style: new Style({
|
||||
stroke: new Stroke(
|
||||
{color: 'hsl(200, 90%, 30%)', width: 5}
|
||||
)})
|
||||
}))
|
||||
});
|
||||
|
||||
|
||||
let map = new olMap({
|
||||
target: 'map',
|
||||
layers: [
|
||||
tile_layer,
|
||||
routes
|
||||
],
|
||||
view: new View({
|
||||
center: fromLonLat([-72.15, 43.90]),
|
||||
zoom: 10
|
||||
})
|
||||
});
|
||||
|
||||
class ColorGenerator {
|
||||
constructor(count) {
|
||||
let mult = Math.floor(360 / count);
|
||||
this.hues = Array.from(Array(count).keys()).map(x => x * mult);
|
||||
|
||||
// Shuffle (this is not a great shuffle, but I don't care)
|
||||
this.hues.forEach((current, index, arr) => {
|
||||
let randomIndex = Math.floor(Math.random() * index);
|
||||
[arr[index], arr[randomIndex]] = [arr[randomIndex], arr[index]];
|
||||
});
|
||||
}
|
||||
get() {
|
||||
return this.hues.pop();
|
||||
}
|
||||
};
|
||||
|
||||
function transformGeometry(geometry) {
|
||||
return geometry.transform(new Projection({code: "EPSG:4326"}),
|
||||
tile_layer.getSource().getProjection());
|
||||
}
|
||||
|
||||
function plotPaths(packets) {
|
||||
let path_layers = new LayerGroup({title: "Station Paths"});
|
||||
map.addLayer(path_layers);
|
||||
|
||||
let paths = packets
|
||||
.filter(packet => packet.date > new Date("2018-07-14") && packet.date < new Date("2018-07-15"))
|
||||
// filter to just positional data
|
||||
.filter(packet => 'data' in packet && 'latitude' in packet.data)
|
||||
// join into Arrays of points
|
||||
.reduce((acc, packet) => {
|
||||
let callsign = packet.from.toString().trim();
|
||||
if (!acc.has(callsign)) acc.set(callsign, []);
|
||||
acc.get(callsign).push([packet.data.longitude, packet.data.latitude, packet]);
|
||||
return acc;
|
||||
}, new Map());
|
||||
|
||||
let colorGen = new ColorGenerator(paths.size);
|
||||
|
||||
// plot on map
|
||||
paths.forEach((points, callsign) => {
|
||||
let color = 'hsl(' + colorGen.get() + ', 75%, 50%)';
|
||||
let path_layer = new VectorLayer({
|
||||
title: "Path",
|
||||
source: new VectorSource({features: [
|
||||
new Feature({geometry: transformGeometry(new LineString(points))})
|
||||
]}),
|
||||
style: new Style({stroke: new Stroke({color: color, width: 2})})
|
||||
});
|
||||
|
||||
let points_layer = new VectorLayer({
|
||||
title: "Points",
|
||||
source: new VectorSource({
|
||||
features: points.map(point => new Feature({
|
||||
geometry: transformGeometry(new Point(point)),
|
||||
packet: point[3] // TODO: this seems a bit bad
|
||||
}))
|
||||
}),
|
||||
style: new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 4,
|
||||
fill: new Fill({color: color})
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
let callsign_layer = new LayerGroup({
|
||||
title: callsign,
|
||||
layers: [path_layer, points_layer]
|
||||
});
|
||||
path_layers.getLayers().push(callsign_layer);
|
||||
});
|
||||
}
|
||||
|
||||
function plotPacketPaths(packets) {
|
||||
function pathToString(path) {
|
||||
return path
|
||||
.filter(station => !station.call.match(/WIDE[12]|qA?|UV[123]|.*\*$|UNCAN/))
|
||||
.map(station => station.toString().trim().replace(/\*$/, ""));
|
||||
}
|
||||
|
||||
let digiCalls =
|
||||
new Set(packets
|
||||
.map(packet => pathToString(packet.via))
|
||||
.reduce((acc, stations) => acc.concat(stations)));
|
||||
|
||||
let digiPos = packets
|
||||
// filter to digis
|
||||
.filter(packet => digiCalls.has(packet.from.toString().trim()))
|
||||
// filter to just positional data
|
||||
.filter(packet => 'data' in packet && 'latitude' in packet.data)
|
||||
// convert to callsign -> position mapping
|
||||
.reduce((stations, packet) =>
|
||||
stations.set(packet.from.toString().trim(),
|
||||
[packet.data.longitude, packet.data.latitude]),
|
||||
new Map());
|
||||
|
||||
let colorGen = new ColorGenerator(digiPos.size);
|
||||
let colorMap = Array.from(digiPos.keys()).reduce(
|
||||
(acc, callsign, index) =>
|
||||
acc.set(callsign, colorGen.hues[index]), new Map());
|
||||
|
||||
// plot digis
|
||||
// TODO: icons
|
||||
let digi_style = new Style({
|
||||
image: new CircleStyle({
|
||||
radius: 5,
|
||||
fill: new Fill({color: 'grey'})
|
||||
}),
|
||||
text: new Text({
|
||||
font: 'bold 11px "Open Sans", "Arial Unicode MS", "sans-serif"',
|
||||
overflow: true,
|
||||
fill: new Fill({color: 'black'})
|
||||
})
|
||||
});
|
||||
let digi_layer = new VectorLayer({
|
||||
title: "Digipeater Labels",
|
||||
zIndex: 1, // TODO: probably not the best way to do this
|
||||
source: new VectorSource({
|
||||
features: Array.from(digiPos.keys()).map(callsign =>
|
||||
new Feature({
|
||||
geometry: transformGeometry(new Point(digiPos.get(callsign))),
|
||||
callsign: callsign
|
||||
})
|
||||
)}),
|
||||
style: feature => {
|
||||
digi_style.setText(new Text({
|
||||
text: feature.get('callsign'),
|
||||
offsetY: 12
|
||||
}));
|
||||
return digi_style;
|
||||
}
|
||||
});
|
||||
map.addLayer(digi_layer);
|
||||
|
||||
let packet_path_layers = new LayerGroup({title: "Packet Paths"});
|
||||
let layers_map = new Map();
|
||||
map.addLayer(packet_path_layers);
|
||||
packets
|
||||
.filter(packet => packet.date > new Date("2018-07-14") && packet.date < new Date("2018-07-15"))
|
||||
// filter by callsign
|
||||
//.filter(packet => packet.from.toString() === "W1HS-9")
|
||||
// filter to just positional data
|
||||
.filter(packet => 'data' in packet && 'latitude' in packet.data)
|
||||
.forEach(packet => {
|
||||
pathToString(packet.via)
|
||||
.forEach((station, index, stations) => {
|
||||
if (digiPos.get(station) === undefined) {
|
||||
console.log(station);
|
||||
}
|
||||
|
||||
// first point in path is originating station
|
||||
let previous = (index === 0) ?
|
||||
[packet.data.longitude, packet.data.latitude] :
|
||||
digiPos.get(stations[index - 1]) || [0, 0];
|
||||
|
||||
let pathFeature = new Feature(transformGeometry(
|
||||
new LineString([previous, digiPos.get(station) || [0, 0]])));
|
||||
|
||||
// TODO: want to color per station that hears it, probably means
|
||||
// making a lot more features
|
||||
let color = colorMap.get(station);
|
||||
pathFeature.setStyle(new Style({
|
||||
stroke: new Stroke(
|
||||
{color: 'hsl(' + color + ', 60%, 60%)', width: 2}
|
||||
)}));
|
||||
|
||||
if (!layers_map.has(station)) {
|
||||
layers_map.set(station, new VectorLayer({
|
||||
title: station,
|
||||
source: new VectorSource(),
|
||||
renderMode: 'image'
|
||||
}));
|
||||
packet_path_layers.getLayers().push(layers_map.get(station));
|
||||
}
|
||||
layers_map.get(station).getSource().addFeature(pathFeature);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function layer_toggle(layer) {
|
||||
if (layer.toggle_element === undefined) {
|
||||
layer.toggle_element = document.createElement('label');
|
||||
|
||||
let checkbox = layer.toggle_element.appendChild(
|
||||
document.createElement('input'));
|
||||
checkbox.type = "checkbox";
|
||||
checkbox.checked = layer.getVisible();
|
||||
checkbox.addEventListener('change', event => {
|
||||
layer.setVisible(event.target.checked);
|
||||
});
|
||||
layer.toggle_element.appendChild(
|
||||
document.createTextNode(layer.get('title')));
|
||||
}
|
||||
return layer.toggle_element;
|
||||
}
|
||||
|
||||
function layer_toggles(layer, parentElement) {
|
||||
if (layer instanceof LayerGroup) {
|
||||
if (layer.group_toggle === undefined) {
|
||||
let label = layer.group_toggle = parentElement.appendChild(
|
||||
document.createElement('label'));
|
||||
let input = label.appendChild(document.createElement('input'));
|
||||
input.type = 'checkbox';
|
||||
input.className = "expand";
|
||||
label.appendChild(document.createElement('span'));
|
||||
|
||||
label.appendChild(layer_toggle(layer)); // whole LayerGroup
|
||||
|
||||
let child_toggle_element = label.appendChild(
|
||||
document.createElement('input'));
|
||||
child_toggle_element.type = 'checkbox';
|
||||
child_toggle_element.checked = true;
|
||||
child_toggle_element.addEventListener('change', event => {
|
||||
layer.getLayers().forEach(subLayer => {
|
||||
subLayer.setVisible(event.target.checked);
|
||||
subLayer.toggle_element.querySelector("input").checked =
|
||||
event.target.checked;
|
||||
});
|
||||
});
|
||||
|
||||
let container = label.appendChild(document.createElement('div'));
|
||||
container.className = 'collapsible-content';
|
||||
layer.getLayers().forEach(
|
||||
subLayer => layer_toggles(subLayer, container));
|
||||
return label;
|
||||
}
|
||||
}
|
||||
else {
|
||||
parentElement.appendChild(layer_toggle(layer));
|
||||
}
|
||||
}
|
||||
|
||||
function render_layer_toggles(event, element) {
|
||||
event.map.getLayers().getArray()
|
||||
.filter(layer => layer.get('title') !== undefined)
|
||||
.forEach(layer => layer_toggles(layer, element));
|
||||
}
|
||||
|
||||
(function makeLayerToogleControl() {
|
||||
let element = document.createElement('div');
|
||||
element.className = 'layer-toggles ol-unselectable ol-control';
|
||||
let inner = element.appendChild(document.createElement('div'));
|
||||
|
||||
let control = new Control(
|
||||
{element: element,
|
||||
render: event => render_layer_toggles(event, inner)});
|
||||
map.addControl(control);
|
||||
})();
|
||||
|
||||
|
||||
function parsePackets(packetLog) {
|
||||
let parser = new APRSParser();
|
||||
return packetLog.trim().split("\n")
|
||||
// parse to Date and APRS packet
|
||||
.map(line => {
|
||||
let packet = parser.parse(line.slice(29));
|
||||
packet.date = new Date(line.slice(0,18));
|
||||
return packet;
|
||||
});
|
||||
}
|
||||
|
||||
let packets = parsePackets(packetLog);
|
||||
plotPaths(packets);
|
||||
plotPacketPaths(packets);
|
Loading…
Reference in New Issue
Block a user