diff --git a/examples/wkb.html b/examples/wkb.html
new file mode 100644
index 0000000000..faa782a5b0
--- /dev/null
+++ b/examples/wkb.html
@@ -0,0 +1,9 @@
+---
+layout: example.html
+title: WKB
+shortdesc: Example of using the WKB parser.
+docs: >
+ Create features from geometries in WKB (Well Known Binary) format.
+tags: "wkb, well known binary"
+---
+
diff --git a/examples/wkb.js b/examples/wkb.js
new file mode 100644
index 0000000000..935f8d3e09
--- /dev/null
+++ b/examples/wkb.js
@@ -0,0 +1,33 @@
+import Map from '../src/ol/Map.js';
+import View from '../src/ol/View.js';
+import WKB from '../src/ol/format/WKB.js';
+import {OSM, Vector as VectorSource} from '../src/ol/source.js';
+import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
+
+const raster = new TileLayer({
+ source: new OSM(),
+});
+
+const wkb = '0103000000010000000500000054E3A59BC4602540643BDF4F8D1739C05C8FC2F5284C4140EC51B81E852B34C0D578E926316843406F1283C0CAD141C01B2FDD2406012B40A4703D0AD79343C054E3A59BC4602540643BDF4F8D1739C0';
+
+const format = new WKB();
+
+const feature = format.readFeature(wkb, {
+ dataProjection: 'EPSG:4326',
+ featureProjection: 'EPSG:3857',
+});
+
+const vector = new VectorLayer({
+ source: new VectorSource({
+ features: [feature],
+ }),
+});
+
+const map = new Map({
+ layers: [raster, vector],
+ target: 'map',
+ view: new View({
+ center: [2952104.0199, -3277504.823],
+ zoom: 4,
+ }),
+});
diff --git a/src/ol/format/WKB.js b/src/ol/format/WKB.js
new file mode 100644
index 0000000000..37c0a5f1c5
--- /dev/null
+++ b/src/ol/format/WKB.js
@@ -0,0 +1,866 @@
+/**
+ * @module ol/format/WKB
+ */
+import Feature from '../Feature.js';
+import FeatureFormat, {transformGeometryWithOptions} from './Feature.js';
+import FormatType from './FormatType.js';
+import GeometryCollection from '../geom/GeometryCollection.js';
+import GeometryLayout from '../geom/GeometryLayout.js';
+import GeometryType from '../geom/GeometryType.js';
+import LineString from '../geom/LineString.js';
+import MultiLineString from '../geom/MultiLineString.js';
+import MultiPoint from '../geom/MultiPoint.js';
+import MultiPolygon from '../geom/MultiPolygon.js';
+import Point from '../geom/Point.js';
+import Polygon from '../geom/Polygon.js';
+import {get as getProjection} from '../proj.js';
+
+import SimpleGeometry from '../geom/SimpleGeometry.js';
+import {assign} from '../obj.js';
+
+// WKB spec: https://www.ogc.org/standards/sfa
+// EWKB spec: https://raw.githubusercontent.com/postgis/postgis/2.1.0/doc/ZMSgeoms.txt
+
+/**
+ * @const
+ * @enum {number}
+ */
+const WKBGeometryType = {
+ POINT: 1,
+ LINE_STRING: 2,
+ POLYGON: 3,
+ MULTI_POINT: 4,
+ MULTI_LINE_STRING: 5,
+ MULTI_POLYGON: 6,
+ GEOMETRY_COLLECTION: 7,
+
+ /*
+ CIRCULAR_STRING: 8,
+ COMPOUND_CURVE: 9,
+ CURVE_POLYGON: 10,
+
+ MULTI_CURVE: 11,
+ MULTI_SURFACE: 12,
+ CURVE: 13,
+ SURFACE: 14,
+ */
+
+ POLYHEDRAL_SURFACE: 15,
+ TIN: 16,
+ TRIANGLE: 17,
+};
+
+class WkbReader {
+ /**
+ * @param {DataView} view source to read
+ */
+ constructor(view) {
+ this.view = view;
+ this.pos = 0;
+
+ this.initialized = false;
+ this.isLittleEndian = false;
+ this.hasZ = false;
+ this.hasM = false;
+ /** @type {number} */
+ this.SRID = null;
+
+ this.layout = GeometryLayout.XY;
+ }
+
+ /**
+ * @return {number} value
+ */
+ readUint8() {
+ return this.view.getUint8(this.pos++);
+ }
+
+ /**
+ * @param {boolean} [isLittleEndian] Whether read value as little endian
+ * @return {number} value
+ */
+ readUint32(isLittleEndian) {
+ return this.view.getUint32(
+ (this.pos += 4) - 4,
+ isLittleEndian !== undefined ? isLittleEndian : this.isLittleEndian
+ );
+ }
+
+ /**
+ * @param {boolean} [isLittleEndian] Whether read value as little endian
+ * @return {number} value
+ */
+ readDouble(isLittleEndian) {
+ return this.view.getFloat64(
+ (this.pos += 8) - 8,
+ isLittleEndian !== undefined ? isLittleEndian : this.isLittleEndian
+ );
+ }
+
+ /**
+ * @return {import('../coordinate.js').Coordinate} coords for Point
+ */
+ readPoint() {
+ /** @type import('../coordinate.js').Coordinate */
+ const coords = [];
+
+ coords.push(this.readDouble());
+ coords.push(this.readDouble());
+ if (this.hasZ) {
+ coords.push(this.readDouble());
+ }
+ if (this.hasM) {
+ coords.push(this.readDouble());
+ }
+
+ return coords;
+ }
+
+ /**
+ * @return {Array} coords for LineString / LinearRing
+ */
+ readLineString() {
+ const numPoints = this.readUint32();
+
+ /** @type Array */
+ const coords = [];
+ for (let i = 0; i < numPoints; i++) {
+ coords.push(this.readPoint());
+ }
+
+ return coords;
+ }
+
+ /**
+ * @return {Array>} coords for Polygon like
+ */
+ readPolygon() {
+ const numRings = this.readUint32();
+
+ /** @type Array> */
+ const rings = [];
+ for (let i = 0; i < numRings; i++) {
+ rings.push(this.readLineString()); // as a LinearRing
+ }
+
+ return rings;
+ }
+
+ /**
+ * @param {number} [expectedTypeId] Expected WKB Type ID
+ * @return {number} WKB Type ID
+ */
+ readWkbHeader(expectedTypeId) {
+ const byteOrder = this.readUint8();
+ const isLittleEndian = byteOrder > 0;
+
+ const wkbType = this.readUint32(isLittleEndian);
+ const wkbTypeThousandth = Math.floor((wkbType & 0x0fffffff) / 1000);
+ const hasZ =
+ Boolean(wkbType & 0x80000000) ||
+ wkbTypeThousandth === 1 ||
+ wkbTypeThousandth === 3;
+ const hasM =
+ Boolean(wkbType & 0x40000000) ||
+ wkbTypeThousandth === 2 ||
+ wkbTypeThousandth === 3;
+ const hasSRID = Boolean(wkbType & 0x20000000);
+ const typeId = (wkbType & 0x0fffffff) % 1000; // Assume 1000 is an upper limit for type ID
+ const layout = ['XY', hasZ ? 'Z' : '', hasM ? 'M' : ''].join('');
+
+ const SRID = hasSRID ? this.readUint32(isLittleEndian) : null;
+
+ if (expectedTypeId !== undefined && expectedTypeId !== typeId) {
+ throw new Error('Unexpected WKB geometry type ' + typeId);
+ }
+
+ if (this.initialized) {
+ // sanity checks
+ if (this.isLittleEndian !== isLittleEndian) {
+ throw new Error('Inconsistent endian');
+ }
+ if (this.layout !== layout) {
+ throw new Error('Inconsistent geometry layout');
+ }
+ if (SRID && this.SRID !== SRID) {
+ throw new Error('Inconsistent coordinate system (SRID)');
+ }
+ } else {
+ this.isLittleEndian = isLittleEndian;
+ this.hasZ = hasZ;
+ this.hasM = hasM;
+ this.layout = layout;
+ this.SRID = SRID;
+ this.initialized = true;
+ }
+
+ return typeId;
+ }
+
+ /**
+ * @param {number} typeId WKB Type ID
+ * @return {any} values read
+ */
+ readWkbPayload(typeId) {
+ switch (typeId) {
+ case WKBGeometryType.POINT:
+ return this.readPoint();
+
+ case WKBGeometryType.LINE_STRING:
+ return this.readLineString();
+
+ case WKBGeometryType.POLYGON:
+ case WKBGeometryType.TRIANGLE:
+ return this.readPolygon();
+
+ case WKBGeometryType.MULTI_POINT:
+ return this.readMultiPoint();
+
+ case WKBGeometryType.MULTI_LINE_STRING:
+ return this.readMultiLineString();
+
+ case WKBGeometryType.MULTI_POLYGON:
+ case WKBGeometryType.POLYHEDRAL_SURFACE:
+ case WKBGeometryType.TIN:
+ return this.readMultiPolygon();
+
+ case WKBGeometryType.GEOMETRY_COLLECTION:
+ return this.readGeometryCollection();
+
+ default:
+ throw new Error(
+ 'Unsupported WKB geometry type ' + typeId + ' is found'
+ );
+ }
+ }
+
+ /**
+ * @param {number} expectedTypeId Expected WKB Type ID
+ * @return {any} values read
+ */
+ readWkbBlock(expectedTypeId) {
+ return this.readWkbPayload(this.readWkbHeader(expectedTypeId));
+ }
+
+ /**
+ * @param {Function} reader reader function for each item
+ * @param {number} [expectedTypeId] Expected WKB Type ID
+ * @return {any} values read
+ */
+ readWkbCollection(reader, expectedTypeId) {
+ const num = this.readUint32();
+
+ const items = [];
+ for (let i = 0; i < num; i++) {
+ const result = reader.call(this, expectedTypeId);
+ if (result) {
+ items.push(result);
+ }
+ }
+
+ return items;
+ }
+
+ /**
+ * @return {Array} coords for MultiPoint
+ */
+ readMultiPoint() {
+ return this.readWkbCollection(this.readWkbBlock, WKBGeometryType.POINT);
+ }
+
+ /**
+ * @return {Array>} coords for MultiLineString like
+ */
+ readMultiLineString() {
+ return this.readWkbCollection(
+ this.readWkbBlock,
+ WKBGeometryType.LINE_STRING
+ );
+ }
+
+ /**
+ * @return {Array>>} coords for MultiPolygon like
+ */
+ readMultiPolygon() {
+ return this.readWkbCollection(this.readWkbBlock, WKBGeometryType.POLYGON);
+ }
+
+ /**
+ * @return {Array} array of geometries
+ */
+ readGeometryCollection() {
+ return this.readWkbCollection(this.readGeometry);
+ }
+
+ /**
+ * @return {import('../geom/Geometry.js').default} geometry
+ */
+ readGeometry() {
+ const typeId = this.readWkbHeader();
+ const result = this.readWkbPayload(typeId);
+
+ switch (typeId) {
+ case WKBGeometryType.POINT:
+ return new Point(
+ /** @type {import('../coordinate.js').Coordinate} */ (result),
+ this.layout
+ );
+
+ case WKBGeometryType.LINE_STRING:
+ return new LineString(
+ /** @type {Array} */ (result),
+ this.layout
+ );
+
+ case WKBGeometryType.POLYGON:
+ case WKBGeometryType.TRIANGLE:
+ return new Polygon(
+ /** @type {Array>} */ (result),
+ this.layout
+ );
+
+ case WKBGeometryType.MULTI_POINT:
+ return new MultiPoint(
+ /** @type {Array} */ (result),
+ this.layout
+ );
+
+ case WKBGeometryType.MULTI_LINE_STRING:
+ return new MultiLineString(
+ /** @type {Array>} */ (result),
+ this.layout
+ );
+
+ case WKBGeometryType.MULTI_POLYGON:
+ case WKBGeometryType.POLYHEDRAL_SURFACE:
+ case WKBGeometryType.TIN:
+ return new MultiPolygon(
+ /** @type {Array>>} */ (result),
+ this.layout
+ );
+
+ case WKBGeometryType.GEOMETRY_COLLECTION:
+ return new GeometryCollection(
+ /** @type {Array} */ (result)
+ );
+
+ default:
+ return null;
+ }
+ }
+}
+
+class WkbWriter {
+ /**
+ * @type {object}
+ * @property {string} [layout] geometryLayout
+ * @property {boolean} [littleEndian=true] littleEndian
+ * @property {boolean} [ewkb=true] Whether writes in EWKB format
+ * @property {object} [nodata] NoData value for each axes
+ * @param {object} opts options
+ */
+ constructor(opts) {
+ opts = opts || {};
+
+ /** @type {string} */
+ this.layout = opts.layout;
+ this.isLittleEndian = opts.littleEndian !== false;
+
+ this.isEWKB = opts.ewkb !== false;
+
+ /** @type {Array>} */
+ this.writeQueue = [];
+
+ /**
+ * @type {object}
+ * @property {number} X NoData value for X
+ * @property {number} Y NoData value for Y
+ * @property {number} Z NoData value for Z
+ * @property {number} M NoData value for M
+ */
+ this.nodata = assign({X: 0, Y: 0, Z: 0, M: 0}, opts.nodata);
+ }
+
+ /**
+ * @param {number} value value
+ */
+ writeUint8(value) {
+ this.writeQueue.push([1, value]);
+ }
+
+ /**
+ * @param {number} value value
+ */
+ writeUint32(value) {
+ this.writeQueue.push([4, value]);
+ }
+
+ /**
+ * @param {number} value value
+ */
+ writeDouble(value) {
+ this.writeQueue.push([8, value]);
+ }
+
+ /**
+ * @param {import('../coordinate.js').Coordinate} coords coords
+ * @param {import("../geom/GeometryLayout").default} layout layout
+ */
+ writePoint(coords, layout) {
+ /**
+ * @type {object}
+ * @property {number} X NoData value for X
+ * @property {number} Y NoData value for Y
+ * @property {number} [Z] NoData value for Z
+ * @property {number} [M] NoData value for M
+ */
+ const coordsObj = assign.apply(
+ null,
+ layout.split('').map((axis, idx) => ({[axis]: coords[idx]}))
+ );
+
+ for (const axis of this.layout) {
+ this.writeDouble(axis in coordsObj ? coordsObj[axis] : this.nodata[axis]);
+ }
+ }
+
+ /**
+ * @param {Array} coords coords
+ * @param {import("../geom/GeometryLayout").default} layout layout
+ */
+ writeLineString(coords, layout) {
+ this.writeUint32(coords.length); // numPoints
+ for (let i = 0; i < coords.length; i++) {
+ this.writePoint(coords[i], layout);
+ }
+ }
+
+ /**
+ * @param {Array>} rings rings
+ * @param {import("../geom/GeometryLayout").default} layout layout
+ */
+ writePolygon(rings, layout) {
+ this.writeUint32(rings.length); // numRings
+ for (let i = 0; i < rings.length; i++) {
+ this.writeLineString(rings[i], layout); // as a LinearRing
+ }
+ }
+
+ /**
+ * @param {number} wkbType WKB Type ID
+ * @param {number} [SRID] SRID
+ */
+ writeWkbHeader(wkbType, SRID) {
+ wkbType %= 1000; // Assume 1000 is an upper limit for type ID
+ if (this.layout.indexOf('Z') >= 0) {
+ wkbType += this.isEWKB ? 0x80000000 : 1000;
+ }
+ if (this.layout.indexOf('M') >= 0) {
+ wkbType += this.isEWKB ? 0x40000000 : 2000;
+ }
+ if (this.isEWKB && Number.isInteger(SRID)) {
+ wkbType |= 0x20000000;
+ }
+
+ this.writeUint8(this.isLittleEndian ? 1 : 0);
+ this.writeUint32(wkbType);
+ if (this.isEWKB && Number.isInteger(SRID)) {
+ this.writeUint32(SRID);
+ }
+ }
+
+ /**
+ * @param {Array} coords coords
+ * @param {string} layout layout
+ */
+ writeMultiPoint(coords, layout) {
+ this.writeUint32(coords.length); // numItems
+ for (let i = 0; i < coords.length; i++) {
+ this.writeWkbHeader(1);
+ this.writePoint(coords[i], layout);
+ }
+ }
+
+ /**
+ * @param {Array>} coords coords
+ * @param {string} layout layout
+ */
+ writeMultiLineString(coords, layout) {
+ this.writeUint32(coords.length); // numItems
+ for (let i = 0; i < coords.length; i++) {
+ this.writeWkbHeader(2);
+ this.writeLineString(coords[i], layout);
+ }
+ }
+
+ /**
+ * @param {Array>>} coords coords
+ * @param {string} layout layout
+ */
+ writeMultiPolygon(coords, layout) {
+ this.writeUint32(coords.length); // numItems
+ for (let i = 0; i < coords.length; i++) {
+ this.writeWkbHeader(3);
+ this.writePolygon(coords[i], layout);
+ }
+ }
+
+ /**
+ * @param {Array} geometries geometries
+ */
+ writeGeometryCollection(geometries) {
+ this.writeUint32(geometries.length); // numItems
+
+ for (let i = 0; i < geometries.length; i++) {
+ this.writeGeometry(geometries[i]);
+ }
+ }
+
+ /**
+ * @param {import("../geom/Geometry.js").default} geom geometry
+ * @param {import("../geom/GeometryLayout.js").default} [layout] layout
+ * @return {import("../geom/GeometryLayout.js").default} minumum layout made by common axes
+ */
+ findMinimumLayout(geom, layout = GeometryLayout.XYZM) {
+ /**
+ * @param {import("../geom/GeometryLayout.js").default} a A
+ * @param {import("../geom/GeometryLayout.js").default} b B
+ * @return {import("../geom/GeometryLayout.js").default} minumum layout made by common axes
+ */
+ const GeometryLayout_min = (a, b) => {
+ if (a === b) {
+ return a;
+ }
+
+ if (a === GeometryLayout.XYZM) {
+ // anything `b` is minimum
+ return b;
+ }
+ if (b === GeometryLayout.XYZM) {
+ // anything `a` is minimum
+ return a;
+ }
+
+ // otherwise, incompatible
+ return GeometryLayout.XY;
+ };
+
+ if (geom instanceof SimpleGeometry) {
+ return GeometryLayout_min(geom.getLayout(), layout);
+ }
+
+ if (geom instanceof GeometryCollection) {
+ const geoms = geom.getGeometriesArray();
+ for (let i = 0; i < geoms.length && layout !== GeometryLayout.XY; i++) {
+ layout = this.findMinimumLayout(geoms[i], layout);
+ }
+ }
+
+ return layout;
+ }
+
+ /**
+ * @param {import("../geom/Geometry.js").default} geom geometry
+ * @param {number} [SRID] SRID
+ */
+ writeGeometry(geom, SRID) {
+ const wkblut = {
+ [GeometryType.POINT]: WKBGeometryType.POINT,
+ [GeometryType.LINE_STRING]: WKBGeometryType.LINE_STRING,
+ [GeometryType.POLYGON]: WKBGeometryType.POLYGON,
+ [GeometryType.MULTI_POINT]: WKBGeometryType.MULTI_POINT,
+ [GeometryType.MULTI_LINE_STRING]: WKBGeometryType.MULTI_LINE_STRING,
+ [GeometryType.MULTI_POLYGON]: WKBGeometryType.MULTI_POLYGON,
+ [GeometryType.GEOMETRY_COLLECTION]: WKBGeometryType.GEOMETRY_COLLECTION,
+ };
+ const geomType = geom.getType();
+ const typeId = wkblut[geomType];
+
+ if (!typeId) {
+ throw new Error('GeometryType ' + geomType + ' is not supported');
+ }
+
+ // first call of writeGeometry() traverse whole geometries to determine its output layout if not specified on constructor.
+ if (!this.layout) {
+ this.layout = this.findMinimumLayout(geom);
+ }
+
+ this.writeWkbHeader(typeId, SRID);
+
+ if (geom instanceof SimpleGeometry) {
+ const writerLUT = {
+ [GeometryType.POINT]: this.writePoint,
+ [GeometryType.LINE_STRING]: this.writeLineString,
+ [GeometryType.POLYGON]: this.writePolygon,
+ [GeometryType.MULTI_POINT]: this.writeMultiPoint,
+ [GeometryType.MULTI_LINE_STRING]: this.writeMultiLineString,
+ [GeometryType.MULTI_POLYGON]: this.writeMultiPolygon,
+ };
+ writerLUT[geomType].call(this, geom.getCoordinates(), geom.getLayout());
+ } else if (geom instanceof GeometryCollection) {
+ this.writeGeometryCollection(geom.getGeometriesArray());
+ }
+ }
+
+ getBuffer() {
+ const byteLength = this.writeQueue.reduce((acc, item) => acc + item[0], 0);
+ const buffer = new ArrayBuffer(byteLength);
+ const view = new DataView(buffer);
+
+ let pos = 0;
+ this.writeQueue.forEach((item) => {
+ switch (item[0]) {
+ case 1:
+ view.setUint8(pos, item[1]);
+ break;
+ case 4:
+ view.setUint32(pos, item[1], this.isLittleEndian);
+ break;
+ case 8:
+ view.setFloat64(pos, item[1], this.isLittleEndian);
+ break;
+ default:
+ break;
+ }
+
+ pos += item[0];
+ });
+
+ return buffer;
+ }
+}
+
+/**
+ * @typedef {Object} Options
+ * @property {boolean} [splitCollection=false] Whether to split GeometryCollections into multiple features on reading.
+ * @property {boolean} [hex=true] Returns hex string instead of ArrayBuffer for output.
+ * @property {boolean} [littleEndian=true] Use littleEndian for output.
+ * @property {boolean} [ewkb=true] Use EWKB format for output.
+ * @property {import("../geom/GeometryLayout").default} [geometryLayout=null] Use specific coordinate layout for output features (null: auto detect)
+ * @property {number} [nodataZ=0] If the `geometryLayout` doesn't match with geometry to be output, this value is used to fill missing coordinate value of Z.
+ * @property {number} [nodataM=0] If the `geometryLayout` doesn't match with geometry to be output, this value is used to fill missing coordinate value of M.
+ * @property {number|boolean} [SRID=true] SRID for output. Specify integer value to enforce the value as a SRID. Specify `true` to extract from `dataProjection`. `false` to suppress the output. This option only takes effect when `ewkb` is `true`.
+ */
+
+/**
+ * @classdesc
+ * Geometry format for reading and writing data in the `Well-Known Binary` (WKB) format.
+ * Also supports `Extended Well-Known Binary` (EWKB) format, used in PostGIS for example.
+ *
+ * @api
+ */
+class WKB extends FeatureFormat {
+ /**
+ * @param {Options} [opt_options] Optional configuration object.
+ */
+ constructor(opt_options) {
+ super();
+
+ const options = opt_options ? opt_options : {};
+
+ this.splitCollection = Boolean(options.splitCollection);
+
+ this.viewCache_ = null;
+
+ this.hex = options.hex !== false;
+ this.littleEndian = options.littleEndian !== false;
+ this.ewkb = options.ewkb !== false;
+
+ this.layout = options.geometryLayout; // null for auto detect
+ this.nodataZ = options.nodataZ || 0;
+ this.nodataM = options.nodataM || 0;
+
+ this.SRID = options.SRID;
+ }
+
+ /**
+ * @return {import("./FormatType.js").default} Format.
+ */
+ getType() {
+ return this.hex ? FormatType.TEXT : FormatType.ARRAY_BUFFER;
+ }
+
+ /**
+ * Read a single feature from a source.
+ *
+ * @param {string|ArrayBuffer|ArrayBufferView} source Source.
+ * @param {import("./Feature.js").ReadOptions} [opt_options] Read options.
+ * @return {import("../Feature.js").FeatureLike} Feature.
+ */
+ readFeature(source, opt_options) {
+ return new Feature({
+ geometry: this.readGeometry(source, opt_options),
+ });
+ }
+
+ /**
+ * Read all features from a source.
+ *
+ * @param {string|ArrayBuffer|ArrayBufferView} source Source.
+ * @param {import("./Feature.js").ReadOptions} [opt_options] Read options.
+ * @return {Array} Features.
+ */
+ readFeatures(source, opt_options) {
+ let geometries = [];
+ const geometry = this.readGeometry(source, opt_options);
+ if (this.splitCollection && geometry instanceof GeometryCollection) {
+ geometries = geometry.getGeometriesArray();
+ } else {
+ geometries = [geometry];
+ }
+ return geometries.map((geometry) => new Feature({geometry}));
+ }
+
+ /**
+ * Read a single geometry from a source.
+ *
+ * @param {string|ArrayBuffer|ArrayBufferView} source Source.
+ * @param {import("./Feature.js").ReadOptions} [opt_options] Read options.
+ * @return {import("../geom/Geometry.js").default} Geometry.
+ */
+ readGeometry(source, opt_options) {
+ const view = getDataView(source);
+ if (!view) {
+ return null;
+ }
+
+ const reader = new WkbReader(view);
+ const geometry = reader.readGeometry();
+
+ this.viewCache_ = view; // cache for internal subsequent call of readProjection()
+ const options = this.getReadOptions(source, opt_options);
+ this.viewCache_ = null; // release
+
+ return transformGeometryWithOptions(geometry, false, options);
+ }
+
+ /**
+ * Read the projection from a source.
+ *
+ * @param {string|ArrayBuffer|ArrayBufferView} source Source.
+ * @return {import("../proj/Projection.js").default|undefined} Projection.
+ */
+ readProjection(source) {
+ const view = this.viewCache_ || getDataView(source);
+ if (!view) {
+ return undefined;
+ }
+
+ const reader = new WkbReader(view);
+ reader.readWkbHeader();
+
+ return (reader.SRID && getProjection('EPSG:' + reader.SRID)) || undefined;
+ }
+
+ /**
+ * Encode a feature in this format.
+ *
+ * @param {import("../Feature.js").default} feature Feature.
+ * @param {import("./Feature.js").WriteOptions} [opt_options] Write options.
+ * @return {string|ArrayBuffer} Result.
+ */
+ writeFeature(feature, opt_options) {
+ return this.writeGeometry(feature.getGeometry(), opt_options);
+ }
+
+ /**
+ * Encode an array of features in this format.
+ *
+ * @param {Array} features Features.
+ * @param {import("./Feature.js").WriteOptions} [opt_options] Write options.
+ * @return {string|ArrayBuffer} Result.
+ */
+ writeFeatures(features, opt_options) {
+ return this.writeGeometry(
+ new GeometryCollection(features.map((f) => f.getGeometry())),
+ opt_options
+ );
+ }
+
+ /**
+ * Write a single geometry in this format.
+ *
+ * @param {import("../geom/Geometry.js").default} geometry Geometry.
+ * @param {import("./Feature.js").WriteOptions} [opt_options] Write options.
+ * @return {string|ArrayBuffer} Result.
+ */
+ writeGeometry(geometry, opt_options) {
+ const options = this.adaptOptions(opt_options);
+
+ const writer = new WkbWriter({
+ layout: this.layout,
+ littleEndian: this.littleEndian,
+ ewkb: this.ewkb,
+
+ nodata: {
+ Z: this.nodataZ,
+ M: this.nodataM,
+ },
+ });
+
+ // extract SRID from `dataProjection`
+ let SRID = Number.isInteger(this.SRID) ? Number(this.SRID) : null;
+ if (this.SRID !== false && !Number.isInteger(this.SRID)) {
+ const dataProjection =
+ options.dataProjection && getProjection(options.dataProjection);
+ if (dataProjection) {
+ const code = dataProjection.getCode();
+ if (code.indexOf('EPSG:') === 0) {
+ SRID = Number(code.substring(5));
+ }
+ }
+ }
+
+ writer.writeGeometry(
+ transformGeometryWithOptions(geometry, true, options),
+ SRID
+ );
+ const buffer = writer.getBuffer();
+
+ return this.hex !== false ? encodeHexString(buffer) : buffer;
+ }
+}
+
+/**
+ * @param {ArrayBuffer} buffer source buffer
+ * @return {string} encoded hex string
+ */
+function encodeHexString(buffer) {
+ const view = new Uint8Array(buffer);
+ return Array.from(view.values())
+ .map((x) => (x < 16 ? '0' : '') + Number(x).toString(16).toUpperCase())
+ .join('');
+}
+
+/**
+ * @param {string} text source text
+ * @return {DataView} decoded binary buffer
+ */
+function decodeHexString(text) {
+ const buffer = new Uint8Array(text.length / 2);
+ for (let i = 0; i < text.length / 2; i++) {
+ buffer[i] = parseInt(text.substr(i * 2, 2), 16);
+ }
+ return new DataView(buffer.buffer);
+}
+
+/**
+ * @param {string | ArrayBuffer | ArrayBufferView} source source
+ * @return {DataView} data view
+ */
+function getDataView(source) {
+ if (typeof source === 'string') {
+ return decodeHexString(source);
+ } else if (ArrayBuffer.isView(source)) {
+ if (source instanceof DataView) {
+ return source;
+ }
+ return new DataView(source.buffer, source.byteOffset, source.byteLength);
+ } else if (source instanceof ArrayBuffer) {
+ return new DataView(source);
+ } else {
+ return null;
+ }
+}
+
+export default WKB;