Skip to content

Commit 9d3250e

Browse files
committed
wip
1 parent cc33b0c commit 9d3250e

2 files changed

Lines changed: 145 additions & 3 deletions

File tree

src/duckdb/src/table/duckdb-table.ts

Lines changed: 144 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,45 @@ const DUCKDB_WKB_COLUMN = 'wkb_geometry';
6868
*/
6969
const 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
}

src/layers/src/trip-layer/trip-layer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,7 +246,7 @@ export default class TripLayer extends Layer {
246246
{label, fields = [], dataContainer, id}: KeplerTable,
247247
foundLayers?: any[]
248248
) {
249-
const geojsonColumns = fields.filter(f => f.type === 'geojson').map(f => f.name);
249+
const geojsonColumns = fields.filter(f => f.type === 'geojson' || f.type === 'geoarrow' || f.type === 'geoarrow-wkb').map(f => f.name);
250250

251251
const defaultColumns = {
252252
geojson: uniq([...GEOJSON_FIELDS.geojson, ...geojsonColumns])

0 commit comments

Comments
 (0)