@@ -68,6 +68,45 @@ const DUCKDB_WKB_COLUMN = 'wkb_geometry';
6868 */
6969const KEPLER_GEOM_FROM_GEOJSON_COLUMN = '_geojson' ;
7070
71+ /**
72+ * Check if a GeoJSON FeatureCollection contains XYZM (4D) coordinates.
73+ * This is used to detect trip data where the 4th coordinate is a timestamp.
74+ * @param geojson The GeoJSON FeatureCollection to check
75+ * @returns true if the GeoJSON contains 4D coordinates
76+ */
77+ function hasXYZMCoordinates ( geojson : any ) : boolean {
78+ if ( ! geojson || geojson . type !== 'FeatureCollection' || ! Array . isArray ( geojson . features ) ) {
79+ return false ;
80+ }
81+
82+ for ( const feature of geojson . features ) {
83+ const coords = feature ?. geometry ?. coordinates ;
84+ if ( ! coords ) continue ;
85+
86+ const geomType = feature ?. geometry ?. type ;
87+
88+ if ( geomType === 'LineString' && Array . isArray ( coords ) ) {
89+ // LineString: [[x,y,z,m], [x,y,z,m], ...]
90+ if ( coords . length > 0 && Array . isArray ( coords [ 0 ] ) && coords [ 0 ] . length >= 4 ) {
91+ return true ;
92+ }
93+ } else if ( geomType === 'MultiLineString' && Array . isArray ( coords ) ) {
94+ // MultiLineString: [[[x,y,z,m], [x,y,z,m], ...], ...]
95+ if (
96+ coords . length > 0 &&
97+ Array . isArray ( coords [ 0 ] ) &&
98+ coords [ 0 ] . length > 0 &&
99+ Array . isArray ( coords [ 0 ] [ 0 ] ) &&
100+ coords [ 0 ] [ 0 ] . length >= 4
101+ ) {
102+ return true ;
103+ }
104+ }
105+ }
106+
107+ return false ;
108+ }
109+
71110/**
72111 * Names of columns that most likely contain binary wkb geometry
73112 */
@@ -139,7 +178,18 @@ export class KeplerGlDuckDbTable extends KeplerTable {
139178 async importGeoJsonData ( { data, db, c} : ImportDataToDuckProps ) : Promise < ImportDataToDuckResult > {
140179 try {
141180 const { rows} = data ;
142- await db . registerFileText ( this . id , JSON . stringify ( rows ) ) ;
181+ const geojsonStr = JSON . stringify ( rows ) ;
182+
183+ // Check if the GeoJSON has XYZM (4D) coordinates (e.g., trip data with timestamps)
184+ const has4DCoords = hasXYZMCoordinates ( rows ) ;
185+
186+ if ( has4DCoords ) {
187+ console . log ( '[importGeoJsonData] Detected XYZM coordinates, using custom read_json parsing' ) ;
188+ return await this . importGeoJsonWithXYZM ( { data, db, c, geojsonStr} ) ;
189+ }
190+
191+ // Standard ST_READ path for non-XYZM GeoJSON
192+ await db . registerFileText ( this . id , geojsonStr ) ;
143193
144194 const createTableSql = `
145195 install spatial;
@@ -157,7 +207,99 @@ export class KeplerGlDuckDbTable extends KeplerTable {
157207 }
158208
159209 return {
160- // _geojson column is created from geometry with keep_wkb flag and contains valid WKB data.
210+ geoarrowMetadata : { [ KEPLER_GEOM_FROM_GEOJSON_COLUMN ] : GEOARROW_EXTENSIONS . WKB }
211+ } ;
212+ }
213+
214+ /**
215+ * Import GeoJSON with XYZM (4D) coordinates using custom read_json parsing.
216+ * This preserves the M coordinate (typically timestamps for trip data) which ST_READ drops.
217+ * The M values are stored in the 4th position of each coordinate array in the _geojson column.
218+ */
219+ private async importGeoJsonWithXYZM ( {
220+ data,
221+ db,
222+ c,
223+ geojsonStr
224+ } : ImportDataToDuckProps & { geojsonStr : string } ) : Promise < ImportDataToDuckResult > {
225+ try {
226+ await db . registerFileText ( this . id , geojsonStr ) ;
227+
228+ await c . query ( `install spatial; load spatial;` ) ;
229+
230+ // Step 1: Parse GeoJSON and create initial table with properties and geometry JSON
231+ // We store the geometry as a JSON string to preserve 4D coordinates
232+ const createTableSql = `
233+ CREATE TABLE '${ this . label } ' AS
234+ WITH raw_features AS (
235+ SELECT unnest(features) as feature
236+ FROM read_json_auto('${ this . id } ')
237+ )
238+ SELECT
239+ feature->'properties' as __props__,
240+ -- Store geometry as JSON string to preserve 4D coordinates
241+ (feature->'geometry')::VARCHAR as "${ KEPLER_GEOM_FROM_GEOJSON_COLUMN } "
242+ FROM raw_features;
243+ ` ;
244+
245+ await c . query ( createTableSql ) ;
246+
247+ // Step 2: Get property keys from the first row to expand them into columns
248+ const propsResult = await c . query ( `
249+ SELECT __props__ FROM '${ this . label } ' WHERE __props__ IS NOT NULL LIMIT 1
250+ ` ) ;
251+
252+ if ( propsResult . numRows > 0 ) {
253+ const propsJson = propsResult . getChildAt ( 0 ) ?. get ( 0 ) ;
254+ if ( propsJson ) {
255+ let propKeys : string [ ] = [ ] ;
256+ try {
257+ const propsObj = typeof propsJson === 'string' ? JSON . parse ( propsJson ) : propsJson ;
258+ if ( propsObj && typeof propsObj === 'object' ) {
259+ propKeys = Object . keys ( propsObj ) ;
260+ }
261+ } catch ( e ) {
262+ console . warn ( '[importGeoJsonWithXYZM] Could not parse properties:' , e ) ;
263+ }
264+
265+ // Add columns for each property
266+ for ( const key of propKeys ) {
267+ // Skip if key would conflict with our geometry column
268+ if ( key === KEPLER_GEOM_FROM_GEOJSON_COLUMN ) continue ;
269+
270+ const safeKey = key . replace ( / " / g, '""' ) ;
271+ try {
272+ await c . query ( `
273+ ALTER TABLE '${ this . label } '
274+ ADD COLUMN "${ safeKey } " VARCHAR;
275+ ` ) ;
276+ await c . query ( `
277+ UPDATE '${ this . label } '
278+ SET "${ safeKey } " = json_extract_string(__props__, '$."${ safeKey } "');
279+ ` ) ;
280+ } catch ( e ) {
281+ console . warn ( `[importGeoJsonWithXYZM] Could not add property column ${ key } :` , e ) ;
282+ }
283+ }
284+ }
285+ }
286+
287+ // Step 3: Drop the intermediate __props__ column
288+ try {
289+ await c . query ( `ALTER TABLE '${ this . label } ' DROP COLUMN __props__;` ) ;
290+ } catch ( e ) {
291+ console . warn ( '[importGeoJsonWithXYZM] Could not drop __props__ column:' , e ) ;
292+ }
293+
294+ console . log ( '[importGeoJsonWithXYZM] Successfully imported GeoJSON with XYZM coordinates preserved' ) ;
295+ } catch ( error ) {
296+ console . error ( 'importGeoJsonWithXYZM' , error ) ;
297+ throw error ;
298+ }
299+
300+ return {
301+ // The _geojson column contains geometry JSON with 4D coordinates
302+ // It will be parsed by parseGeometryFromString which handles JSON strings
161303 geoarrowMetadata : { [ KEPLER_GEOM_FROM_GEOJSON_COLUMN ] : GEOARROW_EXTENSIONS . WKB }
162304 } ;
163305 }
0 commit comments