From 931d8435442327dd8efc59309e92860c3919a74c Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 12:31:25 +0300 Subject: [PATCH 01/18] Add IDEA project files to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4727a9e..4135755 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ public/bundle.js.map .bundle.css npm-debug.* client-config.json +.idea From d7ff337bb943ea152cef7916a29f936eeab41435 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 12:55:25 +0300 Subject: [PATCH 02/18] Add track-connection and geojson-trackserver provider --- src/client/provider-daily-trackserver.js | 14 +++++++++++++ src/client/track-connection.js | 25 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/client/provider-daily-trackserver.js create mode 100644 src/client/track-connection.js diff --git a/src/client/provider-daily-trackserver.js b/src/client/provider-daily-trackserver.js new file mode 100644 index 0000000..6c9ea88 --- /dev/null +++ b/src/client/provider-daily-trackserver.js @@ -0,0 +1,14 @@ +import api from './api' + +function initialize(trackServerAddress) { + return { + queryTracks + } + + function queryTracks(bbox) { + return api.get({url: `${trackServerAddress}/daily-tracks?bbox=${bbox.toBBoxString()}`}) + } +} + + +module.exports = initialize diff --git a/src/client/track-connection.js b/src/client/track-connection.js new file mode 100644 index 0000000..bfd23db --- /dev/null +++ b/src/client/track-connection.js @@ -0,0 +1,25 @@ +import Bacon from 'baconjs' +import DailyTrackServerProvider from './provider-daily-trackserver' + +function connect(providers) { + if (providers.length > 1) { + throw `Only 1 track provider supported for now` + } + if (providers.length === 0) { + return EmptyTrackServerProvider() + } + const provider = providers[0] + if (provider.type === 'daily-trackserver') { + return DailyTrackServerProvider(provider.address) + } else { + throw `Unsupported provider ${provider}` + } +} + +function EmptyTrackServerProvider() { + return { + queryTracks: () => Bacon.once([]) + } +} + +module.exports = connect From e61ffc7beda18a8a1d9a0195bd7ef92c96bd0e90 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 13:13:18 +0300 Subject: [PATCH 03/18] Add dummy track server provider for testing purposes --- src/client/provider-dummy-trackserver.js | 85 ++++++++++++++++++++++++ src/client/track-connection.js | 3 + 2 files changed, 88 insertions(+) create mode 100644 src/client/provider-dummy-trackserver.js diff --git a/src/client/provider-dummy-trackserver.js b/src/client/provider-dummy-trackserver.js new file mode 100644 index 0000000..c2671dd --- /dev/null +++ b/src/client/provider-dummy-trackserver.js @@ -0,0 +1,85 @@ +import Bacon from 'baconjs' + +function initialize() { + return { + queryTracks + } + + function queryTracks() { + return Bacon.once([{ + "date": "2015-09-12T21:00:00.000Z", + "route": { + "coordinates": dummyTrack, + "type": "LineString" + } + }]) + } +} + +module.exports = initialize + + +const dummyTrack = [ + [24.92908833, 60.16096], + [24.92874333, 60.16108333], + [24.92775667, 60.16093667], + [24.92695333, 60.16033], + [24.92484333, 60.15942], + [24.92451833, 60.15845], + [24.92474833, 60.15717167], + [24.92463167, 60.15614167], + [24.923115, 60.15368833], + [24.91990167, 60.15086333], + [24.91928333, 60.15006333], + [24.91914333, 60.14961833], + [24.91963333, 60.148935], + [24.91681333, 60.14681333], + [24.91295667, 60.14484167], + [24.911985, 60.14418667], + [24.91068833, 60.14295167], + [24.90756333, 60.13916], + [24.90702333, 60.13768833], + [24.90621667, 60.12825333], + [24.90563, 60.12466167], + [24.90499667, 60.122085], + [24.90439333, 60.11776333], + [24.903615, 60.11370167], + [24.90346833, 60.11191833], + [24.90361833, 60.111485], + [24.90347, 60.11123333], + [24.89799167, 60.10720333], + [24.89496333, 60.10476667], + [24.886915, 60.09952167], + [24.881305, 60.09628833], + [24.87474167, 60.09287], + [24.87222833, 60.092015], + [24.86859, 60.09202833], + [24.86418833, 60.09156333], + [24.861075, 60.09156833], + [24.85545667, 60.09123333], + [24.82868167, 60.092265], + [24.82256, 60.093525], + [24.80822833, 60.09555333], + [24.806815, 60.09742667], + [24.80533833, 60.09978167], + [24.804705, 60.10182167], + [24.80423833, 60.10438667], + [24.80378167, 60.10836], + [24.802325, 60.11174167], + [24.80118167, 60.11354333], + [24.80063333, 60.11473833], + [24.79798, 60.11574833], + [24.79440667, 60.11672167], + [24.78841667, 60.117675], + [24.78689167, 60.11805833], + [24.786635, 60.11830667], + [24.78680333, 60.118935], + [24.78824, 60.12085833], + [24.79051333, 60.12318333], + [24.79388667, 60.12534667], + [24.796865, 60.12681667], + [24.800175, 60.12871], + [24.801405, 60.12992333], + [24.80265667, 60.13165667], + [24.80341833, 60.13354167] +] diff --git a/src/client/track-connection.js b/src/client/track-connection.js index bfd23db..3d70b73 100644 --- a/src/client/track-connection.js +++ b/src/client/track-connection.js @@ -1,5 +1,6 @@ import Bacon from 'baconjs' import DailyTrackServerProvider from './provider-daily-trackserver' +import DummyTrackServerProvider from './provider-dummy-trackserver' function connect(providers) { if (providers.length > 1) { @@ -11,6 +12,8 @@ function connect(providers) { const provider = providers[0] if (provider.type === 'daily-trackserver') { return DailyTrackServerProvider(provider.address) + } else if (provider.type === 'dummy-trackserver') { + return DummyTrackServerProvider() } else { throw `Unsupported provider ${provider}` } From 67d2b77303e220455292dd1cb8633b95b06105d3 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 20:51:13 +0300 Subject: [PATCH 04/18] Add vesselId parameter to daily track server provider --- src/client/provider-daily-trackserver.js | 4 ++-- src/client/track-connection.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/client/provider-daily-trackserver.js b/src/client/provider-daily-trackserver.js index 6c9ea88..b080eb4 100644 --- a/src/client/provider-daily-trackserver.js +++ b/src/client/provider-daily-trackserver.js @@ -1,12 +1,12 @@ import api from './api' -function initialize(trackServerAddress) { +function initialize(trackServerAddress, vesselId) { return { queryTracks } function queryTracks(bbox) { - return api.get({url: `${trackServerAddress}/daily-tracks?bbox=${bbox.toBBoxString()}`}) + return api.get({url: `${trackServerAddress}/${vesselId}/daily-tracks/?bbox=${bbox.toBBoxString()}`}) } } diff --git a/src/client/track-connection.js b/src/client/track-connection.js index 3d70b73..4fcf50b 100644 --- a/src/client/track-connection.js +++ b/src/client/track-connection.js @@ -11,7 +11,7 @@ function connect(providers) { } const provider = providers[0] if (provider.type === 'daily-trackserver') { - return DailyTrackServerProvider(provider.address) + return DailyTrackServerProvider(provider.address, provider.vesselId) } else if (provider.type === 'dummy-trackserver') { return DummyTrackServerProvider() } else { From 40a9f756a7327e9dfaa33bd3f77e7a59cca42346 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Tue, 20 Jun 2017 06:48:25 +0300 Subject: [PATCH 05/18] Refactor passing configuration --- src/client/provider-daily-trackserver.js | 4 ++-- src/client/track-connection.js | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/provider-daily-trackserver.js b/src/client/provider-daily-trackserver.js index b080eb4..cc9c38f 100644 --- a/src/client/provider-daily-trackserver.js +++ b/src/client/provider-daily-trackserver.js @@ -1,12 +1,12 @@ import api from './api' -function initialize(trackServerAddress, vesselId) { +function initialize({address, vesselId}) { return { queryTracks } function queryTracks(bbox) { - return api.get({url: `${trackServerAddress}/${vesselId}/daily-tracks/?bbox=${bbox.toBBoxString()}`}) + return api.get({url: `${address}/${vesselId}/daily-tracks/?bbox=${bbox.toBBoxString()}`}) } } diff --git a/src/client/track-connection.js b/src/client/track-connection.js index 4fcf50b..99afbe9 100644 --- a/src/client/track-connection.js +++ b/src/client/track-connection.js @@ -9,13 +9,13 @@ function connect(providers) { if (providers.length === 0) { return EmptyTrackServerProvider() } - const provider = providers[0] - if (provider.type === 'daily-trackserver') { - return DailyTrackServerProvider(provider.address, provider.vesselId) - } else if (provider.type === 'dummy-trackserver') { + const providerConfig = providers[0] + if (providerConfig.type === 'daily-trackserver') { + return DailyTrackServerProvider(providerConfig) + } else if (providerConfig.type === 'dummy-trackserver') { return DummyTrackServerProvider() } else { - throw `Unsupported provider ${provider}` + throw `Unsupported provider ${providerConfig}` } } From 4858ae7a50d1c57754a7212f4718545cfea65375 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 21:54:41 +0300 Subject: [PATCH 06/18] Add support for optional basic auth authentication to Api.get --- src/client/api.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/client/api.js b/src/client/api.js index 2952671..a6b9127 100644 --- a/src/client/api.js +++ b/src/client/api.js @@ -15,13 +15,21 @@ function parseJSON(response) { return response.json() } -function get({url}) { - const request = fetch(url) +function get({url, basicAuth}) { + const request = fetch(url, basicAuth ? basicAuthHeaders(basicAuth) : undefined) .then(checkStatus) .then(parseJSON) return Bacon.fromPromise(request) } +function basicAuthHeaders({username, password}) { + return { + headers: { + 'Authorization': 'Basic '+ btoa(`${username}:${password}`), + } + } +} + module.exports = { get -} \ No newline at end of file +} From 8d11d3bb84093b96fd3b742e1c6f3f7af5fe21a8 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Tue, 20 Jun 2017 06:50:32 +0300 Subject: [PATCH 07/18] Use basic auth with track server if username and password are set --- src/client/provider-daily-trackserver.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/client/provider-daily-trackserver.js b/src/client/provider-daily-trackserver.js index cc9c38f..caeeb02 100644 --- a/src/client/provider-daily-trackserver.js +++ b/src/client/provider-daily-trackserver.js @@ -1,12 +1,15 @@ import api from './api' -function initialize({address, vesselId}) { +function initialize({address, vesselId, username, password}) { return { queryTracks } function queryTracks(bbox) { - return api.get({url: `${address}/${vesselId}/daily-tracks/?bbox=${bbox.toBBoxString()}`}) + return api.get({ + url: `${address}/${vesselId}/daily-tracks?bbox=${bbox.toBBoxString()}`, + basicAuth: !!username && !!password ? {username, password} : undefined + }) } } From 8ce64ed78f98417209fd9157c1b3f505e02ef3c6 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Tue, 20 Jun 2017 06:40:39 +0300 Subject: [PATCH 08/18] Rename connection -> dataConnection --- src/client/app.js | 6 +++--- src/client/map.js | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/client/app.js b/src/client/app.js index 14fd759..052f20b 100644 --- a/src/client/app.js +++ b/src/client/app.js @@ -9,7 +9,7 @@ import Bacon from 'baconjs' import _ from 'lodash' import {COG, HDG, MAX_ZOOM, MIN_ZOOM, EXTENSION_LINE_OFF, EXTENSION_LINE_2_MIN, EXTENSION_LINE_5_MIN, EXTENSION_LINE_10_MIN} from './enums' import Map from './map' -import Connection from './data-connection' +import DataConnection from './data-connection' import {toDegrees, toNauticalMiles} from './utils' import InstrumentConfig from './instrument-config' import fullscreen from './fullscreen' @@ -18,7 +18,7 @@ numeral.nullFormat('N/A') const drawObject = Atom({distance: 0, del: false}) -const connection = Connection({providers: settings.get().data, settings}) +const dataConnection = DataConnection({providers: settings.get().data, settings}) fullscreen(settings) @@ -320,7 +320,7 @@ const App = ( if (loading) { return

Loading ...

} else { - return + return } })} diff --git a/src/client/map.js b/src/client/map.js index 827141d..336c81c 100644 --- a/src/client/map.js +++ b/src/client/map.js @@ -13,7 +13,7 @@ import {COG, HDG, MAX_ZOOM, MIN_ZOOM, KNOTS_TO_MS, EXTENSION_LINE_OFF, EXTENSION class Map extends React.Component { componentDidMount() { - initMap(this.props.connection, this.props.settings, this.props.drawObject) + initMap(this.props.dataConnection, this.props.settings, this.props.drawObject) } render() { const {settings} = this.props @@ -26,7 +26,7 @@ class Map extends React.Component { } } -function initMap(connection, settings, drawObject) { +function initMap(dataConnection, settings, drawObject) { console.log('Init map') const initialSettings = settings.get() const map = Leaf.map('map', { @@ -52,7 +52,7 @@ function initMap(connection, settings, drawObject) { pointer.addTo(map) const vesselData = Bacon.combineTemplate({ - vesselData: connection.selfData, + vesselData: dataConnection.selfData, settings }) vesselData.onValue(({vesselData, settings}) => { @@ -86,7 +86,7 @@ function initMap(connection, settings, drawObject) { } }) - handleAisTargets({map, aisData: connection.aisData, settings}) + handleAisTargets({map, aisData: dataConnection.aisData, settings}) handleDrawPath({map, settings, drawObject}) handleMapZoom() handleDragAndFollow() From 89321c330a2568a6cbc95f3d7e76621d4cbbbd8f Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Tue, 20 Jun 2017 06:55:05 +0300 Subject: [PATCH 09/18] Create and pass trackConnection to map --- src/client/app.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/app.js b/src/client/app.js index 052f20b..d01016b 100644 --- a/src/client/app.js +++ b/src/client/app.js @@ -10,6 +10,7 @@ import _ from 'lodash' import {COG, HDG, MAX_ZOOM, MIN_ZOOM, EXTENSION_LINE_OFF, EXTENSION_LINE_2_MIN, EXTENSION_LINE_5_MIN, EXTENSION_LINE_10_MIN} from './enums' import Map from './map' import DataConnection from './data-connection' +import TrackConnection from './track-connection' import {toDegrees, toNauticalMiles} from './utils' import InstrumentConfig from './instrument-config' import fullscreen from './fullscreen' @@ -19,6 +20,7 @@ numeral.nullFormat('N/A') const drawObject = Atom({distance: 0, del: false}) const dataConnection = DataConnection({providers: settings.get().data, settings}) +const trackConnection = TrackConnection(settings.get().tracks) fullscreen(settings) @@ -320,7 +322,7 @@ const App = ( if (loading) { return

Loading ...

} else { - return + return } })} From f14d7a8a8d0e0ebe1a62d9008bf3ac7b8382ee40 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Tue, 20 Jun 2017 07:02:03 +0300 Subject: [PATCH 10/18] Render tracks using given track-connection --- src/client/map.js | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/client/map.js b/src/client/map.js index 336c81c..de5a4a5 100644 --- a/src/client/map.js +++ b/src/client/map.js @@ -13,7 +13,7 @@ import {COG, HDG, MAX_ZOOM, MIN_ZOOM, KNOTS_TO_MS, EXTENSION_LINE_OFF, EXTENSION class Map extends React.Component { componentDidMount() { - initMap(this.props.dataConnection, this.props.settings, this.props.drawObject) + initMap(this.props.dataConnection, this.props.trackConnection, this.props.settings, this.props.drawObject) } render() { const {settings} = this.props @@ -26,7 +26,7 @@ class Map extends React.Component { } } -function initMap(dataConnection, settings, drawObject) { +function initMap(dataConnection, trackConnection, settings, drawObject) { console.log('Init map') const initialSettings = settings.get() const map = Leaf.map('map', { @@ -91,6 +91,7 @@ function initMap(dataConnection, settings, drawObject) { handleMapZoom() handleDragAndFollow() handleInstrumentsToggle() + showTracksOnMapMove() function handleMapZoom() { settings.map('.zoom').skipDuplicates().onValue(zoom => { map.setZoom(zoom) @@ -133,6 +134,14 @@ function initMap(dataConnection, settings, drawObject) { map.invalidateSize(true) }) } + + function showTracksOnMapMove() { + Bacon.fromEvent(map, 'moveend') + .map(() => map.getBounds()) + .skipDuplicates(_.isEqual) + .flatMapLatest(trackConnection.queryTracks) + .onValue(tracks => renderTracks(map, tracks)) + } } function handleAisTargets({map, aisData, settings}) { @@ -257,6 +266,18 @@ function addCharts(map, providers) { }) } +let geoJSONLayers = [] + +function renderTracks(map, tracks) { + geoJSONLayers.forEach(layer => map.removeLayer(layer)) + geoJSONLayers = [] + tracks.forEach(({date, route}) => { + const geoJSONLayer = Leaf.geoJSON(route, {style: {color: '#0088FF'}}) + geoJSONLayers.push(geoJSONLayer) + geoJSONLayer.addTo(map) + }) +} + function addBasemap(map) { map.createPane('basemap') const basemapStyle = { From fd661e23c8f2f20b19b3cbcc26b06c2d9108d2ab Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 20:36:12 +0300 Subject: [PATCH 11/18] Add daily colours for tracks --- src/client/map.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/client/map.js b/src/client/map.js index de5a4a5..c6a061d 100644 --- a/src/client/map.js +++ b/src/client/map.js @@ -267,12 +267,18 @@ function addCharts(map, providers) { } let geoJSONLayers = [] +const trackColors = [ + "#4c79a6", + "#46a65b", + "#a262a6", +] function renderTracks(map, tracks) { geoJSONLayers.forEach(layer => map.removeLayer(layer)) geoJSONLayers = [] - tracks.forEach(({date, route}) => { - const geoJSONLayer = Leaf.geoJSON(route, {style: {color: '#0088FF'}}) + tracks.forEach(({date, route}, i) => { + const dayIndex = (new Date(date).getTime() / 86400000).toFixed() + const geoJSONLayer = Leaf.geoJSON(route, {style: {color: trackColors[dayIndex % trackColors.length]}}) geoJSONLayers.push(geoJSONLayer) geoJSONLayer.addTo(map) }) From 517a38e50905bac20bb7fd81fea3f96dee5721f0 Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 22:31:33 +0300 Subject: [PATCH 12/18] Highlight a track by clicking it --- src/client/map.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/client/map.js b/src/client/map.js index c6a061d..82a14ea 100644 --- a/src/client/map.js +++ b/src/client/map.js @@ -278,7 +278,9 @@ function renderTracks(map, tracks) { geoJSONLayers = [] tracks.forEach(({date, route}, i) => { const dayIndex = (new Date(date).getTime() / 86400000).toFixed() - const geoJSONLayer = Leaf.geoJSON(route, {style: {color: trackColors[dayIndex % trackColors.length]}}) + const basicStyle = {color: trackColors[dayIndex % trackColors.length]} + const geoJSONLayer = Leaf.geoJSON(route, {style: basicStyle}) + geoJSONLayer.on('click', e => geoJSONLayer.setStyle(_.assign({}, basicStyle, {weight: 6}))) geoJSONLayers.push(geoJSONLayer) geoJSONLayer.addTo(map) }) From 75fb3968f342beefa97d0550c2ea7e0958018e9b Mon Sep 17 00:00:00 2001 From: Jouni Hartikainen Date: Sun, 11 Jun 2017 22:37:07 +0300 Subject: [PATCH 13/18] Add "tracks" segment to sample client configuration --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index 460830b..adb47b7 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,18 @@ Example config: "bounds": [19.105224609375, 59.645540251443215, 27.88330078125, 65.84776766596988], "center": [24.805, 60.0888] } + ], + "tracks": [ + { + "type": "daily-trackserver", + "address": "", + "vesselId": "", + "username": "basic auth user", + "password": "basic auth passwd" + }, + { // Or for testing purposes + "type": "dummy-trackserver" + } ] } ``` From e202c9167e7baca4268c19915fb1673c0bf8f659 Mon Sep 17 00:00:00 2001 From: Teppo Kurki Date: Wed, 20 Sep 2017 14:25:23 +0300 Subject: [PATCH 14/18] feature: add Signal K trackserver support Add support for getting tracks from the SK server that the app was loaded from. --- public/index.html | 4 ++++ src/client/provider-signalk-trackserver.js | 16 ++++++++++++++++ src/client/track-connection.js | 9 ++++++--- 3 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 src/client/provider-signalk-trackserver.js diff --git a/public/index.html b/public/index.html index 74c34dc..fe9d85b 100644 --- a/public/index.html +++ b/public/index.html @@ -20,6 +20,10 @@ type: "signalk", address: window.location.protocol + "//" + window.location.host }], + tracks: [{ + type: "signalk", + address: window.location.protocol + "//" + window.location.host + }] };
diff --git a/src/client/provider-signalk-trackserver.js b/src/client/provider-signalk-trackserver.js new file mode 100644 index 0000000..ac160f1 --- /dev/null +++ b/src/client/provider-signalk-trackserver.js @@ -0,0 +1,16 @@ +import api from './api' + +function initialize({address}) { + return { + queryTracks + } + + function queryTracks(bbox) { + return api.get({ + url: `${address}/signalk/v1/api/vessels/self/tracks?bbox=${bbox.toBBoxString()}` + }) + } +} + + +module.exports = initialize diff --git a/src/client/track-connection.js b/src/client/track-connection.js index 99afbe9..9f4307e 100644 --- a/src/client/track-connection.js +++ b/src/client/track-connection.js @@ -1,17 +1,20 @@ import Bacon from 'baconjs' import DailyTrackServerProvider from './provider-daily-trackserver' import DummyTrackServerProvider from './provider-dummy-trackserver' +import SignalKTrackServerProvider from './provider-signalk-trackserver' function connect(providers) { + if (!providers || providers.length === 0) { + return EmptyTrackServerProvider() + } if (providers.length > 1) { throw `Only 1 track provider supported for now` } - if (providers.length === 0) { - return EmptyTrackServerProvider() - } const providerConfig = providers[0] if (providerConfig.type === 'daily-trackserver') { return DailyTrackServerProvider(providerConfig) + } else if (providerConfig.type === 'signalk') { + return SignalKTrackServerProvider(providerConfig) } else if (providerConfig.type === 'dummy-trackserver') { return DummyTrackServerProvider() } else { From a9b4512f869ad9fa4754ffa14e7f32eb74df450c Mon Sep 17 00:00:00 2001 From: Teppo Kurki Date: Wed, 20 Sep 2017 14:28:45 +0300 Subject: [PATCH 15/18] feature: change track format to GeoJSON FeatureCollection Instead of a custom JSON we can use vanilla GeoJSON FeatureCollection and use GeoJSON properties for storing metadata for each track === Feature. --- src/client/map.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client/map.js b/src/client/map.js index 82a14ea..ec827fc 100644 --- a/src/client/map.js +++ b/src/client/map.js @@ -273,13 +273,13 @@ const trackColors = [ "#a262a6", ] -function renderTracks(map, tracks) { +function renderTracks(map, featureCollection) { geoJSONLayers.forEach(layer => map.removeLayer(layer)) geoJSONLayers = [] - tracks.forEach(({date, route}, i) => { - const dayIndex = (new Date(date).getTime() / 86400000).toFixed() + featureCollection.features.forEach((feature, i) => { + const dayIndex = (new Date(feature.properties.starttime).getTime() / 86400000).toFixed() const basicStyle = {color: trackColors[dayIndex % trackColors.length]} - const geoJSONLayer = Leaf.geoJSON(route, {style: basicStyle}) + const geoJSONLayer = Leaf.geoJSON(feature, {style: basicStyle}) geoJSONLayer.on('click', e => geoJSONLayer.setStyle(_.assign({}, basicStyle, {weight: 6}))) geoJSONLayers.push(geoJSONLayer) geoJSONLayer.addTo(map) From 892e48d09401c0694e476da8ce3250ffa788e0ac Mon Sep 17 00:00:00 2001 From: Teppo Kurki Date: Wed, 20 Sep 2017 20:11:45 +0300 Subject: [PATCH 16/18] fix: merge problem with renaming --- src/client/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client/app.js b/src/client/app.js index d01016b..032651c 100644 --- a/src/client/app.js +++ b/src/client/app.js @@ -317,7 +317,7 @@ const App = (
- + {settings.view(L.prop('loadingChartProviders')).skipDuplicates().map(loading => { if (loading) { return

Loading ...

From 6e834c6655469e5a9f4f15647b6803f7f6977409 Mon Sep 17 00:00:00 2001 From: Teppo Kurki Date: Sun, 24 Sep 2017 22:24:05 +0300 Subject: [PATCH 17/18] feature: add support for rendering data on top of tracks If the response contains extra data in addition to the track positions in the individual coordinates arrays and they are enumerated in the top FeatureCollection property dataPaths render them as colored points and rings. Supports 2 distinct series. Includes hover popups to display the value with timestamp. --- package.json | 4 +- src/client/map.js | 95 +++++++++++++++++++--- src/client/provider-signalk-trackserver.js | 2 +- 3 files changed, 87 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 7d99f96..5d04849 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "marine", "signalk-webapp" ], - "dependencies": {}, "devDependencies": { "@mapbox/mbtiles": "0.9.0", "autoprefixer": "7.1.1", @@ -41,8 +40,9 @@ "baret": "1.0.3", "classnames": "2.2.5", "compression": "1.6.2", + "d3-scale": "^1.0.6", "express": "4.15.3", - "geolib": "2.0.22", + "geolib": "^2.0.22", "leaflet": "1.1.0", "leaflet-rotatedmarker": "0.2.0", "less": "2.7.2", diff --git a/src/client/map.js b/src/client/map.js index ec827fc..747d6e0 100644 --- a/src/client/map.js +++ b/src/client/map.js @@ -5,6 +5,7 @@ import classNames from 'classnames' import _ from 'lodash' import numeral from 'numeral' import * as Leaf from 'leaflet' +import {scaleLinear} from 'd3-scale' import {computeDestinationPoint} from 'geolib' import LeafletRotatedMarker from 'leaflet-rotatedmarker' import api from './api' @@ -266,26 +267,98 @@ function addCharts(map, providers) { }) } -let geoJSONLayers = [] +let trackLayers = [] const trackColors = [ "#4c79a6", "#46a65b", "#a262a6", ] -function renderTracks(map, featureCollection) { - geoJSONLayers.forEach(layer => map.removeLayer(layer)) - geoJSONLayers = [] - featureCollection.features.forEach((feature, i) => { - const dayIndex = (new Date(feature.properties.starttime).getTime() / 86400000).toFixed() - const basicStyle = {color: trackColors[dayIndex % trackColors.length]} - const geoJSONLayer = Leaf.geoJSON(feature, {style: basicStyle}) - geoJSONLayer.on('click', e => geoJSONLayer.setStyle(_.assign({}, basicStyle, {weight: 6}))) - geoJSONLayers.push(geoJSONLayer) - geoJSONLayer.addTo(map) +function renderTracks (map, featureCollection) { + trackLayers.forEach(layer => map.removeLayer(layer)) + trackLayers = featureCollection.features.reduce((acc, feature, i) => { + const dayIndex = feature.properties.starttime + ? (new Date(feature.properties.starttime).getTime() / 86400000).toFixed() + : 0 + const basicStyle = { + color: trackColors[dayIndex % trackColors.length], + stroke: 8 + } + const geoJSONLayer = Leaf.geoJSON(feature, { style: basicStyle }) + geoJSONLayer.on('click', e => + geoJSONLayer.setStyle(_.assign({}, basicStyle, { weight: 6 })) + ) + acc.push(geoJSONLayer) + + featureCollection.properties.dataPaths && featureCollection.properties.dataPaths.forEach((path, i) => { + acc.push(toDataLayer(feature, path, i, featureCollection.properties.dataPaths.length)) + }) + return acc + }, []) + trackLayers.forEach(layer => layer.addTo(map)) +} + + + + +function toDataLayer (feature, path, pathIndex, pathsCount) { + const valueColor = scaleLinear() + .domain([2, 5, 10, 25]) + .range(['red', 'yellow', 'green', 'blue']) + const circleStyles = [ + { + radius: 4, + fillOpacity: 1, + fillColor: valueColor, + color: () => '#000', + opacity: 0 + }, + { + radius: 8, + fillOpacity: 0, + fillColor: () => '#000', + color: valueColor, + opacity: 0.6 + } + ] + + const points = [] + + const circleStyle = + circleStyles[(pathIndex + pathsCount - 1) % circleStyles.length] + // [lat, lon, elev, timestamp, ...] + const dataIndex = pathIndex + 4 + feature.geometry.coordinates.forEach(line => { + line.forEach(coordinates => { + if (coordinates.length >= dataIndex) { + const circleMarker = Leaf.circleMarker( + Leaf.latLng(coordinates[1], coordinates[0]), + { + radius: circleStyle.radius, + weight: 5, + fillOpacity: circleStyle.fillOpacity, + fillColor: circleStyle.fillColor(coordinates[dataIndex]), + color: circleStyle.color(coordinates[dataIndex]), + opacity: circleStyle.opacity + } + ) + circleMarker.bindPopup( + `${path} ${coordinates[dataIndex]} \n${new Date(coordinates[3])}` + ) + circleMarker.on('mouseover', e => { + circleMarker.openPopup() + }) + + points.push(circleMarker) + } + }) }) + return Leaf.layerGroup(points) } + + + function addBasemap(map) { map.createPane('basemap') const basemapStyle = { diff --git a/src/client/provider-signalk-trackserver.js b/src/client/provider-signalk-trackserver.js index ac160f1..cf14603 100644 --- a/src/client/provider-signalk-trackserver.js +++ b/src/client/provider-signalk-trackserver.js @@ -7,7 +7,7 @@ function initialize({address}) { function queryTracks(bbox) { return api.get({ - url: `${address}/signalk/v1/api/vessels/self/tracks?bbox=${bbox.toBBoxString()}` + url: `${address}/signalk/v1/api/vessels/self/tracks?bbox=${bbox.toBBoxString()}&paths=navigation.speedOverGround,environmentWindSpeedTrue` }) } } From 76f7924821060f5db16f90841bf41ef4cef5614c Mon Sep 17 00:00:00 2001 From: Teppo Kurki Date: Sun, 8 Oct 2017 11:52:50 +0300 Subject: [PATCH 18/18] feature: fetch units from the server Start making the data to fetch configurable and fetch units from the server, caching them locally to skip a few http requests. --- src/client/map.js | 29 +++++++++++++++----- src/client/provider-signalk-trackserver.js | 32 ++++++++++++++++++---- 2 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/client/map.js b/src/client/map.js index 747d6e0..e5d9d01 100644 --- a/src/client/map.js +++ b/src/client/map.js @@ -136,12 +136,26 @@ function initMap(dataConnection, trackConnection, settings, drawObject) { }) } - function showTracksOnMapMove() { + + function showTracksOnMapMove () { + const paths = ['navigation.speedOverGround', 'environment.depth.belowTransducer'] Bacon.fromEvent(map, 'moveend') .map(() => map.getBounds()) .skipDuplicates(_.isEqual) - .flatMapLatest(trackConnection.queryTracks) - .onValue(tracks => renderTracks(map, tracks)) + .flatMapLatest(bounds => { + return trackConnection.queryTracks(bounds, paths) + }) + .onValue(tracks => { + const units = Bacon.combineAsArray( + paths.map(path => trackConnection.getUnits(path).mapError((err) => "n/a")) + ) + units.onValue(units => { + renderTracks(map, tracks, paths, units) + }) + units.onError((err) => { + console.log(err) + }) + }) } } @@ -274,7 +288,7 @@ const trackColors = [ "#a262a6", ] -function renderTracks (map, featureCollection) { +function renderTracks (map, featureCollection, paths, units) { trackLayers.forEach(layer => map.removeLayer(layer)) trackLayers = featureCollection.features.reduce((acc, feature, i) => { const dayIndex = feature.properties.starttime @@ -291,7 +305,7 @@ function renderTracks (map, featureCollection) { acc.push(geoJSONLayer) featureCollection.properties.dataPaths && featureCollection.properties.dataPaths.forEach((path, i) => { - acc.push(toDataLayer(feature, path, i, featureCollection.properties.dataPaths.length)) + acc.push(toDataLayer(feature, path, units[i], i, featureCollection.properties.dataPaths.length)) }) return acc }, []) @@ -301,7 +315,7 @@ function renderTracks (map, featureCollection) { -function toDataLayer (feature, path, pathIndex, pathsCount) { +function toDataLayer (feature, path, unit, pathIndex, pathsCount) { const valueColor = scaleLinear() .domain([2, 5, 10, 25]) .range(['red', 'yellow', 'green', 'blue']) @@ -342,8 +356,9 @@ function toDataLayer (feature, path, pathIndex, pathsCount) { opacity: circleStyle.opacity } ) + const displayValue = coordinates[dataIndex] ? coordinates[dataIndex].toFixed(2) : "" circleMarker.bindPopup( - `${path} ${coordinates[dataIndex]} \n${new Date(coordinates[3])}` + `
${path}
${displayValue} ${unit}
${new Date(coordinates[3])}` ) circleMarker.on('mouseover', e => { circleMarker.openPopup() diff --git a/src/client/provider-signalk-trackserver.js b/src/client/provider-signalk-trackserver.js index cf14603..c2aa273 100644 --- a/src/client/provider-signalk-trackserver.js +++ b/src/client/provider-signalk-trackserver.js @@ -1,16 +1,38 @@ +import Bacon from 'baconjs' + import api from './api' -function initialize({address}) { +function initialize ({ address }) { + const unitCache = {} return { - queryTracks + queryTracks, + getUnits } - function queryTracks(bbox) { + function queryTracks (bbox, paths) { return api.get({ - url: `${address}/signalk/v1/api/vessels/self/tracks?bbox=${bbox.toBBoxString()}&paths=navigation.speedOverGround,environmentWindSpeedTrue` + url: `${address}/signalk/v1/api/vessels/self/tracks?bbox=${bbox.toBBoxString()}&paths=${paths.join(',')}` }) } -} + function getUnits(path) { + const fromCache = unitCache[path] + if (fromCache === 'not_available') { + return Bacon.once(new Bacon.Error("Unit not available for" + path)) + } + return fromCache ? Bacon.once(fromCache) : getUnitsFromServer(path) + } + + function getUnitsFromServer(path) { + const result = api.get({url: `${address}/signalk/v1/api/vessels/self/${path.replace('.','/')}/meta`}).map(json => json.units) + result.onError(() => { + unitCache[path] = "not_available" + }) + result.onValue(unit => { + unitCache[path] = typeof unit != "undefined" ? unit : "not_available" + }) + return result + } +} module.exports = initialize