@object-ui/plugin-map 3.0.3 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +8 -8
- package/CHANGELOG.md +10 -0
- package/dist/index.css +1 -1
- package/dist/index.js +1127 -1063
- package/dist/index.umd.cjs +49 -49
- package/dist/{maplibre-gl-DSpYxujd.js → maplibre-gl-D7mIxaE5.js} +4782 -4741
- package/dist/src/ObjectMap.d.ts +4 -0
- package/dist/src/ObjectMap.d.ts.map +1 -1
- package/package.json +9 -9
- package/src/ObjectMap.tsx +128 -23
- package/src/index.tsx +1 -1
package/dist/src/ObjectMap.d.ts
CHANGED
|
@@ -8,6 +8,10 @@ export interface ObjectMapProps {
|
|
|
8
8
|
onRowClick?: (record: any) => void;
|
|
9
9
|
onEdit?: (record: any) => void;
|
|
10
10
|
onDelete?: (record: any) => void;
|
|
11
|
+
/** Enable marker clustering for dense data */
|
|
12
|
+
enableClustering?: boolean;
|
|
13
|
+
/** Cluster radius in pixels (default: 50) */
|
|
14
|
+
clusterRadius?: number;
|
|
11
15
|
}
|
|
12
16
|
export declare const ObjectMap: React.FC<ObjectMapProps>;
|
|
13
17
|
export default ObjectMap;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ObjectMap.d.ts","sourceRoot":"","sources":["../../src/ObjectMap.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAY,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"ObjectMap.d.ts","sourceRoot":"","sources":["../../src/ObjectMap.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH;;;;;;;;;;;;GAYG;AAEH,OAAO,KAAuC,MAAM,OAAO,CAAC;AAC5D,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAY,MAAM,kBAAkB,CAAC;AAO/E,OAAO,kCAAkC,CAAC;AAY1C,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACtC,UAAU,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;IACjC,8CAA8C;IAC9C,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,6CAA6C;IAC7C,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAwOD,eAAO,MAAM,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,cAAc,CAwV9C,CAAC;AAEF,eAAe,SAAS,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/plugin-map",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Map visualization plugin for Object UI",
|
|
@@ -24,22 +24,22 @@
|
|
|
24
24
|
}
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@objectstack/spec": "^3.
|
|
28
|
-
"lucide-react": "^0.
|
|
29
|
-
"maplibre-gl": "^5.
|
|
27
|
+
"@objectstack/spec": "^3.2.1",
|
|
28
|
+
"lucide-react": "^0.576.0",
|
|
29
|
+
"maplibre-gl": "^5.19.0",
|
|
30
30
|
"react-map-gl": "^8.1.0",
|
|
31
31
|
"zod": "^4.3.6",
|
|
32
|
-
"@object-ui/components": "3.
|
|
33
|
-
"@object-ui/core": "3.
|
|
34
|
-
"@object-ui/react": "3.
|
|
35
|
-
"@object-ui/types": "3.
|
|
32
|
+
"@object-ui/components": "3.1.1",
|
|
33
|
+
"@object-ui/core": "3.1.1",
|
|
34
|
+
"@object-ui/react": "3.1.1",
|
|
35
|
+
"@object-ui/types": "3.1.1"
|
|
36
36
|
},
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"react": "^18.0.0 || ^19.0.0",
|
|
39
39
|
"react-dom": "^18.0.0 || ^19.0.0"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/react": "19.2.
|
|
42
|
+
"@types/react": "19.2.14",
|
|
43
43
|
"@types/react-dom": "19.2.3",
|
|
44
44
|
"@vitejs/plugin-react": "^5.1.4",
|
|
45
45
|
"typescript": "^5.9.3",
|
package/src/ObjectMap.tsx
CHANGED
|
@@ -23,9 +23,10 @@
|
|
|
23
23
|
import React, { useEffect, useState, useMemo } from 'react';
|
|
24
24
|
import type { ObjectGridSchema, DataSource, ViewData } from '@object-ui/types';
|
|
25
25
|
import { useNavigationOverlay } from '@object-ui/react';
|
|
26
|
-
import { NavigationOverlay } from '@object-ui/components';
|
|
26
|
+
import { NavigationOverlay, cn } from '@object-ui/components';
|
|
27
|
+
import { extractRecords, buildExpandFields } from '@object-ui/core';
|
|
27
28
|
import { z } from 'zod';
|
|
28
|
-
import
|
|
29
|
+
import MapGL, { NavigationControl, Marker, Popup } from 'react-map-gl/maplibre';
|
|
29
30
|
import maplibregl from 'maplibre-gl';
|
|
30
31
|
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
31
32
|
|
|
@@ -47,6 +48,10 @@ export interface ObjectMapProps {
|
|
|
47
48
|
onRowClick?: (record: any) => void;
|
|
48
49
|
onEdit?: (record: any) => void;
|
|
49
50
|
onDelete?: (record: any) => void;
|
|
51
|
+
/** Enable marker clustering for dense data */
|
|
52
|
+
enableClustering?: boolean;
|
|
53
|
+
/** Cluster radius in pixels (default: 50) */
|
|
54
|
+
clusterRadius?: number;
|
|
50
55
|
}
|
|
51
56
|
|
|
52
57
|
interface MapConfig {
|
|
@@ -213,6 +218,72 @@ function extractCoordinates(record: any, config: MapConfig): [number, number] |
|
|
|
213
218
|
return null;
|
|
214
219
|
}
|
|
215
220
|
|
|
221
|
+
interface MarkerData {
|
|
222
|
+
id: string;
|
|
223
|
+
title: string;
|
|
224
|
+
description?: string;
|
|
225
|
+
coordinates: [number, number];
|
|
226
|
+
data: any;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
interface ClusterData {
|
|
230
|
+
id: string;
|
|
231
|
+
coordinates: [number, number];
|
|
232
|
+
markers: MarkerData[];
|
|
233
|
+
isCluster: boolean;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Simple grid-based marker clustering.
|
|
238
|
+
* Groups markers that are close to each other at a given zoom level.
|
|
239
|
+
*/
|
|
240
|
+
function clusterMarkers(markers: MarkerData[], zoom: number, radius: number = 50): ClusterData[] {
|
|
241
|
+
if (markers.length <= 1 || zoom >= 15) {
|
|
242
|
+
return markers.map(m => ({
|
|
243
|
+
id: m.id,
|
|
244
|
+
coordinates: m.coordinates,
|
|
245
|
+
markers: [m],
|
|
246
|
+
isCluster: false,
|
|
247
|
+
}));
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Grid cell size based on zoom (larger cells at lower zoom)
|
|
251
|
+
const cellSize = radius / Math.pow(2, zoom);
|
|
252
|
+
const grid = new Map<string, MarkerData[]>();
|
|
253
|
+
|
|
254
|
+
markers.forEach(marker => {
|
|
255
|
+
const cellX = Math.floor(marker.coordinates[0] / cellSize);
|
|
256
|
+
const cellY = Math.floor(marker.coordinates[1] / cellSize);
|
|
257
|
+
const key = `${cellX}:${cellY}`;
|
|
258
|
+
if (!grid.has(key)) grid.set(key, []);
|
|
259
|
+
grid.get(key)!.push(marker);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
const clusters: ClusterData[] = [];
|
|
263
|
+
grid.forEach((group, key) => {
|
|
264
|
+
if (group.length === 1) {
|
|
265
|
+
clusters.push({
|
|
266
|
+
id: group[0].id,
|
|
267
|
+
coordinates: group[0].coordinates,
|
|
268
|
+
markers: group,
|
|
269
|
+
isCluster: false,
|
|
270
|
+
});
|
|
271
|
+
} else {
|
|
272
|
+
// Compute centroid
|
|
273
|
+
const avgLng = group.reduce((sum, m) => sum + m.coordinates[0], 0) / group.length;
|
|
274
|
+
const avgLat = group.reduce((sum, m) => sum + m.coordinates[1], 0) / group.length;
|
|
275
|
+
clusters.push({
|
|
276
|
+
id: `cluster-${key}`,
|
|
277
|
+
coordinates: [avgLng, avgLat],
|
|
278
|
+
markers: group,
|
|
279
|
+
isCluster: true,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return clusters;
|
|
285
|
+
}
|
|
286
|
+
|
|
216
287
|
export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
217
288
|
schema,
|
|
218
289
|
dataSource,
|
|
@@ -221,6 +292,8 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
221
292
|
onRowClick,
|
|
222
293
|
onEdit,
|
|
223
294
|
onDelete,
|
|
295
|
+
enableClustering,
|
|
296
|
+
clusterRadius = 50,
|
|
224
297
|
...rest
|
|
225
298
|
}) => {
|
|
226
299
|
const [data, setData] = useState<any[]>([]);
|
|
@@ -271,27 +344,21 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
271
344
|
return;
|
|
272
345
|
}
|
|
273
346
|
|
|
274
|
-
if (!dataSource) {
|
|
347
|
+
if (!dataSource || typeof dataSource.find !== 'function') {
|
|
275
348
|
throw new Error('DataSource required for object/api providers');
|
|
276
349
|
}
|
|
277
350
|
|
|
278
351
|
if (dataConfig?.provider === 'object') {
|
|
279
352
|
const objectName = dataConfig.object;
|
|
353
|
+
// Auto-inject $expand for lookup/master_detail fields
|
|
354
|
+
const expand = buildExpandFields(objectSchema?.fields);
|
|
280
355
|
const result = await dataSource.find(objectName, {
|
|
281
356
|
$filter: schema.filter,
|
|
282
357
|
$orderby: convertSortToQueryParams(schema.sort),
|
|
358
|
+
...(expand.length > 0 ? { $expand: expand } : {}),
|
|
283
359
|
});
|
|
284
360
|
|
|
285
|
-
let items: any[] =
|
|
286
|
-
if (Array.isArray(result)) {
|
|
287
|
-
items = result;
|
|
288
|
-
} else if (result && typeof result === 'object') {
|
|
289
|
-
if (Array.isArray((result as any).data)) {
|
|
290
|
-
items = (result as any).data;
|
|
291
|
-
} else if (Array.isArray((result as any).records)) {
|
|
292
|
-
items = (result as any).records;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
361
|
+
let items: any[] = extractRecords(result);
|
|
295
362
|
setData(items);
|
|
296
363
|
} else if (dataConfig?.provider === 'api') {
|
|
297
364
|
console.warn('API provider not yet implemented for ObjectMap');
|
|
@@ -306,7 +373,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
306
373
|
};
|
|
307
374
|
|
|
308
375
|
fetchData();
|
|
309
|
-
}, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort]);
|
|
376
|
+
}, [dataConfig, dataSource, hasInlineData, schema.filter, schema.sort, objectSchema]);
|
|
310
377
|
|
|
311
378
|
// Fetch object schema for field metadata
|
|
312
379
|
useEffect(() => {
|
|
@@ -370,6 +437,8 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
370
437
|
markers.find(m => m.id === selectedMarkerId),
|
|
371
438
|
[markers, selectedMarkerId]);
|
|
372
439
|
|
|
440
|
+
const [currentZoom, setCurrentZoom] = useState(mapConfig.zoom || 3);
|
|
441
|
+
|
|
373
442
|
const navigation = useNavigationOverlay({
|
|
374
443
|
navigation: (schema as any).navigation,
|
|
375
444
|
objectName: schema.objectName,
|
|
@@ -385,6 +454,20 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
385
454
|
);
|
|
386
455
|
}, [markers, searchQuery]);
|
|
387
456
|
|
|
457
|
+
// Cluster markers when clustering is enabled
|
|
458
|
+
const clusteredData = useMemo(() => {
|
|
459
|
+
const shouldCluster = enableClustering ?? ((schema as any).enableClustering || filteredMarkers.length > 100);
|
|
460
|
+
if (!shouldCluster) {
|
|
461
|
+
return filteredMarkers.map(m => ({
|
|
462
|
+
id: m.id,
|
|
463
|
+
coordinates: m.coordinates,
|
|
464
|
+
markers: [m],
|
|
465
|
+
isCluster: false,
|
|
466
|
+
}));
|
|
467
|
+
}
|
|
468
|
+
return clusterMarkers(filteredMarkers, currentZoom, clusterRadius);
|
|
469
|
+
}, [filteredMarkers, currentZoom, enableClustering, clusterRadius, schema]);
|
|
470
|
+
|
|
388
471
|
// Calculate map bounds
|
|
389
472
|
const initialViewState = useMemo(() => {
|
|
390
473
|
if (!filteredMarkers.length) {
|
|
@@ -413,7 +496,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
413
496
|
|
|
414
497
|
if (loading) {
|
|
415
498
|
return (
|
|
416
|
-
<div className={className}>
|
|
499
|
+
<div className={cn("min-w-0 overflow-hidden", className)}>
|
|
417
500
|
<div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
|
|
418
501
|
<div className="text-muted-foreground">Loading map...</div>
|
|
419
502
|
</div>
|
|
@@ -423,7 +506,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
423
506
|
|
|
424
507
|
if (error) {
|
|
425
508
|
return (
|
|
426
|
-
<div className={className}>
|
|
509
|
+
<div className={cn("min-w-0 overflow-hidden", className)}>
|
|
427
510
|
<div className="flex items-center justify-center h-96 bg-muted rounded-lg border">
|
|
428
511
|
<div className="text-destructive">Error: {error.message}</div>
|
|
429
512
|
</div>
|
|
@@ -432,7 +515,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
432
515
|
}
|
|
433
516
|
|
|
434
517
|
return (
|
|
435
|
-
<div className={className}>
|
|
518
|
+
<div className={cn("min-w-0 overflow-hidden", className)}>
|
|
436
519
|
{invalidCount > 0 && (
|
|
437
520
|
<div className="mb-2 p-2 text-sm text-yellow-800 bg-yellow-50 border border-yellow-200 rounded">
|
|
438
521
|
{`${invalidCount} record${invalidCount !== 1 ? 's' : ''} with missing or invalid coordinates excluded from the map.`}
|
|
@@ -450,24 +533,45 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
450
533
|
</div>
|
|
451
534
|
)}
|
|
452
535
|
<div className="relative border rounded-lg overflow-hidden bg-muted h-[300px] sm:h-[400px] md:h-[500px] lg:h-[600px] w-full">
|
|
453
|
-
<
|
|
536
|
+
<MapGL
|
|
454
537
|
initialViewState={initialViewState}
|
|
455
538
|
style={{ width: '100%', height: '100%' }}
|
|
456
539
|
mapStyle="https://demotiles.maplibre.org/style.json"
|
|
457
540
|
touchZoomRotate={true}
|
|
458
541
|
dragRotate={true}
|
|
459
542
|
touchPitch={true}
|
|
543
|
+
onZoom={(e) => setCurrentZoom(Math.round(e.viewState.zoom))}
|
|
460
544
|
>
|
|
461
545
|
<NavigationControl position="top-right" showCompass={true} showZoom={true} />
|
|
462
546
|
|
|
463
|
-
{
|
|
547
|
+
{clusteredData.map(cluster => (
|
|
548
|
+
cluster.isCluster ? (
|
|
549
|
+
<Marker
|
|
550
|
+
key={cluster.id}
|
|
551
|
+
longitude={cluster.coordinates[0]}
|
|
552
|
+
latitude={cluster.coordinates[1]}
|
|
553
|
+
anchor="center"
|
|
554
|
+
>
|
|
555
|
+
<div
|
|
556
|
+
className="flex items-center justify-center rounded-full bg-primary text-primary-foreground font-bold text-xs cursor-pointer hover:scale-110 transition-transform shadow-md"
|
|
557
|
+
style={{
|
|
558
|
+
width: Math.min(48, 24 + cluster.markers.length * 2),
|
|
559
|
+
height: Math.min(48, 24 + cluster.markers.length * 2),
|
|
560
|
+
}}
|
|
561
|
+
title={`${cluster.markers.length} markers`}
|
|
562
|
+
>
|
|
563
|
+
{cluster.markers.length}
|
|
564
|
+
</div>
|
|
565
|
+
</Marker>
|
|
566
|
+
) : (
|
|
464
567
|
<Marker
|
|
465
|
-
key={
|
|
466
|
-
longitude={
|
|
467
|
-
latitude={
|
|
568
|
+
key={cluster.id}
|
|
569
|
+
longitude={cluster.coordinates[0]}
|
|
570
|
+
latitude={cluster.coordinates[1]}
|
|
468
571
|
anchor="bottom"
|
|
469
572
|
onClick={(e) => {
|
|
470
573
|
e.originalEvent.stopPropagation();
|
|
574
|
+
const marker = cluster.markers[0];
|
|
471
575
|
setSelectedMarkerId(marker.id);
|
|
472
576
|
navigation.handleClick(marker.data);
|
|
473
577
|
onMarkerClick?.(marker.data);
|
|
@@ -477,6 +581,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
477
581
|
📍
|
|
478
582
|
</div>
|
|
479
583
|
</Marker>
|
|
584
|
+
)
|
|
480
585
|
))}
|
|
481
586
|
|
|
482
587
|
{selectedMarker && (
|
|
@@ -503,7 +608,7 @@ export const ObjectMap: React.FC<ObjectMapProps> = ({
|
|
|
503
608
|
</div>
|
|
504
609
|
</Popup>
|
|
505
610
|
)}
|
|
506
|
-
</
|
|
611
|
+
</MapGL>
|
|
507
612
|
</div>
|
|
508
613
|
{navigation.isOverlay && (
|
|
509
614
|
<NavigationOverlay {...navigation} title="Location Details">
|
package/src/index.tsx
CHANGED
|
@@ -17,7 +17,7 @@ export type { ObjectMapProps };
|
|
|
17
17
|
|
|
18
18
|
// Register component
|
|
19
19
|
export const ObjectMapRenderer: React.FC<any> = ({ schema, ...props }) => {
|
|
20
|
-
const { dataSource } = useSchemaContext();
|
|
20
|
+
const { dataSource } = useSchemaContext() || {};
|
|
21
21
|
return <ObjectMap schema={schema} dataSource={dataSource} {...props} />;
|
|
22
22
|
};
|
|
23
23
|
|