Skip to content

tobilg/geopackage-ts

Repository files navigation

geopackage-ts

A modern, backend-only TypeScript implementation of the OGC GeoPackage Encoding Standard. Read, write, and query .gpkg files with a clean synchronous API, zero browser dependencies, and full geometry serialization built from scratch.

Features

  • Full GeoPackage support: features, tiles, and attributes with complete OGC spec compliance
  • Synchronous API: powered by better-sqlite3, no Promises needed
  • Zero geometry dependencies: WKB, WKT, and GeoJSON serialization implemented from scratch (no @ngageoint/simple-features-*)
  • R-tree spatial index: create and query spatial indexes using SQLite's built-in R-tree module
  • Lazy iteration: query results returned as IterableIterator<T> for memory-efficient streaming
  • Dual CJS/ESM: ships both CommonJS and ES module builds with full type declarations
  • Minimal footprint: only 2 runtime dependencies (better-sqlite3, proj4)

Requirements

  • Node.js >= 22
  • A C++ compiler toolchain (required by better-sqlite3 native addon)

Installation

npm install geopackage-ts

Quick Start

Open and query an existing GeoPackage

import { GeoPackageManager } from 'geopackage-ts';

// Open a GeoPackage file
const gp = GeoPackageManager.open('countries.gpkg');

// List tables
console.log('Feature tables:', gp.getFeatureTables());
console.log('Tile tables:', gp.getTileTables());

// Query features as GeoJSON
for (const feature of gp.queryForGeoJSONFeatures('countries')) {
  console.log(feature.properties.name, feature.geometry.type);
}

gp.close();

Create a new GeoPackage

import { GeoPackageManager, GeoPackageDataType, buildGeometryData, writeGeometryData } from 'geopackage-ts';
import type { Point, UserColumn } from 'geopackage-ts';

const gp = GeoPackageManager.create('cities.gpkg');

// Define additional columns
const columns: UserColumn[] = [
  {
    index: 2, name: 'name', dataType: GeoPackageDataType.TEXT,
    notNull: false, defaultValue: null, primaryKey: false,
    autoincrement: false, unique: false,
  },
  {
    index: 3, name: 'population', dataType: GeoPackageDataType.INTEGER,
    notNull: false, defaultValue: null, primaryKey: false,
    autoincrement: false, unique: false,
  },
];

// Create a feature table with EPSG:4326
gp.createFeatureTable('cities', 'geom', 'POINT', 4326, columns);

// Insert a feature
const dao = gp.getFeatureDao('cities');
const point: Point = { type: 'Point', hasZ: false, hasM: false, coordinates: [13.405, 52.52] };
const geomBuffer = writeGeometryData(buildGeometryData(point, 4326));

dao.insert({
  table: dao.getTable(),
  values: { geom: geomBuffer, name: 'Berlin', population: 3645000 },
});

// Create a spatial index for fast bounding box queries
gp.indexFeatureTable('cities');

gp.close();

Spatial queries

const gp = GeoPackageManager.open('cities.gpkg');
const dao = gp.getFeatureDao('cities');

// Query features within a bounding box (uses R-tree if available)
for (const row of dao.queryWithBoundingBox({ minX: 5, maxX: 15, minY: 47, maxY: 55 })) {
  console.log(row.name);
}

// Or get GeoJSON directly
for (const feature of dao.queryForGeoJSONWithBoundingBox({ minX: 5, maxX: 15, minY: 47, maxY: 55 })) {
  console.log(feature.properties, feature.geometry);
}

gp.close();

Working with tiles

const gp = GeoPackageManager.create('map.gpkg');

// Create a tile table
gp.createTileTable('world', 3857, {
  minX: -20037508.34, minY: -20037508.34,
  maxX: 20037508.34, maxY: 20037508.34,
});

// Insert tiles
const dao = gp.getTileDao('world');
const pngData = fs.readFileSync('tile_0_0_0.png');
dao.insert({ zoom_level: 0, tile_column: 0, tile_row: 0, tile_data: pngData });

// Query tiles
const tile = dao.queryForTile(0, 0, 0);
if (tile) {
  fs.writeFileSync('output.png', tile.tile_data);
}

// Get available zoom levels
console.log('Zoom levels:', dao.getZoomLevels());

gp.close();

Working with attributes

const gp = GeoPackageManager.create('data.gpkg');

const cols: UserColumn[] = [
  { index: 1, name: 'key', dataType: GeoPackageDataType.TEXT, notNull: true, defaultValue: null, primaryKey: false, autoincrement: false, unique: false },
  { index: 2, name: 'value', dataType: GeoPackageDataType.TEXT, notNull: false, defaultValue: null, primaryKey: false, autoincrement: false, unique: false },
];

