Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Scope tracked in the [v9.4 milestone](https://github.com/visgl/deck.gl-community

- Added generic animation, block, fast-text, UTF8 Arrow string-view, view-layout, and viewport-bounds helpers for trace-style visualizations.

### `@deck.gl-community/json` (NEW module)

- Added shared Zod-backed GeoJSON schemas and inferred TypeScript types for positions, bounding boxes, geometries, features, and feature collections.

### `@deck.gl-community/timeline-layers`

- `TimeAxisLayer` now supports adaptive trace-style duration and timestamp grids plus exported tick formatting helpers.
Expand Down
44 changes: 44 additions & 0 deletions modules/json/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"name": "@deck.gl-community/json",
"description": "Zod schemas for GeoJSON primitives",
"license": "MIT",
"version": "9.3.7",
"publishConfig": {
"access": "public"
},
"repository": {
"type": "git",
"url": "https://github.com/visgl/deck.gl-community"
},
"keywords": [
"geojson",
"zod",
"schema",
"validation",
"ai"
],
"type": "module",
"sideEffects": false,
"types": "./dist/index.d.ts",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"exports": {
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"require": "./dist/index.cjs",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"src"
],
"scripts": {
"test": "vitest run",
"test-watch": "vitest"
},
"dependencies": {
"zod": "^4.0.0"
}
}
16 changes: 16 additions & 0 deletions modules/json/src/geojson/bbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';

/**
* GeoJSON bounding box — either 4 values [minLon, minLat, maxLon, maxLat]
* or 6 values [minLon, minLat, minAlt, maxLon, maxLat, maxAlt] per RFC 7946 §5.
*/
export const BBoxSchema = z.union([
z.tuple([z.number(), z.number(), z.number(), z.number()]),
z.tuple([z.number(), z.number(), z.number(), z.number(), z.number(), z.number()])
]);

export type BBox = z.infer<typeof BBoxSchema>;
18 changes: 18 additions & 0 deletions modules/json/src/geojson/feature-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {FeatureSchema} from './feature';
import {BBoxSchema} from './bbox';

/**
* GeoJSON FeatureCollection per RFC 7946 §3.3.
*/
export const FeatureCollectionSchema = z.object({
type: z.literal('FeatureCollection'),
features: z.array(FeatureSchema),
bbox: BBoxSchema.optional()
});

export type FeatureCollection = z.infer<typeof FeatureCollectionSchema>;
23 changes: 23 additions & 0 deletions modules/json/src/geojson/feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {GeometrySchema} from './geometry';
import {BBoxSchema} from './bbox';

/**
* GeoJSON Feature per RFC 7946 §3.2.
* - geometry may be null (to represent features without geometry)
* - properties may be null
* - id is optional, and may be a string or number
*/
export const FeatureSchema = z.object({
type: z.literal('Feature'),
geometry: GeometrySchema.nullable(),
properties: z.record(z.string(), z.unknown()).nullable(),
id: z.union([z.string(), z.number()]).optional(),
bbox: BBoxSchema.optional()
});

export type Feature = z.infer<typeof FeatureSchema>;
49 changes: 49 additions & 0 deletions modules/json/src/geojson/geometry-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {PointSchema} from './point';
import {LineStringSchema} from './line-string';
import {PolygonSchema} from './polygon';
import {MultiPointSchema} from './multi-point';
import {MultiLineStringSchema} from './multi-line-string';
import {MultiPolygonSchema} from './multi-polygon';
import {BBoxSchema} from './bbox';

/**
* Forward-declare the GeometryCollection schema using z.lazy() to support
* recursive nesting (a GeometryCollection can contain other GeometryCollections).
* RFC 7946 §3.1.8.
*/
export type GeometryCollection = {
type: 'GeometryCollection';
geometries: Array<
| z.infer<typeof PointSchema>
| z.infer<typeof LineStringSchema>
| z.infer<typeof PolygonSchema>
| z.infer<typeof MultiPointSchema>
| z.infer<typeof MultiLineStringSchema>
| z.infer<typeof MultiPolygonSchema>
| GeometryCollection
>;
bbox?: z.infer<typeof BBoxSchema>;
};

export const GeometryCollectionSchema: z.ZodType<GeometryCollection> = z.lazy(() =>
z.object({
type: z.literal('GeometryCollection'),
geometries: z.array(
z.union([
PointSchema,
LineStringSchema,
PolygonSchema,
MultiPointSchema,
MultiLineStringSchema,
MultiPolygonSchema,
GeometryCollectionSchema
])
),
bbox: BBoxSchema.optional()
})
);
32 changes: 32 additions & 0 deletions modules/json/src/geojson/geometry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {PointSchema} from './point';
import {LineStringSchema} from './line-string';
import {PolygonSchema} from './polygon';
import {MultiPointSchema} from './multi-point';
import {MultiLineStringSchema} from './multi-line-string';
import {MultiPolygonSchema} from './multi-polygon';
import {GeometryCollectionSchema} from './geometry-collection';

/**
* Union of all GeoJSON geometry types per RFC 7946 §3.1.
*
* Note: uses z.union rather than z.discriminatedUnion because GeometryCollectionSchema
* is a z.lazy()-wrapped opaque ZodType (needed for recursive nesting), which is not
* directly accepted by z.discriminatedUnion's type constraints. Functionally equivalent
* for validation; z.union tries each variant in order.
*/
export const GeometrySchema = z.union([
PointSchema,
LineStringSchema,
PolygonSchema,
MultiPointSchema,
MultiLineStringSchema,
MultiPolygonSchema,
GeometryCollectionSchema
]);

