feature_collection_to_feather_frame = function(
feature_collection, projection, options = {dictionary_threshold: .75, clip_to_sphere: false}) {
if (projection === undefined) {throw "Must define a projection"}
const properties = new Map()
properties.set("id", [])
const vertices = []
const coordinates = []
const coord_resolutions = []
const centroids = [[], []]
const areas = []
let i = -1;
const path = d3.geoPath()
let clip_shape;
if (options.clip_to_sphere) {
clip_shape = d3.geoProject({"type": "Sphere"}, projection)
}
const bounds = [[Infinity, Infinity], [-Infinity, -Infinity]]
for (let feature of feature_collection.features) {
i++;
properties.get("id")[i] = feature.id;
for (let [k, v] of Object.entries(feature.properties)) {
if (!properties.get(k)) {properties.set(k, [])}
properties.get(k)[i] = v
}
let projected = d3.geoProject(feature.geometry, projection);
if (options.clip_to_sphere) {
const new_coords = clip.intersection(projected.coordinates, clip_shape.coordinates)
if (projected.type == "Polygon" && typeof(new_coords[0][0][0] != "numeric")) {
projected.type = "MultiPolygon"
}
projected.coordinates = new_coords
}
[centroids[0][i], centroids[1][i]] = path.centroid(projected)
areas[i] = path.area(projected)
const loc_bounds = d3.geoPath().bounds(projected)
for (let dim of [0, 1]) {
if (loc_bounds[0][dim] < bounds[0][dim]) {bounds[0][dim] = loc_bounds[0][dim]}
if (loc_bounds[1][dim] > bounds[1][dim]) {bounds[1][dim] = loc_bounds[1][dim]}
}
let loc_coordinates;
if (projected === null) {
coord_resolutions[i] = null
coordinates[i] = null
vertices[i] = null
continue
} else
if (projected.type == "Polygon") {
loc_coordinates = [projected.coordinates]
} else if (projected.type == "MultiPolygon") {
loc_coordinates = projected.coordinates
} else {
throw "All elements must be polygons or multipolgyons."
}
let all_coords = []
let all_vertices = []
for (let polygon of loc_coordinates) {
const current_vertex = all_coords.length/2
const { coords, vertices } = polygon_to_triangles(polygon);
all_coords.push(...coords)
// If need to shift because we may be storing multiple triangle sets on a feature.
all_vertices.push(...vertices.map(d => d + current_vertex))
}
// Flatten to a silly degree just in case?
all_vertices = all_vertices.flat(10)
coordinates[i] = Float32Array.from(all_coords.flat(20))
// The type of the vertex array can be smaller if there
// are fewer coordinates to reference.
let MyArray
if (all_coords.length < 2**8) {
coord_resolutions[i] = 8
MyArray = Uint8Array
} else if (all_coords.length < 2**16) {
coord_resolutions[i] = 16
MyArray = Uint16Array
} else {
// Will not allow more than 4 billion points on a single feature,
// should be fine.
coord_resolutions[i] = 32
MyArray = Uint32Array
}
vertices[i] = MyArray.from(all_vertices.flat(10))
}
const cols = {
"coordinates": pack_binary(coordinates),
"vertices": pack_binary(vertices),
"coord_resolution": arrow.Uint8Vector.from(coord_resolutions),
"pixel_area": arrow.Float32Vector.from(areas),
"centroid_x": arrow.Float32Vector.from(centroids[0]),
"centroid_y": arrow.Float32Vector.from(centroids[1])
}
for (const [k, v] of properties.entries()) {
if (k in cols) {
// silently ignore.
//throw `Duplicate column names--rename ${k} `;
}
cols[k] = arrow.Vector.from({nullable: true, values: v, type: infer_type(v, options.dictionary_threshold)})
}
const named_columns = []
for (const [k, v] of Object.entries(cols)) {
// console.log(k, v)
named_columns.push(arrow.Column.new(k, v))
}
const tab = arrow.Table.new(...named_columns)
secret.bounds = bounds
return tab
}