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.
better-sqlite3, no Promises needed@ngageoint/simple-features-*)IterableIterator<T> for memory-efficient streamingbetter-sqlite3, proj4)better-sqlite3 native addon)npm install geopackage-ts
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();
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();
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();
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();
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();
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);
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,
);
import { GeoPackageManager } from 'geopackage-ts';
const result = GeoPackageManager.validate('data.gpkg');
if (!result.valid) {
console.error('Validation errors:', result.errors);
}
| 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) |
| 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 |
| 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 |
| 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 |
| 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 |
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
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
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.
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
@ngageoint/geopackageThis section covers the key differences when migrating server-side code from @ngageoint/geopackage (v4.x) to geopackage-ts.
- npm install @ngageoint/geopackage
+ npm install geopackage-ts
No WASM setup required. No setSqljsWasmLocateFile() or setCanvasKitWasmLocateFile() calls.
- 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.
- const gp = await GeoPackageManager.open(buffer);
+ const gp = GeoPackageManager.open(buffer);
Both string (file path) and Buffer are accepted by the same open() method.
- 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).
- // 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.
- 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);
+ }
- 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' } });
- // 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.
- 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);
+ }
- 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
+ }
- 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);
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 |
@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) |
MIT