gp.createAttributeTable('config', cols);
const dao = gp.getAttributeDao('config');

dao.insert({ table: dao.getTable(), values: { key: 'version', value: '1.0' } });

for (const row of dao.queryForAll()) {
  console.log(row);
}

gp.close();

Geometry serialization

import { readWKB, writeWKB, readWKT, writeWKT, fromGeoJSON, toGeoJSON, ByteOrder } from 'geopackage-ts';

// WKB round-trip
const point = { type: 'Point' as const, hasZ: false, hasM: false, coordinates: [1.5, 2.5] };
const wkb = writeWKB(point, ByteOrder.LITTLE_ENDIAN);
const parsed = readWKB(wkb); // { type: 'Point', coordinates: [1.5, 2.5], ... }

// WKT round-trip
const geom = readWKT('POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))');
const wkt = writeWKT(geom); // 'POLYGON ((0 0, 10 0, 10 10, 0 10, 0 0))'

// GeoJSON conversion
const gjson = toGeoJSON(geom);       // standard GeoJSON geometry object
const back = fromGeoJSON(gjson);      // internal Geometry type

// GeoPackage Binary (GP header + envelope + WKB)
import { buildGeometryData, writeGeometryData, readGeometryData } from 'geopackage-ts';
const gd = buildGeometryData(geom, 4326);
const blob = writeGeometryData(gd);  // ready to store in a feature table
const decoded = readGeometryData(blob);
console.log(decoded.srsId, decoded.envelope, decoded.geometry);

Coordinate transforms

import { createTransformFromEPSG, transformBoundingBox } from 'geopackage-ts';

const toWebMercator = createTransformFromEPSG(4326, 3857);
const [x, y] = toWebMercator(13.405, 52.52);

const bbox = transformBoundingBox(
  { minX: -180, minY: -85, maxX: 180, maxY: 85 },
  toWebMercator,
);

Validation

import { GeoPackageManager } from 'geopackage-ts';

const result = GeoPackageManager.validate('data.gpkg');
if (!result.valid) {
  console.error('Validation errors:', result.errors);
}

API Overview

Core Classes

Class Description
GeoPackageManager Factory for opening, creating, and validating GeoPackage files
GeoPackage Main entry point: table listing, DAO access, feature/tile/attribute creation
GeoPackageConnection Low-level better-sqlite3 wrapper (query, exec, pragmas, custom functions)

Data Access Objects

DAO Description
FeatureDao Query, insert, update, delete features; GeoJSON iteration; bounding box queries
TileDao Query, insert, delete tiles by zoom/column/row; zoom level management
AttributeDao CRUD for non-spatial attribute tables
UserDao Base DAO with generic query/insert/update/delete operations

Geometry

Function Description
readWKB / writeWKB Parse and write OGC Well-Known Binary (all types, Z/M, both byte orders)
readWKT / writeWKT Parse and write Well-Known Text
fromGeoJSON / toGeoJSON Convert between GeoJSON and internal geometry types
readGeometryData / writeGeometryData Decode/encode GeoPackage Binary format (GP header + envelope + WKB)
computeEnvelope Compute bounding box for any geometry
createPoint / createLineString / createPolygon Factory helpers

Extensions

Function Description
createRTreeIndex Create an R-tree spatial index with auto-sync triggers
isRTreeIndexed Check if a feature table has an R-tree index
ensureDataColumnsTables Set up the schema extension tables
ensureMetadataTables Set up the metadata extension tables

Projection

Function Description
createTransform Create a transform function from SRS definitions
createTransformFromEPSG Create a transform function from EPSG codes
transformBoundingBox Reproject a bounding box
registerProjection Register a custom projection with proj4

Types

All GeoPackage spec table structures are available as TypeScript interfaces:

BoundingBox, SpatialReferenceSystem, Contents, GeometryColumns, TileMatrixSet, TileMatrix, Extension, ValidationResult, ColumnDefinition, TableDefinition

Geometry types use a discriminated union:

Geometry = Point | LineString | Polygon | MultiPoint | MultiLineString | MultiPolygon | GeometryCollection

Architecture

