@orioro/react-maplibre-util 0.4.0 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @orioro/react-maplibre-util
2
2
 
3
+ ## 0.5.0
4
+
5
+ ### Minor Changes
6
+
7
+ - implement onClick handler on LayeredMap layers; implement DynamicImage component and CustomSprite
8
+
3
9
  ## 0.4.0
4
10
 
5
11
  ### Minor Changes
package/README.md CHANGED
@@ -1 +0,0 @@
1
- # Template react lib
@@ -1,3 +1,2 @@
1
1
  import { MaplibreInspectOptions } from '@maplibre/maplibre-gl-inspect';
2
- import '@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.css';
3
2
  export declare function InspectControl(props: MaplibreInspectOptions): null;
@@ -0,0 +1,5 @@
1
+ type CustomSpriteProps = {
2
+ url: string;
3
+ };
4
+ export declare function CustomSprite({ url }: CustomSpriteProps): null;
5
+ export {};
@@ -0,0 +1 @@
1
+ export * from './CustomSprite';
@@ -0,0 +1,7 @@
1
+ import { Map, MapStyleImageMissingEvent, StyleImageMetadata } from 'maplibre-gl';
2
+ type OnGenerateImageRes = Parameters<Map['addImage']>[1] | [Parameters<Map['addImage']>[1], Partial<StyleImageMetadata>];
3
+ type DynamicImagesProps = {
4
+ onGenerateImage: (imageId: string, event: MapStyleImageMissingEvent) => OnGenerateImageRes | Promise<OnGenerateImageRes>;
5
+ };
6
+ export declare function DynamicImages({ onGenerateImage }: DynamicImagesProps): null;
7
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from './DynamicImages';
2
+ export * from './svgImages';
@@ -0,0 +1 @@
1
+ export declare const MARKER = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<svg fill=\"#000000\" width=\"800px\" height=\"800px\" viewBox=\"-64 0 512 512\" xmlns=\"http://www.w3.org/2000/svg\"><path d=\"M172.268 501.67C26.97 291.031 0 269.413 0 192 0 85.961 85.961 0 192 0s192 85.961 192 192c0 77.413-26.97 99.031-172.268 309.67-9.535 13.774-29.93 13.773-39.464 0z\"/></svg>";
@@ -0,0 +1,32 @@
1
+ import { StyleImageMetadata } from 'maplibre-gl';
2
+ type IconPathToSvgOptions = {
3
+ size?: number;
4
+ fill?: string;
5
+ stroke?: string;
6
+ strokeWidth?: string | number;
7
+ viewBox?: string;
8
+ style?: string;
9
+ };
10
+ export declare function iconPathToSvg(path: string, { size, fill, stroke, strokeWidth, viewBox, style, }?: IconPathToSvgOptions): string;
11
+ /**
12
+ * Renders an SVG string onto a canvas and returns an object compatible with maplibre `addImage`.
13
+ *
14
+ * @param svgString - The SVG markup as a string
15
+ * @param pixelRatio - (Optional) device pixel ratio, defaults to window.devicePixelRatio
16
+ * @returns Promise resolving to { width, height, data, pixelRatio }
17
+ */
18
+ export declare function svgToMaplibreImage(svgString: string, pixelRatio?: number): Promise<[
19
+ {
20
+ width: number;
21
+ height: number;
22
+ data: Uint8ClampedArray;
23
+ },
24
+ Partial<StyleImageMetadata>
25
+ ]>;
26
+ export declare function svgIconId(iconId: string, options?: IconPathToSvgOptions): string;
27
+ export declare function svgIconGenerator(iconPathsById: Record<string, string>): (imageId: string) => Promise<[{
28
+ width: number;
29
+ height: number;
30
+ data: Uint8ClampedArray;
31
+ }, Partial<StyleImageMetadata>]>;
32
+ export {};
@@ -1,2 +1,3 @@
1
1
  export * from './LayeredMap';
2
2
  export * from './parseMapViews';
