Documentation

Pro handbook

GeoJSON import and export

Both functions live in @vectorstream/core and work on one partition — one layer.

Importing#

importGeoJSON(featureCollection, options?) builds a new partition. To edit it, pass it to a new editor (or into a layer of one); an existing editor's partition is not replaced.

What it accepts: a FeatureCollection of Polygon (with holes), LineString and Point features. A feature with geometry: null carries the attributes of the outer face. Other geometry types (MultiPolygon, MultiPoint, …) are rejected. Positions need at least two finite numbers; further numbers (elevation) are ignored.

What it expects: cleanly noded data. Shared boundaries must have bit-identical coordinates, and edges must not cross. Identical coordinates become one node, identical segments one edge — so two parcels that share a boundary share an edge after import. The import checks the structure (Euler's formula and similar) and, by default, the geometry: it runs checkGeometry on the result and throws an ImportError ('core.importInvalidGeometry', findings in violations) for overlapping areas, crossing edges, or a line ending in the middle of a foreign edge. For trusted documents (for example your own exportGeoJSON output) the check can be switched off with geometriePruefen: false; see Validity and rejections.

What happens on the way in:

  • Coordinates are rounded onto the partition's grid.
  • Properties become attributes of the element the feature describes: a polygon's go to its face, a line's to every one of its edges, a point's to the node at that position (also a vertex in the middle of a boundary). Values that are not string, number, boolean or null are skipped.
  • A property named id is skipped: IDs are assigned fresh on every import.
  • If two features describe the same element, their properties are merged. If they give one key different values, the first one stays and a warning is reported.
Option Type Default Meaning
gridExponent number 26 Grid mesh size 2−gridExponent.
geometriePruefen boolean true Run checkGeometry on the result and throw an ImportError with the findings if there are any. false skips the check (trusted data); the partition may then carry geometry problems, which the editor tolerates as legacy. Cost: about +50% of the import time (2025 areas: 20 → 31 ms).
onWarnung (warning: Importwarnung) => void — Called for properties that could not be placed (art: 'nicht-zugeordnet') for conflicting values (art: 'widerspruch'), and for internal vs: keys on a feature with geometry, which are dropped (art: 'intern'; such keys belong on the layer's geometry: null feature only). Each warning has detail, code and params. Without it, warnings are dropped.

Errors are thrown as ImportError with a stable code — 'core.importNoGeometry', 'core.importUnsupportedGeometry', 'core.importPositionNotFinite', 'core.importRingTooFewNodes', 'core.importInvalidPartition', 'core.importInvalidGeometry' and others — plus params and a violations list. The message is in the current language.

import {
  ImportError,
  exportGeoJSON,
  importGeoJSON,
  type FeatureCollection,
  type Importwarnung,
  type Partition,
} from '@vectorstream/core';
import type { Editor } from '@vectorstream/editor';

export type LoadResult =
  | { ok: true; partition: Partition; warnings: Importwarnung[] }
  | { ok: false; code: string; message: string };

/** Builds a partition from GeoJSON that is already cleanly noded (shared boundaries bit-identical). */
export function loadGeoJSON(text: string): LoadResult {
  const warnings: Importwarnung[] = [];
  try {
    const data = JSON.parse(text) as FeatureCollection;
    const partition = importGeoJSON(data, { onWarnung: (w) => warnings.push(w) });
    return { ok: true, partition, warnings };
  } catch (error) {
    // `code` is stable ('core.importNoGeometry', …); `message` is in the current language.
    if (error instanceof ImportError) return { ok: false, code: error.code, message: error.message };
    throw error; // e.g. a SyntaxError from JSON.parse
  }
}

/** One FeatureCollection per layer: polygons, then lines, then points, each with an `id`. */
export function saveLayers(editor: Editor): Record<string, FeatureCollection> {
  return Object.fromEntries(editor.ebenen.map((layer) => [layer.id, exportGeoJSON(layer.partition)]));
}

Its test loads the sample parcels (6 nodes, 7 edges — the shared boundary once), checks the error codes for a feature without geometry and for a MultiPoint, the warning for one polygon given twice with different values, and the export below.

Exporting#

exportGeoJSON(partition) returns a FeatureCollection:

  1. one Polygon per bounded face — outer ring first, then holes;
  2. one LineString per line: a chain of edges with the same face on both sides and the same attributes is one feature, not one per segment;
  3. one Point per free-standing node, and per vertex that carries attributes.

Each group is ordered by ID. Every feature's properties are its attributes plus id — the element ID ('f2', 'h9', 'n8'), set last so an attribute named id cannot override it. The attributes of the outer face, if any, are exported as a feature with geometry: null.

Coordinates are the grid-rounded values (51.51 comes out as 51.51000000536442). Exporting, importing and exporting again gives the same geometry and attributes, with IDs that may differ — the example's test checks this for a polygon, a line and a point.

For several layers, export each layer's partition — saveLayers in the example above returns one collection per layer ID.

API: importGeoJSON, ImportOptions, ImportError, exportGeoJSON.

See it on a map.