src/
├── index.ts                     # Public API exports
├── geopackage.ts                # Main GeoPackage class
├── geopackage-manager.ts        # Open / create / validate
├── types.ts                     # Shared types, enums, constants, errors
├── db/
│   ├── connection.ts            # better-sqlite3 wrapper
│   └── table-creator.ts         # DDL for all GeoPackage system tables
├── core/
│   ├── srs.ts                   # gpkg_spatial_ref_sys operations
│   ├── contents.ts              # gpkg_contents operations
│   └── extensions.ts            # gpkg_extensions operations
├── geom/
│   ├── geometry.ts              # Geometry types + envelope utilities
│   ├── geometry-data.ts         # GeoPackage Binary header encode/decode
│   ├── wkb/                     # WKB reader/writer
│   ├── wkt/                     # WKT reader/writer
│   └── geojson/                 # GeoJSON reader/writer
├── features/                    # Feature table, DAO, geometry columns
├── tiles/                       # Tile table, DAO, matrix, utilities
├── attributes/                  # Attribute table and DAO
├── user/                        # Base table/row/column/DAO abstractions
├── extension/
│   ├── rtree-index.ts           # R-tree spatial index
│   ├── schema.ts                # Data columns extension
│   └── metadata.ts              # Metadata extension
├── projection/                  # proj4 wrapper for coordinate transforms
└── io/                          # File copy, export, validation

Documentation

API documentation is generated with TypeDoc. All public APIs have comprehensive TSDoc comments with @param, @returns, @throws, and @example tags.

npm run docs

The generated documentation will be in the docs/ directory.

Scripts

npm run build          # Build dual CJS/ESM output to dist/
npm test               # Run all tests
npm run test:watch     # Run tests in watch mode
npm run test:coverage  # Run tests with coverage
npm run lint           # Lint with Biome
npm run format         # Format with Biome
npm run docs           # Generate API docs with TypeDoc

Migration Guide from @ngageoint/geopackage

This section covers the key differences when migrating server-side code from @ngageoint/geopackage (v4.x) to geopackage-ts.

1. Installation

- npm install @ngageoint/geopackage
+ npm install geopackage-ts

No WASM setup required. No setSqljsWasmLocateFile() or setCanvasKitWasmLocateFile() calls.

2. Opening a GeoPackage

- import { GeoPackageManager } from '@ngageoint/geopackage';
- const gp = await GeoPackageManager.open(filePath);
+ import { GeoPackageManager } from 'geopackage-ts';
+ const gp = GeoPackageManager.open(filePath);

The entire API is synchronous. Remove all await keywords and .then() chains when calling GeoPackage methods. This is a direct consequence of using better-sqlite3 instead of sql.js.

3. Opening from a Buffer

- const gp = await GeoPackageManager.open(buffer);
+ const gp = GeoPackageManager.open(buffer);

Both string (file path) and Buffer are accepted by the same open() method.

4. Creating a GeoPackage

- const gp = await GeoPackageManager.create(filePath);
+ const gp = GeoPackageManager.create(filePath);

The created GeoPackage is automatically initialized with the required system tables and 4 default SRS entries (undefined cartesian, undefined geographic, EPSG:4326, EPSG:3857).

5. Querying features

- // Old: callback or array-based
- const rows = gp.queryForGeoJSONFeaturesInTable('rivers');
- rows.forEach(feature => { ... });

+ // New: lazy iterator
+ for (const feature of gp.queryForGeoJSONFeatures('rivers')) {
+   // feature is a standard GeoJSON Feature
+   console.log(feature.properties, feature.geometry);
+ }

Result sets are returned as IterableIterator<T>, not arrays. Use for...of to iterate lazily, or [...iterator] to collect into an array. No .close() call needed on result sets: better-sqlite3 handles cleanup automatically.

6. Feature DAO

- const dao = gp.getFeatureDao('my_table');
- const resultSet = dao.queryForAll();
- while (resultSet.moveToNext()) {
-   const row = resultSet.getRow();
-   const geometry = row.getGeometry().getGeometry();
- }
- resultSet.close();

+ const dao = gp.getFeatureDao('my_table');
+ for (const feature of dao.queryForGeoJSON()) {
+   // Standard GeoJSON Feature:  no manual geometry decoding needed
+   console.log(feature.geometry.type, feature.properties);
+ }

7. Inserting features

- import { GeoPackageGeometryData } from '@ngageoint/geopackage';
- import { Point } from '@ngageoint/simple-features-js';
- const geomData = new GeoPackageGeometryData();
- geomData.setGeometry(new Point(13.405, 52.52));
- geomData.setSrsId(4326);
- const featureRow = dao.newRow();
- featureRow.setGeometry(geomData);
- featureRow.setValue('name', 'Berlin');
- dao.create(featureRow);