3
+ export * from './layeredMapOnClickHandler';
@@ -0,0 +1,16 @@
1
+ import type { MapGeoJSONFeature, MapMouseEvent } from 'maplibre-gl';
2
+ import { Merge } from 'type-fest';
3
+ type ClickableFeature = Merge<MapGeoJSONFeature, {
4
+ layer: {
5
+ id: string;
6
+ onClick: (feature: MapGeoJSONFeature, event: AugmentedMouseEvent) => any;
7
+ };
8
+ }>;
9
+ type AugmentedMouseEvent = Merge<MapMouseEvent, {
10
+ features: ClickableFeature[];
11
+ }>;
12
+ type LayeredMapOnClickHandlerProps = {
13
+ resolveTargetFeature?: (features: AugmentedMouseEvent['features'], event: AugmentedMouseEvent) => ClickableFeature | Promise<ClickableFeature>;
14
+ };
15
+ export declare function layeredMapOnClickHandler({ resolveTargetFeature, }?: LayeredMapOnClickHandlerProps): (e: AugmentedMouseEvent) => Promise<void>;
16
+ export {};
@@ -1,5 +1,6 @@
1
1
  import { AnyLayer } from 'react-map-gl/dist/esm/exports-maplibre';
2
2
  import { MapView, MapViewLayer, MapViewSource } from '../types';
