|
| 1 | +import {Deck} from "@deck.gl/core"; |
| 2 | +import {FlowmapLayer, PickingType} from '@flowmap.gl/layers'; |
| 3 | +import {getViewStateForLocations} from "@flowmap.gl/data"; |
| 4 | +import {csv} from "d3-fetch"; |
| 5 | +import atlas from "azure-maps-control"; |
| 6 | + |
| 7 | +let deck, locations, flows; |
| 8 | +let map; |
| 9 | + |
| 10 | +function onload () { |
| 11 | + //Fetch the data. |
| 12 | + fetchData().then((data) => { |
| 13 | + //Retrieve the locations and flows from the data. |
| 14 | + ({locations, flows} = data); |
| 15 | + const [width, height] = [globalThis.innerWidth, globalThis.innerHeight]; |
| 16 | + const initialViewState = getViewStateForLocations( |
| 17 | + locations, |
| 18 | + (loc) => [loc.lon, loc.lat], |
| 19 | + [width, height], |
| 20 | + {pad: 0.3} |
| 21 | + ); |
| 22 | + //Initialize a map instance. |
| 23 | + map = new atlas.Map("myMap", { |
| 24 | + style: 'grayscale_dark', |
| 25 | + interactive: false, |
| 26 | + //Deck.gl will be responsible for the interactivity of the map. |
| 27 | + center: [initialViewState.longitude, initialViewState.latitude], |
| 28 | + zoom: initialViewState.zoom, |
| 29 | + bearing: initialViewState.bearing, |
| 30 | + pitch: initialViewState.pitch, |
| 31 | + |
| 32 | + //Add authentication details for connecting to Azure Maps. |
| 33 | + authOptions: { |
| 34 | + //Use Microsoft Entra ID authentication. |
| 35 | + authType: 'anonymous', |
| 36 | + clientId: 'e6b6ab59-eb5d-4d25-aa57-581135b927f0', //Your Azure Maps client id for accessing your Azure Maps account. |
| 37 | + getToken: function (resolve, reject, map) { |
| 38 | + //URL to your authentication service that retrieves an Microsoft Entra ID Token. |
| 39 | + var tokenServiceUrl = 'https://samples.azuremaps.com/api/GetAzureMapsToken'; |
| 40 | + |
| 41 | + fetch(tokenServiceUrl).then(r => r.text()).then(token => resolve(token)); |
| 42 | + } |
| 43 | + |
| 44 | + //Alternatively, use an Azure Maps key. Get an Azure Maps key at https://azure.com/maps. NOTE: The primary key should be used as the key. |
| 45 | + //authType: 'subscriptionKey', |
| 46 | + //subscriptionKey: '[YOUR_AZURE_MAPS_KEY]' |
| 47 | + } |
| 48 | + }); |
| 49 | + |
| 50 | + //Add a style control to the map. |
| 51 | + map.events.add("ready", () => { |
| 52 | + map.controls.add(new atlas.control.StyleControl({ |
| 53 | + mapStyles: ['road', 'grayscale_light', 'grayscale_dark', 'night', 'satellite'], |
| 54 | + }), { |
| 55 | + position: "top-right", |
| 56 | + }); |
| 57 | + }); |
| 58 | + |
| 59 | + //Initialize a deck.gl instance. |
| 60 | + deck = new Deck({ |
| 61 | + canvas: "deck-canvas", |
| 62 | + width: "100%", |
| 63 | + height: "100%", |
| 64 | + initialViewState: initialViewState, |
| 65 | + controller: true, |
| 66 | + map: true, |
| 67 | + |
| 68 | + //Update the map view state when the deck.gl view state changes. |
| 69 | + onViewStateChange: ({viewState}) => { |
| 70 | + map.setCamera({ |
| 71 | + center: [viewState.longitude, viewState.latitude], |
| 72 | + zoom: viewState.zoom, |
| 73 | + bearing: viewState.bearing, |
| 74 | + pitch: viewState.pitch, |
| 75 | + }); |
| 76 | + }, |
| 77 | + layers: [], |
| 78 | + }); |
| 79 | + |
| 80 | + //Add the flowmap layer to the deck.gl instance. |
| 81 | + addLayer(); |
| 82 | + |
| 83 | + //Add event listeners to the controls on the side panel. |
| 84 | + document.querySelectorAll(".control").forEach((control) => { |
| 85 | + control.onchange = addLayer; |
| 86 | + }); |
| 87 | + document.getElementById("darkMode").onchange = onDarkModeChange; |
| 88 | + |
| 89 | + //Move the canvas under the map controls for better accessibility. |
| 90 | + const flowmap = document.getElementById("deck-canvas"); |
| 91 | + const mapContainer = document.getElementById("myMap"); |
| 92 | + mapContainer.insertBefore(flowmap, mapContainer.querySelector(".atlas-control-container")); |
| 93 | + }); |
| 94 | +} |
| 95 | + |
| 96 | +async function fetchData() { |
| 97 | + const [locations, flows] = await Promise.all([ |
| 98 | + csv('/data/flowmap/locations.csv', (row) => ({ |
| 99 | + id: row.id, |
| 100 | + name: row.name, |
| 101 | + lat: +row.lat, |
| 102 | + lon: +row.lon, |
| 103 | + })), |
| 104 | + csv('/data/flowmap/flows.csv', (row) => ({ |
| 105 | + origin: row.origin, |
| 106 | + dest: row.dest, |
| 107 | + count: +row.count, |
| 108 | + })), |
| 109 | + ]); |
| 110 | + return { locations, flows }; |
| 111 | +} |
| 112 | + |
| 113 | +function addLayer() { |
| 114 | + //Set up the flowmap layer whenever the selected options change. |
| 115 | + deck.setProps({ |
| 116 | + layers: [ |
| 117 | + new FlowmapLayer({ |
| 118 | + id: "my-flowmap-layer", |
| 119 | + data: {locations, flows}, |
| 120 | + pickable: true, |
| 121 | + darkMode: getIsChecked("darkMode"), |
| 122 | + colorScheme: getSelectValue("colorScheme"), |
| 123 | + clusteringEnabled: getIsChecked("clusteringEnabled"), |
| 124 | + getLocationId: (loc) => loc.id, |
| 125 | + getLocationLat: (loc) => loc.lat, |
| 126 | + getLocationLon: (loc) => loc.lon, |
| 127 | + getFlowOriginId: (flow) => flow.origin, |
| 128 | + getFlowDestId: (flow) => flow.dest, |
| 129 | + getFlowMagnitude: (flow) => flow.count, |
| 130 | + getLocationName: (loc) => loc.name, |
| 131 | + onHover: (info) => updateTooltip(getTooltipState(info)), |
| 132 | + }), |
| 133 | + ], |
| 134 | + }); |
| 135 | +} |
| 136 | + |
| 137 | +function onDarkModeChange() { |
| 138 | + //Update the map style for better visibility in different modes. |
| 139 | + map.setStyle({style: getIsChecked("darkMode") ? "grayscale_dark" : "grayscale_light"}); |
| 140 | + document.getElementById("deck-canvas").style.mixBlendMode = getIsChecked("darkMode") ? "screen" : "darken"; |
| 141 | + document.getElementById("container").style.backgroundColor = getIsChecked("darkMode") ? "#000" : "#fff"; |
| 142 | + addLayer(); |
| 143 | +} |
| 144 | + |
| 145 | +function updateTooltip(state) { |
| 146 | + const tooltip = document.getElementById("tooltip"); |
| 147 | + if (!state) { |
| 148 | + tooltip.style.display = "none"; |
| 149 | + return; |
| 150 | + } |
| 151 | + tooltip.style.left = `${state.position.left}px`; |
| 152 | + tooltip.style.top = `${state.position.top}px` |
| 153 | + tooltip.innerHTML = state.content; |
| 154 | + tooltip.style.display = "block"; |
| 155 | +} |
| 156 | + |
| 157 | +//Generate the postion and content of the tooltip. |
| 158 | +function getTooltipState(info) { |
| 159 | + if (!info) return undefined; |
| 160 | + |
| 161 | + const {x, y, object} = info; |
| 162 | + const position = {left: x, top: y}; |
| 163 | + switch (object?.type) { |
| 164 | + case PickingType.LOCATION: |
| 165 | + const nameElm = document.createElement("b"); |
| 166 | + nameElm.innerText = object.name.replaceAll("\"", ""); |
| 167 | + const incomingElm = document.createElement("li"); |
| 168 | + incomingElm.innerText = `Incoming trips: ${object.totals.incomingCount}`; |
| 169 | + const outgoingElm = document.createElement("li"); |
| 170 | + outgoingElm.innerText = `Outgoing trips: ${object.totals.outgoingCount}`; |
| 171 | + const internalElm = document.createElement("li"); |
| 172 | + internalElm.innerText = `Internal or round trips: ${object.totals.internalCount}`; |
| 173 | + return { |
| 174 | + position, |
| 175 | + content: [nameElm, incomingElm, outgoingElm, internalElm].map((elm) => elm.outerHTML).join(""), |
| 176 | + }; |
| 177 | + case PickingType.FLOW: |
| 178 | + const titleElm = document.createElement("b"); |
| 179 | + titleElm.innerText = "Route Info"; |
| 180 | + const routeElm = document.createElement("li"); |
| 181 | + routeElm.innerText = `Bike Station: ${object.origin.id.slice(2, -2)} → ${object.dest.id.slice(2, -2)}`; |
| 182 | + const countElm = document.createElement("li"); |
| 183 | + countElm.innerText = `Trips Count: ${object.count}`; |
| 184 | + return { |
| 185 | + position, |
| 186 | + content: [titleElm, routeElm, countElm].map((elm) => elm.outerHTML).join(""), |
| 187 | + }; |
| 188 | + } |
| 189 | + return undefined; |
| 190 | +} |
| 191 | + |
| 192 | +function getSelectValue(id) { |
| 193 | + var elm = document.getElementById(id); |
| 194 | + return elm.options[elm.selectedIndex].value; |
| 195 | +} |
| 196 | +function getIsChecked(id) { |
| 197 | + return document.getElementById(id).checked; |
| 198 | +} |
| 199 | + |
| 200 | +//Initialize when the page loads. |
| 201 | +document.body.onload = onload; |
0 commit comments