From dfd4fb1a1ed6fd1b76beea351045e744af706132 Mon Sep 17 00:00:00 2001 From: Byounghern Kim Date: Thu, 10 Oct 2024 20:08:30 +0900 Subject: [PATCH 1/9] feat: event handlers --- docs/src/examples/EventHandlerExample.tsx | 21 ++++++ docs/src/examples/HelloWorldExample.tsx | 5 +- .../pages/examples/test-event-handler.astro | 8 ++ src/lib/components/GeoMap.ts | 74 ++++++++++++++++++- 4 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 docs/src/examples/EventHandlerExample.tsx create mode 100644 docs/src/pages/examples/test-event-handler.astro diff --git a/docs/src/examples/EventHandlerExample.tsx b/docs/src/examples/EventHandlerExample.tsx new file mode 100644 index 0000000..e450ece --- /dev/null +++ b/docs/src/examples/EventHandlerExample.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import { GeoDataSource, GeoMap } from "../../../src/lib"; + +const EventHandlerExample: React.FC = () => { + return ( +
+ { + console.log("onClick", event); + }} + > + + +
+ ); +}; + +export default EventHandlerExample; diff --git a/docs/src/examples/HelloWorldExample.tsx b/docs/src/examples/HelloWorldExample.tsx index 0502978..16ce392 100644 --- a/docs/src/examples/HelloWorldExample.tsx +++ b/docs/src/examples/HelloWorldExample.tsx @@ -5,7 +5,10 @@ const HelloWorldExample: React.FC = () => { return (
- +
); diff --git a/docs/src/pages/examples/test-event-handler.astro b/docs/src/pages/examples/test-event-handler.astro new file mode 100644 index 0000000..7985c7f --- /dev/null +++ b/docs/src/pages/examples/test-event-handler.astro @@ -0,0 +1,8 @@ +--- +import EventHandlerExample from "../../../src/examples/EventHandlerExample"; +import ExampleLayout from "../../layouts/ExampleLayout.astro"; +--- + + + + diff --git a/src/lib/components/GeoMap.ts b/src/lib/components/GeoMap.ts index ca0bebf..4919d87 100644 --- a/src/lib/components/GeoMap.ts +++ b/src/lib/components/GeoMap.ts @@ -1,13 +1,30 @@ -import { Map as OlMap, View } from "ol"; +import { MapBrowserEvent, Map as OlMap, View } from "ol"; import TileLayer from "ol/layer/Tile"; import { OSM } from "ol/source"; -import React, { forwardRef, useEffect, useId, useRef } from "react"; +import React, { forwardRef, useEffect, useId, useMemo, useRef } from "react"; import { render } from "../render"; -interface Props { +interface Props extends MapEventHandlers { children?: React.ReactNode; className?: string; } +interface MapEventHandlers { + onClick?: (event: MapBrowserEvent) => void; + onContextMenu?: (event: MapBrowserEvent) => void; + onDoubleClick?: (event: MapBrowserEvent) => void; + onPointerUp?: (event: MapBrowserEvent) => void; + onPointerDown?: (event: MapBrowserEvent) => void; + onPointerOver?: (event: MapBrowserEvent) => void; + onPointerOut?: (event: MapBrowserEvent) => void; + onPointerEnter?: (event: MapBrowserEvent) => void; + onPointerLeave?: (event: MapBrowserEvent) => void; + onPointerMove?: (event: MapBrowserEvent) => void; + onPointerMissed?: () => void; + onMoveEnd?: (event: MapBrowserEvent) => void; + onMoveStart?: (event: MapBrowserEvent) => void; + onMove?: (event: MapBrowserEvent) => void; +} + function GeoMap(props: Props, ref: React.Ref) { const map = useRef(null); const mapRef: React.Ref = ref ?? map; @@ -44,6 +61,57 @@ function GeoMap(props: Props, ref: React.Ref) { } }, [props.children]); + const eventHandlers = useMemo(() => { + const handlers: Partial< + Record) => void> + > = {}; + (Object.keys(props) as Array).forEach((key) => { + if (key.startsWith("on") && typeof props[key] === "function") { + handlers[key] = (event: MapBrowserEvent) => { + (props[key] as (event: MapBrowserEvent) => void)(event); + }; + } + }); + return handlers; + }, [props]); + + useEffect(() => { + if (map.current) { + // Remove existing event listeners + Object.keys(eventHandlers).forEach((key) => { + const olEventName = key.charAt(2).toLowerCase() + key.slice(3); + map.current?.un( + olEventName, + eventHandlers[key as keyof MapEventHandlers] + ); + }); + + // Add new event listeners + Object.keys(eventHandlers).forEach((key) => { + const olEventName = key.charAt(2).toLowerCase() + key.slice(3); + map.current?.on( + olEventName, + eventHandlers[key as keyof MapEventHandlers] + ); + }); + } + + // Cleanup function to remove event listeners when component unmounts + return () => { + if (map.current) { + Object.keys(eventHandlers).forEach((key) => { + const olEventName = key.charAt(2).toLowerCase() + key.slice(3); + map.current?.un( + olEventName, + eventHandlers[key as keyof MapEventHandlers] + ); + }); + } + }; + }, [eventHandlers]); + + console.log("eventHandlers", eventHandlers); + return React.createElement("div", { id: id, ref: containerRef, From 85b16ca1559b4ce35ea0a8b341e75aae4210de37 Mon Sep 17 00:00:00 2001 From: Byounghern Kim Date: Thu, 10 Oct 2024 20:59:05 +0900 Subject: [PATCH 2/9] feat: move event to data source --- docs/src/examples/EventHandlerExample.tsx | 18 ++++-- src/lib/components/GeoDataSource.ts | 8 ++- src/lib/components/GeoMap.ts | 74 +---------------------- src/lib/render.ts | 41 ++++++++++--- 4 files changed, 55 insertions(+), 86 deletions(-) diff --git a/docs/src/examples/EventHandlerExample.tsx b/docs/src/examples/EventHandlerExample.tsx index e450ece..99a19f0 100644 --- a/docs/src/examples/EventHandlerExample.tsx +++ b/docs/src/examples/EventHandlerExample.tsx @@ -4,14 +4,22 @@ import { GeoDataSource, GeoMap } from "../../../src/lib"; const EventHandlerExample: React.FC = () => { return (
- { - console.log("onClick", event); - }} - > + { + console.log("onClick", event); + }} + onMissed={() => { + console.log("onMissed"); + }} + onHover={(event) => { + console.log("onHover", event); + }} + onDoubleClick={(event) => { + console.log("onDoubleClick", event); + }} />
diff --git a/src/lib/components/GeoDataSource.ts b/src/lib/components/GeoDataSource.ts index 2131d92..9185ece 100644 --- a/src/lib/components/GeoDataSource.ts +++ b/src/lib/components/GeoDataSource.ts @@ -2,11 +2,17 @@ import VectorLayer from "ol/layer/Vector"; import { createElement } from "react"; import { DATA_SOURCE } from "../utils/config"; -export interface DataSourceProps { +export interface DataSourceProps extends DataSourceEventHandlers { url: string; fitViewToData?: boolean; } +interface DataSourceEventHandlers { + onHover?: (properties: Record) => void; + onClick?: (properties: Record) => void; + onMissed?: () => void; +} + export default function GeoDataSource(props: DataSourceProps) { const layerConstructor = VectorLayer; diff --git a/src/lib/components/GeoMap.ts b/src/lib/components/GeoMap.ts index 4919d87..ca0bebf 100644 --- a/src/lib/components/GeoMap.ts +++ b/src/lib/components/GeoMap.ts @@ -1,30 +1,13 @@ -import { MapBrowserEvent, Map as OlMap, View } from "ol"; +import { Map as OlMap, View } from "ol"; import TileLayer from "ol/layer/Tile"; import { OSM } from "ol/source"; -import React, { forwardRef, useEffect, useId, useMemo, useRef } from "react"; +import React, { forwardRef, useEffect, useId, useRef } from "react"; import { render } from "../render"; -interface Props extends MapEventHandlers { +interface Props { children?: React.ReactNode; className?: string; } -interface MapEventHandlers { - onClick?: (event: MapBrowserEvent) => void; - onContextMenu?: (event: MapBrowserEvent) => void; - onDoubleClick?: (event: MapBrowserEvent) => void; - onPointerUp?: (event: MapBrowserEvent) => void; - onPointerDown?: (event: MapBrowserEvent) => void; - onPointerOver?: (event: MapBrowserEvent) => void; - onPointerOut?: (event: MapBrowserEvent) => void; - onPointerEnter?: (event: MapBrowserEvent) => void; - onPointerLeave?: (event: MapBrowserEvent) => void; - onPointerMove?: (event: MapBrowserEvent) => void; - onPointerMissed?: () => void; - onMoveEnd?: (event: MapBrowserEvent) => void; - onMoveStart?: (event: MapBrowserEvent) => void; - onMove?: (event: MapBrowserEvent) => void; -} - function GeoMap(props: Props, ref: React.Ref) { const map = useRef(null); const mapRef: React.Ref = ref ?? map; @@ -61,57 +44,6 @@ function GeoMap(props: Props, ref: React.Ref) { } }, [props.children]); - const eventHandlers = useMemo(() => { - const handlers: Partial< - Record) => void> - > = {}; - (Object.keys(props) as Array).forEach((key) => { - if (key.startsWith("on") && typeof props[key] === "function") { - handlers[key] = (event: MapBrowserEvent) => { - (props[key] as (event: MapBrowserEvent) => void)(event); - }; - } - }); - return handlers; - }, [props]); - - useEffect(() => { - if (map.current) { - // Remove existing event listeners - Object.keys(eventHandlers).forEach((key) => { - const olEventName = key.charAt(2).toLowerCase() + key.slice(3); - map.current?.un( - olEventName, - eventHandlers[key as keyof MapEventHandlers] - ); - }); - - // Add new event listeners - Object.keys(eventHandlers).forEach((key) => { - const olEventName = key.charAt(2).toLowerCase() + key.slice(3); - map.current?.on( - olEventName, - eventHandlers[key as keyof MapEventHandlers] - ); - }); - } - - // Cleanup function to remove event listeners when component unmounts - return () => { - if (map.current) { - Object.keys(eventHandlers).forEach((key) => { - const olEventName = key.charAt(2).toLowerCase() + key.slice(3); - map.current?.un( - olEventName, - eventHandlers[key as keyof MapEventHandlers] - ); - }); - } - }; - }, [eventHandlers]); - - console.log("eventHandlers", eventHandlers); - return React.createElement("div", { id: id, ref: containerRef, diff --git a/src/lib/render.ts b/src/lib/render.ts index f397d83..b9f730c 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -21,14 +21,15 @@ function createInstance( try { if (type === DATA_SOURCE) { if (typeof props.layerConstructor === "function") { - const geojsonFormat = new GeoJSON(); - const layer = new props.layerConstructor({ - source: new VectorSource({ - url: props.url, - format: geojsonFormat, - }), + const source = new VectorSource({ + url: props.url, + format: new GeoJSON(), }); + const layer = new props.layerConstructor({ + source, + }) as SupportedLayerType; + return { type, element: layer, props }; } } @@ -117,6 +118,26 @@ function appendChildToContainer(container: OlMap, child: OlInstance) { duration: 1000, }); } + + container.on("click", (event) => { + const features = container.getFeaturesAtPixel(event.pixel); + // TODO:: filter by source url + if (features.length > 0) { + child.props.onClick?.(features[0].getProperties()); + } else { + child.props.onMissed?.(); + } + }); + + container.on("pointermove", (event) => { + const features = container.getFeaturesAtPixel(event.pixel); + // TODO:: filter by source url + if (features.length > 0) { + child.props.onHover?.(features[0].getProperties()); + } else { + child.props.onMissed?.(); + } + }); } }); } else if (child.type === POPUP) { @@ -154,10 +175,12 @@ function appendChildToContainer(container: OlMap, child: OlInstance) { } } function removeChild(parent: OlInstance | null, child: OlInstance | null) { - if (parent?.element instanceof OlMap && child) { + if (parent?.element instanceof OlMap && child.type === DATA_SOURCE) { (parent.element as OlMap).removeLayer(child.element as SupportedLayerType); - } - if (child?.element instanceof Overlay) { + (parent.element as OlMap).un("click", child.props.onClick); + (parent.element as OlMap).un("pointermove", child.props.onHover); + (parent.element as OlMap).un("pointermove", child.props.onMissed); + } else if (child?.element instanceof Overlay) { (child as PopupInstance).popupOverlay.setMap(null); } } From a4ddfaa7b8c37f25189a339bc5e2548267f05cd5 Mon Sep 17 00:00:00 2001 From: Byounghern Kim Date: Thu, 10 Oct 2024 21:05:58 +0900 Subject: [PATCH 3/9] fix: export props --- src/lib/index.d.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/lib/index.d.ts b/src/lib/index.d.ts index 6746d97..a5e8586 100644 --- a/src/lib/index.d.ts +++ b/src/lib/index.d.ts @@ -10,6 +10,9 @@ export function GeoMap(props: GeoMapProps): React.ReactElement; export interface DataSourceProps { url: string; fitViewToData?: boolean; + onClick?: (properties: Record) => void; + onMissed?: () => void; + onHover?: (properties: Record) => void; } export function GeoDataSource(props: DataSourceProps): React.ReactElement; From e567ff51c8b781baa4e385a193f0f4a5a3898555 Mon Sep 17 00:00:00 2001 From: Byounghern Kim Date: Thu, 10 Oct 2024 21:06:14 +0900 Subject: [PATCH 4/9] docs: update example --- docs/src/examples/EventHandlerExample.tsx | 48 +++++++++++++---------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/docs/src/examples/EventHandlerExample.tsx b/docs/src/examples/EventHandlerExample.tsx index 99a19f0..aa0a06e 100644 --- a/docs/src/examples/EventHandlerExample.tsx +++ b/docs/src/examples/EventHandlerExample.tsx @@ -1,28 +1,34 @@ -import React from "react"; +import React, { useState } from "react"; import { GeoDataSource, GeoMap } from "../../../src/lib"; const EventHandlerExample: React.FC = () => { + const [clickedFeature, setClickedFeature] = useState(""); + const [hoveredFeature, setHoveredFeature] = useState(""); return ( -
- - { - console.log("onClick", event); - }} - onMissed={() => { - console.log("onMissed"); - }} - onHover={(event) => { - console.log("onHover", event); - }} - onDoubleClick={(event) => { - console.log("onDoubleClick", event); - }} - /> - -
+
+

EventHandlerExample

+
+ + { + setClickedFeature(feature.name); + }} + onMissed={() => { + setHoveredFeature(""); + }} + onHover={(feature) => { + setHoveredFeature(feature.name); + }} + /> + +
+
    +
  • Clicked Feature: {clickedFeature}
  • +
  • Hovered Feature: {hoveredFeature}
  • +
+
); }; From a73fdb7167178774d3cbf87d5c41a9d57b3a1c2f Mon Sep 17 00:00:00 2001 From: Byounghern Kim Date: Thu, 17 Oct 2024 20:59:39 +0900 Subject: [PATCH 5/9] feat: finalize interface --- docs/src/examples/EventHandlerExample.tsx | 4 ++-- src/lib/index.d.ts | 4 ++-- src/lib/render.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/examples/EventHandlerExample.tsx b/docs/src/examples/EventHandlerExample.tsx index aa0a06e..a4e197f 100644 --- a/docs/src/examples/EventHandlerExample.tsx +++ b/docs/src/examples/EventHandlerExample.tsx @@ -13,13 +13,13 @@ const EventHandlerExample: React.FC = () => { fitViewToData={true} url={`${import.meta.env.BASE_URL}sample.geojson`} onClick={(feature) => { - setClickedFeature(feature.name); + setClickedFeature(feature?.name ?? ""); }} onMissed={() => { setHoveredFeature(""); }} onHover={(feature) => { - setHoveredFeature(feature.name); + setHoveredFeature(feature?.name ?? ""); }} /> diff --git a/src/lib/index.d.ts b/src/lib/index.d.ts index a5e8586..fe522f2 100644 --- a/src/lib/index.d.ts +++ b/src/lib/index.d.ts @@ -10,9 +10,9 @@ export function GeoMap(props: GeoMapProps): React.ReactElement; export interface DataSourceProps { url: string; fitViewToData?: boolean; - onClick?: (properties: Record) => void; + onClick?: (properties?: Record) => void; onMissed?: () => void; - onHover?: (properties: Record) => void; + onHover?: (properties?: Record) => void; } export function GeoDataSource(props: DataSourceProps): React.ReactElement; diff --git a/src/lib/render.ts b/src/lib/render.ts index b9f730c..199e5a7 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -125,7 +125,7 @@ function appendChildToContainer(container: OlMap, child: OlInstance) { if (features.length > 0) { child.props.onClick?.(features[0].getProperties()); } else { - child.props.onMissed?.(); + child.props.onClick?.(); } }); From 16791eb98eb505a126013aa7dc3bae3a2926f333 Mon Sep 17 00:00:00 2001 From: behoney Date: Thu, 17 Oct 2024 21:16:11 +0900 Subject: [PATCH 6/9] refactor: extract renderers and style configs --- src/lib/dataSourceStyleConfig.ts | 45 +++++++++ src/lib/render.ts | 125 +----------------------- src/lib/renderers/dataSourceRenderer.ts | 74 ++++++++++++++ src/lib/renderers/popupRenderer.ts | 35 +++++++ 4 files changed, 159 insertions(+), 120 deletions(-) create mode 100644 src/lib/dataSourceStyleConfig.ts create mode 100644 src/lib/renderers/dataSourceRenderer.ts create mode 100644 src/lib/renderers/popupRenderer.ts diff --git a/src/lib/dataSourceStyleConfig.ts b/src/lib/dataSourceStyleConfig.ts new file mode 100644 index 0000000..973d393 --- /dev/null +++ b/src/lib/dataSourceStyleConfig.ts @@ -0,0 +1,45 @@ +export const dataSourceStyles: { + key: string; + cssVar: string; + fallback: string | number; + parse?: (value: string | number) => number; +}[] = [ + { + key: "fill-color", + cssVar: "--data-source-polygon-fill-color", + fallback: "rgba(126, 188, 111, 0.1)", + }, + { + key: "stroke-color", + cssVar: "--data-source-polygon-stroke-color", + fallback: "rgba(91, 124, 186, 1)", + }, + { + key: "stroke-width", + cssVar: "--data-source-polygon-stroke-width", + fallback: 2, + parse: Number.parseFloat, + }, + { + key: "circle-radius", + cssVar: "--data-source-circle-radius", + fallback: 10, + parse: Number.parseFloat, + }, + { + key: "circle-fill-color", + cssVar: "--data-source-circle-fill-color", + fallback: "rgba(255, 0, 0, 0.5)", + }, + { + key: "circle-stroke-color", + cssVar: "--data-source-circle-stroke-color", + fallback: "rgba(0, 0, 255, 1)", + }, + { + key: "circle-stroke-width", + cssVar: "--data-source-circle-stroke-width", + fallback: 2, + parse: Number.parseFloat, + }, +]; diff --git a/src/lib/render.ts b/src/lib/render.ts index 199e5a7..d663b5e 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -1,16 +1,16 @@ import { Map as OlMap, Overlay } from "ol"; import GeoJSON from "ol/format/GeoJSON"; import VectorSource from "ol/source/Vector"; -import ReactDOM from "react-dom"; import ReactReconciler from "react-reconciler"; import { ConcurrentRoot, DefaultEventPriority, } from "react-reconciler/constants.js"; import type { DataSourceProps } from "./components/GeoDataSource"; +import { renderDataSource } from "./renderers/dataSourceRenderer"; +import { renderPopup } from "./renderers/PopupRenderer"; import type { OlInstance, PopupInstance, SupportedLayerType } from "./types"; import { DATA_SOURCE, POPUP } from "./utils/config"; -import { observeCSSVariables } from "./utils/utils"; const roots = new WeakMap(); @@ -52,128 +52,13 @@ function createInstance( function appendChildToContainer(container: OlMap, child: OlInstance) { if (container instanceof OlMap) { if (child.type === DATA_SOURCE) { - const layer = child.element as SupportedLayerType; - - const targetElement = container.getTargetElement() as HTMLElement; - - const applyStyles = () => { - const computedStyle = getComputedStyle(targetElement); - layer.setStyle({ - "fill-color": - computedStyle.getPropertyValue( - "--data-source-polygon-fill-color" - ) || "rgba(126, 188, 111, 0.1)", - "stroke-color": - computedStyle.getPropertyValue( - "--data-source-polygon-stroke-color" - ) || "rgba(91, 124, 186, 1)", - "stroke-width": - Number.parseFloat( - computedStyle.getPropertyValue( - "--data-source-polygon-stroke-width" - ) - ) || 2, - "circle-radius": - Number.parseFloat( - computedStyle.getPropertyValue("--data-source-circle-radius") - ) || 10, - "circle-fill-color": - computedStyle.getPropertyValue("--data-source-circle-fill-color") || - "rgba(255, 0, 0, 0.5)", - "circle-stroke-color": - computedStyle.getPropertyValue( - "--data-source-circle-stroke-color" - ) || "rgba(0, 0, 255, 1)", - "circle-stroke-width": - Number.parseFloat( - computedStyle.getPropertyValue( - "--data-source-circle-stroke-width" - ) - ) || 2, - }); - }; - - container.addLayer(layer); - - const observer = observeCSSVariables(targetElement, applyStyles); - observer.observe(targetElement, { - attributes: true, - attributeFilter: ["style", "class"], - }); - - applyStyles(); - - layer.getSource()?.once("change", () => { - if (layer.getSource()?.getState() === "ready") { - const extent = layer.getSource()?.getExtent(); - - if ( - child.props?.fitViewToData && - extent && - !extent.every((value) => !Number.isFinite(value)) - ) { - container.getView().fit(extent, { - padding: [20, 20, 20, 20], - maxZoom: 19, - duration: 1000, - }); - } - - container.on("click", (event) => { - const features = container.getFeaturesAtPixel(event.pixel); - // TODO:: filter by source url - if (features.length > 0) { - child.props.onClick?.(features[0].getProperties()); - } else { - child.props.onClick?.(); - } - }); - - container.on("pointermove", (event) => { - const features = container.getFeaturesAtPixel(event.pixel); - // TODO:: filter by source url - if (features.length > 0) { - child.props.onHover?.(features[0].getProperties()); - } else { - child.props.onMissed?.(); - } - }); - } - }); + renderDataSource(child, container); } else if (child.type === POPUP) { - const popupInstance = child as PopupInstance; - container.addOverlay(popupInstance.popupOverlay); - - if (!popupInstance) - console.error("popupInstance is null, this feature is WIP"); - - container.on("singleclick", (event) => { - const feature = container.forEachFeatureAtPixel( - event.pixel, - (feature) => feature - ); - - if (feature) { - const coordinate = event.coordinate; - popupInstance.popupOverlay.setPosition(coordinate); - ReactDOM.render( - popupInstance.popupFunc( - feature.getProperties() - ) as React.ReactElement, - popupInstance.popupOverlay.getElement() as HTMLElement - ); - } else { - popupInstance.popupOverlay?.setPosition(undefined); - if (popupInstance.popupOverlay?.getElement()) { - ReactDOM.unmountComponentAtNode( - popupInstance.popupOverlay.getElement() as HTMLElement - ); - } - } - }); + renderPopup(child, container); } } } + function removeChild(parent: OlInstance | null, child: OlInstance | null) { if (parent?.element instanceof OlMap && child.type === DATA_SOURCE) { (parent.element as OlMap).removeLayer(child.element as SupportedLayerType); diff --git a/src/lib/renderers/dataSourceRenderer.ts b/src/lib/renderers/dataSourceRenderer.ts new file mode 100644 index 0000000..37ffdca --- /dev/null +++ b/src/lib/renderers/dataSourceRenderer.ts @@ -0,0 +1,74 @@ +import { Map as OlMap } from "ol"; +import { observeCSSVariables } from ".."; +import { dataSourceStyles } from "../dataSourceStyleConfig"; +import { OlInstance, SupportedLayerType } from "../types"; + +export const renderDataSource = (child: OlInstance, container: OlMap) => { + const layer = child.element as SupportedLayerType; + + const targetElement = container.getTargetElement() as HTMLElement; + + const styleConfig = dataSourceStyles; + + const applyStyles = () => { + const computedStyle = getComputedStyle(targetElement); + const styles = styleConfig.reduce( + (acc, { key, cssVar, fallback, parse }) => { + const value = computedStyle.getPropertyValue(cssVar) || fallback; + acc[key] = parse ? parse(value) : value; + return acc; + }, + {} as Record + ); + + layer.setStyle(styles); + }; + + container.addLayer(layer); + + const observer = observeCSSVariables(targetElement, applyStyles); + observer.observe(targetElement, { + attributes: true, + attributeFilter: ["style", "class"], + }); + + applyStyles(); + + layer.getSource()?.once("change", () => { + if (layer.getSource()?.getState() === "ready") { + const extent = layer.getSource()?.getExtent(); + + if ( + child.props?.fitViewToData && + extent && + !extent.every((value) => !Number.isFinite(value)) + ) { + container.getView().fit(extent, { + padding: [20, 20, 20, 20], + maxZoom: 19, + duration: 1000, + }); + } + + container.on("click", (event) => { + const features = container.getFeaturesAtPixel(event.pixel); + // TODO:: filter by source url + if (features.length > 0) { + child.props.onClick?.(features[0].getProperties()); + } else { + child.props.onClick?.(); + } + }); + + container.on("pointermove", (event) => { + const features = container.getFeaturesAtPixel(event.pixel); + // TODO:: filter by source url + if (features.length > 0) { + child.props.onHover?.(features[0].getProperties()); + } else { + child.props.onMissed?.(); + } + }); + } + }); +}; diff --git a/src/lib/renderers/popupRenderer.ts b/src/lib/renderers/popupRenderer.ts new file mode 100644 index 0000000..c8fe1df --- /dev/null +++ b/src/lib/renderers/popupRenderer.ts @@ -0,0 +1,35 @@ +import { Map as OlMap } from "ol"; +import ReactDOM from "react-dom"; +import { OlInstance, PopupInstance } from "../types"; + +// NOTE:: This function is WIP +export const renderPopup = (child: OlInstance, container: OlMap) => { + const popupInstance = child as PopupInstance; + container.addOverlay(popupInstance.popupOverlay); + + if (!popupInstance) + console.error("popupInstance is null, this feature is WIP"); + + container.on("singleclick", (event) => { + const feature = container.forEachFeatureAtPixel( + event.pixel, + (feature) => feature + ); + + if (feature) { + const coordinate = event.coordinate; + popupInstance.popupOverlay.setPosition(coordinate); + ReactDOM.render( + popupInstance.popupFunc(feature.getProperties()) as React.ReactElement, + popupInstance.popupOverlay.getElement() as HTMLElement + ); + } else { + popupInstance.popupOverlay?.setPosition(undefined); + if (popupInstance.popupOverlay?.getElement()) { + ReactDOM.unmountComponentAtNode( + popupInstance.popupOverlay.getElement() as HTMLElement + ); + } + } + }); +}; From bff7c012b5f27a467d85580bee56e41a8a094366 Mon Sep 17 00:00:00 2001 From: behoney Date: Thu, 17 Oct 2024 21:20:24 +0900 Subject: [PATCH 7/9] fix: import path --- src/lib/renderers/dataSourceRenderer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/renderers/dataSourceRenderer.ts b/src/lib/renderers/dataSourceRenderer.ts index 37ffdca..5c5bc6a 100644 --- a/src/lib/renderers/dataSourceRenderer.ts +++ b/src/lib/renderers/dataSourceRenderer.ts @@ -1,7 +1,7 @@ import { Map as OlMap } from "ol"; -import { observeCSSVariables } from ".."; import { dataSourceStyles } from "../dataSourceStyleConfig"; import { OlInstance, SupportedLayerType } from "../types"; +import { observeCSSVariables } from "../utils/utils"; export const renderDataSource = (child: OlInstance, container: OlMap) => { const layer = child.element as SupportedLayerType; From febde2c8fa2cdb30559a4b62bb031b4db522a446 Mon Sep 17 00:00:00 2001 From: Byounghern Kim Date: Thu, 17 Oct 2024 21:59:32 +0900 Subject: [PATCH 8/9] test: event handlers (#21) --- tests/DataMapExample.spec.ts | 28 +++++++++++------------ tests/DataSourceExample.spec.ts | 28 +++++++++++------------ tests/EventHandlerExample.spec.ts | 37 +++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 30 deletions(-) create mode 100644 tests/EventHandlerExample.spec.ts diff --git a/tests/DataMapExample.spec.ts b/tests/DataMapExample.spec.ts index 7e6854c..b3b5209 100644 --- a/tests/DataMapExample.spec.ts +++ b/tests/DataMapExample.spec.ts @@ -1,23 +1,21 @@ import { Page, expect, test } from "@playwright/test"; +const URL = "http://localhost:4321/examples/test-map"; +const TIMEOUT = 30000; + +const waitForMap = async (page: Page) => { + await page.waitForSelector(".ol-viewport", { + state: "visible", + timeout: TIMEOUT, + }); + await page.waitForTimeout(1000); +}; + test( "renders map correctly", async ({ page }: { page: Page }) => { - // Remove initial timeout and use navigation timeout instead - await page.goto("http://localhost:4321/examples/test-map", { - timeout: 30000, - }); - - // Wait for the map to be visible and stable - await page.waitForSelector(".ol-viewport", { - state: "visible", - timeout: 30000, - }); - - // Optional: Wait for any animations to complete - await page.waitForTimeout(1000); - - // Take the screenshot and compare + await page.goto(URL, { timeout: TIMEOUT }); + await waitForMap(page); expect(await page.screenshot()).toMatchSnapshot("map.png"); }, { timeout: 100000 } diff --git a/tests/DataSourceExample.spec.ts b/tests/DataSourceExample.spec.ts index 8820549..1324933 100644 --- a/tests/DataSourceExample.spec.ts +++ b/tests/DataSourceExample.spec.ts @@ -1,23 +1,21 @@ import { Page, expect, test } from "@playwright/test"; +const URL = "http://localhost:4321/examples/test-data-source"; +const TIMEOUT = 30000; + +const waitForMap = async (page: Page) => { + await page.waitForSelector(".ol-viewport", { + state: "visible", + timeout: TIMEOUT, + }); + await page.waitForTimeout(1000); +}; + test( "renders map correctly", async ({ page }: { page: Page }) => { - // Remove initial timeout and use navigation timeout instead - await page.goto("http://localhost:4321/examples/test-data-source", { - timeout: 30000, - }); - - // Wait for the map to be visible and stable - await page.waitForSelector(".ol-viewport", { - state: "visible", - timeout: 30000, - }); - - // Optional: Wait for any animations to complete - await page.waitForTimeout(1000); - - // Take the screenshot and compare + await page.goto(URL, { timeout: TIMEOUT }); + await waitForMap(page); expect(await page.screenshot()).toMatchSnapshot("map.png"); }, { timeout: 100000 } diff --git a/tests/EventHandlerExample.spec.ts b/tests/EventHandlerExample.spec.ts new file mode 100644 index 0000000..717c8ec --- /dev/null +++ b/tests/EventHandlerExample.spec.ts @@ -0,0 +1,37 @@ +import { Page, expect, test } from "@playwright/test"; + +const URL = "http://localhost:4321/examples/test-event-handler"; +const TIMEOUT = 30000; +const MOUSE_POSITION = { x: 180, y: 150 }; + +const waitForMap = async (page: Page) => { + await page.waitForSelector(".ol-viewport", { + state: "visible", + timeout: TIMEOUT, + }); + await page.waitForTimeout(1000); +}; + +const performMouseActions = async (page: Page) => { + await page.mouse.move(MOUSE_POSITION.x, MOUSE_POSITION.y); + await page.locator("canvas").click({ + button: "left", + position: MOUSE_POSITION, + }); + await page.waitForTimeout(1000); +}; + +const checkFeatureTexts = async (page: Page) => { + const clickFeatureText = page.getByText("Clicked Feature: GEOJSON Characters"); + const hoverFeatureText = page.getByText("Hovered Feature: GEOJSON Characters"); + + await expect(clickFeatureText).toBeVisible(); + await expect(hoverFeatureText).toBeVisible(); +}; + +test("renders map and handles events correctly", async ({ page }) => { + await page.goto(URL, { timeout: TIMEOUT }); + await waitForMap(page); + await performMouseActions(page); + await checkFeatureTexts(page); +}, { timeout: 100000 }); From a1f9eee704fb0dfcb476501d35913295944fe51c Mon Sep 17 00:00:00 2001 From: Byounghern Kim Date: Mon, 21 Oct 2024 20:46:41 +0900 Subject: [PATCH 9/9] build: 0.2.0 (#22) --- .gitignore | 3 +++ docs/.astro/settings.json | 5 ----- docs/src/pages/index.astro | 2 +- package.json | 2 +- src/lib/render.ts | 2 +- src/lib/renderers/popupRenderer.ts | 27 +++++++++++++++------------ tsconfig.node.tsbuildinfo | 1 - 7 files changed, 21 insertions(+), 21 deletions(-) delete mode 100644 docs/.astro/settings.json delete mode 100644 tsconfig.node.tsbuildinfo diff --git a/.gitignore b/.gitignore index 8511436..b315b09 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ dist-ssr *.sw? tsconfig.app.tsbuildinfo test-results +tsconfig.node.tsbuildinfo + +docs/.astro/settings.json diff --git a/docs/.astro/settings.json b/docs/.astro/settings.json deleted file mode 100644 index 803259e..0000000 --- a/docs/.astro/settings.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "_variables": { - "lastUpdateCheck": 1727923824793 - } -} \ No newline at end of file diff --git a/docs/src/pages/index.astro b/docs/src/pages/index.astro index 03b5522..8fffe8e 100644 --- a/docs/src/pages/index.astro +++ b/docs/src/pages/index.astro @@ -41,7 +41,7 @@ function App() {

Why Use React Reconciler?

-

React GeoJSON Map leverages React Reconciler for superior performance and flexibility in geospatial visualization. This approach enables fine-grained control over rendering, custom logic aligned with OpenLayers, and a declarative API for map components. As a result, it offers efficient updates, smooth performance with large datasets, and an intuitive development experience.

+

React GeoJSON Map leverages React Reconciler for superior performance and flexibility in geospatial visualization. This approach enables coarse-grained control over rendering, custom logic aligned with OpenLayers, and a declarative API for map components. As a result, it offers efficient updates, smooth performance with large datasets, and an intuitive development experience.

By using React Reconciler, the library achieves efficient tree updates, better separation of concerns, and reduced overhead compared to React Context. This leads to more granular control over updates, cleaner code separation, and potentially fewer unnecessary re-renders. Consequently, React GeoJSON Map is ideal for building complex, interactive geospatial applications with enhanced performance and maintainability.

diff --git a/package.json b/package.json index 257cb01..83bbd25 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-geojson-map", - "version": "0.1.6", + "version": "0.2.0", "description": "A library for declarative geospatial visualization using React Fiber and OpenLayers", "main": "dist/react-geojson-map.umd.js", "module": "dist/react-geojson-map.es.js", diff --git a/src/lib/render.ts b/src/lib/render.ts index d663b5e..5e3b5fd 100644 --- a/src/lib/render.ts +++ b/src/lib/render.ts @@ -8,7 +8,7 @@ import { } from "react-reconciler/constants.js"; import type { DataSourceProps } from "./components/GeoDataSource"; import { renderDataSource } from "./renderers/dataSourceRenderer"; -import { renderPopup } from "./renderers/PopupRenderer"; +import { renderPopup } from "./renderers/popupRenderer"; import type { OlInstance, PopupInstance, SupportedLayerType } from "./types"; import { DATA_SOURCE, POPUP } from "./utils/config"; diff --git a/src/lib/renderers/popupRenderer.ts b/src/lib/renderers/popupRenderer.ts index c8fe1df..edfca87 100644 --- a/src/lib/renderers/popupRenderer.ts +++ b/src/lib/renderers/popupRenderer.ts @@ -1,5 +1,5 @@ import { Map as OlMap } from "ol"; -import ReactDOM from "react-dom"; +import ReactDOM from "react-dom/client"; import { OlInstance, PopupInstance } from "../types"; // NOTE:: This function is WIP @@ -16,19 +16,22 @@ export const renderPopup = (child: OlInstance, container: OlMap) => { (feature) => feature ); - if (feature) { - const coordinate = event.coordinate; - popupInstance.popupOverlay.setPosition(coordinate); - ReactDOM.render( - popupInstance.popupFunc(feature.getProperties()) as React.ReactElement, - popupInstance.popupOverlay.getElement() as HTMLElement + if (popupInstance.props.overlay) { + const root = ReactDOM.createRoot( + popupInstance.props.overlayPortalContainer ); - } else { - popupInstance.popupOverlay?.setPosition(undefined); - if (popupInstance.popupOverlay?.getElement()) { - ReactDOM.unmountComponentAtNode( - popupInstance.popupOverlay.getElement() as HTMLElement + + if (feature) { + const coordinate = event.coordinate; + popupInstance.popupOverlay.setPosition(coordinate); + root.render( + popupInstance.popupFunc(feature.getProperties()) as React.ReactElement ); + } else { + popupInstance.popupOverlay?.setPosition(undefined); + if (popupInstance.popupOverlay?.getElement()) { + root.unmount(); + } } } }); diff --git a/tsconfig.node.tsbuildinfo b/tsconfig.node.tsbuildinfo deleted file mode 100644 index 98ef2f9..0000000 --- a/tsconfig.node.tsbuildinfo +++ /dev/null @@ -1 +0,0 @@ -{"root":["./vite.config.ts"],"version":"5.6.2"} \ No newline at end of file