+ import { buildGeometryData, writeGeometryData } from 'geopackage-ts';
+ import type { Point } from 'geopackage-ts';
+ const point: Point = { type: 'Point', hasZ: false, hasM: false, coordinates: [13.405, 52.52] };
+ const geomBuffer = writeGeometryData(buildGeometryData(point, 4326));
+ dao.insert({ table: dao.getTable(), values: { geom: geomBuffer, name: 'Berlin' } });

8. Geometry types

- // Old: class-based NGA geometry hierarchy
- import { Point, LineString, Polygon } from '@ngageoint/simple-features-js';
- const point = new Point(1, 2);
- point.hasZ = true;
- point.z = 100;

+ // New: plain objects with discriminated union
+ import type { Point } from 'geopackage-ts';
+ const point: Point = { type: 'Point', hasZ: true, hasM: false, coordinates: [1, 2, 100] };

No @ngageoint/simple-features-js, @ngageoint/simple-features-wkb-js, @ngageoint/simple-features-geojson-js, or @ngageoint/simple-features-proj-js packages needed. All geometry serialization is built in.

9. Spatial queries with R-tree index

- import { RTreeIndexExtension } from '@ngageoint/geopackage';
- const rtree = new RTreeIndexExtension(gp);
- rtree.createWithFeatureTable(featureTable);
- const featureIndexManager = new FeatureIndexManager(gp, 'my_table');
- featureIndexManager.setIndexLocation(FeatureIndexType.RTREE);
- const results = featureIndexManager.queryWithBoundingBox(bbox, projection);

+ // Create the index
+ gp.indexFeatureTable('my_table');
+
+ // Query:  automatically uses R-tree if available
+ const dao = gp.getFeatureDao('my_table');
+ for (const row of dao.queryWithBoundingBox({ minX: 0, maxX: 10, minY: 0, maxY: 10 })) {
+   console.log(row);
+ }

10. Tile access

- const tileDao = gp.getTileDao('my_tiles');
- const resultSet = tileDao.queryForTile(col, row, zoom);
- if (resultSet.getCount() > 0) {
-   resultSet.moveToNext();
-   const tileRow = resultSet.getRow();
-   const tileData = tileRow.getTileData();
- }
- resultSet.close();

+ const tileDao = gp.getTileDao('my_tiles');
+ const tile = tileDao.queryForTile(col, row, zoom);
+ if (tile) {
+   const tileData = tile.tile_data; // Buffer
+ }

11. Projections

- import { ProjectionFactory, Projections } from '@ngageoint/projections-js';
- const proj = ProjectionFactory.getProjection(Projections.EPSG_4326);

+ import { createTransformFromEPSG } from 'geopackage-ts';
+ const transform = createTransformFromEPSG(4326, 3857);
+ const [x, y] = transform(lon, lat);

12. What's been removed

The following features from @ngageoint/geopackage are intentionally not included:

Removed Reason
Canvas / CanvasKit rendering Backend-only: no image generation
FeatureTiles / drawTile() Tile rendering is out of scope
sql.js / WASM SQLite Replaced by native better-sqlite3
Browser support Node.js only
setCanvasKitWasmLocateFile() No WASM dependencies
setSqljsWasmLocateFile() No WASM dependencies
Web Worker support Not needed server-side
Leaflet integration No map framework dependencies
Feature simplification No simplify-js dependency
NGA geometry class hierarchy Replaced with discriminated unions
6-level DAO inheritance Simplified to flat DAO classes
Related Tables extension Not yet implemented
NGA Contents ID extension Not yet implemented
NGA Feature Tile Link Not yet implemented

Quick reference

@ngageoint/geopackage geopackage-ts
await GeoPackageManager.open(path) GeoPackageManager.open(path)
await GeoPackageManager.create(path) GeoPackageManager.create(path)
new Point(x, y) { type: 'Point', hasZ: false, hasM: false, coordinates: [x, y] }
createPoint(x, y) createPoint(x, y)
resultSet.moveToNext() / .close() for (const row of dao.queryForAll())
featureRow.getGeometry() readGeometryData(row.geom)
geoPackageGeometryData.setGeometry(pt) writeGeometryData(buildGeometryData(pt, srsId))
dao.create(row) dao.insert({ table, values })
new RTreeIndexExtension(gp) gp.indexFeatureTable(tableName)
featureIndexManager.queryWithBoundingBox() dao.queryWithBoundingBox(bbox)

License

MIT

About

A server-side pure TypeScript re-implementation of @ngageoint/geopackage

Resources

License

Stars

Watchers

Forks

Contributors