@fmidev/smartmet-alert-client
Version:
Web application for viewing weather and flood alerts
2,138 lines (1,945 loc) • 90.6 kB
JavaScript
// GeoJSON to SVG
// Adapted from https://github.com/mbloch/mapshaper
import Flatbush from 'flatbush'
const rxp = /[&<>"']/g
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
}
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