3
+ import { MapMouseEvent } from 'maplibre-gl';
3
4
  type ParsedSource = MapViewSource & {
4
5
  id: string;
5
6
  viewId: string;
@@ -7,6 +8,7 @@ type ParsedSource = MapViewSource & {
7
8
  type ParsedLayer = MapViewLayer & {
8
9
  id: string;
9
10
  viewId: string;
11
+ onClick?: (feature: GeoJSON.Feature, event: MapMouseEvent) => any;
10
12
  };
11
13
  export type MapViewsParseResult = {
12
14
  srcMapViews: MapView[];
package/dist/index.d.ts CHANGED
@@ -2,6 +2,7 @@ export * from './LayeredMap';
2
2
  export * from './HoverTooltip';
3
3
  export * from './MapWindow';
4
4
  export * from './SyncedMaps';
5
+ export * from './DynamicImages';
5
6
  export * from './useHover';
6
7
  export * from './scales';
7
8
  export * from './util';
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { __assign, __spreadArray, __rest, __makeTemplateObject } from 'tslib';
1
+ import { __assign, __spreadArray, __rest, __awaiter, __generator, __makeTemplateObject } from 'tslib';
2
2
  import React, { forwardRef, useContext, createContext, useRef, useMemo, useImperativeHandle, useState, useCallback, useLayoutEffect, useEffect, createRef } from 'react';
3
3
  import { Map, Source, Layer, useMap, useControl } from 'react-map-gl/maplibre';
4
4
  import { isPlainObject, uniq, uniqBy, pick, omit } from 'lodash-es';
@@ -6,6 +6,7 @@ import { Flex, useRefByKey, useLocalState, DropdownMenu } from '@orioro/react-ui
6
6
  import styled from 'styled-components';
7
7
  import { usePrevious } from 'react-use';
8
8
  import { mergeRefs } from 'react-merge-refs';
9
+ import { strExpr } from '@orioro/util';
9
10
  import { ckmeans } from 'simple-statistics';
10
11
  import { schemeYlOrRd } from 'd3-scale-chromatic';
11
12
  import { maxIndex, range, variance, sum } from 'd3';
@@ -17,7 +18,6 @@ import { Tooltip } from '@radix-ui/themes';
17
18
  import maplibregl from 'maplibre-gl';
18
19
  import mlcontour from 'maplibre-contour';
19
20
  import MaplibreInspect from '@maplibre/maplibre-gl-inspect';
20
- import '@maplibre/maplibre-gl-inspect/dist/maplibre-gl-inspect.css';
21
21
 
22
22
  function sortLayers(layers, _a) {
23
23
  var existingLayers = _a.existingLayers;
@@ -128,7 +128,7 @@ function parseMapViews(orderedViews, _a) {
128
128
  var layerId = layer.absoluteId ? layer.absoluteId : fmtLayerAbsoluteId(viewId, layerRelativeId);
129
129
  // : `${viewId}__${layerRelativeId}`
130
130
  return {
131
- interactiveLayerIds: layer.interactive ? __spreadArray(__spreadArray([], acc.interactiveLayerIds, true), [layerId], false) : acc.interactiveLayerIds,
131
+ interactiveLayerIds: layer.interactive || typeof layer.onClick === 'function' ? __spreadArray(__spreadArray([], acc.interactiveLayerIds, true), [layerId], false) : acc.interactiveLayerIds,
132
132
  _layers: __spreadArray(__spreadArray([], acc._layers, true), [__assign(__assign({}, layer), {
133
133
  viewId: viewId,
134
134
  id: layerId,
@@ -253,6 +253,45 @@ var LayeredMap = /*#__PURE__*/forwardRef(function LayeredMapInner(_a, layeredMap
253
253
  })));
254
254
  });
255
255
 
256
+ function selectFirstClickableFeature(features) {
257
+ return features[0];
258
+ }
259
+ function layeredMapOnClickHandler(_a) {
260
+ var _b = _a === void 0 ? {} : _a,
261
+ _c = _b.resolveTargetFeature,
262
+ resolveTargetFeature = _c === void 0 ? selectFirstClickableFeature : _c;
263
+ return function onClick(e) {
264
+ return __awaiter(this, void 0, void 0, function () {
265
+ var features, clickableFeatures, targetFeature, _a;
266
+ return __generator(this, function (_b) {
267
+ switch (_b.label) {
268
+ case 0:
269
+ features = e.features || [];
270
+ clickableFeatures = features.filter(function (feature) {
271
+ var _a;
272
+ return typeof ((_a = feature.layer) === null || _a === void 0 ? void 0 : _a.onClick) === 'function';
273
+ });
274
+ if (!(clickableFeatures.length > 0)) return [3 /*break*/, 4];
275
+ if (!(clickableFeatures.length === 1)) return [3 /*break*/, 1];
276
+ _a = clickableFeatures[0];
277
+ return [3 /*break*/, 3];
278
+ case 1:
279
+ return [4 /*yield*/, resolveTargetFeature(clickableFeatures, e)];
280
+ case 2:
281
+ _a = _b.sent();
282
+ _b.label = 3;
283
+ case 3:
284
+ targetFeature = _a;
285
+ targetFeature.layer.onClick(targetFeature, e);
286
+ _b.label = 4;
287
+ case 4:
288
+ return [2 /*return*/];
289
+ }
290
+ });
291
+ });
292
+ };
293
+ }
294
+
256
295
  var Container = styled.div(templateObject_1$2 || (templateObject_1$2 = __makeTemplateObject(["\n pointer-events: none;\n position: absolute;\n z-index: 2;\n\n background: rgba(0, 0, 0, 0.5);\n border-radius: 16px;\n box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n border: 1px solid rgba(0, 0, 0, 0.3);\n\n // background-color: black;\n color: white;\n border-radius: 5px;\n font-size: 0.9rem;\n\n max-width: 300px;\n\n hyphens: auto;\n word-break: break-word; /* Avoids overflow */\n overflow-wrap: break-word; /* Ensures long words break */\n white-space: normal;\n"], ["\n pointer-events: none;\n position: absolute;\n z-index: 2;\n\n background: rgba(0, 0, 0, 0.5);\n border-radius: 16px;\n box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);\n backdrop-filter: blur(5px);\n -webkit-backdrop-filter: blur(5px);\n border: 1px solid rgba(0, 0, 0, 0.3);\n\n // background-color: black;\n color: white;\n border-radius: 5px;\n font-size: 0.9rem;\n\n max-width: 300px;\n\n hyphens: auto;\n word-break: break-word; /* Avoids overflow */\n overflow-wrap: break-word; /* Ensures long words break */\n white-space: normal;\n"])));
257
296
  var DataSectionHeading = styled.h3(templateObject_2 || (templateObject_2 = __makeTemplateObject(["\n margin: 0;\n line-height: 1.2;\n font-size: 1rem;\n"], ["\n margin: 0;\n line-height: 1.2;\n font-size: 1rem;\n"])));
258
297
  var DataSectionContainer = styled(Flex)(templateObject_3 || (templateObject_3 = __makeTemplateObject(["\n padding: 15px 10px;\n"], ["\n padding: 15px 10px;\n"])));
@@ -506,14 +545,12 @@ function mapSetFeaturesState(map, features, state) {
506
545
  return;
507
546
  }
508
547
  features.forEach(function (feat) {
509
- if (feat.id) {
548
+ if (typeof feat.id === 'number' || typeof feat.id === 'string') {
510
549
  map.setFeatureState({
511
550
  source: feat.source,
512
551
  sourceLayer: feat.sourceLayer,
513
552
  id: feat.id
514
553
  }, state);
515
- } else {
516
- console.warn("could not get feature id", feat);
517
554
  }
518
555
  });
519
556
  }
@@ -580,7 +617,10 @@ function useHover(props, deps) {
580
617
  onMouseOut: onMouseOut,
581
618
  onDragStart: onDragStart,
582
619
  onDragEnd: onDragEnd,
583
- cursor: isDragging ? 'grabbing' : ((_a = hoverInfo === null || hoverInfo === void 0 ? void 0 : hoverInfo.features) === null || _a === void 0 ? void 0 : _a.length) > 0 ? 'default' : 'grab',
620
+ cursor: isDragging ? 'grabbing' : (((_a = hoverInfo === null || hoverInfo === void 0 ? void 0 : hoverInfo.features) === null || _a === void 0 ? void 0 : _a.length) || 0) > 0 ? hoverInfo.features.some(function (feat) {
621
+ var _a;
622
+ return typeof ((_a = feat.layer) === null || _a === void 0 ? void 0 : _a.onClick) === 'function';
623
+ }) ? 'pointer' : 'default' : 'grab',
584
624
  children: isDragging ? null : /*#__PURE__*/React.createElement(React.Fragment, null, tooltip)
585
625
  }, hoverInfo, isDragging];
586
626
  }
@@ -857,6 +897,147 @@ var SyncedMaps = makeSyncedMaps({
857
897
  });
858
898
  var templateObject_1;
859
899
 
900
+ function DynamicImages(_a) {
901
+ var _this = this;
902
+ var onGenerateImage = _a.onGenerateImage;
903
+ var map = useMap().current;
904
+ useEffect(function () {
905
+ if (!map) return function () {};
906
+ var handler = function handler(event) {
907
+ return __awaiter(_this, void 0, void 0, function () {
908
+ var imageId, result, err_1;
909
+ return __generator(this, function (_a) {
910
+ switch (_a.label) {
911
+ case 0:
912
+ imageId = event.id;
913
+ if (map.hasImage(imageId)) return [2 /*return*/];
914
+ _a.label = 1;
915
+ case 1:
916
+ _a.trys.push([1, 3,, 4]);
917
+ return [4 /*yield*/, onGenerateImage(imageId, event)];
918
+ case 2:
919
+ result = _a.sent();
920
+ if (!map.hasImage(imageId)) {
921
+ if (Array.isArray(result)) {
922
+ map.addImage(imageId, result[0], result[1]);
923
+ } else {
924
+ map.addImage(imageId, result);
925
+ }
926
+ }
927
+ return [3 /*break*/, 4];
928
+ case 3:
929
+ err_1 = _a.sent();
930
+ console.error("Failed to generate image for \"".concat(imageId, "\""), err_1);
931
+ return [3 /*break*/, 4];
932
+ case 4:
933
+ return [2 /*return*/];
934
+ }
935
+ });
936
+ });
937
+ };
938
+ map.on('styleimagemissing', handler);
939
+ return function () {
940
+ return map.off('styleimagemissing', handler);
941
+ };
942
+ }, [map, onGenerateImage]);
943
+ return null;
944
+ }
945
+
946
+ function iconPathToSvg(path, _a) {
947
+ var _b = _a === void 0 ? {} : _a,
948
+ _c = _b.size,
949
+ size = _c === void 0 ? 24 : _c,
950
+ _d = _b.fill,
951
+ fill = _d === void 0 ? 'black' : _d,
952
+ stroke = _b.stroke,
953
+ strokeWidth = _b.strokeWidth,
954
+ _e = _b.viewBox,
955
+ viewBox = _e === void 0 ? '0 0 24 24' : _e,
956
+ style = _b.style;
957
+ if (path.startsWith('<svg')) {
958
+ return path;
959
+ }
960
+ var svgAttrs = ["xmlns=\"http://www.w3.org/2000/svg\"", "width=\"".concat(size, "\""), "height=\"".concat(size, "\""), "viewBox=\"".concat(viewBox, "\""), stroke ? "stroke=\"".concat(stroke, "\"") : '', strokeWidth ? "stroke-width=\"".concat(strokeWidth, "\"") : ''].filter(Boolean).join(' ');
961
+ var pathAttrs = ["fill=\"".concat(fill, "\""), style ? "style=\"".concat(style, "\"") : ''].filter(Boolean).join(' ');
962
+ return "<svg ".concat(svgAttrs, "><path ").concat(pathAttrs, " d=\"").concat(path, "\" /></svg>");
963
+ }
964
+ /**
965
+ * Renders an SVG string onto a canvas and returns an object compatible with maplibre `addImage`.
966
+ *
967
+ * @param svgString - The SVG markup as a string
968
+ * @param pixelRatio - (Optional) device pixel ratio, defaults to window.devicePixelRatio
969
+ * @returns Promise resolving to { width, height, data, pixelRatio }
970
+ */
971
+ function svgToMaplibreImage(svgString_1) {
972
+ return __awaiter(this, arguments, void 0, function (svgString, pixelRatio) {
973
+ if (pixelRatio === void 0) {
974
+ pixelRatio = typeof window !== 'undefined' ? window.devicePixelRatio : 1;
975
+ }
976
+ return __generator(this, function (_a) {
977
+ return [2 /*return*/, new Promise(function (resolve, reject) {
978
+ var svgBlob = new Blob([svgString], {
979
+ type: 'image/svg+xml'
980
+ });
981
+ var url = URL.createObjectURL(svgBlob);
982
+ var img = new Image();
983
+ img.onload = function () {
984
+ var width = img.width * pixelRatio;
985
+ var height = img.height * pixelRatio;
986
+ var canvas = document.createElement('canvas');
987
+ canvas.width = width;
988
+ canvas.height = height;
989
+ var ctx = canvas.getContext('2d');
990
+ ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
991
+ ctx.drawImage(img, 0, 0);
992
+ var imageData = ctx.getImageData(0, 0, width, height);
993
+ URL.revokeObjectURL(url);
994
+ resolve([{
995
+ width: width,
996
+ height: height,
997
+ data: imageData.data
998
+ }, {
999
+ pixelRatio: pixelRatio
1000
+ }]);
1001
+ };
1002
+ img.onerror = function () {
1003
+ URL.revokeObjectURL(url);
1004
+ reject(new Error('Failed to load SVG as image'));
1005
+ };
1006
+ img.src = url;
1007
+ })];
1008
+ });
1009
+ });
1010
+ }
1011
+ function svgIconId(iconId, options) {
1012
+ return options ? "".concat(iconId, "(").concat(JSON.stringify(options), ")") : iconId;
1013
+ }
1014
+ function svgIconGenerator(iconPathsById) {
1015
+ var fns = Object.fromEntries(Object.entries(iconPathsById).map(function (_a) {
1016
+ var iconId = _a[0],
1017
+ iconPath = _a[1];
1018
+ return [iconId, function (options) {
1019
+ if (options === void 0) {
1020
+ options = {};
1021
+ }
1022
+ return function () {
1023
+ return iconPathToSvg(iconPath, options);
1024
+ };
1025
+ }];
1026
+ }));
1027
+ var expr = strExpr({
1028
+ expressions: fns
1029
+ });
1030
+ return function onGenerateSvgImage(imageId) {
1031
+ return __awaiter(this, void 0, void 0, function () {
1032
+ var svg;
1033
+ return __generator(this, function (_a) {
1034
+ svg = expr.apply(imageId, undefined);
1035
+ return [2 /*return*/, svgToMaplibreImage(svg)];
1036
+ });
1037
+ });
1038
+ };
1039
+ }
1040
+
860
1041
  var DEFAULT_MIN_K = 3;
861
1042
  var DEFAULT_MAX_K = 9;
862
1043
  //
@@ -1376,4 +1557,4 @@ function InspectControl(props) {
1376
1557
  return null;
1377
1558
  }
1378
1559
 
1379
- export { $naturalBreaks, ControlContainer, HoverTooltip, InspectControl, LayeredMap, MapWindow, SyncedMaps, TerrainControl, applyReactStyle, augmentFeature, ensureAddLayer, ensureAddSource, ensureRemoveLayer, ensureRemoveSource, fitGeometry, fmtLayerAbsoluteId, getSrcLayer, getSrcViewByLayerId, hoverParseEvent, makeSyncedMaps, mapSetFeaturesState, naturalBreakBounds, parseMapViews, scaleNaturalBreaks, sortLayers, useClientRect, useHover, useLayeredMap, withHover };
1560
+ export { $naturalBreaks, ControlContainer, DynamicImages, HoverTooltip, InspectControl, LayeredMap, MapWindow, SyncedMaps, TerrainControl, applyReactStyle, augmentFeature, ensureAddLayer, ensureAddSource, ensureRemoveLayer, ensureRemoveSource, fitGeometry, fmtLayerAbsoluteId, getSrcLayer, getSrcViewByLayerId, hoverParseEvent, iconPathToSvg, layeredMapOnClickHandler, makeSyncedMaps, mapSetFeaturesState, naturalBreakBounds, parseMapViews, scaleNaturalBreaks, sortLayers, svgIconGenerator, svgIconId, svgToMaplibreImage, useClientRect, useHover, useLayeredMap, withHover };
package/dist/types.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Map } from 'react-map-gl/maplibre';
2
2
  import type { AnyLayer, AnySource } from 'react-map-gl/dist/esm/exports-maplibre';
3
+ import { MapGeoJSONFeature, MapMouseEvent } from 'maplibre-gl';
3
4
  export type MapViewSource = AnySource & {
4
5
  absoluteId?: string;
5
6
  };
@@ -9,6 +10,7 @@ export type MapViewLayer = Omit<AnyLayer, 'id'> & {
9
10
  source?: string;
10
11
  absoluteSourceId?: string;
11
12
  zIndex?: number;
13
+ onClick?: (feature: MapGeoJSONFeature, event: MapMouseEvent) => any;
12
14
  };
13
15
  type MapViewLegend = {
14
16
  type: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orioro/react-maplibre-util",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "packageManager": "yarn@4.0.2",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -55,6 +55,7 @@
55
55
  "@orioro/react-ui-core": "^0.0.6",
56
56
  "@orioro/resolve": "^0.1.2",
57
57
  "@orioro/scale-util": "^0.0.2",
58
+ "@orioro/util": "^0.13.0",
58
59
  "@turf/turf": "^7.2.0",
59
60
  "d3": "^7.9.0",
60
61
  "d3-scale-chromatic": "^3.1.0",