geopackage-ts
v0.1.1
Published
Modern TypeScript GeoPackage implementation for Node.js
Readme
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-sqlite3native addon)
Installation
npm install geopackage-tsQuick 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, validationDocumentation
API documentation is generated with TypeDoc. All public APIs have comprehensive TSDoc comments with @param, @returns, @throws, and @example tags.
npm run docsThe 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 TypeDocMigration 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-tsNo 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
