geojson-to-gml-3
Version:
a package to translate geojson to GML 3.2.1
370 lines (336 loc) • 10.7 kB
text/typescript
// ©️ Steven Kalt
// Spdx-License-Identifier: PolyForm-Noncommercial-1.0.0 OR PolyForm-Free-Trial-1.0.0
import {
Geometry,
GeometryCollection,
LineString,
MultiLineString,
MultiPoint,
MultiPolygon,
Point,
Polygon,
Position,
} from 'geojson';
import {
attrs,
AttrValue,
Name,
Namespace,
NsRegistry,
tag,
tagFn,
ToXml,
Xml,
} from 'minimxml';
import {
type Converter,
CoordinateOrder,
GML,
type Gml,
type MultiGeometry,
type Params,
ToGml,
} from './index.js';
export type NsParam = {
/** the qualified name that points to the xmlns with the value of "http://www.opengis.net/gml/3.2" */
ns: Name;
};
export type IdParam = {
/** the gml:id */
gmlId: string | number | bigint | null;
};
export type CoordOrderParam = {
/** defaults to longitude, latitude */
order: CoordinateOrder;
};
export type SrsNameParam = {
/** a string specifying SRS, e.g. 'EPSG:4326'. Only applies to multigeometries. */
srsName: string;
};
export type GmlIdsParam = {
/** an array of number/string `gml:ids` of member geometries, if any. */
gmlIds: Array<IdParam['gmlId']>;
};
export type SrsDimensionParam = {
/** the dimensionality of each coordinate, i.e. 2 or 3. */
srsDimension: number; // used to be |string
};
/** @private */
export type CoordinateConverter<
Geom extends Exclude<Geometry, GeometryCollection>,
> = (
gml: Namespace,
geom: Geom['coordinates'],
params: Params,
) => ToXml<typeof GML>;
/** @private conversion interface */
type _Converter<Geom extends Geometry = Geometry> = (
gml: Namespace,
geom: Geom,
params: Params,
) => ToXml<typeof GML>;
/**
* As an optimization, generate a function to reorder coordinates.
* @see https://macwright.com/lonlat/
* @private
* @param coords An array of coordinates, [lng, lat, ...etc]
* @return An array of coordinates in the correct order.
*/
export const coordOrder = /* @__PURE__ */ (
order: CoordinateOrder,
): ((input: number[]) => number[]) => {
switch (order) {
case CoordinateOrder.LON_LAT:
return ([lon, lat, ...rest]: number[]) => [lon, lat, ...rest];
case CoordinateOrder.LAT_LON:
return ([lon, lat, ...rest]: number[]) => [lat, lon, ...rest];
default:
throw new Error('invalid order: ' + order);
}
};
/**
* convert a (gml: Namespace, ...) => Gml function to a
* (...) => (r?: NamespaceRegistry) => Gml function
*/
/* @__PURE__ */
export const withGmlNamespace = /* @__PURE__ */ <Geom extends Geometry>(
fn: _Converter<Geom>,
): Converter<Geom> => {
return (geom: Geom, params: Params = {}): ToGml =>
(namespaces?: NsRegistry) => {
namespaces = namespaces ?? new NsRegistry();
const { ns = 'gml' as Name } = params;
const gml = namespaces.getOrInsert(ns, GML as AttrValue);
return fn(gml, geom, params)(namespaces);
};
};
/* @__PURE__ */
export const useCoords =
/* @__PURE__ */
<Geom extends Exclude<Geometry, GeometryCollection>>(
fn: CoordinateConverter<Geom>,
): _Converter<Geom> =>
(gml, geom: Geom, params) =>
fn(gml, geom.coordinates, params);
/* @__PURE__ */
export const gmlPoint: CoordinateConverter<Point> = /* @__PURE__ */ (
gml: Namespace,
coordinates: Point['coordinates'],
params: Partial<
IdParam & SrsDimensionParam & SrsNameParam & CoordOrderParam
> = {},
): ToXml<typeof GML> => {
let {
order = CoordinateOrder.LON_LAT,
srsName = null,
srsDimension = null,
gmlId = null,
} = params;
return tag(
gml.qualify('Point' as Name),
attrs({ srsName, srsDimension, [gml.qualify('id' as Name)]: gmlId }),
tag(
gml.qualify('pos' as Name),
attrs({ srsDimension }),
coordOrder(order)(coordinates).join(' ') as Xml<'text'>,
),
);
};
export const gmlGeometry = /* @__PURE__ */ (
gml: Namespace,
geom: Geometry,
params: Params = {},
): ToXml<typeof GML> => {
switch (geom.type) {
case 'Point':
return gmlPoint(gml, geom.coordinates, params);
case 'LineString':
return gmlLineString(gml, geom.coordinates, params);
case 'Polygon':
return gmlPolygon(gml, geom.coordinates, params);
case 'MultiPoint':
return gmlMultiPoint(gml, geom, params);
case 'MultiLineString':
return gmlMultiLineString(gml, geom, params);
case 'MultiPolygon':
return gmlMultiPolygon(gml, geom, params);
case 'GeometryCollection':
return gmlGeometryCollection(gml, geom, params);
default:
throw new Error(`unknown geometry type: ${(geom as any)?.type}`);
}
};
export const gmlGeometryCollection: _Converter<GeometryCollection> =
/* @__PURE__ */
multi<GeometryCollection>(
'MultiGeometry',
'geometryMembers',
'geometries',
gmlGeometry,
);
export const gmlLineString: CoordinateConverter<LineString> = /* @__PURE__ */ (
gml: Namespace<any, typeof GML>,
coordinates: LineString['coordinates'],
params: Partial<
SrsDimensionParam & SrsNameParam & IdParam & CoordOrderParam
> = {},
): ToXml<typeof GML> => {
let {
srsName = null,
srsDimension = null,
gmlId = null,
order = CoordinateOrder.LON_LAT,
} = params;
return tag(
gml.qualify('LineString' as Name),
attrs({ srsName, [gml.qualify('id' as Name)]: gmlId }),
tag(
gml.qualify('posList' as Name),
attrs({ srsDimension }),
gmlLineStringCoords(coordinates, order),
),
);
};
const gmlLineStringCoords = /* @__PURE__ */ (
coordinates: LineString['coordinates'],
order: CoordinateOrder = CoordinateOrder.LON_LAT,
): Xml<'text'> => {
let result = '';
const _order = coordOrder(order);
for (let pos of coordinates) {
for (let n of _order(pos)) {
result += `${n} `;
}
}
return result.trim() as Xml<'text'>;
};
/**
Construct a gml:LinearRing from an array of coordinates
@function
@param coords the coordinates member of the geojson geometry
@param params optional parameters
@returns a string containing gml representing the input geometry
*/
export const gmlLinearRing: CoordinateConverter<LineString> = /* @__PURE__ */ (
gml: Namespace<any, typeof GML>,
coords: LineString['coordinates'],
params: Partial<
IdParam & SrsNameParam & SrsDimensionParam & CoordOrderParam
> = {},
): ToXml<typeof GML> => {
let {
srsName = null,
srsDimension = null,
gmlId = null,
order = CoordinateOrder.LON_LAT,
} = params;
const _order = coordOrder(order);
return tag(
gml.qualify('LinearRing' as Name),
attrs({ srsName, [gml.qualify('id' as Name)]: gmlId }),
tag(
gml.qualify('posList' as Name),
attrs({ srsDimension }),
coords.map((e) => _order(e).join(' ')).join(' ') as Xml<''>,
),
);
};
export const gmlPolygon: CoordinateConverter<Polygon> = /* @__PURE__ */ (
gml: Namespace<any, typeof GML>,
coordinates: Polygon['coordinates'],
params: Partial<IdParam & SrsNameParam> & Params = {},
): ToXml<typeof GML> => {
let { srsName = null, gmlId = null, ...rest } = params;
// geom.coordinates are arrays of LinearRings
let [poly, ...rings] = coordinates;
const _interior = tagFn(gml.qualify('interior' as Name));
return tag(
gml.qualify('Polygon' as Name),
attrs({ srsName, [gml.qualify('id' as Name)]: gmlId }),
tag(gml.qualify('exterior' as Name), [], gmlLinearRing(gml, poly, rest)),
...rings.map((ring) => _interior([], gmlLinearRing(gml, ring, rest))),
);
};
export const gmlMultiPoint: _Converter<MultiPoint> =
/* @__PURE__ */ multi<MultiPoint>(
'MultiPoint',
'pointMembers', // see file://./../../../spec/gml-3/geometryAggregates.xsd#pointMembers
'coordinates',
gmlPoint,
);
export const gmlMultiLineString: _Converter<MultiLineString> =
/* @__PURE__ */
multi<MultiLineString>(
'MultiCurve', // see file://./../../../spec/gml-3/geometryAggregates.xsd#MultiCurveType
'curveMembers', // see file://./../../../spec/gml-3/geometryBasic0d1d.xsd#CurveArrayPropertyType
'coordinates',
gmlLineString,
);
export const gmlMultiPolygon: _Converter<MultiPolygon> =
/* @__PURE__ */ multi<MultiPolygon>(
'MultiSurface', // see file://./../../../spec/gml-3/geometryAggregates.xsd#MultiSurface
'surfaceMembers',
'coordinates',
gmlPolygon,
);
type ElementOf<T> = T extends Array<infer U> ? U : never;
type Associate<G extends Geometry, Kind, Value> =
G extends Kind ? Value : never;
type GmlName<G extends MultiGeometry> =
| Associate<G, MultiPoint, 'MultiPoint'>
| Associate<G, MultiLineString, 'MultiCurve'>
| Associate<G, MultiPolygon, 'MultiSurface'>
| Associate<G, GeometryCollection, 'MultiGeometry'>;
type MemberName<G extends MultiGeometry> =
| Associate<G, MultiPoint, 'pointMembers'>
| Associate<G, MultiLineString, 'curveMembers'>
| Associate<G, MultiPolygon, 'surfaceMembers'>
| Associate<G, GeometryCollection, 'geometryMembers'>;
type MemberKey<G extends MultiGeometry> = keyof G &
(G extends GeometryCollection ? 'geometries' : 'coordinates');
type MemberKind<G extends MultiGeometry> = ElementOf<G[MemberKey<G>]>;
type ConverterFn<Collection extends MultiGeometry> = (
gml: Namespace,
geom: MemberKind<Collection>,
params: Params,
) => ToXml<typeof GML>;
/**
* A handler to compile geometries to multigeometries
* @function
* @param elementName the name of the GML element corresponding to the input json {@link MultiGeometry}
* @param memberElementName the inner element holding the member geometries.
* @param key the key of the geojson object that points to the array of member
* geometries or coordinates
* @param renderMember a callback to render each member geometry to GML
* @returns a function to render the specified multigeometry collection kind into GML
* @throws if a member geometry cannot be converted to GML
*/
export function multi<Collection extends MultiGeometry>(
elementName: GmlName<Collection>,
memberElementName: MemberName<Collection>,
key: MemberKey<Collection>,
renderMember: ConverterFn<Collection>,
): _Converter<Collection> {
/* @__PURE__ */
return (
gml: Namespace<any, typeof GML>,
geom: Collection,
params: Params & Partial<GmlIdsParam> = {},
) => {
let { srsName = null, gmlId = null, gmlIds = [], ...rest } = params;
return tag(
gml.qualify(elementName as string as Name),
attrs({ srsName, [gml.qualify('id' as Name)]: gmlId }),
tag(
gml.qualify(memberElementName as string as Name),
[],
...(geom[key] as MemberKind<Collection>[]).map((member, i) =>
renderMember(gml, member, {
gmlId: gmlIds[i] ?? null,
...rest,
}),
),
),
);
};
}