199 lines
6.7 KiB
Python
199 lines
6.7 KiB
Python
"""fio-dump"""
|
|
|
|
from functools import partial
|
|
import json
|
|
import logging
|
|
|
|
import click
|
|
import cligj
|
|
|
|
import fiona
|
|
from fiona.fio import helpers, options, with_context_env
|
|
from fiona.model import Feature, ObjectEncoder
|
|
from fiona.transform import transform_geom
|
|
|
|
|
|
@click.command(short_help="Dump a dataset to GeoJSON.")
|
|
@click.argument('input', required=True)
|
|
@click.option('--layer', metavar="INDEX|NAME", callback=options.cb_layer,
|
|
help="Print information about a specific layer. The first "
|
|
"layer is used by default. Layers use zero-based "
|
|
"numbering when accessed by index.")
|
|
@click.option('--encoding', help="Specify encoding of the input file.")
|
|
@cligj.precision_opt
|
|
@cligj.indent_opt
|
|
@cligj.compact_opt
|
|
@click.option('--record-buffered/--no-record-buffered', default=False,
|
|
help="Economical buffering of writes at record, not collection "
|
|
"(default), level.")
|
|
@click.option('--ignore-errors/--no-ignore-errors', default=False,
|
|
help="log errors but do not stop serialization.")
|
|
@click.option('--with-ld-context/--without-ld-context', default=False,
|
|
help="add a JSON-LD context to JSON output.")
|
|
@click.option('--add-ld-context-item', multiple=True,
|
|
help="map a term to a URI and add it to the output's JSON LD "
|
|
"context.")
|
|
@options.open_opt
|
|
@click.pass_context
|
|
@with_context_env
|
|
def dump(
|
|
ctx,
|
|
input,
|
|
encoding,
|
|
precision,
|
|
indent,
|
|
compact,
|
|
record_buffered,
|
|
ignore_errors,
|
|
with_ld_context,
|
|
add_ld_context_item,
|
|
layer,
|
|
open_options,
|
|
):
|
|
|
|
"""Dump a dataset either as a GeoJSON feature collection (the default)
|
|
or a sequence of GeoJSON features."""
|
|
|
|
logger = logging.getLogger(__name__)
|
|
sink = click.get_text_stream('stdout')
|
|
|
|
dump_kwds = {'sort_keys': True}
|
|
if indent:
|
|
dump_kwds['indent'] = indent
|
|
if compact:
|
|
dump_kwds['separators'] = (',', ':')
|
|
item_sep = compact and ',' or ', '
|
|
|
|
if encoding:
|
|
open_options["encoding"] = encoding
|
|
if layer:
|
|
open_options["layer"] = layer
|
|
|
|
def transformer(crs, feat):
|
|
tg = partial(
|
|
transform_geom,
|
|
crs,
|
|
"EPSG:4326",
|
|
antimeridian_cutting=True,
|
|
precision=precision,
|
|
)
|
|
return Feature(
|
|
id=feat.id, properties=feat.properties, geometry=tg(feat.geometry)
|
|
)
|
|
|
|
with fiona.open(input, **open_options) as source:
|
|
meta = source.meta
|
|
meta["fields"] = dict(source.schema["properties"].items())
|
|
|
|
if record_buffered:
|
|
# Buffer GeoJSON data at the feature level for smaller
|
|
# memory footprint.
|
|
indented = bool(indent)
|
|
rec_indent = "\n" + " " * (2 * (indent or 0))
|
|
|
|
collection = {
|
|
"type": "FeatureCollection",
|
|
"fiona:schema": meta["schema"],
|
|
"fiona:crs": meta["crs"],
|
|
"features": [],
|
|
}
|
|
if with_ld_context:
|
|
collection["@context"] = helpers.make_ld_context(add_ld_context_item)
|
|
|
|
head, tail = json.dumps(collection, **dump_kwds).split("[]")
|
|
|
|
sink.write(head)
|
|
sink.write("[")
|
|
|
|
itr = iter(source)
|
|
|
|
# Try the first record.
|
|
try:
|
|
i, first = 0, next(itr)
|
|
first = transformer(first)
|
|
if with_ld_context:
|
|
first = helpers.id_record(first)
|
|
if indented:
|
|
sink.write(rec_indent)
|
|
sink.write(
|
|
json.dumps(first, cls=ObjectEncoder, **dump_kwds).replace(
|
|
"\n", rec_indent
|
|
)
|
|
)
|
|
except StopIteration:
|
|
pass
|
|
except Exception as exc:
|
|
# Ignoring errors is *not* the default.
|
|
if ignore_errors:
|
|
logger.error(
|
|
"failed to serialize file record %d (%s), " "continuing", i, exc
|
|
)
|
|
else:
|
|
# Log error and close up the GeoJSON, leaving it
|
|
# more or less valid no matter what happens above.
|
|
logger.critical(
|
|
"failed to serialize file record %d (%s), " "quitting", i, exc
|
|
)
|
|
sink.write("]")
|
|
sink.write(tail)
|
|
if indented:
|
|
sink.write("\n")
|
|
raise
|
|
|
|
# Because trailing commas aren't valid in JSON arrays
|
|
# we'll write the item separator before each of the
|
|
# remaining features.
|
|
for i, rec in enumerate(itr, 1):
|
|
rec = transformer(rec)
|
|
try:
|
|
if with_ld_context:
|
|
rec = helpers.id_record(rec)
|
|
if indented:
|
|
sink.write(rec_indent)
|
|
sink.write(item_sep)
|
|
sink.write(
|
|
json.dumps(rec, cls=ObjectEncoder, **dump_kwds).replace(
|
|
"\n", rec_indent
|
|
)
|
|
)
|
|
except Exception as exc:
|
|
if ignore_errors:
|
|
logger.error(
|
|
"failed to serialize file record %d (%s), "
|
|
"continuing",
|
|
i, exc)
|
|
else:
|
|
logger.critical(
|
|
"failed to serialize file record %d (%s), "
|
|
"quitting",
|
|
i, exc)
|
|
sink.write("]")
|
|
sink.write(tail)
|
|
if indented:
|
|
sink.write("\n")
|
|
raise
|
|
|
|
# Close up the GeoJSON after writing all features.
|
|
sink.write("]")
|
|
sink.write(tail)
|
|
if indented:
|
|
sink.write("\n")
|
|
|
|
else:
|
|
# Buffer GeoJSON data at the collection level. The default.
|
|
collection = {
|
|
"type": "FeatureCollection",
|
|
"fiona:schema": meta["schema"],
|
|
"fiona:crs": meta["crs"].to_string(),
|
|
}
|
|
if with_ld_context:
|
|
collection["@context"] = helpers.make_ld_context(add_ld_context_item)
|
|
collection["features"] = [
|
|
helpers.id_record(transformer(rec)) for rec in source
|
|
]
|
|
else:
|
|
collection["features"] = [
|
|
transformer(source.crs, rec) for rec in source
|
|
]
|
|
json.dump(collection, sink, cls=ObjectEncoder, **dump_kwds)
|