UNPKG

@fmidev/smartmet-alert-client

Version:

Web application for viewing weather and flood alerts

2,138 lines (1,945 loc) 90.6 kB
// GeoJSON to SVG // Adapted from https://github.com/mbloch/mapshaper import Flatbush from 'flatbush' const rxp = /[&<>"']/g const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&apos;', } const commonProperties = 'class,opacity,stroke,stroke-width,stroke-dasharray,stroke-opacity,fill-opacity'.split( ',' ) const arrayToIndex = function (arr, val) { const init = arguments.length > 1 return arr.reduce(function (index, key) { index[key] = init ? val : true return index }, {}) } const propertiesBySymbolType = { polygon: arrayToIndex(commonProperties.concat('fill', 'fill-pattern')), polyline: arrayToIndex(commonProperties), point: arrayToIndex(commonProperties.concat('fill', 'r')), label: arrayToIndex( commonProperties.concat( 'fill,r,font-family,font-size,text-anchor,font-weight,font-style,letter-spacing,dominant-baseline'.split( ',' ) ) ), } const getPathBounds = function (points) { const bounds = new Bounds() for (let i = 0, n = points.length; i < n; i++) { bounds.mergePoint(points[i][0], points[i][1]) } return bounds } const getPlanarPathArea2 = function (points) { let sum = 0 let ax let ay let bx let by let dx let dy let p for (let i = 0, n = points.length; i < n; i++) { p = points[i] if (i === 0) { ax = 0 ay = 0 dx = -p[0] dy = -p[1] } else { ax = p[0] + dx ay = p[1] + dy sum += ax * by - bx * ay } bx = ax by = ay } return sum / 2 } const exportPointData = function (points) { let data, path if (!points || points.length === 0) { data = { partCount: 0, pointCount: 0 } } else { path = { points, pointCount: points.length, bounds: getPathBounds(points), } data = { bounds: path.bounds, pathData: [path], partCount: 1, pointCount: path.pointCount, } } return data } const exportPathCoords = function (iter) { const points = [] let i = 0 let x let y let prevX let prevY while (iter.hasNext()) { x = iter.x y = iter.y if (i === 0 || prevX != x || prevY != y) { points.push([x, y]) i++ } prevX = x prevY = y } return { points, pointCount: points.length, } } const exportPathData = function (shape, arcs, type) { // kludge until Shapefile exporting is refactored if (type === 'point') return exportPointData(shape) let pointCount = 0 const bounds = new Bounds() const paths = [] if (shape && (type === 'polyline' || type === 'polygon')) { shape.forEach(function (arcIds, i) { const iter = arcs.getShapeIter(arcIds) const path = exportPathCoords(iter) let valid = true path.ids = arcIds if (type === 'polygon') { path.area = getPlanarPathArea2(path.points) valid = path.pointCount > 3 && path.area !== 0 } else if (type === 'polyline') { valid = path.pointCount > 1 } if (valid) { pointCount += path.pointCount path.bounds = getPathBounds(path.points) bounds.mergeBounds(path.bounds) paths.push(path) } else { console.log('Skipping a collapsed', type, 'path') } }) } return { pointCount, pathData: paths, pathCount: paths.length, bounds, } } const getBoundsSearchFunction = function (boxes) { if (!boxes.length) { return function () { return [] } } const index = new Flatbush(boxes.length) boxes.forEach(function (ring) { const b = ring.bounds index.add(b.xmin, b.ymin, b.xmax, b.ymax) }) index.finish() function idxToObj(i) { return boxes[i] } // Receives xmin, ymin, xmax, ymax parameters // Returns subset of original @bounds array return function (a, b, c, d) { return index.search(a, b, c, d).map(idxToObj) } } const groupPolygonRings = function (paths, arcs, reverseWinding) { const holes = [] const groups = [] const sign = reverseWinding ? -1 : 1 ;(paths || []).forEach(function (path) { if (path.area * sign > 0) { groups.push([path]) } else if (path.area * sign < 0) { holes.push(path) } else { // Zero-area ring, skipping } }) if (holes.length === 0) { return groups } const boundsQuery = getBoundsSearchFunction( groups.map(function (group, i) { return { bounds: group[0].bounds, idx: i, } }) ) // Group each hole with its containing ring holes.forEach(function (hole) { let containerId = -1 let containerArea = 0 const holeArea = hole.area * -sign const b = hole.bounds const candidates = boundsQuery(b.xmin, b.ymin, b.xmax, b.ymax) let ring let ringId let ringArea let isContained for (let i = 0, n = candidates.length; i < n; i++) { ringId = candidates[i].idx ring = groups[ringId][0] ringArea = ring.area * sign isContained = ring.bounds.contains(hole.bounds) && ringArea > holeArea if ( isContained && candidates.length > 1 && !testRingInRing(hole, ring, arcs) ) { continue } if (isContained && (containerArea === 0 || ringArea < containerArea)) { containerArea = ringArea containerId = ringId } } if (containerId == -1) { console.log( '[groupPolygonRings()] polygon hole is missing a containing ring, dropping.' ) } else { groups[containerId].push(hole) } }) return groups } const exportPointGeom = function (points, arcs) { let geom = null if (points.length === 1) { geom = { type: 'Point', coordinates: points[0], } } else if (points.length > 1) { geom = { type: 'MultiPoint', coordinates: points, } } return geom } const exportLineGeom = function (ids, arcs) { const obj = exportPathData(ids, arcs, 'polyline') if (obj.pointCount === 0) return null const coords = obj.pathData.map(function (path) { return path.points }) return coords.length === 1 ? { type: 'LineString', coordinates: coords[0], } : { type: 'MultiLineString', coordinates: coords, } } const exportPolygonGeom = function (ids, arcs, opts) { const obj = exportPathData(ids, arcs, 'polygon') if (obj.pointCount === 0) return null const groups = groupPolygonRings(obj.pathData, arcs, opts.invert_y) const reverse = (opts.rfc7946 || opts.v2) && !opts.invert_y const coords = groups.map(function (paths) { return paths.map(function (path) { if (reverse) path.points.reverse() return path.points }) }) return coords.length === 1 ? { type: 'Polygon', coordinates: coords[0], } : { type: 'MultiPolygon', coordinates: coords, } } const GeoJSON = { ID_FIELD: 'FID', typeLookup: { LineString: 'polyline', MultiLineString: 'polyline', Polygon: 'polygon', MultiPolygon: 'polygon', Point: 'point', MultiPoint: 'point', }, pathImporters: { LineString: function (coords, importer) { importer.importLine(coords) }, MultiLineString: function (coords, importer) { for (let i = 0; i < coords.length; i++) { GeoJSON.pathImporters.LineString(coords[i], importer) } }, Polygon: function (coords, importer) { for (let i = 0; i < coords.length; i++) { importer.importRing(coords[i], i > 0) } }, MultiPolygon: function (coords, importer) { for (let i = 0; i < coords.length; i++) { GeoJSON.pathImporters.Polygon(coords[i], importer) } }, Point: function (coord, importer) { importer.importPoints([coord]) }, MultiPoint: function (coords, importer) { importer.importPoints(coords) }, }, translateGeoJSONType: function (type) { return GeoJSON.typeLookup[type] || null }, pathIsRing: function (coords) { const first = coords[0] const last = coords[coords.length - 1] return coords.length >= 4 && first[0] === last[0] && first[1] === last[1] }, importGeometry: function (geom, importer) { const type = geom.type if (type in GeoJSON.pathImporters) { GeoJSON.pathImporters[type](geom.coordinates, importer) } else if (type === 'GeometryCollection') { geom.geometries.forEach(function (geom) { GeoJSON.importGeometry(geom, importer) }) } else { console.log( 'GeoJSON.importGeometry() Unsupported geometry type:', geom.type ) } }, exporters: { polygon: exportPolygonGeom, polyline: exportLineGeom, point: exportPointGeom, }, } const stringifyVertex = function (p) { return p[0] + ' ' + p[1] } const stringifyCP = function (p) { return p[2].toFixed(2) + ' ' + p[3].toFixed(2) } const stringifyBezierArc = function (coords) { const p1 = coords[0] const p2 = coords[1] return ( 'M ' + stringifyVertex(p1) + ' C ' + stringifyCP(p1) + ' ' + stringifyCP(p2) + ' ' + stringifyVertex(p2) ) } const stringifyLineStringCoords = function (coords) { let d if (coords.length === 0) { d = '' } else if ( coords.length === 2 && coords[0].length === 4 && coords[1].length === 4 ) { d = stringifyBezierArc(coords) } else { d = 'M ' + coords.map(stringifyVertex).join(' ') } return d } const importStandardPoint = function (coords, rec, layerOpts) { const isLabel = 'label-text' in rec const symbolType = layerOpts.point_symbol || '' const children = [] const halfSize = rec.r || 0 // radius or half of symbol size let p if (halfSize > 0 || !isLabel) { if (symbolType === 'square') { p = { tag: 'rect', properties: { x: coords[0] - halfSize, y: coords[1] - halfSize, width: halfSize * 2, height: halfSize * 2, }, } } else { p = { tag: 'circle', properties: { cx: coords[0], cy: coords[1], }, } if (halfSize > 0) { p.properties.r = halfSize } } children.push(p) } return children.length > 1 ? { tag: 'g', children } : children[0] } const importPoint = function (coords, rec, layerOpts) { rec = rec || {} return importStandardPoint(coords, rec, layerOpts || {}) } const importLineString = function (coords) { const d = stringifyLineStringCoords(coords) return { tag: 'path', properties: { d }, } } const importPolygon = function (coords) { let d, o for (let i = 0; i < coords.length; i++) { d = o ? o.properties.d + ' ' : '' o = importLineString(coords[i]) o.properties.d = d + o.properties.d + ' Z' } return o } const importMultiPath = function (coords, importer) { let o for (let i = 0; i < coords.length; i++) { if (i === 0) { o = importer(coords[i]) } else { o.properties.d += ' ' + importer(coords[i]).properties.d } } return o } const geojsonImporters = { Point: importPoint, Polygon: importPolygon, MultiPolygon: function (coords) { return importMultiPath(coords, importPolygon) }, } const GeoJSONParser = function () { const idField = GeoJSON.ID_FIELD const importer = new PathImporter() this.parseObject = function (o) { let geom, rec if (!o || !o.type) { geom = null } else if (o.type === 'Feature') { geom = o.geometry rec = o.properties || {} if ('id' in o) { rec[idField] = o.id } } else { geom = o } importer.startShape(rec) if (geom) GeoJSON.importGeometry(geom, importer) } this.done = function () { return importer.done() } } const PathImporter = function () { const bufSize = 20000 let xx = new Float64Array(bufSize) let yy = new Float64Array(bufSize) const shapes = [] const properties = [] const nn = [] const types = [] let collectionType = null const round = null let pathId = -1 let shapeId = -1 let pointId = 0 let dupeCount = 0 let openRingCount = 0 // mix in #addPoint() and #endPath() methods ;(function (o) { const dest = o || {} const n = arguments.length let key let i let src for (i = 1; i < n; i++) { src = arguments[i] || {} for (key in src) { if (src.hasOwnProperty(key)) { dest[key] = src[key] } } } return dest })(this, new PathImportStream(importPathCoords)) this.startShape = function (d) { shapes[++shapeId] = null if (d) properties[shapeId] = d } this.importLine = function (points) { if (points.length < 2) { console.log('Skipping a defective line') return } setShapeType('polyline') this.importPath(points) } this.importPoints = function (points) { setShapeType('point') points = points.filter(function (p) { return p[0] > -1e38 && p[1] > -1e38 }) if (round) { points.forEach(function (p) { p[0] = round(p[0]) p[1] = round(p[1]) }) } points.forEach(appendToShape) } this.importRing = function (points, isHole) { const area = (function (points) { let sum = 0 let ax let ay let bx let by let dx let dy let p for (let i = 0, n = points.length; i < n; i++) { p = points[i] if (i === 0) { ax = 0 ay = 0 dx = -p[0] dy = -p[1] } else { ax = p[0] + dx ay = p[1] + dy sum += ax * by - bx * ay } bx = ax by = ay } return sum / 2 })(points) if (!area || points.length < 4) { console.log('Skipping a defective ring') return } setShapeType('polygon') if ((isHole === true && area > 0) || (isHole === false && area < 0)) { // GeoJSON rings may be either direction -- no point in logging reversal // verbose("Reversing", isHole ? "a CW hole" : "a CCW ring"); points.reverse() } this.importPath(points) } // Import an array of [x, y] Points this.importPath = function importPath(points) { let p for (let i = 0, n = points.length; i < n; i++) { p = points[i] this.addPoint(p[0], p[1]) } this.endPath() } // Return imported dataset // Apply any requested snapping and rounding // Remove duplicate points, check for ring inversions // this.done = function () { const divideFeaturesByType = function (shapes, properties, types) { const typeSet = (function uniq(src) { const index = {} return src.reduce(function (memo, el) { if (el in index === false) { index[el] = true memo.push(el) } return memo }, []) })(types) return typeSet.map(function (geoType) { const p = [] const s = [] let dataNulls = 0 let rec for (let i = 0, n = shapes.length; i < n; i++) { if (types[i] !== geoType) continue if (geoType) s.push(shapes[i]) rec = properties[i] p.push(rec) if (!rec) dataNulls++ } return { geometry_type: geoType, shapes: s, data: dataNulls < s.length ? new DataTable(p) : null, } }) } let arcs let layers let lyr = { name: '' } if (dupeCount > 0) { console.log('Removed duplicate point(s).') } if (openRingCount > 0) { console.log('Closed open polygon ring(s).') } if (pointId > 0) { if (pointId < xx.length) { xx = xx.subarray(0, pointId) yy = yy.subarray(0, pointId) } arcs = new ArcCollection(nn, xx, yy) } if (collectionType === 'mixed') { layers = divideFeaturesByType(shapes, properties, types) } else { lyr = { geometry_type: collectionType } if (collectionType) { lyr.shapes = shapes } if (properties.length > 0) { lyr.data = new DataTable(properties) } layers = [lyr] } const findIncompleteFields = function (records) { const counts = {} let i let j let keys for (i = 0; i < records.length; i++) { keys = Object.keys(records[i] || {}) for (j = 0; j < keys.length; j++) { counts[keys[j]] = (counts[keys[j]] | 0) + 1 } } return Object.keys(counts).filter(function (k) { return counts[k] < records.length }) } const patchMissingFields = function (records, fields) { let rec, i, j, f for (i = 0; i < records.length; i++) { rec = records[i] || (records[i] = {}) for (j = 0; j < fields.length; j++) { f = fields[j] if (f in rec === false) { rec[f] = undefined } } } } layers.forEach(function (lyr) { if (lyr.data) { ;(function (records) { const fields = findIncompleteFields(records) patchMissingFields(records, fields) })(lyr.data.getRecords()) } }) return { arcs: arcs || null, info: {}, layers, } } function setShapeType(t) { const currType = shapeId < types.length ? types[shapeId] : null if (!currType) { types[shapeId] = t if (!collectionType) { collectionType = t } else if (t !== collectionType) { collectionType = 'mixed' } } else if (currType !== t) { stop('Unable to import mixed-geometry GeoJSON features') } } function checkBuffers(needed) { if (needed > xx.length) { const newLen = Math.max(needed, Math.ceil(xx.length * 1.5)) xx = extendBuffer(xx, newLen, pointId) yy = extendBuffer(yy, newLen, pointId) } } function appendToShape(part) { const currShape = shapes[shapeId] || (shapes[shapeId] = []) currShape.push(part) } function appendPath(n) { pathId++ nn[pathId] = n appendToShape([pathId]) } function importPathCoords(xsrc, ysrc, n) { let count = 0 let x, y, prevX, prevY checkBuffers(pointId + n) for (let i = 0; i < n; i++) { x = xsrc[i] y = ysrc[i] if (round) { x = round(x) y = round(y) } if (i > 0 && x === prevX && y === prevY) { dupeCount++ } else { xx[pointId] = x yy[pointId] = y pointId++ count++ } prevY = y prevX = x } // check for open rings if (collectionType === 'polygon' && count > 0) { if (xsrc[0] !== xsrc[n - 1] || ysrc[0] !== ysrc[n - 1]) { checkBuffers(pointId + 1) xx[pointId] = xsrc[0] yy[pointId] = ysrc[0] openRingCount++ pointId++ count++ } } appendPath(count) } } const PathImportStream = function (drain) { let buflen = 10000 let xx = new Float64Array(buflen) let yy = new Float64Array(buflen) let i = 0 this.endPath = function () { drain(xx, yy, i) i = 0 } this.addPoint = function (x, y) { if (i >= buflen) { buflen = Math.ceil(buflen * 1.3) xx = extendBuffer(xx, buflen) yy = extendBuffer(yy, buflen) } xx[i] = x yy[i] = y i++ } } const ArcCollection = function () { let _xx let _yy // coordinates data let _ii let _nn // indexes, sizes let _zz let _zlimit = 0 // simplification let _bb let _allBounds // bounding boxes let _arcIter let _filteredArcIter // path iterators if (arguments.length === 1) { initLegacyArcs(arguments[0]) // want to phase this out } else if (arguments.length === 3) { initXYData.apply(this, arguments) } else { console.log('ArcCollection() Invalid arguments') } function initLegacyArcs(arcs) { const xx = [] const yy = [] const nn = arcs.map(function (points) { const n = points ? points.length : 0 for (let i = 0; i < n; i++) { xx.push(points[i][0]) yy.push(points[i][1]) } return n }) initXYData(nn, xx, yy) } function initXYData(nn, xx, yy) { const size = nn.length if (nn instanceof Array) nn = new Uint32Array(nn) if (xx instanceof Array) xx = new Float64Array(xx) if (yy instanceof Array) yy = new Float64Array(yy) _xx = xx _yy = yy _nn = nn _zz = null _zlimit = 0 _filteredArcIter = null // generate array of starting idxs of each arc _ii = new Uint32Array(size) for (var idx = 0, j = 0; j < size; j++) { _ii[j] = idx idx += nn[j] } if (idx !== _xx.length || _xx.length !== _yy.length) { console.log('ArcCollection#initXYData() Counting error') } initBounds() // Pre-allocate some path iterators for repeated use. _arcIter = new ArcIter(_xx, _yy) return this } function initZData(zz) { if (!zz) { _zz = null _zlimit = 0 _filteredArcIter = null } else { if (zz.length !== _xx.length) console.log('ArcCollection#initZData() mismatched arrays') if (zz instanceof Array) zz = new Float64Array(zz) _zz = zz _filteredArcIter = new FilteredArcIter(_xx, _yy, _zz) } } function initBounds() { const data = calcArcBounds2(_xx, _yy, _nn) _bb = data.bb _allBounds = data.bounds } function calcArcBounds2(xx, yy, nn) { const numArcs = nn.length const bb = new Float64Array(numArcs * 4) const bounds = new Bounds() let arcOffs = 0 let arcLen let j let b const calcArcBounds = function (xx, yy, start, len) { let i = start | 0 const n = isNaN(len) ? xx.length - i : len + i let x let y let xmin let ymin let xmax let ymax if (n > 0) { xmin = xmax = xx[i] ymin = ymax = yy[i] } for (i++; i < n; i++) { x = xx[i] y = yy[i] if (x < xmin) xmin = x if (x > xmax) xmax = x if (y < ymin) ymin = y if (y > ymax) ymax = y } return [xmin, ymin, xmax, ymax] } for (let i = 0; i < numArcs; i++) { arcLen = nn[i] if (arcLen > 0) { j = i * 4 b = calcArcBounds(xx, yy, arcOffs, arcLen) bb[j++] = b[0] bb[j++] = b[1] bb[j++] = b[2] bb[j] = b[3] arcOffs += arcLen bounds.mergeBounds(b) } } return { bb, bounds, } } this.updateVertexData = function (nn, xx, yy, zz) { initXYData(nn, xx, yy) initZData(zz || null) } // Give access to raw data arrays... this.getVertexData = function () { return { xx: _xx, yy: _yy, zz: _zz, bb: _bb, nn: _nn, ii: _ii, } } this.getCopy = function () { const copy = new ArcCollection( new Int32Array(_nn), new Float64Array(_xx), new Float64Array(_yy) ) if (_zz) { copy.setThresholds(new Float64Array(_zz)) copy.setRetainedInterval(_zlimit) } return copy } function getFilteredPointCount() { const zz = _zz const z = _zlimit if (!zz || !z) return this.getPointCount() let count = 0 for (let i = 0, n = zz.length; i < n; i++) { if (zz[i] >= z) count++ } return count } function getFilteredVertexData() { const len2 = getFilteredPointCount() const arcCount = _nn.length const xx2 = new Float64Array(len2) const yy2 = new Float64Array(len2) const zz2 = new Float64Array(len2) const nn2 = new Int32Array(arcCount) let i = 0 let i2 = 0 let n let n2 for (let arcId = 0; arcId < arcCount; arcId++) { n2 = 0 n = _nn[arcId] for (let end = i + n; i < end; i++) { if (_zz[i] >= _zlimit) { xx2[i2] = _xx[i] yy2[i2] = _yy[i] zz2[i2] = _zz[i] i2++ n2++ } } if (n2 === 1) { console.log('Collapsed arc') } else if (n2 === 0) { // collapsed arc... ignoring } nn2[arcId] = n2 } return { xx: xx2, yy: yy2, zz: zz2, nn: nn2, } } this.getFilteredCopy = function () { if (!_zz || _zlimit === 0) return this.getCopy() const data = getFilteredVertexData() const copy = new ArcCollection(data.nn, data.xx, data.yy) copy.setThresholds(data.zz) return copy } // Return arcs as arrays of [x, y] points (intended for testing). this.toArray = function () { const arr = [] this.forEach(function (iter) { const arc = [] while (iter.hasNext()) { arc.push([iter.x, iter.y]) } arr.push(arc) }) return arr } this.toJSON = function () { return this.toArray() } // @cb function(i, j, xx, yy) this.forEachArcSegment = function (arcId, cb) { const fw = arcId >= 0 const absId = fw ? arcId : ~arcId const zlim = this.getRetainedInterval() const n = _nn[absId] const step = fw ? 1 : -1 let v1 = fw ? _ii[absId] : _ii[absId] + n - 1 let v2 = v1 const xx = _xx const yy = _yy const zz = _zz let count = 0 for (let j = 1; j < n; j++) { v2 += step if (zlim === 0 || zz[v2] >= zlim) { cb(v1, v2, xx, yy) v1 = v2 count++ } } return count } // @cb function(i, j, xx, yy) this.forEachSegment = function (cb) { let count = 0 for (let i = 0, n = this.size(); i < n; i++) { count += this.forEachArcSegment(i, cb) } return count } this.transformPoints = function (f) { const xx = _xx const yy = _yy let arcId = -1 let n = 0 let p for (let i = 0, len = xx.length; i < len; i++, n--) { while (n === 0) { n = _nn[++arcId] } p = f(xx[i], yy[i], arcId) if (p) { xx[i] = p[0] yy[i] = p[1] } } initBounds() } // Return an ArcIter object for each path in the dataset // this.forEach = function (cb) { for (let i = 0, n = this.size(); i < n; i++) { cb(this.getArcIter(i), i) } } // Iterate over arcs with access to low-level data // this.forEach2 = function (cb) { for (let arcId = 0, n = this.size(); arcId < n; arcId++) { cb(_ii[arcId], _nn[arcId], _xx, _yy, _zz, arcId) } } this.forEach3 = function (cb) { let start, end, xx, yy, zz for (let arcId = 0, n = this.size(); arcId < n; arcId++) { start = _ii[arcId] end = start + _nn[arcId] xx = _xx.subarray(start, end) yy = _yy.subarray(start, end) if (_zz) zz = _zz.subarray(start, end) cb(xx, yy, zz, arcId) } } this.filter = function (cb) { const test = function (i) { return cb(this.getArcIter(i), i) }.bind(this) return this.deleteArcs(test) } this.deleteArcs = function (test) { const n = this.size() const map = new Int32Array(n) let goodArcs = 0 let goodPoints = 0 for (let i = 0; i < n; i++) { if (test(i)) { map[i] = goodArcs++ goodPoints += _nn[i] } else { map[i] = -1 } } if (goodArcs < n) { condenseArcs(map) } return map } function condenseArcs(map) { let goodPoints = 0 let goodArcs = 0 let k let arcLen for (let i = 0, n = map.length; i < n; i++) { k = map[i] arcLen = _nn[i] if (k > -1) { copyElements(_xx, _ii[i], _xx, goodPoints, arcLen) copyElements(_yy, _ii[i], _yy, goodPoints, arcLen) if (_zz) copyElements(_zz, _ii[i], _zz, goodPoints, arcLen) _nn[k] = arcLen goodPoints += arcLen goodArcs++ } } initXYData( _nn.subarray(0, goodArcs), _xx.subarray(0, goodPoints), _yy.subarray(0, goodPoints) ) if (_zz) initZData(_zz.subarray(0, goodPoints)) } this.dedupCoords = function () { let arcId = 0 let i = 0 let i2 = 0 const arcCount = this.size() const zz = _zz let arcLen let arcLen2 while (arcId < arcCount) { arcLen = _nn[arcId] arcLen2 = this.dedupArcCoords(i, i2, arcLen, _xx, _yy, zz) _nn[arcId] = arcLen2 i += arcLen i2 += arcLen2 arcId++ } if (i > i2) { initXYData(_nn, _xx.subarray(0, i2), _yy.subarray(0, i2)) if (zz) initZData(zz.subarray(0, i2)) } return i - i2 } this.dedupArcCoords = function (src, dest, arcLen, xx, yy, zz) { let n = 0 let n2 = 0 // counters let x, y, i, j, keep while (n < arcLen) { j = src + n x = xx[j] y = yy[j] keep = x == x && y == y && (n2 === 0 || x != xx[j - 1] || y != yy[j - 1]) if (keep) { i = dest + n2 xx[i] = x yy[i] = y n2++ } if (zz && n2 > 0 && (keep || zz[j] > zz[i])) { zz[i] = zz[j] } n++ } return n2 > 1 ? n2 : 0 } this.getVertex = function (arcId, nth) { const i = this.indexOfVertex(arcId, nth) return { x: _xx[i], y: _yy[i], } } // @nth: index of vertex. ~(idx) starts from the opposite endpoint this.indexOfVertex = function (arcId, nth) { const absId = arcId < 0 ? ~arcId : arcId const len = _nn[absId] if (nth < 0) nth = len + nth if (absId !== arcId) nth = len - nth - 1 if (nth < 0 || nth >= len) console.log('[ArcCollection] out-of-range vertex id') return _ii[absId] + nth } // Tests if arc endpoints have same x, y coords // (arc may still have collapsed); this.arcIsClosed = function (arcId) { const i = this.indexOfVertex(arcId, 0) const j = this.indexOfVertex(arcId, -1) return i !== j && _xx[i] === _xx[j] && _yy[i] === _yy[j] } // Tests if first and last segments mirror each other // A 3-vertex arc with same endpoints tests true this.arcIsLollipop = function (arcId) { const len = this.getArcLength(arcId) if (len <= 2 || !this.arcIsClosed(arcId)) return false const i = this.indexOfVertex(arcId, 1) const j = this.indexOfVertex(arcId, -2) return _xx[i] === _xx[j] && _yy[i] === _yy[j] } this.arcIsDegenerate = function (arcId) { const iter = this.getArcIter(arcId) let i = 0 let x let y while (iter.hasNext()) { if (i > 0) { if (x !== iter.x || y !== iter.y) return false } x = iter.x y = iter.y i++ } return true } this.getArcLength = function (arcId) { return _nn[arcId >= 0 ? arcId : ~arcId] } this.getArcIter = function (arcId) { const fw = arcId >= 0 const i = fw ? arcId : ~arcId const iter = _zz && _zlimit ? _filteredArcIter : _arcIter if (i >= _nn.length) { console.log('#getArcId() out-of-range arc id:', arcId) } return iter.init(_ii[i], _nn[i], fw, _zlimit) } this.getShapeIter = function (ids) { return new ShapeIter(this).init(ids) } this.setThresholds = function (thresholds) { const n = this.getPointCount() let zz = null if (!thresholds) { // nop } else if (thresholds.length === n) { zz = thresholds } else if (thresholds.length === this.size()) { zz = flattenThresholds(thresholds, n) } else { console.log('Invalid threshold data') } initZData(zz) return this } function flattenThresholds(arr, n) { const zz = new Float64Array(n) let i = 0 arr.forEach(function (arr) { for (let j = 0, n = arr.length; j < n; i++, j++) { zz[i] = arr[j] } }) if (i != n) error('Mismatched thresholds') return zz } // bake in current simplification level, if any this.flatten = function () { if (_zlimit > 0) { const data = getFilteredVertexData() this.updateVertexData(data.nn, data.xx, data.yy) _zlimit = 0 } else { _zz = null } } this.getRetainedInterval = function () { return _zlimit } this.setRetainedInterval = function (z) { _zlimit = z return this } this.getRetainedPct = function () { return this.getPctByThreshold(_zlimit) } this.setRetainedPct = function (pct) { const clampIntervalByPct = function (z, pct) { if (pct <= 0) z = Infinity else if (pct >= 1) z = 0 return z } if (pct >= 1) { _zlimit = 0 } else { _zlimit = this.getThresholdByPct(pct) _zlimit = clampIntervalByPct(_zlimit, pct) } return this } this.getRemovableThresholds = function (nth) { if (!_zz) console.log('[arcs] Missing simplification data.') const skip = nth | 1 const arr = new Float64Array(Math.ceil(_zz.length / skip)) let z for (var i = 0, j = 0, n = this.getPointCount(); i < n; i += skip) { z = _zz[i] if (z != Infinity) { arr[j++] = z } } return arr.subarray(0, j) } this.getArcThresholds = function (arcId) { if (!(arcId >= 0 && arcId < this.size())) { console.log('[arcs] Invalid arc id:', arcId) } const start = _ii[arcId] const end = start + _nn[arcId] return _zz.subarray(start, end) } // nth (optional): sample every nth threshold (use estimate for speed) this.getPctByThreshold = function (val, nth) { let arr, rank, pct const findRankByValue = function (arr, value) { if (isNaN(value)) return arr.length let rank = 1 for (let i = 0, n = arr.length; i < n; i++) { if (value > arr[i]) rank++ } return rank } if (val > 0) { arr = this.getRemovableThresholds(nth) rank = findRankByValue(arr, val) pct = arr.length > 0 ? 1 - (rank - 1) / arr.length : 1 } else { pct = 1 } return pct } this.getThresholdByPct = (pct, nth) => { return this.getThresholdByPct(pct, this, nth) } this.arcIntersectsBBox = function (i, b1) { const b2 = _bb const j = i * 4 return ( b2[j] <= b1[2] && b2[j + 2] >= b1[0] && b2[j + 3] >= b1[1] && b2[j + 1] <= b1[3] ) } this.arcIsContained = function (i, b1) { const b2 = _bb const j = i * 4 return ( b2[j] >= b1[0] && b2[j + 2] <= b1[2] && b2[j + 1] >= b1[1] && b2[j + 3] <= b1[3] ) } this.arcIsSmaller = function (i, units) { const bb = _bb const j = i * 4 return bb[j + 2] - bb[j] < units && bb[j + 3] - bb[j + 1] < units } // TODO: allow datasets in lat-lng coord range to be flagged as planar this.isPlanar = function () { return !probablyDecimalDegreeBounds(this.getBounds()) } this.size = function () { return (_ii && _ii.length) || 0 } this.getPointCount = function () { return (_xx && _xx.length) || 0 } this.getFilteredPointCount = getFilteredPointCount this.getBounds = function () { return _allBounds.clone() } this.getSimpleShapeBounds = function (arcIds, bounds) { bounds = bounds || new Bounds() for (let i = 0, n = arcIds.length; i < n; i++) { this.mergeArcBounds(arcIds[i], bounds) } return bounds } this.getSimpleShapeBounds2 = function (arcIds, arr) { const bbox = arr || [] const bb = _bb let id = (arcIds[0] >= 0 ? arcIds[0] : ~arcIds[0]) * 4 bbox[0] = bb[id] bbox[1] = bb[++id] bbox[2] = bb[++id] bbox[3] = bb[++id] for (let i = 1, n = arcIds.length; i < n; i++) { id = (arcIds[i] >= 0 ? arcIds[i] : ~arcIds[i]) * 4 if (bb[id] < bbox[0]) bbox[0] = bb[id] if (bb[++id] < bbox[1]) bbox[1] = bb[id] if (bb[++id] > bbox[2]) bbox[2] = bb[id] if (bb[++id] > bbox[3]) bbox[3] = bb[id] } return bbox } // TODO: move this and similar methods out of ArcCollection this.getMultiShapeBounds = function (shapeIds, bounds) { bounds = bounds || new Bounds() if (shapeIds) { // handle null shapes for (let i = 0, n = shapeIds.length; i < n; i++) { this.getSimpleShapeBounds(shapeIds[i], bounds) } } return bounds } this.mergeArcBounds = function (arcId, bounds) { if (arcId < 0) arcId = ~arcId const offs = arcId * 4 bounds.mergeBounds(_bb[offs], _bb[offs + 1], _bb[offs + 2], _bb[offs + 3]) } } class Transform { constructor() { this.mx = this.my = 1 this.bx = this.by = 0 } isNull() { return !this.mx || !this.my || isNaN(this.bx) || isNaN(this.by) } invert() { const inv = new Transform() inv.mx = 1 / this.mx inv.my = 1 / this.my inv.bx = -this.bx / this.mx inv.by = -this.by / this.my return inv } transform(x, y, xy) { xy = xy || [] xy[0] = x * this.mx + this.bx xy[1] = y * this.my + this.by return xy } toString() { return JSON.stringify(Object.assign({}, this)) } } class Bounds { constructor() { if (arguments.length > 0) { this.setBounds.apply(this, arguments) } } toString() { return JSON.stringify({ xmin: this.xmin, xmax: this.xmax, ymin: this.ymin, ymax: this.ymax, }) } toArray() { return this.hasBounds() ? [this.xmin, this.ymin, this.xmax, this.ymax] : [] } hasBounds() { return this.xmin <= this.xmax && this.ymin <= this.ymax } sameBounds(bb) { return ( bb && this.xmin === bb.xmin && this.xmax === bb.xmax && this.ymin === bb.ymin && this.ymax === bb.ymax ) } width() { return this.xmax - this.xmin || 0 } height() { return this.ymax - this.ymin || 0 } area() { return this.width() * this.height() || 0 } empty() { this.xmin = this.ymin = this.xmax = this.ymax = void 0 return this } setBounds(a, b, c, d) { if (arguments.length === 1) { // assume first arg is a Bounds or array if (isArrayLike(a)) { b = a[1] c = a[2] d = a[3] a = a[0] } else { b = a.ymin c = a.xmax d = a.ymax a = a.xmin } } this.xmin = a this.ymin = b this.xmax = c this.ymax = d if (a > c || b > d) this.update() // error("Bounds#setBounds() min/max reversed:", a, b, c, d); return this } centerX() { return (this.xmin + this.xmax) * 0.5 } centerY() { return (this.ymax + this.ymin) * 0.5 } containsPoint(x, y) { return x >= this.xmin && x <= this.xmax && y <= this.ymax && y >= this.ymin } // intended to speed up slightly bubble symbol detection; could use intersects() instead // TODO: fix false positive where circle is just outside a corner of the box containsBufferedPoint(x, y, buf) { if (x + buf > this.xmin && x - buf < this.xmax) { if (y - buf < this.ymax && y + buf > this.ymin) { return true } } return false } intersects(bb) { return ( bb.xmin <= this.xmax && bb.xmax >= this.xmin && bb.ymax >= this.ymin && bb.ymin <= this.ymax ) } contains(bb) { return ( bb.xmin >= this.xmin && bb.ymax <= this.ymax && bb.xmax <= this.xmax && bb.ymin >= this.ymin ) } shift(x, y) { this.setBounds(this.xmin + x, this.ymin + y, this.xmax + x, this.ymax + y) } padBounds(a, b, c, d) { this.xmin -= a this.ymin -= b this.xmax += c this.ymax += d } scale(pct, pctY) { /*, focusX, focusY */ const halfWidth = (this.xmax - this.xmin) * 0.5 const halfHeight = (this.ymax - this.ymin) * 0.5 const kx = pct - 1 const ky = pctY === undefined ? kx : pctY - 1 this.xmin -= halfWidth * kx this.ymin -= halfHeight * ky this.xmax += halfWidth * kx this.ymax += halfHeight * ky } clone() { return new Bounds(this.xmin, this.ymin, this.xmax, this.ymax) } clearBounds() { this.setBounds(new Bounds()) } mergePoint(x, y) { if (this.xmin === void 0) { this.setBounds(x, y, x, y) } else { // this works even if x,y are NaN if (x < this.xmin) this.xmin = x else if (x > this.xmax) this.xmax = x if (y < this.ymin) this.ymin = y else if (y > this.ymax) this.ymax = y } } fillOut(aspect, focusX, focusY) { if (arguments.length < 3) { focusX = 0.5 focusY = 0.5 } const w = this.width() const h = this.height() const currAspect = w / h let pad if (isNaN(aspect) || aspect <= 0) { // error condition; don't pad } else if (currAspect < aspect) { // fill out x dimension pad = h * aspect - w this.xmin -= (1 - focusX) * pad this.xmax += focusX * pad } else { pad = w / aspect - h this.ymin -= (1 - focusY) * pad this.ymax += focusY * pad } return this } update() { let tmp if (this.xmin > this.xmax) { tmp = this.xmin this.xmin = this.xmax this.xmax = tmp } if (this.ymin > this.ymax) { tmp = this.ymin this.ymin = this.ymax this.ymax = tmp } } transform(t) { this.xmin = this.xmin * t.mx + t.bx this.xmax = this.xmax * t.mx + t.bx this.ymin = this.ymin * t.my + t.by this.ymax = this.ymax * t.my + t.by this.update() return this } getTransform(b2, flipY) { const t = new Transform() t.mx = b2.width() / this.width() || 1 // TODO: better handling of 0 w,h t.bx = b2.xmin - t.mx * this.xmin if (flipY) { t.my = -b2.height() / this.height() || 1 t.by = b2.ymax - t.my * this.ymin } else { t.my = b2.height() / this.height() || 1 t.by = b2.ymin - t.my * this.ymin } return t } mergeCircle(x, y, r) { if (r < 0) r = -r this.mergeBounds([x - r, y - r, x + r, y + r]) } mergeBounds(bb) { let a, b, c, d if (bb instanceof Bounds) { a = bb.xmin b = bb.ymin c = bb.xmax d = bb.ymax } else if (arguments.length === 4) { a = arguments[0] b = arguments[1] c = arguments[2] d = arguments[3] } else if (bb.length === 4) { // assume array: [xmin, ymin, xmax, ymax] a = bb[0] b = bb[1] c = bb[2] d = bb[3] } else { console.log('Bounds#mergeBounds() invalid argument:', bb) } if (this.xmin === void 0) { this.setBounds(a, b, c, d) } else { if (a < this.xmin) this.xmin = a if (b < this.ymin) this.ymin = b if (c > this.xmax) this.xmax = c if (d > this.ymax) this.ymax = d } return this } } const DataTable = function (obj) { let records if (Array.isArray(obj)) { records = obj } else { records = [] // integer object: create empty records if (obj != null && obj.constructor === Number && (obj | 0) === obj) { for (let i = 0; i < obj; i++) { records.push({}) } } else if (obj) { console.log('Invalid DataTable constructor argument:', obj) } } this.getRecords = function () { return records } // Same-name method in ShapefileTable doesn't require parsing the entire DBF file this.getReadOnlyRecordAt = function (i) { return copyRecord(records[i]) // deep-copies plain objects but not other constructed objects } this.fieldExists = function (name) { return (function (container, item) { if ( container != null && container.toString === String.prototype.toString ) { return container.indexOf(item) !== -1 } else if (isArrayLike(container)) { return ( (function (arr, item) { for (let i = 0, len = arr.length || 0; i < len; i++) { if (arr[i] === item) return i } return -1 })(container, item) !== -1 ) } console.log('Expected Array or String argument') })(this.getFields(), name) } this.toString = function () { return JSON.stringify(this) } this.toJSON = function () { return this.getRecords() } this.addField = function (name, init) { const useFunction = typeof init === 'function' if ( !(init != null && init.constructor === Number) && !(init != null && init.toString === String.prototype.toString) && !useFunction ) { console.log( 'DataTable#addField() requires a string, number or function for initialization' ) } if (this.fieldExists(name)) console.log( 'DataTable#addField() tried to add a field that already exists:', name ) // var dataFieldRxp = /^[a-zA-Z_][a-zA-Z_0-9]*$/; // if (!dataFieldRxp.test(name)) error("DataTable#addField() invalid field name:", name); this.getRecords().forEach(function (obj, i) { obj[name] = useFunction ? init(obj, i) : init }) } this.getRecordAt = function (i) { return this.getRecords()[i] } this.addIdField = function () { this.addField('FID', function (obj, i) { return i }) } this.deleteField = function (f) { this.getRecords().forEach(function (o) { delete o[f] }) } this.getFields = function () { const applyFieldOrder = function (arr, option) { if (option === 'ascending') { arr.sort(function (a, b) { return a.toLowerCase() < b.toLowerCase() ? -1 : 1 }) } return arr } return (function (records, order) { const first = records[0] const names = first ? Object.keys(first) : [] return applyFieldOrder(names, order) })(this.getRecords()) } this.isEmpty = function () { return this.getFields().length === 0 || this.size() === 0 } this.update = function (f) { const records = this.getRecords() for (let i = 0, n = records.length; i < n; i++) { records[i] = f(records[i], i) } } this.clone = function () { // TODO: this could be sped up using a record constructor function // (see getRecordConstructor() in DbfReader) const records2 = this.getRecords().map(copyRecord) return new DataTable(records2) } this.size = function () { return this.getRecords().length } } const copyRecord = function (o) { const o2 = {} let key let val if (!o) return null for (key in o) { if (o.hasOwnProperty(key)) { val = o[key] if (val === o) { // avoid infinite recursion if val is a circular reference, by copying all properties except key val = extendUtil({}, val) delete val[key] } o2[key] = val && val.constructor === Object ? copyRecord(val) : val } } return o2 } const extendBuffer = function (src, newLen, copyLen) { const len = Math.max(src.length, newLen) const n = copyLen || src.length const dest = new src.constructor(len) copyElements(src, 0, dest, 0, n) return dest } const copyElements = function (src, i, dest, j, n, rev) { if (src === dest && j > i) console.log('copy error') let inc = 1 let offs = 0 if (rev) { inc = -1 offs = n - 1 } for (let k = 0; k < n; k++, offs += inc) { dest[k + j] = src[i + offs] } } function ShapeIter(arcs) { this._arcs = arcs this._i = 0 this._n = 0 this.x = 0 this.y = 0 this.hasNext = function () { const arc = this._arc if (this._i < this._n === false) { return false } if (arc.hasNext()) { this.x = arc.x this.y = arc.y return true } this.nextArc() return this.hasNext() } this.init = function (ids) { this._ids = ids this._n = ids.length this.reset() return this } this.nextArc = function () { const i = this._i + 1 if (i < this._n) { this._arc = this._arcs.getArcIter(this._ids[i]) if (i > 0) this._arc.hasNext() // skip first point } this._i = i } this.reset = function () { this._i = -1 this.nextArc() } } const FilteredArcIter = function (xx, yy, zz) { let _zlim = 0 let _i = 0 let _inc = 1 let _stop = 0 this.init = function (i, len, fw, zlim) { _zlim = zlim || 0 if (fw) { _i = i _inc = 1 _stop = i + len } else { _i = i + len - 1 _inc = -1 _stop = i - 1 } return this } this.hasNext = function () { const zarr = zz const i = _i let j = i const zlim = _zlim const stop = _stop const inc = _inc if (i === stop) return false do { j += inc } while (j !== stop && zarr[j] < zlim) _i = j this.x = xx[i] this.y = yy[i] this.i = i return true } } const ArcIter = function (xx, yy) { this._i = 0 this._n = 0 this._inc = 1 this._xx = xx this._yy = yy this.i = 0 this.x = 0 this.y = 0 this.init = function (i, len, fw) { if (fw) { this._i = i this._inc = 1 } else { this._i = i + len - 1 this._inc = -1 } this._n = len return this } this.hasNext = function () { const i = this._i if (this._n > 0) { this._i = i + this._inc this.x = this._xx[i] this.y = this._yy[i] this.i = i this._n-- return true } return false } } const isArrayLike = function (obj) { if (!obj) return false if (Array.isArray(obj)) return true if (obj != null && obj.toString === String.prototype.toString) return false if (obj.length === 0) return true return obj.length > 0 } const ArcIndex = function (pointCount) { const hashTableSize = Math.floor(pointCount * 0.25 + 1) const hash = getXYHash(hashTableSize) const hashTable = new Int32Array(hashTableSize) const chainIds = [] const arcs = [] let arcPoints = 0 initializeArray(hashTable, -1) this.addArc = function (xx, yy) { const end = xx.length - 1 const key = hash(xx[end], yy[end]) const chainId = hashTable[key] const arcId = arcs.length hashTable[key] = arcId arcs.push([xx, yy]) arcPoints += xx.length chainIds.push(chainId) return arcId } this.findDuplicateArc = function (xx, yy, start, end, getNext, getPrev) { let arcId = findArcNeighbor(xx, yy, start, end, getNext) if (arcId === null) { arcId = findArcNeighbor(xx, yy, end, st