├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── model └── match.json ├── package.json ├── samples ├── honolulu.json └── honolulu.osm.pbf ├── src ├── debug.js ├── index.js └── normalizer.js ├── test └── index.test.js ├── train ├── evaluate.js ├── match-cache.js ├── match-train.js └── match.js └── utils ├── debugger.html ├── debugger.js ├── demo.js ├── generate-fixture.js ├── graph.html ├── graph.js ├── map.js └── merge-demo.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "block-scoped-var": 2, 4 | "comma-dangle": [2, "only-multiline"], 5 | "comma-style": 2, 6 | "indent": [2, 2], 7 | "keyword-spacing": 2, 8 | "no-console": 0, 9 | "no-constant-condition": 0, 10 | "no-else-return": 0, 11 | "no-extra-parens": [2, "functions"], 12 | "no-lonely-if": 2, 13 | "no-new": 2, 14 | "no-proto": 2, 15 | "no-unused-vars": [2,{"args": "none"}], 16 | "no-use-before-define": [2,"nofunc"], 17 | "no-useless-escape": 0, 18 | "space-before-blocks": 2, 19 | "space-before-function-paren": [2, "never"], 20 | "space-in-parens": 2, 21 | "require-jsdoc": ["warn", { 22 | "require": { 23 | "FunctionExpression": true 24 | } 25 | }], 26 | "valid-jsdoc": ["warn", { 27 | "requireReturn": false 28 | }] 29 | }, 30 | "env": { 31 | "node": "true", 32 | "es6": "true" 33 | }, 34 | "extends": ["@mapbox/eslint-config-geocoding"] 35 | } 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | package-lock.json 64 | nc.osm.pbf 65 | nc.json 66 | ncdot.json 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 SharedStreets 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [WIP] mashnet 2 | --- 3 | 4 | - [Overview](https://github.com/sharedstreets/mashnet#overview) 5 | - [API](https://github.com/sharedstreets/mashnet#API) 6 | - [Model](https://github.com/sharedstreets/mashnet#model) 7 | - [Workflow](https://github.com/sharedstreets/mashnet#workflow) 8 | - [Actions](https://github.com/sharedstreets/mashnet#actions) 9 | - [Misc](https://github.com/sharedstreets/mashnet#misc) 10 | - [Install](https://github.com/sharedstreets/mashnet#install) 11 | - [Test](https://github.com/sharedstreets/mashnet#test) 12 | - [Coverage](https://github.com/sharedstreets/mashnet#coverage) 13 | - [Lint](https://github.com/sharedstreets/mashnet#lint) 14 | - [Train](https://github.com/sharedstreets/mashnet#traine) 15 | 16 | --- 17 | 18 | ## Overview 19 | 20 | `Mashnet` is a street network conflation library, used to merge road graphs for mapping and routing. It is designed to work with both human mapped data and ML derived networks, aiming for clean and consistent merging, even with disparate input datasets. Use `mashnet` to detect missing edges in the road graph, and enhance existing edges with new metadata. 21 | 22 | _Example of merging 3 road networks into a single, routable network:_ 23 | 24 | ![](https://i.imgur.com/ihvsQZR.jpg) 25 | 26 | ## Stability 27 | 28 | Mashnet is under active development. It is usable in its current form, and the overall workflow is established, however, the API is subject to change rapidly until further notice. 29 | 30 | ## API 31 | 32 | ### new 33 | 34 | The `mashnet` constructor is used to instantiate a new network. An optional path may be provided to an existing serialized road graph. 35 | 36 | ```js 37 | const Mashnet = require('mashnet') 38 | 39 | const net = new Mashnet('./honolulu.json') 40 | ``` 41 | 42 | ### scan 43 | 44 | Scan takes a proposed street and returns a list of similar edges in the existing graph. The edge list is ranked by similarity score. 45 | 46 | ```js 47 | const street = { 48 | type: "Feature", 49 | properties: {}, 50 | geometry: { 51 | type: "LineString", 52 | coordinates: [ 53 | [-157.9146158695221, 21.346424354025306], 54 | [-157.9154634475708, 21.347043906401122], 55 | [-157.9165470600128, 21.348442886005444] 56 | ] 57 | } 58 | } 59 | 60 | const scores = net.scan(street) 61 | ``` 62 | 63 | ### match 64 | 65 | Match takes a list of edge scores and returns a confidence score that the top ranked edge represents the same street as the proposed input street. 66 | 67 | ```js 68 | const isMatch = net.match(scores) 69 | ``` 70 | 71 | ### merge 72 | 73 | Merge accepts an existing metadata Id and a new set of metadata. The function will add or overwrite any new metadata to the existing blob. This function should be performed when a likely match is detected. 74 | 75 | ```js 76 | const metadata = { 77 | highway: "motorway", 78 | surface: "asphalt", 79 | max_speed: 70 80 | } 81 | const isMatch = net.match(scores) 82 | if (isMatch > 0.95) { 83 | net.merge(scores[0].id, metadata) 84 | } 85 | ``` 86 | 87 | ### query 88 | 89 | Query the graph by a bbox. Returns a subgraph of nodes, vertices, and edges. Used internally for isolating graph partitions, but also useful for analyzing areas of interest. 90 | 91 | ```js 92 | const subgraph = net.query([minX, minY, maxX, maxY]); 93 | ``` 94 | 95 | ### snap 96 | 97 | Accepts a proposed street and returns a set of "snaps" to the existing road network. Each snap of the proposed street will include information about nearby nodes, vertices, and edges. 98 | 99 | ```js 100 | const snaps = net.snap(street); 101 | ``` 102 | 103 | ### split 104 | 105 | Accepts a set of snaps, and creates a set of logical changeset chunks. Some of these chunks will be new edges and some will be existing edges, indicated by the presence of void snaps. 106 | 107 | ```js 108 | const splits = net.split(snaps); 109 | ``` 110 | 111 | ### materialize 112 | 113 | Accepts a set of changeset splits, and returns an array of GeoJSON LineStrings, which can be used to visualize the changesets. 114 | 115 | ```js 116 | const lines = net.materialize(splits); 117 | ``` 118 | 119 | ### commit 120 | 121 | Accepts a set of changeset splits, and commits them to the graph. Optionally accepts metadata that can be merged into matching pre-existing edges. 122 | 123 | ```js 124 | net.commit(splits, metadata); 125 | ``` 126 | 127 | ### propose 128 | 129 | Accepts a proposed street, and returns a set of changeset splits. This function is a wrapper of `snap` + `split`. These splits are not applied to the graph automatically with the `propose` function, allowing them to be visualized or fed to a task manager before modifying the network. 130 | 131 | ```js 132 | const street = { 133 | type: "Feature", 134 | properties: { 135 | "max_speed": 30 136 | }, 137 | geometry: { 138 | type: "LineString", 139 | coordinates: [ 140 | [-157.9146158695221, 21.346424354025306], 141 | [-157.9154634475708, 21.347043906401122], 142 | [-157.9165470600128, 21.348442886005444] 143 | ] 144 | } 145 | } 146 | 147 | net.propose(street) 148 | ``` 149 | 150 | ### apply 151 | 152 | Apply accepts a new street represented as a GeoJSON LineString Feature with properties representing a metadata blob. The apply function will merge metadata and insert new edges where detected. 153 | 154 | ```js 155 | const street = { 156 | type: "Feature", 157 | properties: { 158 | "max_speed": 30 159 | }, 160 | geometry: { 161 | type: "LineString", 162 | coordinates: [ 163 | [-157.9146158695221, 21.346424354025306], 164 | [-157.9154634475708, 21.347043906401122], 165 | [-157.9165470600128, 21.348442886005444] 166 | ] 167 | } 168 | } 169 | 170 | net.apply(street) 171 | ``` 172 | 173 | 174 | ### toJSON 175 | 176 | Serializes the loaded graph to a JSON format that can be transferred or stored to disk. 177 | 178 | ```js 179 | const data = net.toJSON() 180 | fs.writeFileSync('honolulu.json', JSON.stringify(data)) 181 | ``` 182 | 183 | ### fromJSON 184 | 185 | Loads a JSON representation of a street network into memory for performing operations. This can also be accomplished using the `mashnet` constructor. 186 | 187 | ```js 188 | const honolulu = require('honolulu.json') 189 | net.fromJSON(honolulu) 190 | ``` 191 | 192 | ## Model 193 | 194 | Many types of geospatial data come in the form of geometry. Street networks are a special case of geospatial data that benefits from a graph data structure. This graph structure is an efficient representation that allows for links between features to be conveyed. For conflation, this is especially important, since adding streets to an existing network can cause changes to the network, such as splitting a street or inserting an intersection. 195 | 196 | - edge 197 | - id (unique) 198 | - list of vertex ids 199 | - vertex 200 | - id (unique) 201 | - x coordinate 202 | - y coordinate 203 | - node 204 | - id (matches unique vertex id) 205 | - list of connected edge ids 206 | - metadata 207 | - id (matches unique edge id) 208 | - json blob 209 | - nodetree 210 | - RTree of nodes for quick scans 211 | - edgetree 212 | - RTree of edges for quick scans 213 | 214 | ## Workflow 215 | 216 | A conflation network can be created from scratch or with a bootstrapped graph from an existing network, such as OpenStreetMap or any other basemap that contains topological road links. After bootstrapping, new data is merged in iteratively, road by road. When adding a new street, we first look for an existing duplicate street. If one is found, the new street will be merged into the existing edge. If a match is not found, a new edge will be created and inserted into the graph. 217 | 218 | ## Actions 219 | 220 | - *constructor* 221 | - initialize an existing graph database or create a new one 222 | - *normalize* 223 | - identify intersection nodes 224 | - split edges crossing intersections 225 | - merge redundant edges 226 | - *match* 227 | - looks for a matching edge in the graph 228 | - returns an ID with a confidence score 229 | - uses a trained classifier 230 | - inputs 231 | - quadkey haversine score of line 232 | - quadkey haversine score of buffered west-most node 233 | - quadkey haversine score of buffered east-most node 234 | - curve score 235 | - linear distance 236 | - inputs are normalized from pre-computed planet wide scan of extremes for each heuristic 237 | - *add* 238 | - attempts to add a street to the road graph 239 | - looks for a matching existing edge 240 | - if found, merge metadata into existing edge 241 | - looks for intersections splitting proposed edge 242 | - if found, use use existing nodes or create new nodes 243 | - add new vertices and edges 244 | - re-normalize graph (possibly not needed) 245 | - add may fail, in which case it will remain pending 246 | - *merge* 247 | - combine two metadata sets 248 | - if not present on match, add new metadata 249 | - if present, follow merge strategy (do nothing, use new, numeric average, etc) 250 | - merge may fail, in which case it will remain pending 251 | 252 | 253 | ## Install 254 | 255 | ```sh 256 | npm i mashnet 257 | ``` 258 | 259 | ## Test 260 | 261 | ```sh 262 | npm t 263 | ``` 264 | 265 | ## Coverage 266 | 267 | ```sh 268 | npm run coverage 269 | ``` 270 | 271 | ## Lint 272 | 273 | Runs a linter across codebase and automatically formats all code using [prettier](https://prettier.io). 274 | 275 | ```sh 276 | npm run lint 277 | ``` 278 | 279 | ## Train 280 | 281 | A pre-trained neural network is included with `mashnet`. A new network can be trained with custom parameters or training data. 282 | 283 | ```sh 284 | npm run train 285 | ``` 286 | -------------------------------------------------------------------------------- /model/match.json: -------------------------------------------------------------------------------- 1 | {"sizes":[35,17,1],"layers":[{"distance_0":{},"scale_0":{},"straight_0":{},"curve_0":{},"scan_0":{},"terminal_0":{},"bearing_0":{},"distance_1":{},"scale_1":{},"straight_1":{},"curve_1":{},"scan_1":{},"terminal_1":{},"bearing_1":{},"distance_2":{},"scale_2":{},"straight_2":{},"curve_2":{},"scan_2":{},"terminal_2":{},"bearing_2":{},"distance_3":{},"scale_3":{},"straight_3":{},"curve_3":{},"scan_3":{},"terminal_3":{},"bearing_3":{},"distance_4":{},"scale_4":{},"straight_4":{},"curve_4":{},"scan_4":{},"terminal_4":{},"bearing_4":{}},{"0":{"bias":10.654808044433594,"weights":{"distance_0":-5.2737836837768555,"scale_0":0.15625667572021484,"straight_0":4.02732515335083,"curve_0":-2.634652614593506,"scan_0":-4.785482406616211,"terminal_0":-0.22554391622543335,"bearing_0":-7.0920891761779785,"distance_1":0.8630256652832031,"scale_1":0.028496354818344116,"straight_1":4.135848045349121,"curve_1":-3.8229405879974365,"scan_1":5.870483875274658,"terminal_1":2.052196502685547,"bearing_1":10.491277694702148,"distance_2":-3.076021432876587,"scale_2":0.04735679179430008,"straight_2":-1.948622226715088,"curve_2":-1.663529872894287,"scan_2":-3.352985382080078,"terminal_2":-0.16975554823875427,"bearing_2":6.218719959259033,"distance_3":-0.9247734546661377,"scale_3":-0.04070206359028816,"straight_3":0.9795641303062439,"curve_3":-2.663141965866089,"scan_3":-2.9560701847076416,"terminal_3":-2.2259557247161865,"bearing_3":0.7001703381538391,"distance_4":-2.1087710857391357,"scale_4":-0.29084035754203796,"straight_4":-1.4386621713638306,"curve_4":0.6267020106315613,"scan_4":-4.535634517669678,"terminal_4":-3.8816587924957275,"bearing_4":-2.471040964126587}},"1":{"bias":-19.24310874938965,"weights":{"distance_0":8.251943588256836,"scale_0":-0.19216912984848022,"straight_0":-2.449270486831665,"curve_0":0.9692084789276123,"scan_0":13.18887710571289,"terminal_0":3.1534361839294434,"bearing_0":7.808324337005615,"distance_1":0.4605500400066376,"scale_1":-0.2829706072807312,"straight_1":0.8719379901885986,"curve_1":-0.9994726181030273,"scan_1":1.7389923334121704,"terminal_1":3.168353796005249,"bearing_1":-0.8855516314506531,"distance_2":-1.189922571182251,"scale_2":-0.23524639010429382,"straight_2":-2.5974538326263428,"curve_2":-2.7492733001708984,"scan_2":2.7823123931884766,"terminal_2":2.9803571701049805,"bearing_2":-7.23396635055542,"distance_3":0.2115936428308487,"scale_3":-0.2687937319278717,"straight_3":-1.094118356704712,"curve_3":-3.3945815563201904,"scan_3":2.373504161834717,"terminal_3":4.005021095275879,"bearing_3":0.5581567883491516,"distance_4":-1.5231040716171265,"scale_4":0.28477832674980164,"straight_4":2.494570732116699,"curve_4":-2.6111931800842285,"scan_4":2.408303737640381,"terminal_4":0.24473178386688232,"bearing_4":-1.2885675430297852}},"2":{"bias":-15.358087539672852,"weights":{"distance_0":9.187307357788086,"scale_0":-0.28572145104408264,"straight_0":1.663403034210205,"curve_0":1.9456827640533447,"scan_0":9.755621910095215,"terminal_0":2.035676956176758,"bearing_0":13.405447959899902,"distance_1":2.777714490890503,"scale_1":-0.3738133907318115,"straight_1":2.003175735473633,"curve_1":-3.0183253288269043,"scan_1":-1.2469689846038818,"terminal_1":2.0576586723327637,"bearing_1":-0.3159773051738739,"distance_2":4.655961990356445,"scale_2":-0.4017702639102936,"straight_2":4.286219596862793,"curve_2":-1.9328323602676392,"scan_2":-1.232725739479065,"terminal_2":2.3886878490448,"bearing_2":-4.7612152099609375,"distance_3":-0.6455512642860413,"scale_3":-0.30136194825172424,"straight_3":-5.637510776519775,"curve_3":-0.5479309558868408,"scan_3":-2.796229362487793,"terminal_3":0.3673180639743805,"bearing_3":-10.629416465759277,"distance_4":-1.1150470972061157,"scale_4":-0.10592781752347946,"straight_4":-0.8862064480781555,"curve_4":-3.833738327026367,"scan_4":-7.442777633666992,"terminal_4":-5.933203220367432,"bearing_4":0.038735102862119675}},"3":{"bias":9.653352737426758,"weights":{"distance_0":-3.771345853805542,"scale_0":-0.04489218816161156,"straight_0":-0.720495343208313,"curve_0":1.2322726249694824,"scan_0":-6.02598762512207,"terminal_0":-0.6933428645133972,"bearing_0":-2.561366081237793,"distance_1":-0.6164518594741821,"scale_1":0.08230756968259811,"straight_1":-0.7034209966659546,"curve_1":1.6194078922271729,"scan_1":2.0187880992889404,"terminal_1":1.249560832977295,"bearing_1":1.1467994451522827,"distance_2":-5.323920726776123,"scale_2":-0.06312616914510727,"straight_2":-4.545108318328857,"curve_2":3.9277284145355225,"scan_2":-2.8612451553344727,"terminal_2":-2.3483612537384033,"bearing_2":-4.517178535461426,"distance_3":1.7926510572433472,"scale_3":0.060084566473960876,"straight_3":4.476073741912842,"curve_3":-0.24978087842464447,"scan_3":0.9652158617973328,"terminal_3":-0.35408109426498413,"bearing_3":-2.001859426498413,"distance_4":4.152239799499512,"scale_4":0.25131312012672424,"straight_4":2.968977451324463,"curve_4":-3.5939340591430664,"scan_4":-2.175503730773926,"terminal_4":0.8422044515609741,"bearing_4":-0.44989511370658875}},"4":{"bias":16.253564834594727,"weights":{"distance_0":-8.553736686706543,"scale_0":-0.048328105360269547,"straight_0":1.6208388805389404,"curve_0":-0.7432874441146851,"scan_0":-14.25271224975586,"terminal_0":-1.5245335102081299,"bearing_0":4.983911514282227,"distance_1":-1.2427079677581787,"scale_1":0.22178752720355988,"straight_1":-1.2096060514450073,"curve_1":3.890404462814331,"scan_1":0.05413789674639702,"terminal_1":2.3175463676452637,"bearing_1":-4.6814470291137695,"distance_2":1.271388053894043,"scale_2":-0.0023205075412988663,"straight_2":0.10738412290811539,"curve_2":1.202338695526123,"scan_2":-1.2350631952285767,"terminal_2":-0.01779519021511078,"bearing_2":-4.046361923217773,"distance_3":-1.6732107400894165,"scale_3":0.22997654974460602,"straight_3":0.8946317434310913,"curve_3":1.6356964111328125,"scan_3":-1.5613071918487549,"terminal_3":-0.6301916837692261,"bearing_3":-0.038171615451574326,"distance_4":1.0382710695266724,"scale_4":0.0239713117480278,"straight_4":0.607716977596283,"curve_4":-1.7019996643066406,"scan_4":-0.48754826188087463,"terminal_4":0.6955585479736328,"bearing_4":-2.81335186958313}},"5":{"bias":19.737817764282227,"weights":{"distance_0":-6.029700756072998,"scale_0":0.02916734665632248,"straight_0":8.560383796691895,"curve_0":-2.710430860519409,"scan_0":-19.707853317260742,"terminal_0":-7.150648593902588,"bearing_0":-3.743760585784912,"distance_1":2.332930326461792,"scale_1":0.09849914908409119,"straight_1":4.450944423675537,"curve_1":3.447352886199951,"scan_1":1.0499664545059204,"terminal_1":-3.8150076866149902,"bearing_1":-6.91535758972168,"distance_2":-0.00243494869209826,"scale_2":-0.01155516691505909,"straight_2":0.9217659831047058,"curve_2":3.031029224395752,"scan_2":-0.47437548637390137,"terminal_2":-0.03531813248991966,"bearing_2":0.21631133556365967,"distance_3":0.24831508100032806,"scale_3":0.316923052072525,"straight_3":4.4745612144470215,"curve_3":1.8054730892181396,"scan_3":-0.32045885920524597,"terminal_3":1.7774507999420166,"bearing_3":-3.0729293823242188,"distance_4":-3.5884652137756348,"scale_4":0.16693352162837982,"straight_4":-6.0231122970581055,"curve_4":6.244856357574463,"scan_4":-0.2522968351840973,"terminal_4":-3.649946689605713,"bearing_4":-0.2156863808631897}},"6":{"bias":12.63586711883545,"weights":{"distance_0":-5.471111297607422,"scale_0":0.23869171738624573,"straight_0":4.1878461837768555,"curve_0":-1.8683204650878906,"scan_0":-9.650993347167969,"terminal_0":0.6494702100753784,"bearing_0":-2.862337350845337,"distance_1":-0.5290247201919556,"scale_1":0.20036867260932922,"straight_1":-0.6405080556869507,"curve_1":1.719667911529541,"scan_1":1.8523660898208618,"terminal_1":2.0965957641601562,"bearing_1":-1.80104660987854,"distance_2":1.0361164808273315,"scale_2":0.04367641359567642,"straight_2":0.4028834104537964,"curve_2":1.5746020078659058,"scan_2":-0.8828330636024475,"terminal_2":-1.6528103351593018,"bearing_2":-0.9314168691635132,"distance_3":-3.3807945251464844,"scale_3":-0.13396698236465454,"straight_3":-1.153351068496704,"curve_3":-1.7203086614608765,"scan_3":0.9429038166999817,"terminal_3":-0.021296091377735138,"bearing_3":1.4552390575408936,"distance_4":4.220030307769775,"scale_4":-0.06603638082742691,"straight_4":0.6509942412376404,"curve_4":-1.731520652770996,"scan_4":-3.077302932739258,"terminal_4":-2.0566046237945557,"bearing_4":-0.6162313222885132}},"7":{"bias":20.19385528564453,"weights":{"distance_0":-15.007790565490723,"scale_0":0.26860639452934265,"straight_0":2.036139726638794,"curve_0":-5.338533878326416,"scan_0":-6.6996684074401855,"terminal_0":1.636144995689392,"bearing_0":-0.8185803890228271,"distance_1":0.832276463508606,"scale_1":0.2628750205039978,"straight_1":0.3969458043575287,"curve_1":-1.4731507301330566,"scan_1":-2.246849536895752,"terminal_1":-3.1319475173950195,"bearing_1":4.740230560302734,"distance_2":0.6154060363769531,"scale_2":0.31674662232398987,"straight_2":3.3117260932922363,"curve_2":-3.2770237922668457,"scan_2":-3.1593549251556396,"terminal_2":2.1604135036468506,"bearing_2":-1.8025928735733032,"distance_3":-0.7158182859420776,"scale_3":0.2415546327829361,"straight_3":0.06888000667095184,"curve_3":1.8892061710357666,"scan_3":-4.612534523010254,"terminal_3":2.960144519805908,"bearing_3":-0.730450451374054,"distance_4":-2.0042107105255127,"scale_4":0.1178748831152916,"straight_4":0.9257983565330505,"curve_4":1.8517100811004639,"scan_4":-1.5806211233139038,"terminal_4":1.8566782474517822,"bearing_4":-1.5501019954681396}},"8":{"bias":16.011260986328125,"weights":{"distance_0":-9.477177619934082,"scale_0":-0.015657704323530197,"straight_0":4.389458656311035,"curve_0":-3.0499842166900635,"scan_0":-9.413481712341309,"terminal_0":-1.8300052881240845,"bearing_0":2.8439786434173584,"distance_1":-2.2069458961486816,"scale_1":-0.04977182671427727,"straight_1":0.28429391980171204,"curve_1":-3.615176200866699,"scan_1":-3.0977582931518555,"terminal_1":1.247079610824585,"bearing_1":8.872220993041992,"distance_2":-0.7267959713935852,"scale_2":-0.10277555882930756,"straight_2":1.3575446605682373,"curve_2":-4.434599876403809,"scan_2":3.0438544750213623,"terminal_2":5.132152080535889,"bearing_2":5.174840450286865,"distance_3":-0.8180188536643982,"scale_3":-0.02769090235233307,"straight_3":-0.47820112109184265,"curve_3":3.4156696796417236,"scan_3":1.424704670906067,"terminal_3":0.5915356874465942,"bearing_3":-5.400853157043457,"distance_4":-0.25980809330940247,"scale_4":-0.10733263939619064,"straight_4":-0.9530190825462341,"curve_4":-2.92988657951355,"scan_4":-2.232224464416504,"terminal_4":-1.943473219871521,"bearing_4":-6.841097354888916}},"9":{"bias":11.06243896484375,"weights":{"distance_0":-2.6634016036987305,"scale_0":0.27541956305503845,"straight_0":3.542184352874756,"curve_0":1.1990270614624023,"scan_0":-14.85367488861084,"terminal_0":-7.684682369232178,"bearing_0":-9.836318969726562,"distance_1":0.7260164618492126,"scale_1":0.2913261651992798,"straight_1":0.21271081268787384,"curve_1":4.724925518035889,"scan_1":-4.103180408477783,"terminal_1":-4.790255069732666,"bearing_1":-3.766237735748291,"distance_2":-1.5531792640686035,"scale_2":0.1688668429851532,"straight_2":0.3903370797634125,"curve_2":3.077491044998169,"scan_2":-1.7361979484558105,"terminal_2":-2.898926019668579,"bearing_2":1.6168334484100342,"distance_3":-6.581395149230957,"scale_3":0.04616982862353325,"straight_3":1.6383408308029175,"curve_3":-3.1469430923461914,"scan_3":3.5080339908599854,"terminal_3":1.3509513139724731,"bearing_3":4.640654563903809,"distance_4":6.244152545928955,"scale_4":-0.0048057506792247295,"straight_4":4.420989990234375,"curve_4":1.7592960596084595,"scan_4":3.1574196815490723,"terminal_4":3.9823200702667236,"bearing_4":3.1980319023132324}},"10":{"bias":-9.635952949523926,"weights":{"distance_0":2.0296647548675537,"scale_0":-0.20383793115615845,"straight_0":-3.809995651245117,"curve_0":0.01568596623837948,"scan_0":6.250181674957275,"terminal_0":1.903968095779419,"bearing_0":2.3188204765319824,"distance_1":0.08437924087047577,"scale_1":-0.046555787324905396,"straight_1":-0.3041422367095947,"curve_1":-2.4201548099517822,"scan_1":0.31801509857177734,"terminal_1":0.609131395816803,"bearing_1":6.18058967590332,"distance_2":-1.5994585752487183,"scale_2":0.0674768015742302,"straight_2":-3.831348180770874,"curve_2":0.2156226485967636,"scan_2":3.7451117038726807,"terminal_2":2.606553077697754,"bearing_2":4.9043354988098145,"distance_3":2.347710609436035,"scale_3":-0.29377320408821106,"straight_3":-0.04131323844194412,"curve_3":-0.9275931715965271,"scan_3":-3.265444755554199,"terminal_3":-2.436305046081543,"bearing_3":-3.895256757736206,"distance_4":-1.9862021207809448,"scale_4":0.1380782574415207,"straight_4":-1.7317070960998535,"curve_4":2.3550853729248047,"scan_4":-2.453124523162842,"terminal_4":-2.189260244369507,"bearing_4":1.464221715927124}},"11":{"bias":12.145480155944824,"weights":{"distance_0":-4.712985992431641,"scale_0":0.327621191740036,"straight_0":1.5590344667434692,"curve_0":0.69497150182724,"scan_0":-14.741880416870117,"terminal_0":-4.7401227951049805,"bearing_0":-5.382319450378418,"distance_1":-0.0976882204413414,"scale_1":0.1712522804737091,"straight_1":-1.3767873048782349,"curve_1":3.963719606399536,"scan_1":3.8621950149536133,"terminal_1":4.601659774780273,"bearing_1":4.619938373565674,"distance_2":-1.470534086227417,"scale_2":0.09321615844964981,"straight_2":1.2162327766418457,"curve_2":-1.925243854522705,"scan_2":2.8347012996673584,"terminal_2":4.681092262268066,"bearing_2":-0.7787497639656067,"distance_3":-0.5477643013000488,"scale_3":0.2921687066555023,"straight_3":0.8618690371513367,"curve_3":0.2569999098777771,"scan_3":-0.9973241686820984,"terminal_3":-0.8843929767608643,"bearing_3":3.126303195953369,"distance_4":2.804028272628784,"scale_4":0.05406634509563446,"straight_4":4.0903730392456055,"curve_4":-6.832258701324463,"scan_4":-0.9062598943710327,"terminal_4":-0.23978176712989807,"bearing_4":-1.0103380680084229}},"12":{"bias":-12.036700248718262,"weights":{"distance_0":11.81360149383545,"scale_0":-0.3117064833641052,"straight_0":6.7851643562316895,"curve_0":-1.9245439767837524,"scan_0":5.013635635375977,"terminal_0":2.6158487796783447,"bearing_0":4.977640151977539,"distance_1":-0.19027598202228546,"scale_1":-0.2932719588279724,"straight_1":1.1793087720870972,"curve_1":-3.633917808532715,"scan_1":-1.2579190731048584,"terminal_1":0.8605019450187683,"bearing_1":-0.20250505208969116,"distance_2":2.50514817237854,"scale_2":-0.16249997913837433,"straight_2":-1.64665687084198,"curve_2":-1.5496225357055664,"scan_2":-10.165485382080078,"terminal_2":-2.2124407291412354,"bearing_2":1.3039405345916748,"distance_3":3.3089187145233154,"scale_3":-0.195026695728302,"straight_3":3.5174856185913086,"curve_3":-1.3296431303024292,"scan_3":-3.492725372314453,"terminal_3":-1.035532832145691,"bearing_3":-8.262060165405273,"distance_4":2.3951916694641113,"scale_4":0.010254115797579288,"straight_4":5.127528190612793,"curve_4":-1.2613542079925537,"scan_4":2.74349308013916,"terminal_4":0.77898770570755,"bearing_4":-4.909964084625244}},"13":{"bias":-18.041784286499023,"weights":{"distance_0":11.986117362976074,"scale_0":-0.3828429579734802,"straight_0":0.3620217442512512,"curve_0":-0.05708197131752968,"scan_0":13.810517311096191,"terminal_0":3.6334781646728516,"bearing_0":7.7014288902282715,"distance_1":-1.4859960079193115,"scale_1":-0.4715062081813812,"straight_1":-2.4715912342071533,"curve_1":-3.9565670490264893,"scan_1":-1.7465561628341675,"terminal_1":2.857861042022705,"bearing_1":-0.29983261227607727,"distance_2":4.627782821655273,"scale_2":-0.4345766007900238,"straight_2":4.447288990020752,"curve_2":-3.3081130981445312,"scan_2":-1.1593796014785767,"terminal_2":1.8875504732131958,"bearing_2":2.2139036655426025,"distance_3":0.8469338417053223,"scale_3":-0.3820268511772156,"straight_3":-2.9245283603668213,"curve_3":-2.5477848052978516,"scan_3":-2.472529888153076,"terminal_3":2.139995574951172,"bearing_3":3.4871296882629395,"distance_4":-1.5174469947814941,"scale_4":-0.14913330972194672,"straight_4":-2.520026683807373,"curve_4":-4.339496612548828,"scan_4":-6.790702819824219,"terminal_4":-3.145195484161377,"bearing_4":-5.697997570037842}},"14":{"bias":10.946200370788574,"weights":{"distance_0":-1.5493013858795166,"scale_0":-0.053408507257699966,"straight_0":4.806632041931152,"curve_0":-0.06781714409589767,"scan_0":-10.18528938293457,"terminal_0":-1.9684066772460938,"bearing_0":-1.9333994388580322,"distance_1":1.1959590911865234,"scale_1":0.22145044803619385,"straight_1":1.7876633405685425,"curve_1":0.1876567304134369,"scan_1":2.4739980697631836,"terminal_1":2.028968334197998,"bearing_1":-1.6613147258758545,"distance_2":0.06934838742017746,"scale_2":0.022859511896967888,"straight_2":-0.7130894064903259,"curve_2":0.8262390494346619,"scan_2":-0.7936692237854004,"terminal_2":-0.7048054933547974,"bearing_2":0.7940624952316284,"distance_3":-1.0348005294799805,"scale_3":-0.11888532340526581,"straight_3":0.38674864172935486,"curve_3":-1.2852858304977417,"scan_3":3.5932254791259766,"terminal_3":0.33473047614097595,"bearing_3":-4.920970439910889,"distance_4":-0.3494790196418762,"scale_4":0.042660780251026154,"straight_4":-3.152634620666504,"curve_4":-2.208855152130127,"scan_4":-0.9529023766517639,"terminal_4":-0.8387234210968018,"bearing_4":0.40220150351524353}},"15":{"bias":12.905449867248535,"weights":{"distance_0":-3.6164681911468506,"scale_0":0.21939754486083984,"straight_0":5.156559467315674,"curve_0":-1.971106767654419,"scan_0":-5.174827575683594,"terminal_0":1.7641788721084595,"bearing_0":-7.494826793670654,"distance_1":-1.825096607208252,"scale_1":0.003885151818394661,"straight_1":-3.706677198410034,"curve_1":2.04634690284729,"scan_1":-0.7106528878211975,"terminal_1":-1.9221283197402954,"bearing_1":-1.3966636657714844,"distance_2":2.2219834327697754,"scale_2":0.18190814554691315,"straight_2":0.6433021426200867,"curve_2":0.5323615074157715,"scan_2":0.508189857006073,"terminal_2":-1.6975452899932861,"bearing_2":-4.692754745483398,"distance_3":0.4565720558166504,"scale_3":-0.054941512644290924,"straight_3":0.3974374234676361,"curve_3":-0.6556403636932373,"scan_3":0.9902175664901733,"terminal_3":-1.597880482673645,"bearing_3":1.3504717350006104,"distance_4":-1.524795413017273,"scale_4":-0.0718604251742363,"straight_4":-1.6822589635849,"curve_4":2.0016753673553467,"scan_4":-0.7352409958839417,"terminal_4":-2.9175469875335693,"bearing_4":3.3357207775115967}},"16":{"bias":-11.154667854309082,"weights":{"distance_0":7.732945442199707,"scale_0":-0.1787235587835312,"straight_0":1.471677303314209,"curve_0":0.5385195016860962,"scan_0":9.400040626525879,"terminal_0":2.650175094604492,"bearing_0":5.675671100616455,"distance_1":3.5060653686523438,"scale_1":0.04651835188269615,"straight_1":4.161136150360107,"curve_1":-1.1214063167572021,"scan_1":-5.058105945587158,"terminal_1":-5.326796531677246,"bearing_1":3.1524477005004883,"distance_2":-4.168981075286865,"scale_2":-0.3435659110546112,"straight_2":-6.626214981079102,"curve_2":-0.096147820353508,"scan_2":-0.6074027419090271,"terminal_2":-1.3783385753631592,"bearing_2":0.6266195178031921,"distance_3":-3.127493381500244,"scale_3":-0.0011246969224885106,"straight_3":-5.122586727142334,"curve_3":-2.7323222160339355,"scan_3":4.91627311706543,"terminal_3":1.2976200580596924,"bearing_3":7.894196033477783,"distance_4":-5.407954216003418,"scale_4":0.005073408596217632,"straight_4":-5.364075183868408,"curve_4":-1.0179938077926636,"scan_4":2.241980791091919,"terminal_4":-0.2755903899669647,"bearing_4":0.5187987089157104}}},{"match":{"bias":-3.4500629901885986,"weights":{"0":-10.18834114074707,"1":10.682554244995117,"2":11.921621322631836,"3":-9.266988754272461,"4":-10.623098373413086,"5":-10.71368408203125,"6":-8.702827453613281,"7":-10.71178913116455,"8":-8.40239429473877,"9":-10.009513854980469,"10":6.1086320877075195,"11":-8.680758476257324,"12":10.123970031738281,"13":12.284858703613281,"14":-7.775933742523193,"15":-10.293346405029297,"16":7.380444049835205}}}],"outputLookup":true,"inputLookup":true,"activation":"sigmoid","trainOpts":{"iterations":15000,"errorThresh":0.001,"log":true,"logPeriod":10,"learningRate":0.2,"momentum":0.1,"callbackPeriod":10,"beta1":0.9,"beta2":0.999,"epsilon":1e-8}} 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mashnet", 3 | "version": "1.0.0", 4 | "description": "map conflation toolset", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "tap -R spec --no-coverage test/index.test.js", 8 | "coverage": "tap test/index.test.js", 9 | "lint": "eslint --fix src/*.js test/*.js utils/*.js && prettier --write *.js **/*.js", 10 | "pretty": "prettier --write *.js **/*.js", 11 | "train": "node train/match.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/sharedstreets/mashnet.git" 16 | }, 17 | "keywords": [ 18 | "maps", 19 | "streets", 20 | "graph" 21 | ], 22 | "author": "morganherlocker", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/sharedstreets/mashnet/issues" 26 | }, 27 | "homepage": "https://github.com/sharedstreets/mashnet#readme", 28 | "devDependencies": { 29 | "@mapbox/eslint-config-geocoding": "^2.0.2", 30 | "chance": "^1.1.4", 31 | "eslint": "^6.8.0", 32 | "eslint-plugin-node": "^11.0.0", 33 | "mkdirp": "^0.5.1", 34 | "prettier": "^1.19.1", 35 | "rimraf": "^3.0.0", 36 | "tap": "^14.10.5" 37 | }, 38 | "dependencies": { 39 | "@mapbox/graph-normalizer": "^3.1.1", 40 | "@mapbox/tile-cover": "^3.0.2", 41 | "@turf/turf": "^5.1.6", 42 | "brain.js": "^2.0.0-alpha.9", 43 | "byline": "^5.0.0", 44 | "level": "^6.0.0", 45 | "level-mem": "^5.0.1", 46 | "minimist": "^1.2.0", 47 | "osm-pbf-parser": "^2.3.0", 48 | "rbush": "^3.0.1", 49 | "softmax-fn": "^1.0.8", 50 | "through2": "^3.0.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /samples/honolulu.osm.pbf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sharedstreets/mashnet/4035df9c846974044162e92edc2bd96a37b188bd/samples/honolulu.osm.pbf -------------------------------------------------------------------------------- /src/debug.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function(obj) { 4 | console.log(JSON.stringify(obj)); 5 | }; 6 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const RTree = require("rbush"); 4 | const turf = require("@turf/turf"); 5 | const cover = require("@mapbox/tile-cover"); 6 | const tilebelt = require("@mapbox/tilebelt"); 7 | const softmax = require("softmax-fn"); 8 | const brain = require("brain.js"); 9 | const debug = require("./debug"); 10 | 11 | // set constants 12 | const UNITS = { units: "kilometers" }; 13 | const DEG2RAD = Math.PI / 180.0; 14 | const RAD2DEG = 180.0 / Math.PI; 15 | const MAX_NODE_SHIFT = 0.01; 16 | const MAX_VERTEX_SHIFT = 0.0075; 17 | const MAX_PHANTOM_SHIFT = 0.005; 18 | const DEBUG_COLOR_1 = "#ff66ff"; // pink 19 | const DEBUG_COLOR_2 = "#00ff00"; // green 20 | const DEBUG_COLOR_3 = "#66ffff"; // cyan 21 | const DEBUG_COLOR_4 = "#ff9900"; // orange 22 | const MATCH_DEPTH = 5; 23 | 24 | // constructor 25 | const Mashnet = function(ways) { 26 | this.edges = new Map(); 27 | this.vertices = new Map(); 28 | this.nodes = new Map(); 29 | this.metadata = new Map(); 30 | this.nodetree = new RTree(); 31 | this.edgetree = new RTree(); 32 | this.id = 0; 33 | this.nn = new brain.NeuralNetwork(); 34 | 35 | // load pretrained match model, if present 36 | let matchModel; 37 | try { 38 | matchModel = require("../model/match.json"); 39 | this.nn.fromJSON(matchModel); 40 | } catch (e) { 41 | throw new Error("unable to load model"); 42 | } 43 | 44 | for (const way of ways) { 45 | if (way.geometry.coordinates.length === way.properties.refs.length) { 46 | // setup vertices 47 | let i = 0; 48 | for (const ref of way.properties.refs) { 49 | this.vertices.set(ref, way.geometry.coordinates[i]); 50 | i++; 51 | } 52 | 53 | // setup nodes 54 | // start 55 | const start = way.properties.refs[0]; 56 | let adjacent_start = this.nodes.get(start); 57 | if (!adjacent_start) { 58 | adjacent_start = new Set(); 59 | } 60 | adjacent_start.add(way.properties.id); 61 | this.nodes.set(start, adjacent_start); 62 | 63 | // end 64 | const end = way.properties.refs[way.properties.refs.length - 1]; 65 | let adjacent_end = this.nodes.get(end); 66 | if (!adjacent_end) { 67 | adjacent_end = new Set(); 68 | } 69 | adjacent_end.add(way.properties.id); 70 | this.nodes.set(end, adjacent_end); 71 | 72 | // setup edges 73 | this.edges.set(way.properties.id, way.properties.refs); 74 | 75 | // setup metadata 76 | const metadata = {}; 77 | for (const property of Object.keys(way.properties)) { 78 | if (["id", "refs"].indexOf(property) === -1) { 79 | metadata[property] = way.properties[property]; 80 | } 81 | } 82 | this.metadata.set(way.properties.id, metadata); 83 | } 84 | } 85 | 86 | // setup nodetree 87 | let nodeItems = []; 88 | for (const node of this.nodes) { 89 | const vertex = this.vertices.get(node[0]); 90 | const item = { 91 | minX: vertex[0], 92 | minY: vertex[1], 93 | maxX: vertex[0], 94 | maxY: vertex[1], 95 | id: node[0] 96 | }; 97 | nodeItems.push(item); 98 | } 99 | this.nodetree.load(nodeItems); 100 | nodeItems = null; 101 | 102 | // setup edgetree 103 | let edgeItems = []; 104 | for (const edge of this.edges) { 105 | let minX = Infinity; 106 | let minY = Infinity; 107 | let maxX = -Infinity; 108 | let maxY = -Infinity; 109 | for (const ref of edge[1]) { 110 | const vertex = this.vertices.get(ref); 111 | if (vertex[0] < minX) minX = vertex[0]; 112 | if (vertex[1] < minY) minY = vertex[1]; 113 | if (vertex[0] > maxX) maxX = vertex[0]; 114 | if (vertex[1] > maxY) maxY = vertex[1]; 115 | } 116 | const item = { 117 | minX: minX, 118 | minY: minY, 119 | maxX: maxX, 120 | maxY: maxY, 121 | id: edge[0] 122 | }; 123 | edgeItems.push(item); 124 | } 125 | this.edgetree.load(edgeItems); 126 | edgeItems = null; 127 | }; 128 | 129 | Mashnet.prototype.scan = function(addition) { 130 | if (process.env.DEBUG) { 131 | debug({ 132 | type: "log", 133 | message: "SCAN" 134 | }); 135 | debug({ 136 | type: "fit", 137 | bbox: turf.bbox(addition) 138 | }); 139 | debug({ 140 | type: "draw", 141 | geometry: addition.geometry, 142 | style: { 143 | width: 4, 144 | color: DEBUG_COLOR_1, 145 | opacity: 0.7 146 | }, 147 | fade: 100000 148 | }); 149 | } 150 | 151 | // find matching edge candidates 152 | 153 | // get candidates 154 | const buffer = 0.1; 155 | const bbox = turf.bbox(addition); 156 | const sw = turf.destination(turf.point(bbox.slice(0, 2)), buffer, 225, UNITS); 157 | const ne = turf.destination(turf.point(bbox.slice(2, 4)), buffer, 45, UNITS); 158 | 159 | const candidates = this.edgetree.search({ 160 | minX: sw.geometry.coordinates[0], 161 | minY: sw.geometry.coordinates[1], 162 | maxX: ne.geometry.coordinates[0], 163 | maxY: ne.geometry.coordinates[1] 164 | }); 165 | 166 | if (process.env.DEBUG) { 167 | debug({ 168 | type: "fit", 169 | bbox: sw.geometry.coordinates.concat(ne.geometry.coordinates) 170 | }); 171 | debug({ 172 | type: "draw", 173 | geometry: turf.lineString(turf.bboxPolygon(bbox).geometry.coordinates[0]) 174 | .geometry, 175 | style: { 176 | width: 0.5, 177 | color: DEBUG_COLOR_2, 178 | opacity: 0.9 179 | }, 180 | fade: 3000 181 | }); 182 | debug({ 183 | type: "draw", 184 | geometry: turf.lineString( 185 | turf.envelope(turf.featureCollection([sw, ne])).geometry.coordinates[0] 186 | ).geometry, 187 | style: { 188 | width: 0.8, 189 | color: DEBUG_COLOR_2, 190 | opacity: 0.6 191 | } 192 | }); 193 | 194 | const boxes = []; 195 | for (const candidate of candidates) { 196 | boxes.push( 197 | turf.lineString( 198 | turf.bboxPolygon([ 199 | candidate.minX, 200 | candidate.minY, 201 | candidate.maxX, 202 | candidate.maxY 203 | ]).geometry.coordinates[0] 204 | ).geometry.coordinates 205 | ); 206 | } 207 | if (boxes.length) { 208 | debug({ 209 | type: "fit", 210 | bbox: turf.bbox(turf.multiLineString(boxes)) 211 | }); 212 | debug({ 213 | type: "draw", 214 | geometry: turf.multiLineString(boxes).geometry, 215 | style: { 216 | width: 0.3, 217 | color: "#5AFF52", 218 | opacity: 0.9 219 | }, 220 | fade: 5000 221 | }); 222 | } 223 | } 224 | 225 | // get scores 226 | const a = heuristics(addition); 227 | 228 | let matches = []; 229 | for (const candidate of candidates) { 230 | const refs = this.edges.get(candidate.id); 231 | const coordinates = []; 232 | for (const ref of refs) { 233 | coordinates.push(this.vertices.get(ref)); 234 | } 235 | const line = turf.lineString(coordinates); 236 | 237 | if (process.env.DEBUG) { 238 | debug({ 239 | type: "fit", 240 | bbox: turf.bbox(turf.featureCollection([sw, ne, line])) 241 | }); 242 | debug({ 243 | type: "draw", 244 | geometry: turf.lineString(turf.envelope(line).geometry.coordinates[0]) 245 | .geometry, 246 | style: { 247 | width: 0.5, 248 | color: DEBUG_COLOR_2, 249 | opacity: 0.95 250 | }, 251 | fade: 2000 252 | }); 253 | debug({ 254 | type: "draw", 255 | geometry: line.geometry, 256 | style: { 257 | width: 4, 258 | color: DEBUG_COLOR_2, 259 | opacity: 0.7 260 | }, 261 | fade: 2000 262 | }); 263 | } 264 | 265 | const b = heuristics(line); 266 | const scores = compare(a, b); 267 | 268 | if (process.env.DEBUG) { 269 | debug({ 270 | type: "log", 271 | message: "---" 272 | }); 273 | for (const s of Object.keys(scores)) { 274 | debug({ 275 | type: "log", 276 | message: s + ": " + scores[s].toFixed(6), 277 | color: 278 | "rgb(" + 279 | (100 + Math.round(Math.abs(scores[s] - 1) * 105)) + 280 | "," + 281 | (100 + Math.round(scores[s] * 50)) + 282 | "," + 283 | (100 + Math.round(scores[s] * 50)) + 284 | ");" 285 | }); 286 | } 287 | } 288 | 289 | const weights = { 290 | distance: 1, 291 | scale: 1, 292 | straight: 1, 293 | curve: 1, 294 | scan: 1, 295 | terminal: 1, 296 | bearing: 1 297 | }; 298 | 299 | let score = 0; 300 | for (const s of Object.keys(scores)) { 301 | score += scores[s] * weights[s]; 302 | } 303 | 304 | if (score > 0) { 305 | const match = { 306 | id: candidate.id, 307 | line: line, 308 | score: score 309 | }; 310 | for (const s of Object.keys(scores)) { 311 | match[s] = scores[s]; 312 | } 313 | matches.push(match); 314 | } 315 | } 316 | 317 | const softmaxScores = softmax( 318 | matches.map(match => { 319 | return match.score; 320 | }) 321 | ); 322 | let i = 0; 323 | for (const sm of softmaxScores) { 324 | matches[i].softmax = sm; 325 | i++; 326 | } 327 | 328 | matches = matches.sort((a, b) => { 329 | return b.softmax - a.softmax; 330 | }); 331 | 332 | if (process.env.DEBUG) { 333 | debug({ 334 | type: "clear" 335 | }); 336 | debug({ 337 | type: "draw", 338 | geometry: matches[0].line.geometry, 339 | style: { 340 | color: DEBUG_COLOR_3, 341 | width: 7, 342 | opacity: 0.9 343 | }, 344 | fade: 6000 345 | }); 346 | } 347 | 348 | return matches; 349 | }; 350 | 351 | Mashnet.prototype.match = function(scores) { 352 | if (!Array.isArray(scores)) { 353 | throw new Error("Mashnet.prototype.match must receive an array of scores"); 354 | } else if (scores.length === 0) { 355 | return 0; 356 | } else { 357 | const input = {}; 358 | for (let k = 0; k < MATCH_DEPTH; k++) { 359 | if (scores[k]) { 360 | input["distance_" + k] = scores[k].distance; 361 | input["scale_" + k] = scores[k].scale; 362 | input["straight_" + k] = scores[k].straight; 363 | input["curve_" + k] = scores[k].curve; 364 | input["scan_" + k] = scores[k].scan; 365 | input["terminal_" + k] = scores[k].terminal; 366 | input["bearing_" + k] = scores[k].bearing; 367 | } else { 368 | input["distance_" + k] = 0.0; 369 | input["scale_" + k] = 0.0; 370 | input["straight_" + k] = 0.0; 371 | input["curve_" + k] = 0.0; 372 | input["scan_" + k] = 0.0; 373 | input["terminal_" + k] = 0.0; 374 | input["bearing_" + k] = 0.0; 375 | } 376 | } 377 | const prediction = this.nn.run(input); 378 | return prediction.match; 379 | } 380 | }; 381 | 382 | function compare(a, b) { 383 | const maxDistance = Math.max(a.distance, b.distance); 384 | const minDistance = Math.min(a.distance, b.distance); 385 | let scale = (a.distance + b.distance) / 100; 386 | if (scale > 1) scale = 1; 387 | const maxStraight = Math.max(a.straight, b.straight); 388 | const minStraight = Math.min(a.straight, b.straight); 389 | const maxCurve = Math.max(a.curve, b.curve); 390 | const minCurve = Math.min(a.curve, b.curve); 391 | 392 | const scan = similarity(a.scan, b.scan); 393 | const terminal = similarity(a.terminal, b.terminal); 394 | 395 | const bearingForward = bearingDistance(a.bearing, b.bearing); 396 | const bearingBack = bearingDistance(b.bearing, a.bearing); 397 | const bearing = Math.max(bearingForward, bearingBack); 398 | 399 | return { 400 | distance: minDistance / maxDistance, 401 | scale: scale, 402 | straight: minStraight / maxStraight, 403 | curve: minCurve / maxCurve, 404 | scan: scan, 405 | terminal: terminal, 406 | bearing: Math.abs(bearing - 180) / 180 407 | }; 408 | } 409 | 410 | function bearingDistance(b1, b2) { 411 | const b1Rad = b1 * DEG2RAD; 412 | const b2Rad = b2 * DEG2RAD; 413 | const b1y = Math.cos(b1Rad); 414 | const b1x = Math.sin(b1Rad); 415 | const b2y = Math.cos(b2Rad); 416 | const b2x = Math.sin(b2Rad); 417 | const crossp = b1y * b2x - b2y * b1x; 418 | const dotp = b1x * b2x + b1y * b2y; 419 | if (crossp > 0) { 420 | return Math.acos(dotp) * RAD2DEG; 421 | } else { 422 | return -Math.acos(dotp) * RAD2DEG; 423 | } 424 | } 425 | 426 | function similarity(a, b) { 427 | const union = new Set(); 428 | for (const scan of a) { 429 | union.add(scan); 430 | } 431 | for (const scan of b) { 432 | union.add(scan); 433 | } 434 | const overlap = new Set(); 435 | for (const key of union) { 436 | if (a.has(key) && b.has(key)) { 437 | overlap.add(key); 438 | } 439 | } 440 | let sim = 0; 441 | if (union.size > 0) { 442 | sim = overlap.size / union.size; 443 | } 444 | 445 | if (process.env.DEBUG) { 446 | const abCells = []; 447 | const cCells = []; 448 | 449 | for (const scan of a) { 450 | abCells.push( 451 | turf.bboxPolygon(tilebelt.tileToBBOX(tilebelt.quadkeyToTile(scan))) 452 | .geometry.coordinates[0] 453 | ); 454 | } 455 | for (const scan of b) { 456 | abCells.push( 457 | turf.bboxPolygon(tilebelt.tileToBBOX(tilebelt.quadkeyToTile(scan))) 458 | .geometry.coordinates[0] 459 | ); 460 | } 461 | for (const scan of overlap) { 462 | cCells.push( 463 | turf.bboxPolygon(tilebelt.tileToBBOX(tilebelt.quadkeyToTile(scan))) 464 | .geometry.coordinates[0] 465 | ); 466 | } 467 | 468 | debug({ 469 | type: "draw", 470 | geometry: turf.multiLineString(abCells).geometry, 471 | style: { 472 | color: DEBUG_COLOR_3, 473 | opacity: 0.8 474 | }, 475 | fade: 1000 476 | }); 477 | debug({ 478 | type: "draw", 479 | geometry: turf.multiLineString(cCells).geometry, 480 | style: { 481 | color: DEBUG_COLOR_4, 482 | opacity: 0.8 483 | }, 484 | fade: 2500 485 | }); 486 | } 487 | 488 | return sim; 489 | } 490 | 491 | function heuristics(line) { 492 | const buffer = 0.05; 493 | const z = 23; 494 | const zs = { min_zoom: z, max_zoom: z }; 495 | const start = turf.point(line.geometry.coordinates[0]); 496 | const end = turf.point( 497 | line.geometry.coordinates[line.geometry.coordinates.length - 1] 498 | ); 499 | 500 | const distance = turf.lineDistance(line, UNITS); 501 | const straight = turf.distance(start, end, UNITS); 502 | const curve = straight / distance; 503 | const indexes = cover.indexes(turf.buffer(line, buffer, UNITS).geometry, zs); 504 | const scan = new Set(); 505 | for (const index of indexes) { 506 | scan.add(index); 507 | } 508 | 509 | const terminalIndexes = cover.indexes( 510 | turf.buffer( 511 | turf.multiPoint([ 512 | line.geometry.coordinates[0], 513 | line.geometry.coordinates[line.geometry.coordinates.length - 1] 514 | ]), 515 | buffer * 2, 516 | UNITS 517 | ).geometry, 518 | zs 519 | ); 520 | const terminal = new Set(); 521 | for (const index of terminalIndexes) { 522 | terminal.add(index); 523 | } 524 | 525 | const bearing = turf.bearing(start, end); 526 | 527 | return { 528 | line: line, 529 | distance: distance, 530 | straight: straight, 531 | curve: curve, 532 | scan: scan, 533 | terminal: terminal, 534 | bearing: bearing 535 | }; 536 | } 537 | 538 | Mashnet.prototype.merge = function(existing, addition) { 539 | // merge existing edge 540 | const metadata = this.metadata.get(existing); 541 | for (const property of Object.keys(addition)) { 542 | metadata[property] = addition[property]; 543 | } 544 | this.metadata.set(existing, metadata); 545 | }; 546 | 547 | Mashnet.prototype.snap = function(addition) { 548 | const phantoms = phantomify(addition.geometry.coordinates); 549 | const snaps = []; 550 | const buffer = MAX_NODE_SHIFT * 1.5; 551 | const bbox = turf.bbox(addition); 552 | const sw = turf.destination(turf.point(bbox.slice(0, 2)), buffer, 225, UNITS); 553 | const ne = turf.destination(turf.point(bbox.slice(2, 4)), buffer, 45, UNITS); 554 | const subgraph = this.query([ 555 | sw.geometry.coordinates[0], 556 | sw.geometry.coordinates[1], 557 | ne.geometry.coordinates[0], 558 | ne.geometry.coordinates[1] 559 | ]); 560 | 561 | let anchors = []; 562 | for (const edge of subgraph.edges) { 563 | const coordinates = edge[1].map(ref => { 564 | return subgraph.vertices.get(ref); 565 | }); 566 | anchors = anchors.concat( 567 | phantomify(coordinates).map(c => { 568 | return c.concat(edge[0]); 569 | }) 570 | ); 571 | } 572 | 573 | for (const phantom of phantoms) { 574 | const snap = { 575 | node: { 576 | distance: Infinity, 577 | id: null 578 | }, 579 | vertex: { 580 | distance: Infinity, 581 | id: null 582 | }, 583 | anchor: { 584 | distance: Infinity, 585 | id: null, 586 | pair: null 587 | }, 588 | void: { 589 | pair: null 590 | } 591 | }; 592 | 593 | // nodes 594 | for (const node of subgraph.nodes) { 595 | const pair = subgraph.vertices.get(node[0]); 596 | const distance = turf.distance(turf.point(pair), turf.point(phantom)); 597 | if (distance < MAX_NODE_SHIFT && snap.node.distance > distance) { 598 | snap.node = { 599 | distance: distance, 600 | id: node[0] 601 | }; 602 | } 603 | } 604 | 605 | // vertices 606 | if (!snap.node.id) { 607 | for (const vertex of subgraph.vertices) { 608 | const pair = subgraph.vertices.get(vertex[0]); 609 | const distance = turf.distance(turf.point(pair), turf.point(phantom)); 610 | if (distance < MAX_VERTEX_SHIFT && snap.vertex.distance > distance) { 611 | snap.vertex = { 612 | distance: distance, 613 | id: vertex[0] 614 | }; 615 | } 616 | } 617 | } 618 | 619 | // anchors 620 | if (!snap.node.id && !snap.vertex.id) { 621 | for (const anchor of anchors) { 622 | const pair = anchor.slice(0, 2); 623 | const distance = turf.distance(turf.point(pair), turf.point(phantom)); 624 | if (distance < MAX_PHANTOM_SHIFT && snap.anchor.distance > distance) { 625 | snap.anchor = { 626 | distance: distance, 627 | id: anchor[2], 628 | pair: pair 629 | }; 630 | } 631 | } 632 | } 633 | 634 | // void 635 | if (!snap.node.id && !snap.vertex.id && !snap.anchor.id) { 636 | snap.void = { 637 | pair: phantom 638 | }; 639 | } 640 | 641 | // filter duplicate adjacent snaps 642 | if (snaps.length > 0) { 643 | const last = snaps[snaps.length - 1]; 644 | if ( 645 | !(snap.node.id && last.node.id && snap.node.id === last.node.id) && 646 | !( 647 | snap.vertex.id && 648 | last.vertex.id && 649 | snap.vertex.id === last.vertex.id 650 | ) && 651 | !( 652 | snap.anchor.id && 653 | last.anchor.id && 654 | snap.anchor.id === last.anchor.id && 655 | snap.anchor.pair.join(",") === last.anchor.pair.join(",") 656 | ) 657 | ) { 658 | snaps.push(snap); 659 | } 660 | } else { 661 | snaps.push(snap); 662 | } 663 | } 664 | 665 | return snaps; 666 | }; 667 | 668 | /* Mashnet.prototype.crossing = function (coordinates, subgraph) { 669 | const crossings = []; 670 | const edges = []; 671 | for (let edge of subgraph.edges) { 672 | for (let i = 0; i < edge[1].length - 1; i++) { 673 | edges.push([ 674 | subgraph.vertices.get(edge[1][i]), 675 | subgraph.vertices.get(edge[1][i+1]), 676 | edge[0] 677 | ]); 678 | } 679 | } 680 | 681 | for (let i = 0; i < coordinates.length - 1; i++) { 682 | const segment = [coordinates[i], coordinates[i+1]] 683 | for (let edge of edges) { 684 | const intersect = intersects( 685 | edge[0], 686 | 687 | ) 688 | if(intersect) console.log(JSON.stringify(intersect)) 689 | } 690 | } 691 | } 692 | 693 | function intersects(a,b,c,d,p,q,r,s) { 694 | var det, gamma, lambda; 695 | det = (c - a) * (s - q) - (r - p) * (d - b); 696 | if (det === 0) { 697 | return false; 698 | } else { 699 | lambda = ((s - q) * (r - a) + (p - r) * (s - b)) / det; 700 | gamma = ((b - d) * (r - a) + (c - a) * (s - b)) / det; 701 | return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1); 702 | } 703 | };*/ 704 | 705 | function phantomify(coordinates) { 706 | const line = turf.lineString(coordinates); 707 | const pairs = [coordinates[0]]; 708 | const distance = turf.length(line); 709 | const step = MAX_PHANTOM_SHIFT / distance; 710 | let progress = 0.0; 711 | while (progress + step < 1.0) { 712 | progress += step; 713 | const pair = turf.along(line, progress * distance).geometry.coordinates; 714 | pairs.push(pair); 715 | } 716 | pairs.push(coordinates[coordinates.length - 1]); 717 | return pairs; 718 | } 719 | 720 | Mashnet.prototype.query = function(bbox) { 721 | const subgraph = { 722 | edges: new Map(), 723 | vertices: new Map(), 724 | nodes: new Map(), 725 | edgeTree: new RTree(), 726 | nodeTree: new RTree() 727 | }; 728 | 729 | this.edgetree 730 | .search({ 731 | minX: bbox[0], 732 | minY: bbox[1], 733 | maxX: bbox[2], 734 | maxY: bbox[3] 735 | }) 736 | .forEach(e => { 737 | subgraph.edgeTree.insert(e); 738 | const refs = this.edges.get(e.id); 739 | subgraph.edges.set(e.id, refs); 740 | for (const ref of refs) { 741 | const vertex = this.vertices.get(ref); 742 | subgraph.vertices.set(ref, vertex); 743 | } 744 | }); 745 | 746 | this.nodetree 747 | .search({ 748 | minX: bbox[0], 749 | minY: bbox[1], 750 | maxX: bbox[2], 751 | maxY: bbox[3] 752 | }) 753 | .forEach(n => { 754 | subgraph.nodeTree.insert(n); 755 | const edges = this.nodes.get(n.id); 756 | subgraph.nodes.set(n.id, edges); 757 | }); 758 | 759 | return subgraph; 760 | }; 761 | 762 | Mashnet.prototype.split = function(snaps) { 763 | const splits = [[snaps.shift()]]; 764 | 765 | while (snaps.length) { 766 | const snap = snaps.shift(); 767 | splits[splits.length - 1].push(snap); 768 | const next = snaps[0]; 769 | 770 | if ( 771 | !snap.void.pair && 772 | // do not split anchors along matching edge 773 | !( 774 | snaps.length > 0 && 775 | snap.anchor.id && 776 | next.anchor.id && 777 | snap.anchor.id === next.anchor.id 778 | ) 779 | ) { 780 | splits.push([snap]); 781 | } 782 | } 783 | 784 | return splits; 785 | }; 786 | 787 | Mashnet.prototype.materialize = function(splits) { 788 | const lines = []; 789 | let i = 0; 790 | for (const split of splits) { 791 | const pairs = []; 792 | for (const snap of split) { 793 | if (snap.node.id) { 794 | pairs.push(this.vertices.get(snap.node.id)); 795 | } else if (snap.vertex.id) { 796 | pairs.push(this.vertices.get(snap.vertex.id)); 797 | } else if (snap.anchor.id) { 798 | pairs.push(snap.anchor.pair); 799 | } else { 800 | pairs.push(snap.void.pair); 801 | } 802 | } 803 | 804 | const line = turf.lineString(pairs); 805 | 806 | let hasVoid = false; 807 | for (const snap of split) { 808 | if (snap.void.pair) { 809 | hasVoid = true; 810 | continue; 811 | } 812 | } 813 | if (hasVoid) { 814 | line.properties.action = "create"; 815 | } else { 816 | line.properties.action = "merge"; 817 | } 818 | line.properties.changeset = i; 819 | lines.push(line); 820 | i++; 821 | } 822 | return lines; 823 | }; 824 | 825 | Mashnet.prototype.commit = function(splits, metadata) { 826 | // integrates changesets into graph 827 | for (const split of splits) { 828 | let merge = true; 829 | for (const snap of split) { 830 | if (snap.void.pair) { 831 | merge = false; 832 | continue; 833 | } 834 | } 835 | 836 | if (merge) { 837 | // search for matching edge 838 | const line = this.materialize([split])[0]; 839 | const scores = this.scan(line); 840 | const isMatch = this.match(scores); 841 | // merge edge if top match passes threshold 842 | if (isMatch > 0.95) { 843 | this.merge(scores[0].id, metadata); 844 | } 845 | } else { 846 | // insert new edge 847 | const edgeId = this.id++; 848 | const refs = []; 849 | for (const snap of split) { 850 | if (snap.node.id) { 851 | // node 852 | refs.push(snap.node.id); 853 | } else if (snap.vertex.id) { 854 | // vertex 855 | refs.push(snap.vertex.id); 856 | } else if (snap.anchor.id) { 857 | // edge anchor 858 | const id = this.id++; 859 | refs.push(id); 860 | this.vertices.set(id, snap.anchor.pair); 861 | this.nodes.set(id, [edgeId]); 862 | } else { 863 | // void 864 | const id = this.id++; 865 | refs.push(id); 866 | this.vertices.set(id, snap.void.pair); 867 | } 868 | } 869 | } 870 | } 871 | }; 872 | 873 | Mashnet.prototype.propose = function(addition) { 874 | // wraps snap+split 875 | }; 876 | 877 | Mashnet.prototype.apply = function(addition) { 878 | // wraps snap+split+commit 879 | }; 880 | 881 | // NOTE: pre-production legacy API, to be deprecated; preserved for initial demo 882 | Mashnet.prototype.append = function(addition) { 883 | const buffer = MAX_NODE_SHIFT * 1.5; 884 | const bbox = turf.bbox(addition); 885 | const sw = turf.destination(turf.point(bbox.slice(0, 2)), buffer, 225, UNITS); 886 | const ne = turf.destination(turf.point(bbox.slice(2, 4)), buffer, 45, UNITS); 887 | 888 | const candidates = this.edgetree.search({ 889 | minX: sw.geometry.coordinates[0], 890 | minY: sw.geometry.coordinates[1], 891 | maxX: ne.geometry.coordinates[0], 892 | maxY: ne.geometry.coordinates[1] 893 | }); 894 | 895 | // build local data 896 | const edges = new Map(); 897 | const nodes = new Map(); 898 | const vertices = new Map(); 899 | const phantoms = new Map(); 900 | // build edges 901 | for (const candidate of candidates) { 902 | edges.set(candidate.id, this.edges.get(candidate.id)); 903 | } 904 | 905 | for (const edge of edges) { 906 | // build vertices 907 | const coordinates = []; 908 | for (const ref of edge[1]) { 909 | const pair = this.vertices.get(ref); 910 | vertices.set(ref, pair); 911 | coordinates.push(pair); 912 | } 913 | 914 | // build nodes 915 | nodes.set(edge[1][0], vertices.get(edge[1][0])); 916 | nodes.set( 917 | edge[1][edge[1].length - 1], 918 | vertices.get(edge[1][edge[1].length - 1]) 919 | ); 920 | 921 | // build phantoms 922 | const line = turf.lineString(coordinates); 923 | const distance = turf.length(line); 924 | const step = MAX_PHANTOM_SHIFT / distance; 925 | let progress = 0.0; 926 | while (progress + step < 1.0) { 927 | progress += step; 928 | const pair = turf.along(line, progress * distance).geometry.coordinates; 929 | phantoms.set(phantoms.size, { 930 | edge: edge[0], 931 | pair: pair 932 | }); 933 | } 934 | } 935 | 936 | // build local trees 937 | const nodeTree = new RTree(); 938 | const vertexTree = new RTree(); 939 | const phantomTree = new RTree(); 940 | 941 | // build local node tree 942 | for (const node of nodes) { 943 | const pair = vertices.get(node[0]); 944 | const item = { 945 | minX: pair[0], 946 | minY: pair[1], 947 | maxX: pair[0], 948 | maxY: pair[1], 949 | id: node[0] 950 | }; 951 | nodeTree.insert(item); 952 | } 953 | 954 | // build local vertex tree 955 | for (const vertex of vertices) { 956 | const item = { 957 | minX: vertex[1][0], 958 | minY: vertex[1][1], 959 | maxX: vertex[1][0], 960 | maxY: vertex[1][1], 961 | id: vertex[0] 962 | }; 963 | vertexTree.insert(item); 964 | } 965 | 966 | // build local phantom tree 967 | for (const phantom of phantoms) { 968 | const item = { 969 | minX: phantom[1].pair[0], 970 | minY: phantom[1].pair[1], 971 | maxX: phantom[1].pair[0], 972 | maxY: phantom[1].pair[1], 973 | edge: phantom[1].edge, 974 | pair: phantom[1].pair 975 | }; 976 | phantomTree.insert(item); 977 | } 978 | 979 | // build potential change list 980 | const pairs = addition.geometry.coordinates; 981 | // insert proposed vertices 982 | const changes = [ 983 | { 984 | along: 0.0, 985 | pair: pairs[0], 986 | phantom: false 987 | } 988 | ]; 989 | const distance = turf.length(addition); 990 | for (let i = 1; i < pairs.length; i++) { 991 | const pair = pairs[i]; 992 | const along = 993 | turf.length( 994 | turf.lineSlice(turf.point(pairs[0]), turf.point(pair), addition) 995 | ) / distance; 996 | 997 | changes.push({ 998 | along: along, 999 | pair: pair, 1000 | phantom: false 1001 | }); 1002 | } 1003 | // insert phantom vertices 1004 | const step = MAX_PHANTOM_SHIFT / distance; 1005 | let progress = 0.0; 1006 | while (progress + step < 1.0) { 1007 | progress += step; 1008 | const pair = turf.along(addition, progress * distance, UNITS).geometry 1009 | .coordinates; 1010 | changes.push({ 1011 | along: progress, 1012 | pair: pair, 1013 | phantom: true 1014 | }); 1015 | } 1016 | // sort change list 1017 | changes.sort((a, b) => { 1018 | return a.along - b.along; 1019 | }); 1020 | 1021 | // create commits from changes 1022 | const commits = []; 1023 | for (const change of changes) { 1024 | let closestNode; 1025 | let closestVertex; 1026 | let closestPhantom; 1027 | 1028 | // get closest node 1029 | const nodeCandidates = searchTree(change.pair, MAX_NODE_SHIFT, nodeTree); 1030 | for (const nodeCandidate of nodeCandidates) { 1031 | const pair = vertices.get(nodeCandidate.id); 1032 | const apart = turf.distance( 1033 | turf.point(pair), 1034 | turf.point(change.pair), 1035 | UNITS 1036 | ); 1037 | if (!closestNode) { 1038 | closestNode = { 1039 | id: nodeCandidate.id, 1040 | pair: pair, 1041 | distance: apart 1042 | }; 1043 | } else if (apart < closestNode.distance) { 1044 | closestNode = { 1045 | id: nodeCandidate.id, 1046 | pair: pair, 1047 | distance: apart 1048 | }; 1049 | } 1050 | } 1051 | 1052 | // get closest vertex 1053 | if (!closestNode) { 1054 | const vertexCandidates = searchTree( 1055 | change.pair, 1056 | MAX_VERTEX_SHIFT, 1057 | vertexTree 1058 | ); 1059 | for (const vertexCandidate of vertexCandidates) { 1060 | const pair = vertices.get(vertexCandidate.id); 1061 | const apart = turf.distance( 1062 | turf.point(pair), 1063 | turf.point(change.pair), 1064 | UNITS 1065 | ); 1066 | if (!closestVertex) { 1067 | closestVertex = { 1068 | id: vertexCandidate.id, 1069 | pair: pair, 1070 | distance: apart 1071 | }; 1072 | } else if (apart < closestVertex.distance) { 1073 | closestVertex = { 1074 | id: vertexCandidate.id, 1075 | pair: pair, 1076 | distance: apart 1077 | }; 1078 | } 1079 | } 1080 | } 1081 | 1082 | // get closest phantom 1083 | if (!closestVertex) { 1084 | const phantomCandidates = searchTree( 1085 | change.pair, 1086 | MAX_PHANTOM_SHIFT, 1087 | phantomTree 1088 | ); 1089 | for (const phantomCandidate of phantomCandidates) { 1090 | const pair = phantomCandidate.pair; 1091 | const apart = turf.distance( 1092 | turf.point(pair), 1093 | turf.point(change.pair), 1094 | UNITS 1095 | ); 1096 | if (!closestPhantom) { 1097 | closestPhantom = { 1098 | edge: phantomCandidate.edge, 1099 | pair: pair, 1100 | distance: apart 1101 | }; 1102 | } else if (apart < closestPhantom.distance) { 1103 | closestPhantom = { 1104 | edge: phantomCandidate.edge, 1105 | pair: pair, 1106 | distance: apart 1107 | }; 1108 | } 1109 | } 1110 | } 1111 | 1112 | if (closestNode && closestNode.distance <= MAX_NODE_SHIFT) { 1113 | if ( 1114 | !commits.length || 1115 | (commits[commits.length - 1].type !== "node" && 1116 | commits[commits.length - 1].id !== closestNode.id) 1117 | ) { 1118 | commits.push({ 1119 | type: "node", 1120 | id: closestNode.id 1121 | }); 1122 | } 1123 | } else if (closestVertex && closestVertex.distance <= MAX_VERTEX_SHIFT) { 1124 | if ( 1125 | !commits.length || 1126 | (commits[commits.length - 1].type !== "vertex" && 1127 | commits[commits.length - 1].id !== closestVertex.id) 1128 | ) { 1129 | commits.push({ 1130 | type: "vertex", 1131 | id: closestVertex.id 1132 | }); 1133 | } 1134 | } else if (closestPhantom && closestPhantom.distance <= MAX_PHANTOM_SHIFT) { 1135 | commits.push({ 1136 | type: "phantom", 1137 | edge: closestPhantom.edge 1138 | }); 1139 | } else if (!change.phantom) { 1140 | commits.push({ 1141 | type: "new", 1142 | pair: change.pair 1143 | }); 1144 | } 1145 | } 1146 | 1147 | // split commits 1148 | let next = commits.shift(); 1149 | let insert = [next]; 1150 | const inserts = []; 1151 | while (commits.length) { 1152 | next = commits.shift(); 1153 | if (next) { 1154 | insert.push(next); 1155 | 1156 | // cut edge if node, vertex, or last new 1157 | if ( 1158 | next.type === "node" || 1159 | next.type === "vertex" || 1160 | next.type === "phantom" || 1161 | commits.length === 0 1162 | ) { 1163 | inserts.push(insert); 1164 | insert = [next]; 1165 | } 1166 | } 1167 | } 1168 | 1169 | for (const insert of inserts) { 1170 | // classify 1171 | let potentialMatch = false; 1172 | for (const item of insert) { 1173 | if (item.type === "phantom") { 1174 | potentialMatch = true; 1175 | } 1176 | } 1177 | 1178 | if (potentialMatch) { 1179 | // attempt merge 1180 | // scan 1181 | // if is match, merge 1182 | // else ignore 1183 | } else { 1184 | const id = this.id++; 1185 | const refs = []; 1186 | for (const item of insert) { 1187 | if (item.type === "node") { 1188 | // add edge to node list 1189 | const node = this.nodes.get(item.id); 1190 | node.add(id); 1191 | this.nodes.set(item.id, node); 1192 | // add ref to edge 1193 | refs.push(item.id); 1194 | } else if (item.type === "vertex") { 1195 | // get parents 1196 | const parents = []; 1197 | for (const edge of edges) { 1198 | if (edge[1].indexOf(item.id) !== -1) { 1199 | parents.push(edge); 1200 | } 1201 | } 1202 | // delete parents 1203 | for (const parent of parents) { 1204 | this.edges.delete(parent[0]); 1205 | } 1206 | // split parents 1207 | for (const parent of parents) { 1208 | const a = { 1209 | id: parent[0] + "!0", 1210 | refs: parent[1].slice(0, parent[1].indexOf(item.id) + 1) 1211 | }; 1212 | const b = { 1213 | id: parent[0] + "!1", 1214 | refs: parent[1].slice( 1215 | parent[1].indexOf(item.id), 1216 | parent[1].length 1217 | ) 1218 | }; 1219 | this.edges.set(a.id, a.refs); 1220 | this.edges.set(b.id, b.refs); 1221 | } 1222 | 1223 | // add ref to edge 1224 | refs.push(item.id); 1225 | // re-node 1226 | } else if (item.type === "phantom") { 1227 | // set phantom id 1228 | item.id = this.id++; 1229 | // insert new vertex 1230 | this.vertices.set(item.id, item.pair); 1231 | // get parents 1232 | const parents = []; 1233 | for (const edge of edges) { 1234 | if (edge[1].indexOf(item.id) !== -1) { 1235 | parents.push(edge); 1236 | } 1237 | } 1238 | // delete parents 1239 | for (const parent of parents) { 1240 | this.edges.delete(parent[0]); 1241 | } 1242 | // split parents 1243 | for (const parent of parents) { 1244 | // todo: detect forward and back nodes, split in between 1245 | const a = { 1246 | id: parent[0] + "!0", 1247 | refs: parent[1].slice(0, parent[1].indexOf(item.id) + 1) 1248 | }; 1249 | const b = { 1250 | id: parent[0] + "!1", 1251 | refs: parent[1].slice( 1252 | parent[1].indexOf(item.id), 1253 | parent[1].length 1254 | ) 1255 | }; 1256 | this.edges.set(a.id, a.refs); 1257 | this.edges.set(b.id, b.refs); 1258 | } 1259 | // add ref to edge 1260 | refs.push(item.id); 1261 | // re-node 1262 | } else if (item.type === "new") { 1263 | // set new id 1264 | item.id = this.id++; 1265 | // insert new vertex 1266 | this.vertices.set(item.id, item.pair); 1267 | // add ref to edge 1268 | refs.push(item.id); 1269 | } 1270 | } 1271 | // add new edge 1272 | this.edges.set(id, refs); 1273 | 1274 | const coordinates = []; 1275 | for (const ref of refs) { 1276 | coordinates.push(this.vertices.get(ref)); 1277 | } 1278 | const newLine = turf.lineString(coordinates); 1279 | if (turf.length(newLine) > 0.05) { 1280 | const scores = this.scan(newLine); 1281 | 1282 | if (scores[0].scan > 0 && scores[0].scan < 0.1) { 1283 | console.log(JSON.stringify(turf.lineString(coordinates))); 1284 | } 1285 | } 1286 | } 1287 | } 1288 | }; 1289 | 1290 | function searchTree(pair, buffer, tree) { 1291 | const sw = turf.destination(turf.point(pair), buffer * 1.5, 225, UNITS); 1292 | const ne = turf.destination(turf.point(pair), buffer * 1.5, 45, UNITS); 1293 | 1294 | return tree.search({ 1295 | minX: sw.geometry.coordinates[0], 1296 | minY: sw.geometry.coordinates[1], 1297 | maxX: ne.geometry.coordinates[0], 1298 | maxY: ne.geometry.coordinates[1] 1299 | }); 1300 | } 1301 | 1302 | Mashnet.prototype.add = function(addition) { 1303 | if (process.env.DEBUG) { 1304 | debug({ 1305 | type: "log", 1306 | message: "ADD" 1307 | }); 1308 | debug({ 1309 | type: "fit", 1310 | bbox: turf.bbox(addition) 1311 | }); 1312 | debug({ 1313 | type: "draw", 1314 | geometry: addition.geometry, 1315 | style: { 1316 | width: 4, 1317 | color: DEBUG_COLOR_1, 1318 | opacity: 0.7 1319 | }, 1320 | fade: 100000 1321 | }); 1322 | } 1323 | 1324 | // add new edge 1325 | // get candidates 1326 | const buffer = 0.01; 1327 | const bbox = turf.bbox(addition); 1328 | const sw = turf.destination(turf.point(bbox.slice(0, 2)), buffer, 225, UNITS); 1329 | const ne = turf.destination(turf.point(bbox.slice(2, 4)), buffer, 45, UNITS); 1330 | 1331 | const candidates = this.edgetree.search({ 1332 | minX: sw.geometry.coordinates[0], 1333 | minY: sw.geometry.coordinates[1], 1334 | maxX: ne.geometry.coordinates[0], 1335 | maxY: ne.geometry.coordinates[1] 1336 | }); 1337 | 1338 | const nodes = new Map(); 1339 | const vertices = new Map(); 1340 | for (const candidate of candidates) { 1341 | const refs = this.edges.get(candidate.id); 1342 | 1343 | for (const ref of refs) { 1344 | vertices.set(ref, turf.point(this.vertices.get(ref))); 1345 | } 1346 | 1347 | nodes.set(refs[0], vertices.get(refs[0])); 1348 | nodes.set(refs[refs.length - 1], vertices.get(refs[refs.length - 1])); 1349 | } 1350 | 1351 | if (process.env.DEBUG) { 1352 | const lines = []; 1353 | 1354 | for (const candidate of candidates) { 1355 | const coordinates = []; 1356 | const refs = this.edges.get(candidate.id); 1357 | 1358 | for (const ref of refs) { 1359 | coordinates.push(this.vertices.get(ref)); 1360 | } 1361 | lines.push(coordinates); 1362 | } 1363 | 1364 | debug({ 1365 | type: "fit", 1366 | bbox: turf.bbox(turf.multiLineString(lines)) 1367 | }); 1368 | debug({ 1369 | type: "log", 1370 | message: candidates.length + " edge candidates" 1371 | }); 1372 | debug({ 1373 | type: "draw", 1374 | geometry: turf.multiLineString(lines).geometry, 1375 | style: { 1376 | width: 2, 1377 | color: DEBUG_COLOR_2, 1378 | opacity: 0.7 1379 | }, 1380 | fade: 100000 1381 | }); 1382 | debug({ 1383 | type: "log", 1384 | message: vertices.size + " vertex candidates" 1385 | }); 1386 | const vertexPts = []; 1387 | for (const vertex of vertices) { 1388 | vertexPts.push(vertex[1].geometry.coordinates); 1389 | } 1390 | debug({ 1391 | type: "draw", 1392 | geometry: turf.multiPoint(vertexPts).geometry, 1393 | style: { 1394 | width: 4, 1395 | color: DEBUG_COLOR_2, 1396 | opacity: 0.7 1397 | }, 1398 | fade: 100000 1399 | }); 1400 | 1401 | debug({ 1402 | type: "log", 1403 | message: nodes.size + " node candidates" 1404 | }); 1405 | const nodePts = []; 1406 | for (const node of nodes) { 1407 | nodePts.push(node[1].geometry.coordinates); 1408 | } 1409 | debug({ 1410 | type: "draw", 1411 | geometry: turf.multiPoint(nodePts).geometry, 1412 | style: { 1413 | width: 8, 1414 | color: DEBUG_COLOR_3, 1415 | opacity: 0.7 1416 | }, 1417 | fade: 100000 1418 | }); 1419 | debug({ 1420 | // todo: delete 1421 | type: "fit", 1422 | bbox: turf.bbox(turf.multiLineString(lines)) 1423 | }); 1424 | } 1425 | 1426 | const steps = []; 1427 | for (const coordinate of addition.geometry.coordinates) { 1428 | const nodeDistances = []; 1429 | const vertexDistances = []; 1430 | const pt = turf.point(coordinate); 1431 | for (const node of nodes) { 1432 | const distance = turf.distance(pt, node[1]); 1433 | nodeDistances.push({ 1434 | id: node[0], 1435 | distance: distance 1436 | }); 1437 | } 1438 | for (const vertex of vertices) { 1439 | const distance = turf.distance(pt, vertex[1]); 1440 | vertexDistances.push({ 1441 | id: vertex[0], 1442 | distance: distance 1443 | }); 1444 | } 1445 | nodeDistances.sort((a, b) => { 1446 | return a.distance - b.distance; 1447 | }); 1448 | vertexDistances.sort((a, b) => { 1449 | return a.distance - b.distance; 1450 | }); 1451 | let closestNode; 1452 | let closestVertex; 1453 | if (nodeDistances.length) { 1454 | closestNode = nodeDistances[0]; 1455 | } 1456 | if (vertexDistances.length) { 1457 | closestVertex = vertexDistances[0]; 1458 | } 1459 | 1460 | if (process.env.DEBUG) { 1461 | for (const item of nodeDistances) { 1462 | const line = turf.lineString([ 1463 | coordinate, 1464 | nodes.get(item.id).geometry.coordinates 1465 | ]); 1466 | debug({ 1467 | type: "draw", 1468 | geometry: line.geometry, 1469 | style: { 1470 | width: 1, 1471 | color: DEBUG_COLOR_1, 1472 | opacity: 0.9 1473 | }, 1474 | fade: 3000 1475 | }); 1476 | } 1477 | 1478 | for (const item of vertexDistances) { 1479 | const line = turf.lineString([ 1480 | coordinate, 1481 | vertices.get(item.id).geometry.coordinates 1482 | ]); 1483 | debug({ 1484 | type: "draw", 1485 | geometry: line.geometry, 1486 | style: { 1487 | width: 1, 1488 | color: DEBUG_COLOR_4, 1489 | opacity: 0.9 1490 | }, 1491 | fade: 3000 1492 | }); 1493 | } 1494 | } 1495 | 1496 | if (closestNode.distance <= MAX_NODE_SHIFT) { 1497 | if (process.env.DEBUG) { 1498 | debug({ 1499 | type: "draw", 1500 | geometry: nodes.get(closestNode.id).geometry, 1501 | style: { 1502 | width: 20, 1503 | color: DEBUG_COLOR_1, 1504 | opacity: 0.9 1505 | }, 1506 | fade: 8000 1507 | }); 1508 | } 1509 | steps.push({ 1510 | type: "node", 1511 | id: closestNode.id 1512 | }); 1513 | continue; 1514 | } else if (closestVertex.distance <= MAX_VERTEX_SHIFT) { 1515 | if (process.env.DEBUG) { 1516 | debug({ 1517 | type: "draw", 1518 | geometry: vertices.get(closestVertex.id).geometry, 1519 | style: { 1520 | width: 20, 1521 | color: DEBUG_COLOR_1, 1522 | opacity: 0.9 1523 | }, 1524 | fade: 8000 1525 | }); 1526 | } 1527 | steps.push({ 1528 | type: "vertex", 1529 | id: closestVertex.id 1530 | }); 1531 | continue; 1532 | } else { 1533 | steps.push({ 1534 | type: "insert", 1535 | id: "n?" + this.id++, 1536 | coordinate: coordinate 1537 | }); 1538 | continue; 1539 | } 1540 | } 1541 | 1542 | let next = steps.shift(); 1543 | let insert = [next]; 1544 | while (steps.length) { 1545 | next = steps.shift(); 1546 | if (next) { 1547 | insert.push(next); 1548 | 1549 | if (next.type === "node" || next.type === "vertex") { 1550 | // insert edge 1551 | const id = "e?" + this.id++; 1552 | const refs = []; 1553 | for (const item of insert) { 1554 | refs.push(item.id); 1555 | } 1556 | this.edges.set(id, refs); 1557 | 1558 | // normalize 1559 | const start = this.nodes.get(refs[0]); 1560 | if (start) { 1561 | // update existing node 1562 | start.add(id); 1563 | this.nodes.set(refs[0], start); 1564 | } else { 1565 | // create new node 1566 | this.nodes.set(next.id, new Set()); 1567 | // split edges 1568 | 1569 | /* todo: split edges if a vertex was upgraded 1570 | for (const candidate of candidates) { 1571 | const candidateRefs = this.edges.get(candidate.id); 1572 | } 1573 | */ 1574 | } 1575 | const end = this.nodes.get(refs[refs.length - 1]); 1576 | if (end) { 1577 | // update existing node 1578 | end.add(id); 1579 | this.nodes.set(refs[refs.length - 1], end); 1580 | } else { 1581 | // create new node 1582 | this.nodes.set(next.id, new Set()); 1583 | 1584 | // split edges 1585 | /* todo: split edges if a vertex was upgraded 1586 | for (const candidate of candidates) { 1587 | const candidateRefs = this.edges.get(candidate.id); 1588 | } 1589 | */ 1590 | } 1591 | 1592 | // new edge 1593 | insert = [next]; 1594 | } 1595 | } 1596 | } 1597 | }; 1598 | 1599 | Mashnet.prototype.toJSON = function() { 1600 | // serialize 1601 | const json = { 1602 | edges: [], 1603 | vertices: [], 1604 | nodes: [], 1605 | metadata: [], 1606 | nodetree: this.nodetree.toJSON(), 1607 | edgetree: this.edgetree.toJSON(), 1608 | id: this.id 1609 | }; 1610 | 1611 | for (const edge of this.edges) { 1612 | json.edges.push(edge); 1613 | } 1614 | for (const vertex of this.vertices) { 1615 | json.vertices.push(vertex); 1616 | } 1617 | for (const node of this.nodes) { 1618 | json.nodes.push(node); 1619 | } 1620 | for (const data of this.metadata) { 1621 | json.metadata.push(data); 1622 | } 1623 | 1624 | return json; 1625 | }; 1626 | 1627 | Mashnet.prototype.fromJSON = function(json) { 1628 | // deserialize 1629 | for (const edge of json.edges) { 1630 | this.edges.set(edge[0], edge[1]); 1631 | } 1632 | for (const vertex of json.vertices) { 1633 | this.vertices.set(vertex[0], vertex[1]); 1634 | } 1635 | for (const node of json.nodes) { 1636 | this.nodes.set(node[0], node[1]); 1637 | } 1638 | for (const data of json.metadata) { 1639 | this.metadata.set(data[0], data[1]); 1640 | } 1641 | this.edgetree = this.edgetree.fromJSON(json.edgetree); 1642 | this.nodetree = this.nodetree.fromJSON(json.nodetree); 1643 | this.id = json.id; 1644 | }; 1645 | 1646 | module.exports = Mashnet; 1647 | -------------------------------------------------------------------------------- /src/normalizer.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // FORKED FROM https://github.com/mapbox/graph-normalizer 3 | 4 | var lineString = require("turf-linestring"); 5 | 6 | /** 7 | * Given ways, split any ways that cross over an intersections 8 | * @param {Object} ways an array of ways 9 | * @param {Object} options an options object defining mergeHighways, mergeTunnels, and mergeBridges (all default to false) 10 | * @return {Object} ways another array of ways 11 | */ 12 | function mergeWays(wayList, options) { 13 | // default options 14 | if (!options) options = {}; 15 | options.mergeHighways = 16 | options.mergeHighways === undefined ? false : options.mergeHighways; 17 | options.mergeTunnels = 18 | options.mergeTunnels === undefined ? false : options.mergeTunnels; 19 | options.mergeBridges = 20 | options.mergeBridges === undefined ? false : options.mergeBridges; 21 | options.mergeJunctions = 22 | options.mergeJunctions === undefined ? false : options.mergeJunctions; 23 | options.mergeAccess = 24 | options.mergeAccess === undefined ? false : options.mergeAccess; 25 | 26 | // build node and way hashes 27 | var nodes = new Map(); 28 | var ways = {}; 29 | 30 | wayList.forEach(function(way) { 31 | // normalize oneways to always equal 0 (bidirectional) or 1 (oneway in direction of coords) 32 | if (way.properties.oneway === -1) { 33 | way.properties.oneway = 1; 34 | way.properties.refs = way.properties.refs.reverse(); 35 | way.geometry.coordinates = way.geometry.coordinates.reverse(); 36 | } 37 | 38 | ways[way.properties.id] = way; 39 | way.properties.refs.forEach(function(ref) { 40 | if (!nodes.has(ref)) nodes.set(ref, new Set()); 41 | 42 | nodes.get(ref).add(way.properties.id); 43 | }); 44 | }); 45 | 46 | // build merge queue 47 | nodes.forEach(function(ownerIds, node) { 48 | // delete nodes that do not have exactly 2 owners 49 | // nodes with < 2 owners are non terminal nodeHash 50 | // nodes with > 2 oweners are intersections 51 | if (ownerIds.size !== 2) nodes.delete(node); 52 | }); 53 | 54 | // filter merges with mismatched highway or oneway tags 55 | nodes.forEach(function(ownerIds, node) { 56 | var owners = []; 57 | ownerIds.forEach(function(id) { 58 | owners.push(ways[id]); 59 | }); 60 | 61 | if ( 62 | owners[0].properties.oneway !== owners[1].properties.oneway || 63 | (!options.mergeHighways && 64 | owners[0].properties.highway !== owners[1].properties.highway) || 65 | (!options.mergeBridges && 66 | owners[0].properties.bridge !== owners[1].properties.bridge) || 67 | (!options.mergeTunnels && 68 | owners[0].properties.tunnel !== owners[1].properties.tunnel) || 69 | (!options.mergeJunctions && 70 | owners[0].properties.junction !== owners[1].properties.junction) || 71 | (!options.mergeAccess && 72 | owners[0].properties.access !== owners[1].properties.access) 73 | ) 74 | nodes.delete(node); 75 | }); 76 | 77 | // keep merging until all merge nodes have been eliminated 78 | while (nodes.size) { 79 | var nodeIterator = nodes.keys(); 80 | var nodeId = nodeIterator.next().value; 81 | var node = nodes.get(nodeId); 82 | 83 | var owners = []; 84 | 85 | node.forEach(function(id) { 86 | owners.push(ways[id]); 87 | }); 88 | 89 | var opening = null; 90 | var closing = null; 91 | var validMerge = true; 92 | 93 | // if owners < 2, this way cannot be merged due to an edge case 94 | if ( 95 | owners.filter(function(owner) { 96 | return owner; 97 | }).length === 2 98 | ) { 99 | if (owners[0].properties.oneway === 1) { 100 | // oneway merge 101 | // assign opening and closing way 102 | owners.forEach(function(owner) { 103 | if ( 104 | owner.properties.refs[owner.properties.refs.length - 1] === nodeId 105 | ) { 106 | opening = owner; 107 | } else if (owner.properties.refs[0] === nodeId) { 108 | closing = owner; 109 | } 110 | }); 111 | // if an opening and closing way were not found, 112 | // the ways do not face the same direction 113 | if (!opening || !closing) validMerge = false; 114 | } else { 115 | // bidirectional merge 116 | 117 | // We order the ways in order of ids to ensure ID consistency. 118 | if (owners[0].properties.id < owners[1].properties.id) { 119 | opening = owners[0]; 120 | closing = owners[1]; 121 | } else { 122 | opening = owners[1]; 123 | closing = owners[0]; 124 | } 125 | 126 | // if opening and closing are not present for a bidirectional... 127 | // most likely one of the ways loops in on itself in an odd way 128 | if (!opening || !closing) validMerge = false; 129 | else { 130 | // flip bidirectional ways if they are not oriented correctly 131 | if ( 132 | opening.properties.refs[opening.properties.refs.length - 1] !== 133 | nodeId 134 | ) { 135 | opening.properties.refs = opening.properties.refs.reverse(); 136 | opening.geometry.coordinates = opening.geometry.coordinates.reverse(); 137 | } 138 | 139 | if (closing.properties.refs[0] !== nodeId) { 140 | closing.properties.refs = closing.properties.refs.reverse(); 141 | closing.geometry.coordinates = closing.geometry.coordinates.reverse(); 142 | } 143 | } 144 | } 145 | } else validMerge = false; 146 | 147 | if (validMerge) { 148 | // combine the opening way with the closing way 149 | // omit the first ref of the closing way to avoid repeating the shared node 150 | var combined = lineString( 151 | opening.geometry.coordinates.concat( 152 | closing.geometry.coordinates.slice( 153 | 1, 154 | closing.geometry.coordinates.length 155 | ) 156 | ), 157 | { 158 | id: opening.properties.id + "," + closing.properties.id, 159 | refs: opening.properties.refs.concat( 160 | closing.properties.refs.slice(1, closing.properties.refs.length) 161 | ) 162 | } 163 | ); 164 | 165 | // persist oneway, highway, bridge, tunnel, junction and access tags if they are present 166 | if (opening.properties.hasOwnProperty("oneway")) 167 | combined.properties.oneway = opening.properties.oneway; 168 | 169 | if (options.mergeHighways) { 170 | // if highway tags are the same, keep them, else set as unclassified 171 | if ( 172 | opening.properties.hasOwnProperty("highway") && 173 | closing.properties.hasOwnProperty("highway") && 174 | opening.properties.highway === closing.properties.highway 175 | ) { 176 | combined.properties.highway = opening.properties.highway; 177 | } else { 178 | combined.properties.highway = "unclassified"; 179 | } 180 | } else if (opening.properties.hasOwnProperty("highway")) 181 | combined.properties.highway = opening.properties.highway; 182 | 183 | if (options.mergeBridges) { 184 | if (opening.properties.hasOwnProperty("bridge")) 185 | combined.properties.bridge = opening.properties.bridge; 186 | else if (closing.properties.hasOwnProperty("bridge")) 187 | combined.properties.bridge = closing.properties.bridge; 188 | } else if (opening.properties.hasOwnProperty("bridge")) 189 | combined.properties.bridge = opening.properties.bridge; 190 | 191 | if (options.mergeTunnels) { 192 | if (opening.properties.hasOwnProperty("tunnel")) 193 | combined.properties.tunnel = opening.properties.tunnel; 194 | else if (closing.properties.hasOwnProperty("tunnel")) 195 | combined.properties.tunnel = closing.properties.tunnel; 196 | } else if (opening.properties.hasOwnProperty("tunnel")) 197 | combined.properties.tunnel = opening.properties.tunnel; 198 | 199 | if (options.mergeJunctions) { 200 | if (opening.properties.hasOwnProperty("junction")) 201 | combined.properties.junction = opening.properties.junction; 202 | else if (closing.properties.hasOwnProperty("junction")) 203 | combined.properties.junction = closing.properties.junction; 204 | } else if (opening.properties.hasOwnProperty("junction")) 205 | combined.properties.junction = opening.properties.junction; 206 | 207 | if (options.mergeAccess) { 208 | if (opening.properties.hasOwnProperty("access")) 209 | combined.properties.access = opening.properties.access; 210 | else if (closing.properties.hasOwnProperty("access")) 211 | combined.properties.access = closing.properties.access; 212 | } else if (opening.properties.hasOwnProperty("access")) 213 | combined.properties.access = opening.properties.access; 214 | 215 | // insert combined way into hash 216 | ways[combined.properties.id] = combined; 217 | 218 | // update terminal nodes of combined way 219 | // patch starting node 220 | if (nodes.has(combined.properties.refs[0])) { 221 | var starting = nodes.get(combined.properties.refs[0]); 222 | starting.delete(opening.properties.id); 223 | starting.delete(closing.properties.id); 224 | starting.add(combined.properties.id); 225 | } 226 | 227 | // patch ending node 228 | if ( 229 | nodes.has(combined.properties.refs[combined.properties.refs.length - 1]) 230 | ) { 231 | var ending = nodes.get( 232 | combined.properties.refs[combined.properties.refs.length - 1] 233 | ); 234 | ending.delete(opening.properties.id); 235 | ending.delete(closing.properties.id); 236 | ending.add(combined.properties.id); 237 | } 238 | 239 | // delete merged ways from hash 240 | delete ways[opening.properties.id]; 241 | delete ways[closing.properties.id]; 242 | } 243 | // delete merged node from heap 244 | nodes.delete(nodeId); 245 | } 246 | 247 | var merged = Object.keys(ways).map(function(id) { 248 | return ways[id]; 249 | }); 250 | 251 | return merged; 252 | } 253 | 254 | /** 255 | * Given ways, split any ways that cross over an intersections 256 | * @param {Object} ways an array of ways 257 | * @return {Object} ways another array of ways 258 | */ 259 | 260 | function splitWays(ways) { 261 | // construct node hash 262 | // nodeHash is a hash of nodes => ways 263 | // each way represents a node "owner" 264 | var nodeHash = {}; 265 | ways.forEach(function(way) { 266 | way.properties.refs.forEach(function(ref) { 267 | if (!nodeHash[ref]) nodeHash[ref] = 0; 268 | 269 | nodeHash[ref] += 1; 270 | }); 271 | }); 272 | 273 | var splitWays = []; 274 | 275 | ways.forEach(function(way) { 276 | var splits = 0; 277 | var last = 0; 278 | var current = 0; 279 | 280 | way.properties.refs.forEach(function(ref, i) { 281 | current++; 282 | 283 | // ignore terminal nodes 284 | if (i > 0 && i < way.properties.refs.length - 1) { 285 | // find the number of ways that contain the node 286 | var ownerCount = nodeHash[ref]; 287 | 288 | // look for nodes with more than 1 owner 289 | if (ownerCount > 1) { 290 | // add front of split way to splitWays 291 | var waySlice = lineString( 292 | way.geometry.coordinates.slice(last, current), 293 | { 294 | id: way.properties.id + "!" + splits, 295 | refs: way.properties.refs.slice(last, current) 296 | } 297 | ); 298 | 299 | // persist these tags if they are present: 300 | if (way.properties.hasOwnProperty("oneway")) 301 | waySlice.properties.oneway = way.properties.oneway; 302 | if (way.properties.hasOwnProperty("highway")) 303 | waySlice.properties.highway = way.properties.highway; 304 | if (way.properties.hasOwnProperty("bridge")) 305 | waySlice.properties.bridge = way.properties.bridge; 306 | if (way.properties.hasOwnProperty("tunnel")) 307 | waySlice.properties.tunnel = way.properties.tunnel; 308 | if (way.properties.hasOwnProperty("name")) 309 | waySlice.properties.name = way.properties.name; 310 | if (way.properties.hasOwnProperty("ref")) 311 | waySlice.properties.ref = way.properties.ref; 312 | if (way.properties.hasOwnProperty("access")) 313 | waySlice.properties.access = way.properties.access; 314 | if (way.properties.hasOwnProperty("junction")) 315 | waySlice.properties.junction = way.properties.junction; 316 | 317 | splitWays.push(waySlice); 318 | 319 | splits++; 320 | last = i; 321 | } 322 | } 323 | }); 324 | 325 | // add the remainder of the way 326 | if (last < current) { 327 | var waySlice = lineString(way.geometry.coordinates.slice(last, current), { 328 | id: way.properties.id + "!" + splits, 329 | refs: way.properties.refs.slice(last, current) 330 | }); 331 | 332 | // persist these tags if they are present: 333 | if (way.properties.hasOwnProperty("oneway")) 334 | waySlice.properties.oneway = way.properties.oneway; 335 | if (way.properties.hasOwnProperty("highway")) 336 | waySlice.properties.highway = way.properties.highway; 337 | if (way.properties.hasOwnProperty("bridge")) 338 | waySlice.properties.bridge = way.properties.bridge; 339 | if (way.properties.hasOwnProperty("tunnel")) 340 | waySlice.properties.tunnel = way.properties.tunnel; 341 | if (way.properties.hasOwnProperty("name")) 342 | waySlice.properties.name = way.properties.name; 343 | if (way.properties.hasOwnProperty("ref")) 344 | waySlice.properties.ref = way.properties.ref; 345 | if (way.properties.hasOwnProperty("access")) 346 | waySlice.properties.access = way.properties.access; 347 | if (way.properties.hasOwnProperty("junction")) 348 | waySlice.properties.junction = way.properties.junction; 349 | 350 | splitWays.push(waySlice); 351 | } 352 | }); 353 | 354 | return splitWays; 355 | } 356 | 357 | module.exports = function(ways, opts) { 358 | ways = splitWays(ways); 359 | ways = mergeWays(ways); 360 | return ways; 361 | }; 362 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const test = require("tap").test; 4 | const path = require("path"); 5 | // const turf = require('@turf/turf'); 6 | 7 | const Mashnet = require("../src/index.js"); 8 | 9 | test("mashnet", async t => { 10 | const honolulu = require(path.join(__dirname, "../samples/honolulu.json")); 11 | 12 | const net = new Mashnet(honolulu); 13 | 14 | const addition = { 15 | type: "Feature", 16 | properties: {}, 17 | geometry: { 18 | type: "LineString", 19 | coordinates: [ 20 | [-157.9146158695221, 21.346424354025306], 21 | [-157.9154634475708, 21.347043906401122], 22 | [-157.9165470600128, 21.348442886005444] 23 | ] 24 | } 25 | }; 26 | 27 | // scan 28 | 29 | const scores = net.scan(addition); 30 | 31 | t.ok(scores.length > 0, "found matches"); 32 | t.equal(scores[0].line.type, "Feature", "result contains matched feature"); 33 | 34 | // match 35 | 36 | const isMatch = net.match(scores); 37 | 38 | t.ok(isMatch, "returns a match score"); 39 | 40 | const metadata = { 41 | max_speed: 70 42 | }; 43 | 44 | // merge 45 | 46 | net.merge(scores[0].id, metadata); 47 | 48 | const data = net.metadata.get(scores[0].id); 49 | t.equal( 50 | JSON.stringify(data), 51 | '{"highway":"residential","name":"Ala Akulikuli Street","max_speed":70}', 52 | "metadata merged" 53 | ); 54 | 55 | // query 56 | 57 | const bbox = [ 58 | -157.84507155418396, 59 | 21.29764138193422, 60 | -157.84247517585754, 61 | 21.299940472209933 62 | ]; 63 | const subgraph = net.query(bbox); 64 | 65 | t.ok(subgraph.edges.size, "subgraph edges present"); 66 | t.ok(subgraph.nodes.size, "subgraph nodes present"); 67 | t.ok(subgraph.vertices.size, "subgraph vertices present"); 68 | t.ok(subgraph.edgeTree.all().length, "subgraph edgeTree present"); 69 | t.ok(subgraph.nodeTree.all().length, "subgraph nodeTree present"); 70 | 71 | // snap 72 | 73 | const street = { 74 | type: "Feature", 75 | properties: {}, 76 | geometry: { 77 | type: "LineString", 78 | coordinates: [ 79 | [-157.91675090789795, 21.380355978162594], 80 | [-157.9176950454712, 21.378317904666634], 81 | [-157.91451930999756, 21.37412178163886], 82 | [-157.9172658920288, 21.36864665932247], 83 | [-157.91460514068604, 21.358894839625684] 84 | ] 85 | } 86 | }; 87 | 88 | const snaps = net.snap(street); 89 | 90 | t.equal(snaps.length, 492, "snap phantoms to network"); 91 | 92 | // split 93 | 94 | const splits = net.split(snaps); 95 | 96 | t.equal(splits.length, 35, "splits snaps into chunks"); 97 | 98 | // materialize 99 | 100 | const changesets = net.materialize(splits); 101 | 102 | t.equal(changesets.length, 35, "creates geojson linestrings from splits"); 103 | 104 | // visualize changesets: 105 | // console.log(JSON.stringify(turf.featureCollection(changesets))); 106 | 107 | // commit 108 | 109 | net.commit(splits); 110 | 111 | t.done(); 112 | }); 113 | -------------------------------------------------------------------------------- /train/evaluate.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const turf = require("@turf/turf"); 4 | const rimraf = require("rimraf"); 5 | const mkdirp = require("mkdirp"); 6 | const brain = require("brain.js"); 7 | const Chance = require("chance"); 8 | const Mashnet = require("../src/index.js"); 9 | 10 | const SHIFT = 0.005; 11 | const JITTER = 0.0008; 12 | const UNITS = { units: "kilometers" }; 13 | MATCH_DEPTH = 5; 14 | 15 | const honolulu = require(path.join(__dirname, "../samples/honolulu.json")); 16 | const chance = new Chance(); 17 | 18 | var net = new Mashnet(honolulu); 19 | 20 | var i = 0; 21 | var total = 0; 22 | var misses = 0; 23 | var fakes = 0; 24 | for (let edge of net.edges) { 25 | i++; 26 | if (i > 0) { 27 | total++; 28 | console.log( 29 | "misses: " + 30 | (misses / total).toFixed(4) + 31 | "% - fakes: " + 32 | (fakes / total).toFixed(4) + 33 | "% total: " + 34 | total 35 | ); 36 | 37 | var fake = perturb(net, edge[1]); 38 | 39 | if (chance.bool()) { 40 | // drop 41 | var copy = JSON.parse(JSON.stringify(edge)); 42 | net.edges.delete(edge[0]); 43 | net.edgetree.remove(treecopy(net, edge), (a, b) => { 44 | return a.id === b.id; 45 | }); 46 | 47 | // match 48 | const scores = net.scan(fake); 49 | const prediction = net.match(scores); 50 | 51 | if (prediction > 0.9) { 52 | fakes++; 53 | fake.properties.match = prediction; 54 | console.log("fake"); 55 | console.log( 56 | JSON.stringify( 57 | turf.featureCollection([ 58 | fake, 59 | turf.lineString(scores[0].line.geometry.coordinates) 60 | ]) 61 | ) 62 | ); 63 | } 64 | 65 | // reinsert 66 | net.edges.set(copy[0], copy[1]); 67 | net.edgetree.insert(treecopy(net, edge)); 68 | } else { 69 | // match 70 | const scores = net.scan(fake); 71 | const prediction = net.match(scores); 72 | 73 | if (prediction < 0.1) { 74 | misses++; 75 | fake.properties.match = prediction; 76 | console.log("miss"); 77 | console.log( 78 | JSON.stringify( 79 | turf.featureCollection([ 80 | fake, 81 | turf.lineString(scores[0].line.geometry.coordinates) 82 | ]) 83 | ) 84 | ); 85 | } 86 | } 87 | } 88 | } 89 | 90 | function perturb(net, edge) { 91 | const shift = chance.normal() * SHIFT; 92 | const drift = Math.random() * 360; 93 | 94 | var coordinates = []; 95 | for (let ref of edge) { 96 | var vertex = net.vertices.get(ref); 97 | var point = turf.point(vertex); 98 | var shifted = turf.destination(point, shift, drift, UNITS); 99 | var jittered = turf.destination( 100 | shifted, 101 | chance.normal() * JITTER, 102 | Math.random() * 360, 103 | UNITS 104 | ); 105 | coordinates.push(jittered.geometry.coordinates); 106 | } 107 | var line = turf.lineString(coordinates, { stroke: "#F46BFF" }); 108 | return line; 109 | } 110 | 111 | function treecopy(net, edge) { 112 | var minX = Infinity; 113 | var minY = Infinity; 114 | var maxX = -Infinity; 115 | var maxY = -Infinity; 116 | for (let ref of edge[1]) { 117 | var vertex = net.vertices.get(ref); 118 | if (vertex[0] < minX) minX = vertex[0]; 119 | if (vertex[1] < minY) minY = vertex[1]; 120 | if (vertex[0] > maxX) maxX = vertex[0]; 121 | if (vertex[1] > maxY) maxY = vertex[1]; 122 | } 123 | return { 124 | minX: minX, 125 | minY: minY, 126 | maxX: maxX, 127 | maxY: maxY, 128 | id: edge[0] 129 | }; 130 | } 131 | -------------------------------------------------------------------------------- /train/match-cache.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const turf = require("@turf/turf"); 4 | const rimraf = require("rimraf"); 5 | const mkdirp = require("mkdirp"); 6 | const brain = require("brain.js"); 7 | const Chance = require("chance"); 8 | const Mashnet = require("../src/index.js"); 9 | 10 | const SHIFT = 0.003; 11 | const JITTER = 0.0008; 12 | const UNITS = { units: "kilometers" }; 13 | const TRAIN_COUNT = 30000; 14 | const ITERATIONS = 10000; 15 | const MATCH_DEPTH = 5; 16 | 17 | const honolulu = require(path.join(__dirname, "../samples/honolulu.json")); 18 | const chance = new Chance(); 19 | const modelDir = path.join(__dirname, "../model/"); 20 | const model = path.join(modelDir, "match.json"); 21 | const cache = path.join(modelDir, "cache.json"); 22 | mkdirp.sync(modelDir); 23 | 24 | var net = new Mashnet(honolulu); 25 | 26 | var samples = []; 27 | var i = 0; 28 | for (let edge of net.edges) { 29 | i++; 30 | console.log(i); 31 | if (i < TRAIN_COUNT) { 32 | var fake = perturb(net, edge[1]); 33 | 34 | if (chance.bool()) { 35 | // drop 36 | var copy = JSON.parse(JSON.stringify(edge)); 37 | net.edges.delete(edge[0]); 38 | net.edgetree.remove(treecopy(net, edge), (a, b) => { 39 | return a.id === b.id; 40 | }); 41 | 42 | // match 43 | const scores = net.scan(fake); 44 | var input = {}; 45 | for (let k = 0; k < MATCH_DEPTH; k++) { 46 | if (scores[k]) { 47 | input["distance_" + k] = scores[k].distance; 48 | input["scale_" + k] = scores[k].scale; 49 | input["straight_" + k] = scores[k].straight; 50 | input["curve_" + k] = scores[k].curve; 51 | input["scan_" + k] = scores[k].scan; 52 | input["terminal_" + k] = scores[k].terminal; 53 | input["bearing_" + k] = scores[k].bearing; 54 | } else { 55 | input["distance_" + k] = 0.0; 56 | input["scale_" + k] = 0.0; 57 | input["straight_" + k] = 0.0; 58 | input["curve_" + k] = 0.0; 59 | input["scan_" + k] = 0.0; 60 | input["terminal_" + k] = 0.0; 61 | input["bearing_" + k] = 0.0; 62 | } 63 | } 64 | fs.appendFileSync( 65 | cache, 66 | JSON.stringify({ 67 | input: input, 68 | output: { match: 0 } 69 | }) + "\n" 70 | ); 71 | 72 | // reinsert 73 | net.edges.set(copy[0], copy[1]); 74 | net.edgetree.insert(treecopy(net, edge)); 75 | } else { 76 | // match 77 | const scores = net.scan(fake); 78 | console.log( 79 | JSON.stringify( 80 | turf.featureCollection([ 81 | turf.lineString(scores[0].line.geometry.coordinates, { 82 | stroke: "#f0f" 83 | }), 84 | turf.lineString(fake.geometry.coordinates, { stroke: "#0ff" }) 85 | ]) 86 | ) 87 | ); 88 | var input = {}; 89 | for (let k = 0; k < MATCH_DEPTH; k++) { 90 | if (scores[k]) { 91 | input["distance_" + k] = scores[k].distance; 92 | input["scale_" + k] = scores[k].scale; 93 | input["straight_" + k] = scores[k].straight; 94 | input["curve_" + k] = scores[k].curve; 95 | input["scan_" + k] = scores[k].scan; 96 | input["terminal_" + k] = scores[k].terminal; 97 | input["bearing_" + k] = scores[k].bearing; 98 | } else { 99 | input["distance_" + k] = 0.0; 100 | input["scale_" + k] = 0.0; 101 | input["straight_" + k] = 0.0; 102 | input["curve_" + k] = 0.0; 103 | input["scan_" + k] = 0.0; 104 | input["terminal_" + k] = 0.0; 105 | input["bearing_" + k] = 0.0; 106 | } 107 | } 108 | fs.appendFileSync( 109 | cache, 110 | JSON.stringify({ 111 | input: input, 112 | output: { match: 1 } 113 | }) + "\n" 114 | ); 115 | } 116 | } 117 | } 118 | 119 | function perturb(net, edge) { 120 | const shift = chance.normal() * SHIFT; 121 | const drift = Math.random() * 360; 122 | 123 | var coordinates = []; 124 | for (let ref of edge) { 125 | var vertex = net.vertices.get(ref); 126 | var point = turf.point(vertex); 127 | var shifted = turf.destination(point, shift, drift, UNITS); 128 | var jittered = turf.destination( 129 | shifted, 130 | chance.normal() * JITTER, 131 | Math.random() * 360, 132 | UNITS 133 | ); 134 | coordinates.push(jittered.geometry.coordinates); 135 | } 136 | var line = turf.lineString(coordinates, { stroke: "#F46BFF" }); 137 | return line; 138 | } 139 | 140 | function treecopy(net, edge) { 141 | var minX = Infinity; 142 | var minY = Infinity; 143 | var maxX = -Infinity; 144 | var maxY = -Infinity; 145 | for (let ref of edge[1]) { 146 | var vertex = net.vertices.get(ref); 147 | if (vertex[0] < minX) minX = vertex[0]; 148 | if (vertex[1] < minY) minY = vertex[1]; 149 | if (vertex[0] > maxX) maxX = vertex[0]; 150 | if (vertex[1] > maxY) maxY = vertex[1]; 151 | } 152 | return { 153 | minX: minX, 154 | minY: minY, 155 | maxX: maxX, 156 | maxY: maxY, 157 | id: edge[0] 158 | }; 159 | } 160 | -------------------------------------------------------------------------------- /train/match-train.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const mkdirp = require("mkdirp"); 4 | const brain = require("brain.js"); 5 | 6 | const ITERATIONS = 15000; 7 | 8 | const modelDir = path.join(__dirname, "../model/"); 9 | const model = path.join(modelDir, "match.json"); 10 | const cachePath = path.join(modelDir, "cache.json"); 11 | mkdirp.sync(modelDir); 12 | 13 | const nn = new brain.NeuralNetwork(); 14 | 15 | const cache = fs 16 | .readFileSync(cachePath) 17 | .toString() 18 | .split("\n") 19 | .filter(line => { 20 | return line.length > 0; 21 | }) 22 | .map(JSON.parse); 23 | console.log(cache.length + " samples"); 24 | nn.train(cache.slice(0, 50000), { 25 | log: true, 26 | logPeriod: 10, 27 | iterations: ITERATIONS, 28 | learningRate: 0.2, 29 | errorThresh: 0.001 30 | }); 31 | fs.writeFileSync(model, JSON.stringify(nn.toJSON())); 32 | -------------------------------------------------------------------------------- /train/match.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const fs = require("fs"); 3 | const turf = require("@turf/turf"); 4 | const rimraf = require("rimraf"); 5 | const mkdirp = require("mkdirp"); 6 | const brain = require("brain.js"); 7 | const Chance = require("chance"); 8 | const Mashnet = require("../src/index.js"); 9 | 10 | const SHIFT = 0.003; 11 | const JITTER = 0.0005; 12 | const UNITS = { units: "kilometers" }; 13 | const TRAIN_COUNT = 10000; 14 | const ITERATIONS = 10000; 15 | 16 | const honolulu = require(path.join(__dirname, "./fixtures/honolulu.json")); 17 | const chance = new Chance(); 18 | const modelDir = path.join(__dirname, "../model/"); 19 | const model = path.join(modelDir, "match.json"); 20 | mkdirp.sync(modelDir); 21 | 22 | var net = new Mashnet(honolulu); 23 | 24 | var samples = []; 25 | var i = 0; 26 | for (let edge of net.edges) { 27 | i++; 28 | console.log(i); 29 | if (i < TRAIN_COUNT) { 30 | var fake = perturb(net, edge[1]); 31 | 32 | if (chance.bool()) { 33 | // drop 34 | var copy = JSON.parse(JSON.stringify(edge)); 35 | net.edges.delete(edge[0]); 36 | net.edgetree.remove(treecopy(net, edge), (a, b) => { 37 | return a.id === b.id; 38 | }); 39 | 40 | // match 41 | const match = net.match(fake)[0]; 42 | if (match && match.score) 43 | samples.push({ 44 | input: { 45 | distance: match.distance, 46 | scale: match.scale, 47 | straight: match.straight, 48 | curve: match.curve, 49 | scan: match.scan, 50 | terminal: match.terminal, 51 | bearing: match.bearing, 52 | softmax: match.softmax 53 | }, 54 | output: { match: 0 } 55 | }); 56 | 57 | // reinsert 58 | net.edges.set(copy[0], copy[1]); 59 | net.edgetree.insert(treecopy(net, edge)); 60 | } else { 61 | // match 62 | const match = net.match(fake)[0]; 63 | samples.push({ 64 | input: { 65 | distance: match.distance, 66 | scale: match.scale, 67 | straight: match.straight, 68 | curve: match.curve, 69 | scan: match.scan, 70 | terminal: match.terminal, 71 | bearing: match.bearing, 72 | softmax: match.softmax 73 | }, 74 | output: { match: 1 } 75 | }); 76 | } 77 | } 78 | } 79 | 80 | const nn = new brain.NeuralNetwork(); 81 | nn.train(samples, { 82 | log: true, 83 | logPeriod: 1000, 84 | iterations: ITERATIONS, 85 | learningRate: 0.1, 86 | errorThresh: 0.0005 87 | }); 88 | fs.writeFileSync(model, JSON.stringify(nn.toJSON())); 89 | 90 | console.log("done."); 91 | 92 | function perturb(net, edge) { 93 | const shift = chance.normal() * SHIFT; 94 | const drift = Math.random() * 360; 95 | 96 | var coordinates = []; 97 | for (let ref of edge) { 98 | var vertex = net.vertices.get(ref); 99 | var point = turf.point(vertex); 100 | var shifted = turf.destination(point, shift, drift, UNITS); 101 | var jittered = turf.destination( 102 | shifted, 103 | chance.normal() * JITTER, 104 | Math.random() * 360, 105 | UNITS 106 | ); 107 | coordinates.push(jittered.geometry.coordinates); 108 | } 109 | var line = turf.lineString(coordinates, { stroke: "#F46BFF" }); 110 | return line; 111 | } 112 | 113 | function treecopy(net, edge) { 114 | var minX = Infinity; 115 | var minY = Infinity; 116 | var maxX = -Infinity; 117 | var maxY = -Infinity; 118 | for (let ref of edge[1]) { 119 | var vertex = net.vertices.get(ref); 120 | if (vertex[0] < minX) minX = vertex[0]; 121 | if (vertex[1] < minY) minY = vertex[1]; 122 | if (vertex[0] > maxX) maxX = vertex[0]; 123 | if (vertex[1] > maxY) maxY = vertex[1]; 124 | } 125 | return { 126 | minX: minX, 127 | minY: minY, 128 | maxX: maxX, 129 | maxY: maxY, 130 | id: edge[0] 131 | }; 132 | } 133 | -------------------------------------------------------------------------------- /utils/debugger.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | mashnet 6 | 7 | 8 | 9 | 25 | 26 | 27 |
28 |
29 | 227 | 228 | 229 | 230 | -------------------------------------------------------------------------------- /utils/debugger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // node debug-map.js ./actions.json ./animation.html 4 | 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | 8 | const html = fs.readFileSync(path.join(__dirname, "debugger.html")).toString(); 9 | 10 | const token = 11 | "pk.eyJ1IjoibW9yZ2FuaGVybG9ja2VyIiwiYSI6Ii1zLU4xOWMifQ.FubD68OEerk74AYCLduMZQ"; 12 | 13 | const actions = fs 14 | .readFileSync(process.argv[2]) 15 | .toString() 16 | .split("\n") 17 | .filter(line => { 18 | return line.length; 19 | }) 20 | .map(JSON.parse); 21 | /* .filter(line => { 22 | return line.type !== 'log'; 23 | });*/ 24 | 25 | const render = html 26 | .split("{{token}}") 27 | .join(token) 28 | .split("{{actions}}") 29 | .join(JSON.stringify(actions)); 30 | 31 | fs.writeFileSync(process.argv[3], render); 32 | -------------------------------------------------------------------------------- /utils/demo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const path = require("path"); 4 | 5 | const Mashnet = require("../src/index.js"); 6 | 7 | const honolulu = require(path.join(__dirname, "../samples/honolulu.json")); 8 | 9 | const net = new Mashnet(honolulu); 10 | /* 11 | var addition = { 12 | type: "Feature", 13 | properties: {}, 14 | geometry: { 15 | type: "LineString", 16 | coordinates: [ 17 | [-157.9146158695221, 21.346424354025306], 18 | [-157.9154634475708, 21.347043906401122], 19 | [-157.9165470600128, 21.348442886005444] 20 | ] 21 | } 22 | }; 23 | 24 | // scan 25 | 26 | var scores = net.scan(addition); 27 | 28 | 29 | // match 30 | 31 | const isMatch = net.match(scores); 32 | 33 | const metadata = { 34 | max_speed: 70 35 | }; 36 | 37 | // merge 38 | 39 | net.merge(scores[0].id, metadata); 40 | 41 | const data = net.metadata.get(scores[0].id); 42 | */ 43 | // add 44 | 45 | const street = { 46 | type: "Feature", 47 | properties: {}, 48 | geometry: { 49 | type: "LineString", 50 | coordinates: [ 51 | [-157.91604816913605, 21.35034147982776], 52 | [-157.91581213474274, 21.35018409732726], 53 | [-157.91565924882886, 21.350114149495003], 54 | [-157.91538298130035, 21.349984246289427], 55 | [-157.9150503873825, 21.34975441725907], 56 | [-157.91475266218185, 21.349584543396308] 57 | ] 58 | } 59 | }; 60 | 61 | net.add(street); 62 | -------------------------------------------------------------------------------- /utils/generate-fixture.js: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | "use strict"; 4 | 5 | // generate a fixture from a osm.pbf file 6 | // node generate-fixture honolulu.osm.pbf honolulu.json 7 | 8 | const fs = require("fs"); 9 | const through = require("through2"); 10 | const parser = require("osm-pbf-parser"); 11 | const turf = require("@turf/turf"); 12 | const normalize = require("../src/normalizer.js"); 13 | 14 | if (require.main === module) { 15 | if (!process.argv[2] || !process.argv[3]) { 16 | console.error(); 17 | console.error("Generate a graph friendly road network given an OSM PBF"); 18 | console.error(); 19 | console.error("Usage ./generate-fixture.js "); 20 | console.error(); 21 | process.exit(1); 22 | } 23 | 24 | const pbf = process.argv[2]; 25 | const fixture = process.argv[3]; 26 | 27 | run(pbf, fixture) 28 | .then(graph => { 29 | fs.writeFileSync(fixture, JSON.stringify(graph)); 30 | }) 31 | .catch(err => { 32 | throw err; 33 | }); 34 | } else { 35 | module.exports = run; 36 | } 37 | 38 | /** 39 | * Given the location of an OSM pbf file, 40 | * output a normalized graph 41 | * 42 | * @param {String} pbf path to osm.pbf 43 | * 44 | * @returns {Promise} 45 | */ 46 | async function run(pbf) { 47 | const ways = await loadPBF(pbf); 48 | const graph = normalize(ways); 49 | return graph; 50 | } 51 | 52 | async function loadPBF(pbf) { 53 | let data = { 54 | ways: [], 55 | nodes: new Map() 56 | }; 57 | data = await loadWays(pbf, data); 58 | data = await loadNodes(pbf, data); 59 | 60 | const edges = []; 61 | for (const way of data.ways) { 62 | const coordinates = []; 63 | let complete = true; 64 | for (const ref of way.refs) { 65 | const coordinate = data.nodes.get(ref); 66 | 67 | if (coordinate && coordinate.length === 2) { 68 | coordinates.push(coordinate); 69 | } else { 70 | complete = false; 71 | } 72 | } 73 | 74 | if (complete && coordinates.length >= 2) { 75 | const edge = turf.lineString(coordinates, { id: way.id, refs: way.refs }); 76 | for (const tag of Object.keys(way.tags)) { 77 | edge.properties[tag] = way.tags[tag]; 78 | } 79 | 80 | edges.push(edge); 81 | } 82 | } 83 | 84 | return edges; 85 | } 86 | 87 | async function loadWays(pbf, data) { 88 | return new Promise((resolve, reject) => { 89 | const parse = parser(); 90 | 91 | // load ways 92 | fs.createReadStream(pbf) 93 | .pipe(parse) 94 | .pipe( 95 | through.obj((items, enc, next) => { 96 | for (const item of items) { 97 | if (item.type === "way") { 98 | if (item.tags.highway) { 99 | data.ways.push(item); 100 | for (const ref of item.refs) { 101 | data.nodes.set(ref, []); 102 | } 103 | } 104 | } 105 | } 106 | next(); 107 | }) 108 | ) 109 | .on("finish", () => { 110 | resolve(data); 111 | }); 112 | }); 113 | } 114 | 115 | async function loadNodes(pbf, data) { 116 | return new Promise((resolve, reject) => { 117 | const parse = parser(); 118 | 119 | // load ways 120 | fs.createReadStream(pbf) 121 | .pipe(parse) 122 | .pipe( 123 | through.obj((items, enc, next) => { 124 | for (const item of items) { 125 | if (item.type === "node") { 126 | if (data.nodes.has(item.id)) { 127 | data.nodes.set(item.id, [item.lon, item.lat]); 128 | } 129 | } 130 | } 131 | next(); 132 | }) 133 | ) 134 | .on("finish", () => { 135 | resolve(data); 136 | }); 137 | }); 138 | } 139 | -------------------------------------------------------------------------------- /utils/graph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Draw GeoJSON points 6 | 7 | 8 | 9 | 13 | 14 | 15 |
16 | 71 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /utils/graph.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // node graph.js ./graph.json ./graph.html 4 | 5 | const fs = require("fs"); 6 | const path = require("path"); 7 | const turf = require("@turf/turf"); 8 | const Mashnet = require("../src/index.js"); 9 | 10 | const html = fs.readFileSync(path.join(__dirname, "graph.html")).toString(); 11 | 12 | const token = 13 | "pk.eyJ1IjoibW9yZ2FuaGVybG9ja2VyIiwiYSI6Ii1zLU4xOWMifQ.FubD68OEerk74AYCLduMZQ"; 14 | 15 | const honolulu = require(path.join(__dirname, "../samples/honolulu.json")); 16 | const net = new Mashnet(honolulu); 17 | 18 | const edges = turf.featureCollection([]); 19 | const nodes = turf.featureCollection([]); 20 | const vertices = turf.featureCollection([]); 21 | 22 | for (const edge of net.edges) { 23 | const coordinates = []; 24 | for (const ref of edge[1]) { 25 | coordinates.push(net.vertices.get(ref)); 26 | } 27 | edges.features.push(turf.lineString(coordinates)); 28 | } 29 | 30 | for (const node of net.nodes) { 31 | const coordinates = net.vertices.get(node[0]); 32 | nodes.features.push(turf.point(coordinates)); 33 | } 34 | 35 | for (const vertex of net.vertices) { 36 | vertices.features.push(turf.point(vertex[1])); 37 | } 38 | 39 | const render = html 40 | .split("{{token}}") 41 | .join(token) 42 | .split("{{edges}}") 43 | .join(JSON.stringify(edges)) 44 | .split("{{vertices}}") 45 | .join(JSON.stringify(vertices)) 46 | .split("{{nodes}}") 47 | .join(JSON.stringify(nodes)); 48 | 49 | fs.writeFileSync(process.argv[2], render); 50 | -------------------------------------------------------------------------------- /utils/map.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const turf = require("@turf/turf"); 5 | 6 | const Mashnet = require("../src/index.js"); 7 | 8 | const network = JSON.parse(fs.readFileSync(process.argv[2])); 9 | 10 | const net = new Mashnet([]); 11 | net.fromJSON(network); 12 | 13 | for (const edge of net.edges) { 14 | const metadata = net.metadata.get(edge[0]); 15 | const coordinates = []; 16 | for (const ref of edge[1]) { 17 | coordinates.push(net.vertices.get(ref)); 18 | } 19 | const line = turf.lineString(coordinates, metadata); 20 | console.log(JSON.stringify(line)); 21 | } 22 | -------------------------------------------------------------------------------- /utils/merge-demo.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const fs = require("fs"); 4 | const turf = require("@turf/turf"); 5 | const cover = require("@mapbox/tile-cover"); 6 | 7 | const Mashnet = require("../src/index.js"); 8 | 9 | const osm = JSON.parse(fs.readFileSync(process.argv[2])); 10 | const dot = JSON.parse(fs.readFileSync(process.argv[3])); 11 | // const predictions = []; 12 | const net = new Mashnet(osm); 13 | const quadkeys = new Set(); 14 | for (const edge of osm) { 15 | const keys = cover.indexes(edge.geometry, { min_zoom: 17, max_zoom: 17 }); 16 | for (const key of keys) { 17 | quadkeys.add(key); 18 | } 19 | } 20 | 21 | let i = 0; 22 | let total = 0; 23 | let then = Date.now(); 24 | let now = then; 25 | let totalTime = 0; 26 | for (const edge of dot.features) { 27 | now = Date.now(); 28 | const delta = now - then; 29 | totalTime += delta; 30 | console.log(totalTime / i); 31 | then = now; 32 | // console.log("i:", ((i / dot.features.length) * 100).toFixed(4) + "%"); 33 | const line = turf.lineString(edge.geometry.coordinates[0], edge.properties); 34 | 35 | const keys = cover.indexes(edge.geometry, { min_zoom: 17, max_zoom: 17 }); 36 | let found = false; 37 | for (const key of keys) { 38 | if (quadkeys.has(key)) { 39 | found = true; 40 | continue; 41 | } 42 | } 43 | 44 | if (found && line.geometry.coordinates.length < 100) { 45 | i++; 46 | total++; 47 | // console.log('line:') 48 | // console.log(JSON.stringify(line)) 49 | try { 50 | net.append(line); 51 | } catch (e) { 52 | // error found 53 | } 54 | /* const scores = net.scan(line); 55 | const match = net.match(scores); 56 | edge.properties.score = match; 57 | // console.log(JSON.stringify(edge)) 58 | if (match < 0.5) { 59 | miss++; 60 | console.log(miss / total); 61 | } 62 | predictions.push(edge);*/ 63 | } 64 | } 65 | /* 66 | fs.writeFileSync( 67 | process.argv[4], 68 | JSON.stringify(turf.featureCollection(predictions)) 69 | ); 70 | */ 71 | --------------------------------------------------------------------------------