Skip to content

Commit

Permalink
v0.2.0 (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
behoney authored Oct 21, 2024
2 parents 3c79e96 + a1f9eee commit 2f5ceff
Show file tree
Hide file tree
Showing 17 changed files with 299 additions and 149 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ dist-ssr
*.sw?
tsconfig.app.tsbuildinfo
test-results
tsconfig.node.tsbuildinfo

docs/.astro/settings.json
5 changes: 0 additions & 5 deletions docs/.astro/settings.json

This file was deleted.

35 changes: 35 additions & 0 deletions docs/src/examples/EventHandlerExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { useState } from "react";
import { GeoDataSource, GeoMap } from "../../../src/lib";

const EventHandlerExample: React.FC = () => {
const [clickedFeature, setClickedFeature] = useState("");
const [hoveredFeature, setHoveredFeature] = useState("");
return (
<section>
<h1>EventHandlerExample</h1>
<div style={{ width: "300px", height: "300px" }}>
<GeoMap>
<GeoDataSource
fitViewToData={true}
url={`${import.meta.env.BASE_URL}sample.geojson`}
onClick={(feature) => {
setClickedFeature(feature?.name ?? "");
}}
onMissed={() => {
setHoveredFeature("");
}}
onHover={(feature) => {
setHoveredFeature(feature?.name ?? "");
}}
/>
</GeoMap>
</div>
<ul>
<li>Clicked Feature: {clickedFeature}</li>
<li>Hovered Feature: {hoveredFeature}</li>
</ul>
</section>
);
};

export default EventHandlerExample;
5 changes: 4 additions & 1 deletion docs/src/examples/HelloWorldExample.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ const HelloWorldExample: React.FC = () => {
return (
<div style={{ width: "300px", height: "300px" }}>
<GeoMap>
<GeoDataSource fitViewToData={true} url="hello-world.geojson" />
<GeoDataSource
fitViewToData={true}
url={`${import.meta.env.BASE_URL}hello-world.geojson`}
/>
</GeoMap>
</div>
);
Expand Down
8 changes: 8 additions & 0 deletions docs/src/pages/examples/test-event-handler.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
import EventHandlerExample from "../../../src/examples/EventHandlerExample";
import ExampleLayout from "../../layouts/ExampleLayout.astro";
---

<ExampleLayout title="Default Map Example">
<EventHandlerExample client:load />
</ExampleLayout>
2 changes: 1 addition & 1 deletion docs/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function App() {


<h2>Why Use React Reconciler?</h2>
<p>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.</p>
<p>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.</p>

<p>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.</p>

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
8 changes: 7 additions & 1 deletion src/lib/components/GeoDataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>) => void;
onClick?: (properties: Record<string, any>) => void;
onMissed?: () => void;
}

export default function GeoDataSource(props: DataSourceProps) {
const layerConstructor = VectorLayer;

Expand Down
45 changes: 45 additions & 0 deletions src/lib/dataSourceStyleConfig.ts
Original file line number Diff line number Diff line change
@@ -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,
},
];
3 changes: 3 additions & 0 deletions src/lib/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ export function GeoMap(props: GeoMapProps): React.ReactElement;
export interface DataSourceProps {
url: string;
fitViewToData?: boolean;
onClick?: (properties?: Record<string, any>) => void;
onMissed?: () => void;
onHover?: (properties?: Record<string, any>) => void;
}

export function GeoDataSource(props: DataSourceProps): React.ReactElement;
Expand Down
126 changes: 17 additions & 109 deletions src/lib/render.ts
Original file line number Diff line number Diff line change
@@ -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<OlMap, ReactReconciler.FiberRoot>();

Expand All @@ -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 };
}
}
Expand All @@ -51,113 +52,20 @@ 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,
});
}
}
});
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) {
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);
}
}
Expand Down
Loading

0 comments on commit 2f5ceff

Please sign in to comment.