WIP: Migrate Map to Vue

This commit is contained in:
Adam Goldsmith 2019-07-15 18:08:15 -04:00
parent 2d9233fabb
commit 87b8f9e9ba
6 changed files with 404 additions and 377 deletions

203
package-lock.json generated
View File

@ -1015,6 +1015,22 @@
"regenerator-runtime": "^0.12.0" "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": { "@babel/template": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.2.2.tgz",
@ -1126,6 +1142,103 @@
"physical-cpu-count": "^2.0.0" "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": { "@types/q": {
"version": "1.5.2", "version": "1.5.2",
"resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz", "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.2.tgz",
@ -1929,6 +2042,11 @@
"upath": "^1.1.1" "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": { "cipher-base": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz",
@ -2142,8 +2260,7 @@
"core-js": { "core-js": {
"version": "2.6.9", "version": "2.6.9",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.9.tgz",
"integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A==", "integrity": "sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A=="
"dev": true
}, },
"core-util-is": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
@ -2623,6 +2740,11 @@
"node-addon-api": "^1.6.0" "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": { "debug": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
@ -2750,6 +2872,15 @@
"randombytes": "^2.0.0" "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": { "dom-serializer": {
"version": "0.1.1", "version": "0.1.1",
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.1.1.tgz",
@ -4740,6 +4871,11 @@
"integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==", "integrity": "sha512-FSYbp3lyKjyj3E7fMl6rYvUdX0FBXaluGqlFoYESWQlyUTq8R+wp0rkFxoYFqZlHCvsUXGjyJmLQSnXToYhOSA==",
"dev": true "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": { "merge-source-map": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", "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": { "ms": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@ -5144,6 +5285,11 @@
"rbush": "2.0.2" "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": { "on-finished": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz",
@ -5321,6 +5467,21 @@
"safe-buffer": "^5.1.1" "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": { "parse-json": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz",
@ -6329,6 +6490,14 @@
"inherits": "^2.0.1" "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": { "safe-buffer": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
@ -7028,6 +7197,11 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true "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": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@ -7294,8 +7468,7 @@
"uuid": { "uuid": {
"version": "3.3.2", "version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==", "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
"dev": true
}, },
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.0.3", "version": "2.0.3",
@ -7358,6 +7531,23 @@
"integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==",
"dev": true "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": { "w3c-hr-time": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.1.tgz",
@ -7391,6 +7581,11 @@
"iconv-lite": "0.4.24" "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": { "whatwg-mimetype": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",

View File

@ -5,9 +5,11 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"aprs-parser": "^1.0.4", "aprs-parser": "^1.0.4",
"distinct-colors": "^1.0.4",
"ol": "^5.3.3", "ol": "^5.3.3",
"vue": "^2.6.10", "vue": "^2.6.10",
"vue-hot-reload-api": "^2.3.3", "vue-hot-reload-api": "^2.3.3",
"vuelayers": "^0.11.4",
"ws": "^5.2.2" "ws": "^5.2.2"
}, },
"devDependencies": { "devDependencies": {

203
src/Map.vue Normal file
View 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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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);