UNPKG

geobuf-to-grid

Version:
136 lines (112 loc) 4.53 kB
/** * takes spatial data from GeoJSON and coerces it to regular grid * @author mattwigway */ import extent from 'geojson-extent' import inside from 'turf-inside' import {pixelToLat, pixelToLon, lonToPixel, latToPixel} from './mercator' const HEADER_SIZE = 5 // ints /** * Take geojson point or polygon data and convert it to a grid. Return a map from keys to raw array buffers formatted as follows * (4 byte int) zoom level * (4 byte int) west (x) offset * (4 byte int) north (y) offset * (4 byte int) width * (4 byte int) height * repeated 4 byte int values for each pixel, rows first, delta-coded */ export default function grid (data, zoom) { // for now assume that all numeric properties are to be included, and that all features have the same properties // geojson-extent has a fit if some features don't have properties data.features = data.features.filter(f => f.properties != null) let exemplar = data.features[0] // figure out bounding box let bbox = extent(data) // bbox is w, s, e, n let west = lonToPixel(bbox[0], zoom) let south = latToPixel(bbox[1], zoom) let east = lonToPixel(bbox[2], zoom) let north = latToPixel(bbox[3], zoom) let width = east - west let height = south - north // +y is south console.log(`n ${north} e ${east} s ${south} w ${west} width ${width} height ${height}`) let out = new Map() // loop over all features, accumulate to grid data.features.forEach(feat => { if (feat.properties === undefined || feat.geometry === undefined) return // figure out relevant pixels for this feature and how much of the feature they overlap let pixels = [] if (feat.geometry.type === 'Polygon') { let fbbox = extent(feat.geometry) let fwest = lonToPixel(fbbox[0], zoom) let fsouth = latToPixel(fbbox[1], zoom) let feast = lonToPixel(fbbox[2], zoom) let fnorth = latToPixel(fbbox[3], zoom) for (let x = fwest; x <= feast; x++) { for (let y = fnorth; y <= fsouth; y++) { let pt = { geometry: { type: 'Point', coordinates: [pixelToLon(x, zoom), pixelToLat(y, zoom)] } } if (inside(pt, feat)) { pixels.push((y - north) * width + (x - west)) } } } if (pixels.length === 0) { pixels.push((fnorth - north) * width + fwest - west) } } else if (feat.geometry.type === 'Point') { let x = lonToPixel(feat.geometry.coordinates[0], zoom) - west let y = latToPixel(feat.geometry.coordinates[1], zoom) - north pixels.push(y * width + x) } else { console.log('Attempt to calculate accessibility to unsupported feature type ' + feat.geometry.type) return } for (let key in feat.properties) { if (!feat.properties.hasOwnProperty(key)) continue if (!out.has(key)) out.set(key, getBlankGrid({ zoom, west, north, width, height })) let array = out.get(key) // TODO once we have a weight-per-pixel this won't work // NB the grids are ints, so we round the value. This does not bias the results if the fractional // part of the input data is symmetrically distributed about 0 and not correlated with any other variables of // interest. This means that if a count has been distributed over many cells and each cell has a small fractional // component let val = Math.round(feat.properties[key]) if (isNaN(val)) return // distribute the value in a way that preserves the total. First add the integer part to all // integer divide, note |0 let integerPart = val / pixels.length | 0 pixels.forEach(p => array[p + HEADER_SIZE] += integerPart) // randomly distribute the remainder let remainder = val % pixels.length for (let i = 0; i < remainder; i++) { array[pixels[Math.floor(Math.random() * pixels.length)] + HEADER_SIZE]++ } } }) // delta-code values, extract raw array buffers let ret = new Map() out.forEach((array, key) => { // delta-code for efficient compression for (let i = HEADER_SIZE, prev = 0; i < array.length; i++) { let current = array[i] array[i] = current - prev prev = current } ret.set(key, array) }) return ret } function getBlankGrid({zoom, west, north, width, height}) { let arr = new Int32Array(HEADER_SIZE + width * height) arr[0] = zoom arr[1] = west arr[2] = north arr[3] = width arr[4] = height return arr }