export type Geometry = z.infer<typeof GeometrySchema>;
39 changes: 39 additions & 0 deletions modules/json/src/geojson/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

export {BBoxSchema} from './bbox';
export type {BBox} from './bbox';

export {PositionSchema} from './position';
export type {Position} from './position';

export {PointSchema} from './point';
export type {Point} from './point';

export {LineStringSchema} from './line-string';
export type {LineString} from './line-string';

export {PolygonSchema, LinearRingSchema} from './polygon';
export type {Polygon} from './polygon';

export {MultiPointSchema} from './multi-point';
export type {MultiPoint} from './multi-point';

export {MultiLineStringSchema} from './multi-line-string';
export type {MultiLineString} from './multi-line-string';

export {MultiPolygonSchema} from './multi-polygon';
export type {MultiPolygon} from './multi-polygon';

export {GeometryCollectionSchema} from './geometry-collection';
export type {GeometryCollection} from './geometry-collection';

export {GeometrySchema} from './geometry';
export type {Geometry} from './geometry';

export {FeatureSchema} from './feature';
export type {Feature} from './feature';

export {FeatureCollectionSchema} from './feature-collection';
export type {FeatureCollection} from './feature-collection';
18 changes: 18 additions & 0 deletions modules/json/src/geojson/line-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {PositionSchema} from './position';
import {BBoxSchema} from './bbox';

/**
* GeoJSON LineString — array of two or more positions per RFC 7946 §3.1.4.
*/
export const LineStringSchema = z.object({
type: z.literal('LineString'),
coordinates: z.array(PositionSchema).min(2),
bbox: BBoxSchema.optional()
});

export type LineString = z.infer<typeof LineStringSchema>;
15 changes: 15 additions & 0 deletions modules/json/src/geojson/multi-line-string.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {PositionSchema} from './position';
import {BBoxSchema} from './bbox';

export const MultiLineStringSchema = z.object({
type: z.literal('MultiLineString'),
coordinates: z.array(z.array(PositionSchema).min(2)),
bbox: BBoxSchema.optional()
});

export type MultiLineString = z.infer<typeof MultiLineStringSchema>;
15 changes: 15 additions & 0 deletions modules/json/src/geojson/multi-point.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {PositionSchema} from './position';
import {BBoxSchema} from './bbox';

export const MultiPointSchema = z.object({
type: z.literal('MultiPoint'),
coordinates: z.array(PositionSchema),
bbox: BBoxSchema.optional()
});

export type MultiPoint = z.infer<typeof MultiPointSchema>;
15 changes: 15 additions & 0 deletions modules/json/src/geojson/multi-polygon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {LinearRingSchema} from './polygon';
import {BBoxSchema} from './bbox';

export const MultiPolygonSchema = z.object({
type: z.literal('MultiPolygon'),
coordinates: z.array(z.array(LinearRingSchema)),
bbox: BBoxSchema.optional()
});

export type MultiPolygon = z.infer<typeof MultiPolygonSchema>;
15 changes: 15 additions & 0 deletions modules/json/src/geojson/point.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {PositionSchema} from './position';
import {BBoxSchema} from './bbox';

export const PointSchema = z.object({
type: z.literal('Point'),
coordinates: PositionSchema,
bbox: BBoxSchema.optional()
});

export type Point = z.infer<typeof PointSchema>;
40 changes: 40 additions & 0 deletions modules/json/src/geojson/polygon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';
import {PositionSchema} from './position';
import {BBoxSchema} from './bbox';

/**
* A linear ring for a Polygon:
* - Must have ≥ 4 positions (RFC 7946 §3.1.6)
* - First and last position must be identical (ring closure)
*/
const LinearRingSchema = z
.array(PositionSchema)
.min(4)
.refine(
ring => {
const first = ring[0];
const last = ring[ring.length - 1];
// Compare all coordinate components
return first.length === last.length && first.every((v, i) => v === last[i]);
},
{message: 'Linear ring must be closed: first and last position must be identical'}
);

/**
* GeoJSON Polygon — array of linear rings per RFC 7946 §3.1.6.
* First ring is the exterior; subsequent rings are holes.
*/
export const PolygonSchema = z.object({
type: z.literal('Polygon'),
coordinates: z.array(LinearRingSchema),
bbox: BBoxSchema.optional()
});

export type Polygon = z.infer<typeof PolygonSchema>;

/** Exported for reuse in other schemas that need to validate individual rings. */
export {LinearRingSchema};
17 changes: 17 additions & 0 deletions modules/json/src/geojson/position.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

import {z} from 'zod';

/**
* GeoJSON Position — either [longitude, latitude] or [longitude, latitude, altitude].
* RFC 7946 §3.1.1: "A position is an array of numbers. There MUST be two or more elements."
* We support exactly 2D and 3D per the spec's normative wording.
*/
export const PositionSchema = z.union([
z.tuple([z.number(), z.number()]),
z.tuple([z.number(), z.number(), z.number()])
]);

export type Position = z.infer<typeof PositionSchema>;
5 changes: 5 additions & 0 deletions modules/json/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// deck.gl-community
// SPDX-License-Identifier: MIT
// Copyright (c) vis.gl contributors

export * from './geojson/index';
Loading
Loading