nktk.js (9491B)
1 import Pbf from 'pbf'; 2 import {TrackView} from './nktk_pb'; 3 import {arrayBufferToString, stringToArrayBuffer} from '~/lib/binary-strings'; 4 import utf8 from 'utf8'; 5 import * as urlSafeBase64 from './urlSafeBase64'; 6 7 const arcUnit = ((1 << 24) - 1) / 360; 8 9 function PackedStreamReader(s) { 10 this._string = s; 11 this.position = 0; 12 } 13 14 function unpackNumber(s, position) { 15 var x, 16 n = 0; 17 x = s.charCodeAt(position); 18 if (isNaN(x)) { 19 throw new Error('Unexpected end of line while unpacking number'); 20 } 21 if (x < 128) { 22 n = x - 64; 23 return [n, 1]; 24 } 25 n = x & 0x7f; 26 x = s.charCodeAt(position + 1); 27 if (isNaN(x)) { 28 throw new Error('Unexpected end of line while unpacking number'); 29 } 30 if (x < 128) { 31 n |= x << 7; 32 n -= 8192; 33 return [n, 2]; 34 } 35 n |= (x & 0x7f) << 7; 36 x = s.charCodeAt(position + 2); 37 if (isNaN(x)) { 38 throw new Error('Unexpected end of line while unpacking number'); 39 } 40 if (x < 128) { 41 n |= x << 14; 42 n -= 1048576; 43 return [n, 3]; 44 } 45 n |= (x & 0x7f) << 14; 46 x = s.charCodeAt(position + 3); 47 if (isNaN(x)) { 48 throw new Error('Unexpected end of line while unpacking number'); 49 } 50 n |= x << 21; 51 n -= 268435456; 52 return [n, 4]; 53 } 54 55 PackedStreamReader.prototype.readNumber = function() { 56 var n = unpackNumber(this._string, this.position); 57 this.position += n[1]; 58 return n[0]; 59 }; 60 61 PackedStreamReader.prototype.readString = function(size) { 62 var s = this._string.slice(this.position, this.position + size); 63 this.position += size; 64 return s; 65 }; 66 67 function deltaEncodeSegment(points) { 68 let deltaLats = [], 69 deltaLons = []; 70 let lastLon = 0, 71 lastLat = 0, 72 lon, lat; 73 for (let i = 0, len = points.length; i < len; i++) { 74 let p = points[i]; 75 lon = Math.round(p.lng * arcUnit); 76 lat = Math.round(p.lat * arcUnit); 77 let deltaLon = lon - lastLon; 78 let deltaLat = lat - lastLat; 79 deltaLats.push(deltaLat); 80 deltaLons.push(deltaLon); 81 lastLon = lon; 82 lastLat = lat; 83 } 84 return {deltaLats, deltaLons}; 85 } 86 87 function deltaDecodeSegment(deltaLats, deltaLons) { 88 let encodedLat = 0, 89 encodedLon = 0; 90 const points = []; 91 for (let i = 0; i < deltaLats.length; i++) { 92 encodedLat += deltaLats[i]; 93 encodedLon += deltaLons[i]; 94 points.push({lat: encodedLat / arcUnit, lng: encodedLon / arcUnit}); 95 } 96 return points; 97 } 98 99 function saveNktk(segments, name, color, measureTicksShown, waypoints, trackHidden) { 100 const trackView = { 101 view: { 102 color, 103 shown: !trackHidden, 104 ticksShown: measureTicksShown, 105 } 106 }; 107 const track = trackView.track = {name}; 108 if (segments && segments.length) { 109 let deltaEncodedSegments = []; 110 for (let segment of segments) { 111 let {deltaLats, deltaLons} = deltaEncodeSegment(segment); 112 deltaEncodedSegments.push({lats: deltaLats, lons: deltaLons}); 113 } 114 track.segments = deltaEncodedSegments; 115 } 116 if (waypoints && waypoints.length) { 117 let midLon = 0, 118 midLat = 0; 119 waypoints.forEach((wp) => { 120 midLon += wp.latlng.lng; 121 midLat += wp.latlng.lat; 122 } 123 ); 124 midLon = Math.round(midLon * arcUnit / waypoints.length); 125 midLat = Math.round(midLat * arcUnit / waypoints.length); 126 track.waypoints = { 127 midLat, midLon 128 }; 129 130 let packedWaypoints = []; 131 for (let waypoint of waypoints) { 132 packedWaypoints.push({ 133 name: waypoint.label, 134 lat: Math.round(waypoint.latlng.lat * arcUnit) - midLat, 135 lon: Math.round(waypoint.latlng.lng * arcUnit) - midLon 136 }); 137 } 138 track.waypoints.waypoints = packedWaypoints; 139 } 140 const pbf = new Pbf(); 141 const versionStr = String.fromCharCode(4 + 64); 142 TrackView.write(trackView, pbf); 143 const s = versionStr + arrayBufferToString(pbf.finish()); 144 return urlSafeBase64.encode(s); 145 } 146 147 function parseNktkOld(s, version) { 148 var name, 149 n, 150 segments = [], 151 segment, 152 segmentsCount, 153 pointsCount, 154 arcUnit = ((1 << 24) - 1) / 360, 155 x, y, 156 error, midX, midY, /* symbol,*/ waypointName, 157 wayPoints = [], 158 color, measureTicksShown, 159 trackHidden = false; 160 s = new PackedStreamReader(s); 161 try { 162 n = s.readNumber(); 163 name = s.readString(n); 164 name = utf8.decode(name); 165 segmentsCount = s.readNumber(); 166 for (let i = 0; i < segmentsCount; i++) { 167 segment = []; 168 pointsCount = s.readNumber(); 169 x = 0; 170 y = 0; 171 for (let j = 0; j < pointsCount; j++) { 172 x += s.readNumber(); 173 y += s.readNumber(); 174 segment.push({lng: x / arcUnit, lat: y / arcUnit}); 175 } 176 segments.push(segment); 177 segment = null; 178 } 179 } catch (e) { 180 if (e.message.match('Unexpected end of line while unpacking number')) { 181 error = ['CORRUPT']; 182 if (segment) { 183 segments.push(segment); 184 } 185 } else { 186 throw e; 187 } 188 } 189 try { 190 color = s.readNumber(); 191 measureTicksShown = s.readNumber(); 192 } catch (e) { 193 if (e.message.match('Unexpected end of line while unpacking number')) { 194 color = 0; 195 measureTicksShown = 0; 196 if (version > 0) { 197 error = ['CORRUPT']; 198 } 199 } else { 200 throw e; 201 } 202 } 203 if (version >= 3) { 204 try { 205 trackHidden = Boolean(s.readNumber()); 206 } catch (e) { 207 if (e.message.match('Unexpected end of line while unpacking number')) { 208 error = ['CORRUPT']; 209 } else { 210 throw e; 211 } 212 } 213 } 214 if (version >= 2) { 215 try { 216 pointsCount = s.readNumber(); 217 if (pointsCount) { 218 midX = s.readNumber(); 219 midY = s.readNumber(); 220 } 221 for (let i = 0; i < pointsCount; i++) { 222 n = s.readNumber(); 223 waypointName = s.readString(n); 224 waypointName = utf8.decode(waypointName); 225 226 // let symbol = s.readNumber(); 227 s.readNumber(); 228 229 x = s.readNumber() + midX; 230 y = s.readNumber() + midY; 231 wayPoints.push({ 232 name: waypointName, 233 lat: y / arcUnit, 234 lng: x / arcUnit, 235 236 } 237 ); 238 } 239 } catch (e) { 240 if (e.message.match('Unexpected end of line while unpacking number')) { 241 error = ['CORRUPT']; 242 } else { 243 throw e; 244 } 245 } 246 } 247 var geoData = { 248 name: name || "Text encoded track", 249 tracks: segments, 250 error: error, 251 points: wayPoints, 252 color: color, 253 measureTicksShown: measureTicksShown, 254 trackHidden: trackHidden 255 }; 256 return [geoData]; 257 } 258 259 function parseTrackUrlData(s) { 260 s = urlSafeBase64.decode(s); 261 if (!s) { 262 return [{name: 'Text encoded track', error: ['CORRUPT']}]; 263 } 264 return parseNktkOld(s, 0); 265 } 266 267 function parseNktkProtobuf(s) { 268 const pbf = new Pbf(stringToArrayBuffer(s)); 269 let trackView; 270 try { 271 trackView = TrackView.read(pbf); 272 } catch (e) { 273 return [{name: 'Text encoded track', error: ['CORRUPT']}]; 274 } 275 const geoData = { 276 name: trackView.track.name || "Text encoded track", 277 color: trackView.view.color, 278 trackHidden: !trackView.view.shown, 279 measureTicksShown: trackView.view.ticksShown, 280 }; 281 const segments = trackView.track.segments; 282 if (segments && segments.length) { 283 geoData.tracks = segments.map((segment) => deltaDecodeSegment(segment.lats, segment.lons)); 284 } 285 if (trackView.track.waypoints && trackView.track.waypoints.waypoints.length) { 286 const waypoints = geoData.points = []; 287 for (let waypoint of trackView.track.waypoints.waypoints) { 288 waypoints.push({ 289 name: waypoint.name, 290 lat: (waypoint.lat + trackView.track.waypoints.midLat) / arcUnit, 291 lng: (waypoint.lon + trackView.track.waypoints.midLon) / arcUnit 292 }); 293 } 294 } 295 return [geoData]; 296 } 297 298 function parseNktkFragment(s) { 299 s = urlSafeBase64.decode(s); 300 if (!s) { 301 return [{name: 'Text encoded track', error: ['CORRUPT']}]; 302 } 303 const reader = new PackedStreamReader(s); 304 let version = reader.readNumber(); 305 if (version === 1 || version === 2 || version === 3) { 306 return parseNktkOld(s.substring(reader.position), version); 307 } else if (version === 4) { 308 return parseNktkProtobuf(s.substring(reader.position)); 309 } 310 return [{name: 'Text encoded track', error: ['CORRUPT']}]; 311 } 312 313 function parseNktkSequence(s) { 314 return s.split('/') 315 .map(parseNktkFragment) 316 .reduce((acc, cur) => { 317 acc.push(...cur); 318 return acc; 319 }); 320 } 321 322 export {saveNktk, parseTrackUrlData, parseNktkSequence, parseNktkFragment};