From ece6f352d8af484926ad97c959b1d6dd90fecd51 Mon Sep 17 00:00:00 2001 From: Momtchil Momtchev Date: Fri, 30 Jun 2023 18:40:39 +0200 Subject: [PATCH] improve the event handling performance (#165) * reformat the changelog * feature event testing framework * make the addFeature test an end-to-end test * more end-to-end testing * more end-to-end testing * implement true global layer handlers * clean up dead code * move the RStyle sub-tree out of the main hierarchy * implement event counting for layers * update the doc * openlayers 6 compatibility * add a pointermove bypass * more OL6 compatibility * separate drag event from pointermove events * improve typings * remove dead code * end-to-end testing of popups * add the rlayer components to the context * support installing handlers via a method * avoid a warning for the deprecated constructor form * forgo the safety * adapt RPopup to the new events * switch handlers to a getter * use object encapsulation * add doc * additional roverlay options * this needs to be protected * add a note * prefer the generics form * add hooks * openlayers 6 compatibility * merge main --- .eslintrc.json | 14 +- CHANGELOG.md | 11 +- README.md | 21 ++- examples/Addon.tsx | 2 +- examples/Controls.tsx | 2 +- examples/GeoTIFF.tsx | 3 +- examples/Geolocation.tsx | 43 ++++-- examples/IGC.tsx | 13 +- src/REvent.tsx | 138 ++++++++++++----- src/RFeature.tsx | 84 +++++++++-- src/RGeolocation.tsx | 2 +- src/RMap.tsx | 10 +- src/ROverlay.tsx | 38 ++++- src/RPopup.tsx | 48 ++++-- src/context.ts | 44 ++++++ src/control/RAttribution.tsx | 4 +- src/control/RControlBase.tsx | 4 +- src/control/RCustom.tsx | 2 +- src/control/RFullScreen.tsx | 2 +- src/control/RLayers.tsx | 2 +- src/control/RMousePosition.tsx | 2 +- src/control/ROverviewMap.tsx | 4 +- src/control/RRotate.tsx | 2 +- src/control/RScaleLine.tsx | 2 +- src/control/RZoom.tsx | 2 +- src/control/RZoomSlider.tsx | 2 +- src/control/RZoomToExtent.tsx | 2 +- src/index.ts | 2 +- src/interaction/RBaseInteraction.tsx | 6 +- src/interaction/RDoubleClickZoom.tsx | 2 +- src/interaction/RDragBox.tsx | 2 +- src/interaction/RDragPan.tsx | 2 +- src/interaction/RDragRotate.tsx | 2 +- src/interaction/RDragZoom.tsx | 2 +- src/interaction/RDraw.tsx | 2 +- src/interaction/RKeyboardPan.tsx | 2 +- src/interaction/RKeyboardZoom.tsx | 2 +- src/interaction/RModify.tsx | 2 +- src/interaction/RMouseWheelZoom.tsx | 8 +- src/interaction/RPinchRotate.tsx | 2 +- src/interaction/RPinchZoom.tsx | 2 +- src/interaction/RPointer.tsx | 7 +- src/interaction/RTranslate.tsx | 2 +- src/layer/RLayer.tsx | 7 +- src/layer/RLayerBaseVector.tsx | 75 ++++------ src/layer/RLayerCluster.tsx | 8 +- src/layer/RLayerGraticule.tsx | 6 +- src/layer/RLayerHeatmap.tsx | 4 +- src/layer/RLayerImage.tsx | 6 +- src/layer/RLayerRaster.tsx | 2 +- src/layer/RLayerRasterMBTiles.tsx | 8 +- src/layer/RLayerStamen.tsx | 2 +- src/layer/RLayerTile.tsx | 6 +- src/layer/RLayerTileJSON.tsx | 4 +- src/layer/RLayerTileWMS.tsx | 6 +- src/layer/RLayerTileWebGL.tsx | 6 +- src/layer/RLayerVector.tsx | 4 +- src/layer/RLayerVectorImage.tsx | 4 +- src/layer/RLayerVectorMBTiles.tsx | 11 +- src/layer/RLayerVectorTile.tsx | 25 +++- src/layer/RLayerWMS.tsx | 6 +- src/layer/RLayerWMTS.tsx | 6 +- src/layer/ROSM.tsx | 4 +- src/layer/ROSMWebGL.tsx | 4 +- src/style/RBackground.tsx | 4 +- src/style/RBaseStyle.tsx | 10 +- src/style/RCircle.tsx | 4 +- src/style/RFill.tsx | 6 +- src/style/RIcon.tsx | 4 +- src/style/RImage.tsx | 12 +- src/style/RRegularBase.tsx | 8 +- src/style/RRegularShape.tsx | 9 +- src/style/RStroke.tsx | 6 +- src/style/RStyle.tsx | 19 ++- src/style/RStyleArray.tsx | 2 +- src/style/RText.tsx | 6 +- test/RFeature.test.tsx | 139 +++++++----------- test/RMap.test.tsx | 11 +- test/ROverlay.test.tsx | 29 +++- test/RPopup.test.tsx | 19 ++- test/RVectorLayer.test.tsx | 53 ++++--- test/RVectorTiles.test.tsx | 94 +++++------- test/__snapshots__/RMap.test.tsx.snap | 2 +- test/__snapshots__/ROverlay.test.tsx.snap | 4 +- test/__snapshots__/RVectorLayer.test.tsx.snap | 4 +- test/common.ts | 63 +++++++- 86 files changed, 802 insertions(+), 460 deletions(-) diff --git a/.eslintrc.json b/.eslintrc.json index 5e203bc2..972097bc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -56,5 +56,15 @@ "react": { "version": "detect" } - } -} \ No newline at end of file + }, + "overrides": [ + { + "files": [ + "test/*.tsx" + ], + "rules": { + "@typescript-eslint/no-non-null-assertion": "off" + } + } + ] +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e15beb2..41d43db1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [2.0.0] + +- Vastly improved event handling performance avoiding expensive `forEachFeatureAtPixel` on layers that do not have event handlers +- Add the `useOL()` and `useRLayersComponent()` component hooks allowing to easily access the containing OpenLayers and _rlayers_ components +- Layer event handlers are now independent of feature event handlers, if both the feature and its containing layer have declared an event handler, both will be called +- Fix `onClick` handlers on `RLayerVectorTiles` layers +- Use TypeScript `protected` and `private` to restrict methods that are not expected to be directly used from user code +- Support all positioning options in `ROverlay` + ### [1.5.3] - Add `onFeaturesLoadStart` and `onFeaturesLoadError` events to all vector layers @@ -18,7 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Declare `RLayerRasterMBTiles` and `RLayerVectorMBTiles` as public exports -### [1.5.0] 2023-05-30 +## [1.5.0] 2023-05-30 - Add remote `.mbtiles` support with [`ol-mbtiles`](https://github.com/mmomtchev/ol-mbtiles) - Add new _rlayers_-specific events to `RLayerWMTS` and `RLayerRasterMBTiles` to distinguish them from the OpenLayers `onSourceReady` events diff --git a/README.md b/README.md index 146dd4f3..c013edde 100644 --- a/README.md +++ b/README.md @@ -134,20 +134,25 @@ You can also check the GPLed [XC-DB](https://github.com/mmomtchev/xc-db.git) for Composition works by using _React_ Contexts. Every nested element uses the context of its nearest parent. -Currently a context has an `RContextType` and can contain the following elements: +The underlying OpenLayers objects can be accessed using the `useOL()` hook - check the [`Geolocation`](https://mmomtchev.github.io/rlayers/#/geolocation) example to see how. -- `RContext.map` provided by a map, every other element, except an `RStyle` must have a map parent -- `RContext.layer` and `RContext.source` provided by all layers - not required for anything at the moment, but can be used to access the underlying _OpenLayers_ objects -- `RContext.vectorlayer` and `RContext.vectorsource` provided by vector layers only - required for `` -- `RContext.location` and `RContext.feature` provided by a map feature - required for `` and `` -- `RContext.style` provided by a style definition - the only one which can be outside of a map +Currently `useOL()` has an `RContextType` and can contain the following elements: + +- `map` provided by a map, every other element, except an `RStyle` must have a map parent +- `layer` and `source` provided by all layers - not required for anything at the moment, but can be used to access the underlying _OpenLayers_ objects +- `vectorlayer` and `vectorsource` provided by vector layers only - required for `` +- `vectortilelayer` provided by vector tile layers only +- `location` and `feature` provided by a map feature - required for `` and `` +- `style` provided by a style definition - the only one which can be outside of a map + +Additionally, `useRLayersComponent()` allows retrieving the containing _rlayers_ component. #### Accessing the underlying _OpenLayers_ objects and API The underlying _OpenLayers_ objects can be accessed in a number of different ways: -- Through the context objects by using `React.Context.Consumer` - [the custom controls example](https://mmomtchev.github.io/rlayers/#/controls) contains an example for using the _OpenLayers_ `map` from `RContext` -- In an event handler, when it is a normal function and not an arrow lambda, `this` will contain the _rlayers_ component and `this.context` will contain the context - [the geolocation example](https://mmomtchev.github.io/rlayers/#/geolocation) accesses `this.context.map` to adjust the view +- Through the context objects by using `React.Context.Consumer` +- In an event handler that is a normal function and not an arrow lambda, `this` will contain the _rlayers_ component and `this.context` will contain the context - this is an alternative to using `useOL()` - In all event handlers, _OpenLayers_ will pass the target object in `event.target` and the map in `event.map` - [the popups example](https://mmomtchev.github.io/rlayers/#/popups) uses this method - And finally, accessing arbitrary elements, even outside their contexts, is possible by using React references - `React.RefObject`s. [The high performance example](https://mmomtchev.github.io/rlayers/#/igc) contains an example of this. The underlying _OpenLayers_ objects can be accessed through the `ol` property of every component. Additionaly, for `layer` objects, the underlying _OpenLayers_ source can be accessed through `source`: ```ts diff --git a/examples/Addon.tsx b/examples/Addon.tsx index 64ef81ca..f08b673c 100644 --- a/examples/Addon.tsx +++ b/examples/Addon.tsx @@ -25,7 +25,7 @@ class MyLayerMapbox extends RLayer { // Tiled layers must extend RLayerRaster, non-tiled vector layers must extend RLayerVector // This allows you to have the same features as RLayers built-in components // Completely custom layers must extend RLayer - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { // You must call the parent constructor super(props, context); diff --git a/examples/Controls.tsx b/examples/Controls.tsx index 49e2cb7e..f8709e36 100644 --- a/examples/Controls.tsx +++ b/examples/Controls.tsx @@ -4,7 +4,7 @@ import {fromLonLat, toLonLat} from 'ol/proj'; import 'ol/ol.css'; import 'rlayers/control/layers.css'; -import {RMap, RContext, ROSM, RControl} from 'rlayers'; +import {RMap, ROSM, RControl} from 'rlayers'; import {RView} from 'rlayers/RMap'; const origin = [2.364, 48.82]; diff --git a/examples/GeoTIFF.tsx b/examples/GeoTIFF.tsx index 5a3b68a6..8b3e4c69 100644 --- a/examples/GeoTIFF.tsx +++ b/examples/GeoTIFF.tsx @@ -34,7 +34,7 @@ class RLayerGeoTIFF extends RLayer { ol: LayerTile; source: GeoTIFF; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.createSource(); this.ol = new LayerTile({source: this.source}); @@ -65,6 +65,7 @@ class RLayerGeoTIFF extends RLayer { this.createSource(); this.ol.setSource(this.source); + // Call this after replacing a source this.attachOldEventHandlers(this.source); } } diff --git a/examples/Geolocation.tsx b/examples/Geolocation.tsx index 573aa148..5d520810 100644 --- a/examples/Geolocation.tsx +++ b/examples/Geolocation.tsx @@ -4,31 +4,33 @@ import {Geometry, Point} from 'ol/geom'; import {Geolocation as OLGeoLoc} from 'ol'; import 'ol/ol.css'; -import {RMap, ROSM, RLayerVector, RFeature, RGeolocation, RStyle} from 'rlayers'; +import {RMap, ROSM, RLayerVector, RFeature, RGeolocation, RStyle, useOL} from 'rlayers'; import locationIcon from './svg/location.svg'; -export default function Geolocation(): JSX.Element { +function GeolocComp(): JSX.Element { const [pos, setPos] = React.useState(new Point(fromLonLat([0, 0]))); const [accuracy, setAccuracy] = React.useState(undefined as Geometry | undefined); + // Low-level access to the OpenLayers API + const {map} = useOL(); + return ( - - + <> { + const geoloc = e.target as OLGeoLoc; + setPos(new Point(geoloc.getPosition())); + setAccuracy(geoloc.getAccuracyGeometry()); - // Low-level access to the OpenLayers API - this.context.map.getView().fit(geoloc.getAccuracyGeometry(), { - duration: 250, - maxZoom: 15 - }); - }, [])} + map.getView().fit(geoloc.getAccuracyGeometry(), { + duration: 250, + maxZoom: 15 + }); + }, + [map] + )} /> @@ -38,6 +40,15 @@ export default function Geolocation(): JSX.Element { + + ); +} + +export default function Geolocation(): JSX.Element { + return ( + + + ); } diff --git a/examples/IGC.tsx b/examples/IGC.tsx index 2e321895..dcc83730 100644 --- a/examples/IGC.tsx +++ b/examples/IGC.tsx @@ -14,6 +14,7 @@ import {fromLonLat} from 'ol/proj'; import IGC from 'ol/format/IGC'; import {getVectorContext} from 'ol/render'; import {Geometry, LineString, Point} from 'ol/geom'; +import {Coordinate} from 'ol/coordinate'; import { RMap, @@ -52,10 +53,10 @@ const origin = fromLonLat([6, 45.7]); // This part is re-rendered on every pointermove export default function IGCComp(): JSX.Element { const [time, setTime] = React.useState(''); - const [point, setPoint] = React.useState(null as Point); - const [line, setLine] = React.useState(null as LineString); + const [point, setPoint] = React.useState(null); + const [line, setLine] = React.useState(null); const [slider, setSlider] = React.useState(0); - const [highlights, setHighlights] = React.useState([]); + const [highlights, setHighlights] = React.useState([]); const [flight, setFlight] = React.useState({ start: Infinity, stop: -Infinity, @@ -71,12 +72,12 @@ export default function IGCComp(): JSX.Element { blueCircle: useRStyle(), // This is a technique for an array of React.RefObjects // It is ugly but it works - flightPath: React.useRef([]) as React.RefObject + flightPath: React.useRef([]) }; // createRef instead of useRef here will severely impact performance - const igcVectorLayer = React.useRef() as React.RefObject; - const highlightVectorLayer = React.useRef() as React.RefObject; + const igcVectorLayer = React.useRef(); + const highlightVectorLayer = React.useRef(); return ( diff --git a/src/REvent.tsx b/src/REvent.tsx index 194bbf61..95eb2d08 100644 --- a/src/REvent.tsx +++ b/src/REvent.tsx @@ -3,57 +3,123 @@ import React from 'react'; import {RContext, RContextType} from './context'; import debug from './debug'; +export const handlersSymbol = '_rlayers_handlers'; +export type OLEvent = 'change'; +export type Handler = (e: unknown) => boolean | void; +export type Handlers = Record; + export class RlayersBase extends React.PureComponent { static contextType = RContext; context: RContextType; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - ol: any; + ol: BaseObject; eventSources: BaseObject[]; - handlers: Record boolean | void>; - // 'change' is available on all objects - olEventName(ev: string): 'change' { - return ev.substring(2).toLowerCase() as 'change'; + protected static getOLObject(prop: string, ol: BaseObject) { + let handlers = ol.get(prop); + if (handlers === undefined) { + handlers = {}; + ol.set(prop, handlers); + } + return handlers as T; + } + + protected get handlers() { + return RlayersBase.getOLObject(handlersSymbol, this.ol); + } + + /** + * Get the lowercase names of the currently installed handlers + */ + protected getCurrentEvents() { + return Object.keys(this.props) + .filter((p) => p.startsWith('on')) + .map((ev) => ({event: ev.toLowerCase().slice(2), prop: ev})) + .reduce((a, x) => ({...a, [x.event]: this.props[x.prop]}), {}) as Handlers; + } + + /** + * Get the uppercase name of this event + */ + protected getHandlerProp(event: OLEvent): string | void { + for (const p of Object.keys(this.props)) if (p.toLowerCase() === 'on' + event) return p; } - attachEventHandlers(): void { + protected incrementHandlers(ev: OLEvent): void { + return; + } + protected decrementHandlers(ev: OLEvent): void { + return; + } + + protected attachEventHandlers(): void { + const handlers = this.handlers; + const handlersList = Object.keys(handlers ?? {}); const eventSources = this.eventSources ?? [this.ol]; - const newEvents = Object.keys(this.props).filter((p) => p.startsWith('on')); - const eventsToCheck = newEvents.concat( - Object.keys(this.handlers ?? {}).filter((ev) => !newEvents.includes(ev)) - ); + const newEvents = this.getCurrentEvents(); + const newEventsList = Object.keys(newEvents); + const eventsToCheck = newEventsList.concat( + handlersList.filter((ev) => !newEventsList.includes(ev)) + ) as OLEvent[]; for (const p of eventsToCheck) { - if (this.handlers === undefined) this.handlers = {}; - if (this.handlers[p] !== undefined && this.props[p] === undefined) { - debug('removing previously installed handler', this, p, this.handlers[p]); - for (const source of eventSources) source.un(this.olEventName(p), this.handlers[p]); - this.handlers[p] = undefined; + if (handlers[p] !== undefined && newEvents[p] === undefined) { + debug('removing previously installed handler', this, p, handlers[p]); + for (const source of eventSources) source.un(p, handlers[p]); + handlers[p] = undefined; + this.decrementHandlers(p); } - if (this.handlers[p] === undefined && this.props[p] !== undefined) { - debug('installing handler', this, p, this.props[p]); - this.handlers[p] = (e: unknown) => this.props[p].call(this, e); - for (const source of eventSources) source.on(this.olEventName(p), this.handlers[p]); + if (handlers[p] === undefined && newEvents[p] !== undefined) { + debug('installing handler', this, p, newEvents[p]); + const prop = this.getHandlerProp(p); + if (!prop) throw new Error('Internal error'); + handlers[p] = (e: unknown) => this.props[prop].call(this, e); + for (const source of eventSources) source.on(p as OLEvent, handlers[p]); + this.incrementHandlers(p); } } } // Used when replacing a source - attachOldEventHandlers(newSource: BaseObject): void { - // No events have been attached yet - if (!this.handlers) return; - const events = Object.keys(this.props).filter((p) => p.startsWith('on')); - for (const e of events) { - if (this.props[e]) { - debug('reinstalling existing handler', this, e, this.props[e]); - newSource.on(this.olEventName(e), this.handlers[e]); + protected attachOldEventHandlers(newSource: BaseObject): void { + const handlers = this.handlers; + const events = this.getCurrentEvents(); + for (const e of Object.keys(events)) { + if (events[e]) { + debug('reinstalling existing handler', this, e, events[e]); + newSource.on(e as OLEvent, handlers[e]); } } } - refresh(prevProps?: P): void { + protected refresh(prevProps?: P): void { this.attachEventHandlers(); } + /** + * Programmatically add an event handler to an RLayers component. + * + * @param {string} ev OpenLayers event + * @param {Handler} cb Callback + */ + on(ev: OLEvent, cb: Handler): void { + this.ol.on(ev, cb); + this.incrementHandlers(ev); + } + + /** + * Programmatically add an event handler to an RLayers component. + * + * Although public, use of this method is discouraged as it lacks + * any safety against calling un on a method that has not been + * registered. + * + * @param {string} ev OpenLayers event + * @param {Handler} cb Callback + */ + un(ev: OLEvent, cb: Handler): void { + this.decrementHandlers(ev); + this.ol.un(ev, cb); + } + componentDidMount(): void { debug('didMount', this); this.refresh(); @@ -84,13 +150,15 @@ export class RlayersBase extends React.PureComponent { } componentWillUnmount(): void { - debug('willUnmount', this, this.handlers); + const handlers = this.handlers; + debug('willUnmount', this, handlers); const eventSources = this.eventSources ?? [this.ol]; - for (const h of Object.keys(this.handlers ?? {})) { - debug('cleaning up handler', this, h, this.handlers[h]); - if (this.handlers[h]) { - for (const source of eventSources) source.un(this.olEventName(h), this.handlers[h]); - this.handlers[h] = undefined; + for (const h of Object.keys(handlers ?? {}) as OLEvent[]) { + debug('cleaning up handler', this, h, handlers[h]); + if (handlers[h]) { + for (const source of eventSources) source.un(h, handlers[h]); + handlers[h] = undefined; + this.decrementHandlers(h); } } } diff --git a/src/RFeature.tsx b/src/RFeature.tsx index 235c4855..39c56c7a 100644 --- a/src/RFeature.tsx +++ b/src/RFeature.tsx @@ -7,9 +7,11 @@ import SourceVector from 'ol/source/Vector'; import Geometry from 'ol/geom/Geometry'; import BaseEvent from 'ol/events/Event'; import {getCenter} from 'ol/extent'; +import {Layer} from 'ol/layer'; import {RContext, RContextType} from './context'; -import {RlayersBase} from './REvent'; +import {OLEvent, RlayersBase, handlersSymbol} from './REvent'; +import {FeatureHandlers, featureHandlersSymbol} from './layer/RLayerBaseVector'; import RStyle, {RStyleLike} from './style/RStyle'; import debug from './debug'; @@ -84,15 +86,20 @@ type FeatureRef = { * */ export default class RFeature extends RlayersBase> { - static pointerEvents: ('click' | 'pointerdrag' | 'pointermove' | 'singleclick' | 'dblclick')[] = - ['click', 'pointerdrag', 'pointermove', 'singleclick', 'dblclick']; - static lastFeaturesEntered: FeatureRef[] = []; - static lastFeaturesDragged: FeatureRef[] = []; + private static pointerEvents: ( + | 'click' + | 'pointerdrag' + | 'pointermove' + | 'singleclick' + | 'dblclick' + )[] = ['click', 'pointerdrag', 'pointermove', 'singleclick', 'dblclick']; + private static lastFeaturesEntered: FeatureRef[] = []; + private static lastFeaturesDragged: FeatureRef[] = []; static hitTolerance = 3; ol: Feature; onchange: () => boolean | void; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); if (!this?.context?.vectorlayer) throw new Error('An RFeature must be part of a vector layer'); @@ -113,15 +120,36 @@ export default class RFeature extends RlayersBase( + featureHandlersSymbol, + this.context.vectorlayer + ); + featureHandlers[ev] = (featureHandlers[ev] ?? 0) + 1; + } + protected decrementHandlers(ev: OLEvent): void { + const featureHandlers = RlayersBase.getOLObject( + featureHandlersSymbol, + this.context.vectorlayer + ); + featureHandlers[ev]--; + } + + protected static dispatchEvent(fr: FeatureRef, event: RFeatureUIEvent): boolean { if (!fr.feature) return true; - if (fr.feature.dispatchEvent) return fr.feature.dispatchEvent(event); + if (fr.feature.dispatchEvent) { + const stop = fr.feature.dispatchEvent(event); + if (stop) return stop; + } if (!event.target) event.target = fr.feature; - if (fr.layer?.get('_on' + event.type)) return fr.layer.get('_on' + event.type)(event); + const layerHandler = fr.layer?.get(handlersSymbol)[event.type]; + if (layerHandler) { + return layerHandler.call(null, event); + } return true; } - static eventRelay(e: RFeatureUIEvent): boolean { + private static eventRelay(e: RFeatureUIEvent): boolean { const triggered: FeatureRef[] = []; e.map.forEachFeatureAtPixel( e.pixel, @@ -130,7 +158,34 @@ export default class RFeature extends RlayersBase, CanvasVectorLayerRenderer> ) => triggered.push({feature: f, layer: l}) && false, { - hitTolerance: RFeature.hitTolerance + hitTolerance: RFeature.hitTolerance, + layerFilter: (layer) => { + const handlers = RlayersBase.getOLObject( + featureHandlersSymbol, + layer + ); + switch (e.type) { + case 'click': + return handlers['click'] > 0; + case 'dblclick': + return handlers['dblclick'] > 0; + case 'singleclick': + return handlers['singleclick'] > 0; + case 'pointermove': + return ( + (handlers['pointermove'] ?? 0) + + (handlers['pointerenter'] ?? 0) + + (handlers['pointerleave'] ?? 0) > + 0 + ); + case 'pointerdrag': + return ( + (handlers['pointerdrag'] ?? 0) + (handlers['pointerdragend'] ?? 0) > + 0 + ); + } + return Object.keys(handlers).reduce((a, x) => a + handlers[x], 0) > 0; + } } ); @@ -185,7 +240,7 @@ export default class RFeature extends RlayersBase> { ol: Geolocation; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); if (!this?.context?.map) throw new Error('A Geolocation must be part of a map'); const projection = props.projection ?? this.context.map.getView().getProjection(); diff --git a/src/RMap.tsx b/src/RMap.tsx index ce17488b..d5941047 100644 --- a/src/RMap.tsx +++ b/src/RMap.tsx @@ -123,7 +123,7 @@ export interface RMapProps extends PropsWithChildren { */ export default class RMap extends RlayersBase> { ol: Map; - target: React.RefObject; + private target: React.RefObject; constructor(props: Readonly) { super(props); @@ -154,7 +154,7 @@ export default class RMap extends RlayersBase> this.ol.setTarget(this.target.current); } - updateView = (e: MapEvent): void => { + private updateView = (e: MapEvent): void => { const view = this.ol.getView(); if (typeof this.props?.view[1] === 'function') this.props.view[1]({ @@ -164,7 +164,7 @@ export default class RMap extends RlayersBase> }); }; - refresh(prevProps?: RMapProps): void { + protected refresh(prevProps?: RMapProps): void { super.refresh(prevProps); const view = this.ol.getView(); for (const p of ['minZoom', 'maxZoom', 'constrainResolution']) { @@ -189,7 +189,9 @@ export default class RMap extends RlayersBase> style={{width: this.props.width, height: this.props.height}} ref={this.target} > - {this.props.children} + + {this.props.children} + ); } diff --git a/src/ROverlay.tsx b/src/ROverlay.tsx index f46fba90..098fe73e 100644 --- a/src/ROverlay.tsx +++ b/src/ROverlay.tsx @@ -4,6 +4,19 @@ import {Overlay} from 'ol'; import {RContextType} from './context'; import {RlayersBase} from './REvent'; +// TODO: Use the OpenLayers 7 type after OpenLayers 6 support +// is dropped +export type Positioning = + | 'bottom-left' + | 'bottom-center' + | 'bottom-right' + | 'center-left' + | 'center-center' + | 'center-right' + | 'top-left' + | 'top-center' + | 'top-right'; + /** * @propsfor ROverlay */ @@ -15,7 +28,12 @@ export interface ROverlayProps extends PropsWithChildren { /** Automatically pan the map when the element is rendered * @default false */ autoPan?: boolean; - // TODO: support the full options in rlayers 1.5.0 / ol 7.0 + /** Offset the overlay on the x and y axes relative to the containing feature + * @default [0,0] */ + offset?: number[]; + /** Anchor point + * @default 'top-left' */ + positioning?: Positioning; /** Automatically position the overlay so that it fits in the viewport * @default false */ autoPosition?: boolean; @@ -35,19 +53,21 @@ export interface ROverlayProps extends PropsWithChildren { */ export class ROverlayBase

extends RlayersBase> { ol: Overlay; - containerRef: React.RefObject; + protected containerRef: React.RefObject; - constructor(props: Readonly

, context: React.Context) { + constructor(props: Readonly

, context?: React.Context) { super(props, context); if (!this.context?.location) throw new Error('An overlay must be part of a location provider (ie RFeature)'); this.ol = new Overlay({ - autoPan: props.autoPan + autoPan: props.autoPan, + offset: props.offset, + positioning: props.positioning }); this.containerRef = React.createRef(); } - setPosition(): void { + protected setPosition(): void { this.ol.setPosition(this.context.location); if (this.props.autoPosition && this.containerRef?.current) { this.containerRef.current.style.position = 'absolute'; @@ -70,10 +90,16 @@ export class ROverlayBase

extends RlayersBase { showing: number | undefined; hiding: number | undefined; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.visible = false; } @@ -46,29 +47,46 @@ export default class RPopup extends ROverlayBase { this.unregister(); } - setPosition(): void { + protected setPosition(): void { this.ol.setPosition(this.visible ? this.context.location : undefined); } - unregister(): void { - this.context.feature.un('click' as 'change', this.toggle); - this.context.feature.un('pointerenter' as 'change', this.show); - this.context.feature.un('pointerleave' as 'change', this.hide); - } - - refresh(): void { - this.ol.setElement(this.containerRef.current); - this.unregister(); - switch (this.props.trigger) { + private unregister(prevProps?: RPopupProps): void { + if (!prevProps) return; + switch (prevProps.trigger) { default: case 'click': - this.context.feature.on('click' as 'change', this.toggle); + this.context.rFeature.un('click' as OLEvent, this.toggle); break; case 'hover': - this.context.feature.on('pointerenter' as 'change', this.show); - this.context.feature.on('pointerleave' as 'change', this.hide); + this.context.rFeature.un('pointerenter' as OLEvent, this.show); + this.context.rFeature.un('pointerhide' as OLEvent, this.hide); break; } + } + + protected refresh(prevProps?: RPopupProps): void { + this.ol.setElement(this.containerRef.current); + if (prevProps?.trigger !== this.props.trigger) { + this.unregister(prevProps); + switch (this.props.trigger) { + default: + case 'click': + if (prevProps?.trigger === 'hover') { + this.context.rFeature.un('pointerenter' as OLEvent, this.show); + this.context.rFeature.un('pointerhide' as OLEvent, this.hide); + } + this.context.rFeature.on('click' as OLEvent, this.toggle); + break; + case 'hover': + if (prevProps?.trigger === 'click') { + this.context.rFeature.un('click' as OLEvent, this.toggle); + } + this.context.rFeature.on('pointerenter' as OLEvent, this.show); + this.context.rFeature.on('pointerleave' as OLEvent, this.hide); + break; + } + } this.setPosition(); } diff --git a/src/context.ts b/src/context.ts index 04cd94bc..6439412c 100644 --- a/src/context.ts +++ b/src/context.ts @@ -15,6 +15,12 @@ import Style from 'ol/style/Style'; import Geometry from 'ol/geom/Geometry'; import LayerRenderer from 'ol/renderer/Layer'; +import RMap from './RMap'; +import RLayer, {RLayerProps} from './layer/RLayer'; +import RLayerBaseVector, {RLayerBaseVectorProps} from './layer/RLayerBaseVector'; +import RFeature from './RFeature'; +import RLayerVectorTile from './layer/RLayerVectorTile'; + export const RContext = React.createContext({} as RContextType); /** @@ -46,4 +52,42 @@ export interface RContextType { readonly style?: Style; /** The current style array */ readonly styleArray?: Style[]; + + /** The current RMap component */ + readonly rMap?: RMap; + /** The current RLayer component */ + readonly rLayer?: RLayer; + /** The current RLayerVector component */ + readonly rLayerVector?: RLayerBaseVector; + /** The current RLayerVectorTile component */ + readonly rLayerVectorTile?: RLayerVectorTile; + /** The current RFeature component */ + readonly rFeature?: RFeature; +} + +export function useOL() { + const context = React.useContext(RContext); + return { + map: context.map, + layer: context.layer, + source: context.source, + vectorlayer: context.vectorlayer, + vectorsource: context.vectorsource, + vectortilelayer: context.vectortilelayer, + feature: context.feature, + location: context.location, + style: context.style, + styleArray: context.styleArray + }; +} + +export function useRLayersComponent() { + const context = React.useContext(RContext); + return { + rMap: context.rMap, + rLayer: context.rLayer, + rLayerVector: context.rLayerVector, + rLayerVectorTile: context.rLayerVectorTile, + rFeature: context.rFeature + }; } diff --git a/src/control/RAttribution.tsx b/src/control/RAttribution.tsx index 08a7c360..9573be69 100644 --- a/src/control/RAttribution.tsx +++ b/src/control/RAttribution.tsx @@ -23,12 +23,12 @@ export interface RAttributionProps extends RControlProps { export default class RAttribution extends RControlBase> { ol: Attribution; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new Attribution(this.toOLProps(props)); } - refresh(prevProps?: RAttributionProps): void { + protected refresh(prevProps?: RAttributionProps): void { super.refresh(prevProps); if (prevProps?.collapsed !== this.props.collapsed) this.ol.setCollapsed(this.props.collapsed); diff --git a/src/control/RControlBase.tsx b/src/control/RControlBase.tsx index 43282b2b..b874261a 100644 --- a/src/control/RControlBase.tsx +++ b/src/control/RControlBase.tsx @@ -31,7 +31,7 @@ export interface RControlOptions extends OLOptions { export default class RControlBase

extends RlayersBase { ol: Control; - constructor(props: Readonly

, context: React.Context) { + constructor(props: Readonly

, context?: React.Context) { super(props, context); if (!this.context?.map) throw new Error('A control must be part of a map'); } @@ -43,7 +43,7 @@ export default class RControlBase

extends RlayersBas }; } - refresh(prevProps?: P): void { + protected refresh(prevProps?: P): void { super.refresh(prevProps); this.ol.setProperties(this.toOLProps(this.props)); } diff --git a/src/control/RCustom.tsx b/src/control/RCustom.tsx index 80a42db2..b0d4e6d5 100644 --- a/src/control/RCustom.tsx +++ b/src/control/RCustom.tsx @@ -16,7 +16,7 @@ export default class RCustom extends RControlBase; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.targetRef = React.createRef(); } diff --git a/src/control/RFullScreen.tsx b/src/control/RFullScreen.tsx index de222b87..ebd1fc1d 100644 --- a/src/control/RFullScreen.tsx +++ b/src/control/RFullScreen.tsx @@ -29,7 +29,7 @@ export interface RFullScreenProps extends RControlProps { export default class RFullScreen extends RControlBase> { ol: FullScreen; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new FullScreen(this.toOLProps(props)); } diff --git a/src/control/RLayers.tsx b/src/control/RLayers.tsx index 4b31b4eb..33172692 100644 --- a/src/control/RLayers.tsx +++ b/src/control/RLayers.tsx @@ -30,7 +30,7 @@ export default class RLayers extends RControlBase { ol: Control; targetRef: React.RefObject; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.targetRef = React.createRef(); this.state = {collapsed: true, visible: [true]}; diff --git a/src/control/RMousePosition.tsx b/src/control/RMousePosition.tsx index 72bfe25b..95e7d95b 100644 --- a/src/control/RMousePosition.tsx +++ b/src/control/RMousePosition.tsx @@ -32,7 +32,7 @@ export default class RMousePosition extends RControlBase< > { ol: OLMousePosition; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new OLMousePosition(this.toOLProps(props)); } diff --git a/src/control/ROverviewMap.tsx b/src/control/ROverviewMap.tsx index 6b09812d..37cfb7e6 100644 --- a/src/control/ROverviewMap.tsx +++ b/src/control/ROverviewMap.tsx @@ -33,7 +33,7 @@ export interface ROverviewProps extends RControlProps { export default class ROverviewMap extends RControlBase> { ol: OverviewMap; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new OverviewMap(this.toOLProps(props)); } @@ -48,7 +48,7 @@ export default class ROverviewMap extends RControlBase> { ol: OLRRotate; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new OLRRotate(this.toOLProps(props)); } diff --git a/src/control/RScaleLine.tsx b/src/control/RScaleLine.tsx index 54619cf0..744a74ef 100644 --- a/src/control/RScaleLine.tsx +++ b/src/control/RScaleLine.tsx @@ -28,7 +28,7 @@ export interface RScaleLineProps extends RControlProps { export default class RScaleLine extends RControlBase> { ol: ScaleLine; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new ScaleLine(this.toOLProps(props)); } diff --git a/src/control/RZoom.tsx b/src/control/RZoom.tsx index 69a7472d..970e0625 100644 --- a/src/control/RZoom.tsx +++ b/src/control/RZoom.tsx @@ -32,7 +32,7 @@ export interface RZoomProps extends RControlProps { export default class RZoom extends RControlBase> { ol: Zoom; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new Zoom(this.toOLProps(props)); } diff --git a/src/control/RZoomSlider.tsx b/src/control/RZoomSlider.tsx index b5b0b724..b078ec61 100644 --- a/src/control/RZoomSlider.tsx +++ b/src/control/RZoomSlider.tsx @@ -23,7 +23,7 @@ export interface RZoomSliderProps extends RControlProps { export default class RZoomSlider extends RControlBase> { ol: ZoomSlider; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new ZoomSlider(this.toOLProps(props)); } diff --git a/src/control/RZoomToExtent.tsx b/src/control/RZoomToExtent.tsx index 067bc5ae..b7fbf808 100644 --- a/src/control/RZoomToExtent.tsx +++ b/src/control/RZoomToExtent.tsx @@ -13,7 +13,7 @@ export interface RZoomToExtentProps extends RControlProps { export default class RZoomToExtent extends RControlBase> { ol: OLZoomToExtent; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new OLZoomToExtent(this.toOLProps(props)); } diff --git a/src/index.ts b/src/index.ts index 95395e4c..353ab9d8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ export {default as RenderEvent} from 'ol/render/Event'; export {VectorSourceEvent} from 'ol/source/Vector'; export {RlayersBase} from './REvent'; -export {RContext, RContextType} from './context'; +export {RContext, RContextType, useOL, useRLayersComponent} from './context'; export {default as RMap, RMapProps} from './RMap'; export {default as RLayer, RLayerProps} from './layer/RLayer'; diff --git a/src/interaction/RBaseInteraction.tsx b/src/interaction/RBaseInteraction.tsx index 17be2d16..6ae8146d 100644 --- a/src/interaction/RBaseInteraction.tsx +++ b/src/interaction/RBaseInteraction.tsx @@ -11,11 +11,11 @@ import debug from '../debug'; * It is meant to be be extended by more specific interactions */ export default class RBaseInteraction

extends RlayersBase> { - static classProps: string[] = []; + protected static classProps: string[] = []; classProps: string[]; ol: Interaction; - constructor(props: P, context: React.Context) { + constructor(props: P, context?: React.Context) { super(props, context); if (!this.context?.map?.addInteraction) throw new Error('An interaction must be part of a map'); @@ -26,7 +26,7 @@ export default class RBaseInteraction

extends RlayersBase { - static classProps = ['duration', 'delta']; + protected static classProps = ['duration', 'delta']; ol: DoubleClickZoom; createOL(props: RDoubleClickZoomProps): DoubleClickZoom { diff --git a/src/interaction/RDragBox.tsx b/src/interaction/RDragBox.tsx index 0052311f..3a1d07aa 100644 --- a/src/interaction/RDragBox.tsx +++ b/src/interaction/RDragBox.tsx @@ -32,7 +32,7 @@ export interface RDragBoxProps { /** A dragbox, can be used for selecting features, see `RDragZoom` for zooming */ export default class RDragBox extends RPointer { - static classProps = ['className', 'condition', 'minArea', 'boxEndCondition']; + protected static classProps = ['className', 'condition', 'minArea', 'boxEndCondition']; ol: DragBox; createOL(props: RDragBoxProps): DragBox { diff --git a/src/interaction/RDragPan.tsx b/src/interaction/RDragPan.tsx index 404cb4cf..50f7e743 100644 --- a/src/interaction/RDragPan.tsx +++ b/src/interaction/RDragPan.tsx @@ -24,7 +24,7 @@ export interface RDragPanProps { /** Panning by dragging */ export default class RDragPan extends RBaseInteraction { - static classProps = ['condition', 'kinetic']; + protected static classProps = ['condition', 'kinetic']; ol: DragPan; createOL(props: RDragPanProps): DragPan { diff --git a/src/interaction/RDragRotate.tsx b/src/interaction/RDragRotate.tsx index 37347d96..911bd3ed 100644 --- a/src/interaction/RDragRotate.tsx +++ b/src/interaction/RDragRotate.tsx @@ -21,7 +21,7 @@ export interface RDragRotateProps { /** Rotation by clicking and dragging */ export default class RDragRotate extends RBaseInteraction { - static classProps = ['condition', 'duration']; + protected static classProps = ['condition', 'duration']; ol: DragRotate; createOL(props: RDragRotateProps): DragRotate { diff --git a/src/interaction/RDragZoom.tsx b/src/interaction/RDragZoom.tsx index ee769232..cf92510f 100644 --- a/src/interaction/RDragZoom.tsx +++ b/src/interaction/RDragZoom.tsx @@ -30,7 +30,7 @@ export interface RDragZoomProps { /** Zoom by dragging a box, see `RDragBox` for selecting features */ export default class RDragZoom extends RBaseInteraction { - static classProps = ['className', 'condition', 'duration', 'out', 'minArea']; + protected static classProps = ['className', 'condition', 'duration', 'out', 'minArea']; ol: DragZoom; createOL(props: RDragZoomProps): DragZoom { diff --git a/src/interaction/RDraw.tsx b/src/interaction/RDraw.tsx index 5c27a293..addec365 100644 --- a/src/interaction/RDraw.tsx +++ b/src/interaction/RDraw.tsx @@ -57,7 +57,7 @@ export interface RDrawProps { /** Pointer interaction for drawing features */ export default class RDraw extends RPointer { - static classProps = [ + protected static classProps = [ 'condition', 'finishCondition', 'freehandCondition', diff --git a/src/interaction/RKeyboardPan.tsx b/src/interaction/RKeyboardPan.tsx index 2324908b..8a7acf9a 100644 --- a/src/interaction/RKeyboardPan.tsx +++ b/src/interaction/RKeyboardPan.tsx @@ -24,7 +24,7 @@ export interface RKeyboardPanProps { /** Pan with the arrow keys on the keyboard */ export default class RKeyboardPan extends RBaseInteraction { - static classProps = ['condition', 'duration', 'pixelDelta']; + protected static classProps = ['condition', 'duration', 'pixelDelta']; ol: KeyboardPan; createOL(props: RKeyboardPanProps): KeyboardPan { diff --git a/src/interaction/RKeyboardZoom.tsx b/src/interaction/RKeyboardZoom.tsx index 2af89503..d7af1f2f 100644 --- a/src/interaction/RKeyboardZoom.tsx +++ b/src/interaction/RKeyboardZoom.tsx @@ -24,7 +24,7 @@ export interface RKeyboardZoomProps { /** Zoom with +/- keys on the keyboard */ export default class RKeyboardZoom extends RBaseInteraction { - static classProps = ['condition', 'duration', 'delta']; + protected static classProps = ['condition', 'duration', 'delta']; ol: KeyboardZoom; createOL(props: RKeyboardZoomProps): KeyboardZoom { diff --git a/src/interaction/RModify.tsx b/src/interaction/RModify.tsx index 2a6f1724..138d9a07 100644 --- a/src/interaction/RModify.tsx +++ b/src/interaction/RModify.tsx @@ -39,7 +39,7 @@ export interface RModifyProps { /** Pointer interaction for modifying existing features */ export default class RModify extends RPointer { - static classProps = [ + protected static classProps = [ 'condition', 'deleteCondition', 'insertVertexCondition', diff --git a/src/interaction/RMouseWheelZoom.tsx b/src/interaction/RMouseWheelZoom.tsx index 292e1bf9..1e183a00 100644 --- a/src/interaction/RMouseWheelZoom.tsx +++ b/src/interaction/RMouseWheelZoom.tsx @@ -30,7 +30,13 @@ export interface RMouseWheelZoomProps { /** Mouse wheel zoom */ export default class RMouseWheelZoom extends RBaseInteraction { - static classProps = ['condition', 'maxDelta', 'duration', 'useAnchor', 'constrainResolution']; + protected static classProps = [ + 'condition', + 'maxDelta', + 'duration', + 'useAnchor', + 'constrainResolution' + ]; ol: MouseWheelZoom; createOL(props: RMouseWheelZoomProps): MouseWheelZoom { diff --git a/src/interaction/RPinchRotate.tsx b/src/interaction/RPinchRotate.tsx index 8667ce25..6fd6471a 100644 --- a/src/interaction/RPinchRotate.tsx +++ b/src/interaction/RPinchRotate.tsx @@ -21,7 +21,7 @@ export interface RPinchRotateProps { /** Rotation by pinching */ export default class RPinchRotate extends RBaseInteraction { - static classProps = ['threshold', 'duration']; + protected static classProps = ['threshold', 'duration']; ol: PinchRotate; createOL(props: RPinchRotateProps): PinchRotate { diff --git a/src/interaction/RPinchZoom.tsx b/src/interaction/RPinchZoom.tsx index a800fb8b..2a3f72f2 100644 --- a/src/interaction/RPinchZoom.tsx +++ b/src/interaction/RPinchZoom.tsx @@ -18,7 +18,7 @@ export interface RPinchZoomProps { /** Zoom by pinching */ export default class RPinchZoom extends RBaseInteraction { - static classProps = ['duration']; + protected static classProps = ['duration']; ol: PinchZoom; createOL(props: RPinchZoomProps): PinchZoom { diff --git a/src/interaction/RPointer.tsx b/src/interaction/RPointer.tsx index 33b44092..c9242e69 100644 --- a/src/interaction/RPointer.tsx +++ b/src/interaction/RPointer.tsx @@ -30,7 +30,12 @@ export interface RPointerProps { * It is meant to be be extended by more specific interactions */ export default class RPointer

extends RBaseInteraction

{ - static classProps = ['handleDownEvent', 'handleDragEvent', 'handleMoveEvent', 'handleUpEvent']; + protected static classProps = [ + 'handleDownEvent', + 'handleDragEvent', + 'handleMoveEvent', + 'handleUpEvent' + ]; classProps: string[]; ol: Pointer; } diff --git a/src/interaction/RTranslate.tsx b/src/interaction/RTranslate.tsx index 0d31b32c..5d89dd95 100644 --- a/src/interaction/RTranslate.tsx +++ b/src/interaction/RTranslate.tsx @@ -36,7 +36,7 @@ export interface RTranslateProps { * A feature translation interaction */ export default class RTranslate extends RPointer { - static classProps = ['features', 'layers', 'filter', 'hitTolerance']; + protected static classProps = ['features', 'layers', 'filter', 'hitTolerance']; ol: Translate; createOL(props: RTranslateProps): Translate { diff --git a/src/layer/RLayer.tsx b/src/layer/RLayer.tsx index 359e89d6..237c4e16 100644 --- a/src/layer/RLayer.tsx +++ b/src/layer/RLayer.tsx @@ -45,12 +45,12 @@ export default class RLayer

extends RlayersBase>; source: Source; - constructor(props: Readonly

, context: React.Context) { + constructor(props: Readonly

, context?: React.Context) { super(props, context); if (!this.context?.map?.addLayer) throw new Error('A layer must be part of a map'); } - refresh(prevProps?: P): void { + protected refresh(prevProps?: P): void { super.refresh(prevProps); for (const p of [ 'visible', @@ -87,7 +87,8 @@ export default class RLayer

extends RlayersBase diff --git a/src/layer/RLayerBaseVector.tsx b/src/layer/RLayerBaseVector.tsx index 792e5c1c..4f267abf 100644 --- a/src/layer/RLayerBaseVector.tsx +++ b/src/layer/RLayerBaseVector.tsx @@ -17,8 +17,13 @@ import {RContext, RContextType} from '../context'; import {default as RLayer, RLayerProps} from './RLayer'; import {default as RFeature, RFeatureUIEvent} from '../RFeature'; import {default as RStyle, RStyleLike} from '../style/RStyle'; +import {OLEvent, RlayersBase} from '../REvent'; import debug from '../debug'; + +export const featureHandlersSymbol = '_rlayers_feature_handlers'; +export type FeatureHandlers = Record; + /** * @propsfor RLayerBaseVector */ @@ -90,17 +95,17 @@ export interface RLayerBaseVectorProps extends RLayerProps { this: RLayerBaseVector, e: VectorSourceEvent ) => boolean | void; - /** Default onPointerMove handler for loaded features */ + /** onPointerMove handler for all loaded features */ onPointerMove?: ( this: RLayerBaseVector, e: RFeatureUIEvent ) => boolean | void; - /** Default onPointerEnter handler for loaded features */ + /** onPointerEnter handler for all loaded features */ onPointerEnter?: ( this: RLayerBaseVector, e: RFeatureUIEvent ) => boolean | void; - /** Default onPointerLeave handler for loaded features */ + /** onPointerLeave handler for all loaded features */ onPointerLeave?: ( this: RLayerBaseVector, e: RFeatureUIEvent @@ -126,61 +131,39 @@ export default class RLayerBaseVector

extends R | WebGLPointsLayerRenderer >; source: SourceVector; - static relayedEvents = { - click: 'Click', - pointermove: 'PointerMove', - pointerenter: 'PointerEnter', - pointerleave: 'PointerLeave' - }; - constructor(props: Readonly

, context: React.Context) { + constructor(props: Readonly

, context?: React.Context) { super(props, context); RFeature.initEventRelay(this.context.map); this.eventSources = this.createSource(props); - this.source.on('featuresloadend', this.newFeature); - this.source.on('addfeature', this.newFeature); - this.attachEventHandlers(); + super.refresh(); } - createSource(props: Readonly

): BaseObject[] { + protected createSource(props: Readonly

): BaseObject[] { throw new Error('RLayerBaseVector is an abstract class'); } - newFeature = (e: VectorSourceEvent): void => { - if (e.feature) this.attachFeatureHandlers([e.feature]); - if (e.features) this.attachFeatureHandlers(e.features); - }; - - attachFeatureHandlers(features: Feature[], prevProps?: P): void { - for (const ev of Object.values(RLayerBaseVector.relayedEvents)) - if (this.props['on' + ev] !== (prevProps && prevProps['on' + ev])) - for (const f of features) f.on(ev.toLowerCase() as 'change', this.eventRelay); - } - - eventRelay = (e: MapBrowserEvent): boolean => { - if (this.props['on' + RLayerBaseVector.relayedEvents[e.type]]) - return ( - this.props['on' + RLayerBaseVector.relayedEvents[e.type]].call(this, e) !== false - ); - return true; - }; - - componentWillUnmount(): void { - super.componentWillUnmount(); - for (const ev of Object.values(RLayerBaseVector.relayedEvents)) - this.source.forEachFeature((f) => { - f.un(ev.toLowerCase() as 'change', this.eventRelay); - return false; - }); - } - - refresh(prevProps?: P): void { + protected refresh(prevProps?: P): void { super.refresh(prevProps); - this.attachFeatureHandlers(this.source.getFeatures(), prevProps); if (prevProps?.style !== this.props.style) this.ol.setStyle(RStyle.getStyle(this.props.style)); } + incrementHandlers(ev: OLEvent): void { + const featureHandlers = RlayersBase.getOLObject( + featureHandlersSymbol, + this.ol + ); + featureHandlers[ev] = (featureHandlers[ev] ?? 0) + 1; + } + decrementHandlers(ev: OLEvent): void { + const featureHandlers = RlayersBase.getOLObject( + featureHandlersSymbol, + this.ol + ); + featureHandlers[ev]--; + } + render(): JSX.Element { return (

@@ -191,7 +174,9 @@ export default class RLayerBaseVector

extends R layer: this.ol, source: this.source, vectorlayer: this.ol, - vectorsource: this.source + vectorsource: this.source, + rLayer: this, + rLayerVector: this } as RContextType } > diff --git a/src/layer/RLayerCluster.tsx b/src/layer/RLayerCluster.tsx index 24ed8deb..86e61096 100644 --- a/src/layer/RLayerCluster.tsx +++ b/src/layer/RLayerCluster.tsx @@ -30,7 +30,7 @@ export default class RLayerCluster extends RLayerBaseVector source: SourceCluster; cluster: SourceVector; - createSource(props: Readonly): BaseObject[] { + protected createSource(props: Readonly): BaseObject[] { this.cluster = new SourceVector({ features: this.props.features, url: this.props.url, @@ -48,7 +48,7 @@ export default class RLayerCluster extends RLayerBaseVector return [this.ol, this.source, this.cluster]; } - refresh(prev?: RLayerClusterProps): void { + protected refresh(prev?: RLayerClusterProps): void { super.refresh(prev); if (prev?.distance !== this.props.distance) this.source.setDistance(this.props.distance); if (prev?.url !== this.props.url) { @@ -67,7 +67,9 @@ export default class RLayerCluster extends RLayerBaseVector layer: this.ol, source: this.cluster, vectorlayer: this.ol, - vectorsource: this.cluster + vectorsource: this.cluster, + rLayer: this, + rLayerVector: this } as RContextType } > diff --git a/src/layer/RLayerGraticule.tsx b/src/layer/RLayerGraticule.tsx index f77f755b..d5e4c5de 100644 --- a/src/layer/RLayerGraticule.tsx +++ b/src/layer/RLayerGraticule.tsx @@ -70,12 +70,12 @@ export interface RLayerGraticuleProps extends RLayerProps { export default class RLayerGraticule extends RLayer { ol: LayerGraticule; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.createSource(); } - createSource(): void { + protected createSource(): void { const stroke = RStyle.getStyleStatic(this.props.strokeStyle)?.getStroke?.(); const lonText = RStyle.getStyleStatic(this.props.lonLabelStyle)?.getText?.(); const latText = RStyle.getStyleStatic(this.props.latLabelStyle)?.getText?.(); @@ -89,7 +89,7 @@ export default class RLayerGraticule extends RLayer { return; } - refresh(prevProps?: RLayerGraticuleProps): void { + protected refresh(prevProps?: RLayerGraticuleProps): void { super.refresh(prevProps); const old = this.context.map.removeLayer(this.ol); this.createSource(); diff --git a/src/layer/RLayerHeatmap.tsx b/src/layer/RLayerHeatmap.tsx index d783a626..6497559d 100644 --- a/src/layer/RLayerHeatmap.tsx +++ b/src/layer/RLayerHeatmap.tsx @@ -37,7 +37,7 @@ export default class RLayerHeatmap extends RLayerBaseVector ol: LayerHeatmap; source: SourceVector; - createSource(props: Readonly): BaseObject[] { + protected createSource(props: Readonly): BaseObject[] { this.source = new SourceVector({ features: this.props.features, url: this.props.url, @@ -50,7 +50,7 @@ export default class RLayerHeatmap extends RLayerBaseVector return [this.ol, this.source]; } - refresh(prev?: RLayerHeatmapProps): void { + protected refresh(prev?: RLayerHeatmapProps): void { super.refresh(prev); if (prev?.blur !== this.props.blur) this.ol.setBlur(this.props.blur); if (prev?.radius !== this.props.radius) this.ol.setRadius(this.props.radius); diff --git a/src/layer/RLayerImage.tsx b/src/layer/RLayerImage.tsx index be85a415..3a2284e9 100644 --- a/src/layer/RLayerImage.tsx +++ b/src/layer/RLayerImage.tsx @@ -32,14 +32,14 @@ export default class RLayerImage extends RLayer { ol: LayerImage; source: SourceImage; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.createSource(); this.ol = new LayerImage({source: this.source}); this.eventSources = [this.ol, this.source]; } - createSource(): void { + protected createSource(): void { const options = { url: this.props.url, projection: this.props.projection, @@ -51,7 +51,7 @@ export default class RLayerImage extends RLayer { this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: RLayerImageProps): void { + protected refresh(prevProps?: RLayerImageProps): void { super.refresh(prevProps); if (this.props.url && prevProps?.url !== this.props.url) { this.createSource(); diff --git a/src/layer/RLayerRaster.tsx b/src/layer/RLayerRaster.tsx index 0bb482f5..16ce9d6f 100644 --- a/src/layer/RLayerRaster.tsx +++ b/src/layer/RLayerRaster.tsx @@ -19,7 +19,7 @@ export interface RLayerRasterProps extends RLayerProps { onTileLoadError?: (this: RLayerRaster, e: TileSourceEvent) => void; } -/** The common base of all tiled layers, not meant to be used directly */ +/** The common base of all tiled (even if not raster) layers, not meant to be used directly */ export default class RLayerRaster

extends RLayer

{ // eslint-disable-next-line @typescript-eslint/no-explicit-any ol: Layer>; diff --git a/src/layer/RLayerRasterMBTiles.tsx b/src/layer/RLayerRasterMBTiles.tsx index 9c336946..42688208 100644 --- a/src/layer/RLayerRasterMBTiles.tsx +++ b/src/layer/RLayerRasterMBTiles.tsx @@ -62,7 +62,7 @@ export default class RLayerRasterMBTiles extends RLayerRaster, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.addon = import('ol-mbtiles'); this.ol = new LayerTile(); @@ -71,7 +71,7 @@ export default class RLayerRasterMBTiles extends RLayerRaster mod.importMBTiles({ @@ -99,7 +99,7 @@ export default class RLayerRasterMBTiles extends RLayerRaster { ol: LayerTile; source: Stamen; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.source = new Stamen({layer: this.props.layer}); this.ol = new LayerTile({source: this.source}); diff --git a/src/layer/RLayerTile.tsx b/src/layer/RLayerTile.tsx index b0307065..ca763da3 100644 --- a/src/layer/RLayerTile.tsx +++ b/src/layer/RLayerTile.tsx @@ -43,14 +43,14 @@ export default class RLayerTile extends RLayerRaster { ol: LayerTile; source: XYZ; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.createSource(); this.ol = new LayerTile({source: this.source}); this.eventSources = [this.ol, this.source]; } - createSource(): void { + protected createSource(): void { this.source = new XYZ({ url: this.props.url, interpolate: !this.props.noIterpolation, @@ -61,7 +61,7 @@ export default class RLayerTile extends RLayerRaster { this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: RLayerTileProps): void { + protected refresh(prevProps?: RLayerTileProps): void { super.refresh(prevProps); if (prevProps?.tileGrid !== this.props.tileGrid || prevProps?.url !== this.props.url) { this.createSource(); diff --git a/src/layer/RLayerTileJSON.tsx b/src/layer/RLayerTileJSON.tsx index bd1b3554..a932ff86 100644 --- a/src/layer/RLayerTileJSON.tsx +++ b/src/layer/RLayerTileJSON.tsx @@ -25,7 +25,7 @@ export default class RLayerTileJSON extends RLayerRaster { ol: LayerTile; source: TileJSON; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.source = new TileJSON({ url: this.props.url @@ -34,7 +34,7 @@ export default class RLayerTileJSON extends RLayerRaster { this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: RLayerTileJSONProps): void { + protected refresh(prevProps?: RLayerTileJSONProps): void { super.refresh(prevProps); if (this.props.url && prevProps?.url !== this.props.url) { this.source.setUrl(this.props.url); diff --git a/src/layer/RLayerTileWMS.tsx b/src/layer/RLayerTileWMS.tsx index b06cd7c7..2f0da68e 100644 --- a/src/layer/RLayerTileWMS.tsx +++ b/src/layer/RLayerTileWMS.tsx @@ -21,14 +21,14 @@ export default class RLayerTileWMS extends RLayerRaster { ol: TileLayer; source: TileWMS; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.createSource(); this.ol = new TileLayer({source: this.source}); this.eventSources = [this.ol, this.source]; } - createSource(): void { + protected createSource(): void { const {params, url, projection} = this.props; const options = {params, url, projection}; @@ -36,7 +36,7 @@ export default class RLayerTileWMS extends RLayerRaster { this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: RLayerTileWMSProps): void { + protected refresh(prevProps?: RLayerTileWMSProps): void { super.refresh(prevProps); this.createSource(); this.ol.setSource(this.source); diff --git a/src/layer/RLayerTileWebGL.tsx b/src/layer/RLayerTileWebGL.tsx index 392e41d9..c68e452a 100644 --- a/src/layer/RLayerTileWebGL.tsx +++ b/src/layer/RLayerTileWebGL.tsx @@ -37,7 +37,7 @@ export default class RLayerTileWebGL extends RLayerWebGL { ol: LayerTileWebGL; source: XYZ; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.createSource(); this.ol = new LayerTileWebGL({ @@ -48,7 +48,7 @@ export default class RLayerTileWebGL extends RLayerWebGL { this.eventSources = [this.ol, this.source]; } - createSource(): void { + protected createSource(): void { this.source = new XYZ({ url: this.props.url, interpolate: !this.props.noIterpolation, @@ -59,7 +59,7 @@ export default class RLayerTileWebGL extends RLayerWebGL { this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: RLayerTileWebGLProps): void { + protected refresh(prevProps?: RLayerTileWebGLProps): void { super.refresh(prevProps); if (prevProps?.tileGrid !== this.props.tileGrid || prevProps?.url !== this.props.url) { this.createSource(); diff --git a/src/layer/RLayerVector.tsx b/src/layer/RLayerVector.tsx index da102fb1..7ca66de8 100644 --- a/src/layer/RLayerVector.tsx +++ b/src/layer/RLayerVector.tsx @@ -23,7 +23,7 @@ export default class RLayerVector extends RLayerBaseVector>; source: SourceVector; - createSource(props: Readonly): BaseObject[] { + protected createSource(props: Readonly): BaseObject[] { this.source = new SourceVector({ features: this.props.features, url: this.props.url, @@ -40,7 +40,7 @@ export default class RLayerVector extends RLayerBaseVector>; source: SourceVector; - createSource(props: Readonly): BaseObject[] { + protected createSource(props: Readonly): BaseObject[] { this.source = new SourceVector({ features: this.props.features, url: this.props.url, @@ -40,7 +40,7 @@ export default class RLayerVectorImage extends RLayerBaseVector, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.addon = import('ol-mbtiles'); this.ol = new LayerVectorTile({ @@ -107,7 +107,7 @@ export default class RLayerVectorMBTiles extends RLayerRaster mod.importMBTiles({ @@ -135,7 +135,7 @@ export default class RLayerVectorMBTiles extends RLayerRaster ev.startsWith('on')) @@ -175,7 +175,8 @@ export default class RLayerVectorMBTiles extends RLayerRaster diff --git a/src/layer/RLayerVectorTile.tsx b/src/layer/RLayerVectorTile.tsx index dfce77a7..c0be4757 100644 --- a/src/layer/RLayerVectorTile.tsx +++ b/src/layer/RLayerVectorTile.tsx @@ -6,6 +6,8 @@ import FeatureFormat from 'ol/format/Feature'; import {RContext, RContextType} from '../context'; import {default as RLayer, RLayerProps} from './RLayer'; import {default as RFeature, RFeatureUIEvent} from '../RFeature'; +import {OLEvent, RlayersBase} from '../REvent'; +import {FeatureHandlers, featureHandlersSymbol} from './RLayerBaseVector'; import RStyle, {RStyleLike} from '../style/RStyle'; import debug from '../debug'; @@ -57,7 +59,7 @@ export default class RLayerVectorTile extends RLayer { ol: LayerVectorTile; source: SourceVectorTile; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.source = new SourceVectorTile({ url: this.props.url, @@ -73,7 +75,22 @@ export default class RLayerVectorTile extends RLayer { RFeature.initEventRelay(this.context.map); } - refresh(prevProps?: RLayerVectorTileProps): void { + protected incrementHandlers(ev: OLEvent): void { + const featureHandlers = RlayersBase.getOLObject( + featureHandlersSymbol, + this.ol + ); + featureHandlers[ev] = (featureHandlers[ev] ?? 0) + 1; + } + protected decrementHandlers(ev: OLEvent): void { + const featureHandlers = RlayersBase.getOLObject( + featureHandlersSymbol, + this.ol + ); + featureHandlers[ev]--; + } + + protected refresh(prevProps?: RLayerVectorTileProps): void { super.refresh(prevProps); const handlers = Object.keys(this.props) .filter((ev) => ev.startsWith('on')) @@ -95,7 +112,9 @@ export default class RLayerVectorTile extends RLayer { { ...this.context, layer: this.ol, - vectortilelayer: this.ol + vectortilelayer: this.ol, + rLayer: this, + rLayerVectorTile: this } as RContextType } > diff --git a/src/layer/RLayerWMS.tsx b/src/layer/RLayerWMS.tsx index 66f71900..c33cfd00 100644 --- a/src/layer/RLayerWMS.tsx +++ b/src/layer/RLayerWMS.tsx @@ -20,14 +20,14 @@ export default class RLayerWMS extends RLayerRaster { ol: ImageLayer; source: ImageWMS; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.createSource(); this.ol = new ImageLayer({source: this.source}); this.eventSources = [this.ol, this.source]; } - createSource(): void { + protected createSource(): void { const {params, url} = this.props; const options = {params, url}; @@ -35,7 +35,7 @@ export default class RLayerWMS extends RLayerRaster { this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: RLayerWMSProps): void { + protected refresh(prevProps?: RLayerWMSProps): void { super.refresh(prevProps); this.createSource(); this.ol.setSource(this.source); diff --git a/src/layer/RLayerWMTS.tsx b/src/layer/RLayerWMTS.tsx index 2fac7b93..225f80c4 100644 --- a/src/layer/RLayerWMTS.tsx +++ b/src/layer/RLayerWMTS.tsx @@ -34,14 +34,14 @@ export default class RLayerWMTS extends RLayerRaster { parser: WMTSCapabilities; options: Options; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.ol = new LayerTile({source: this.source}); this.parser = new WMTSCapabilities(); this.createSource(); } - createSource(): Promise { + protected createSource(): Promise { debug('createSource', this); return fetch(this.props.url) .then((r) => r.text()) @@ -68,7 +68,7 @@ export default class RLayerWMTS extends RLayerRaster { }); } - refresh(prevProps?: RLayerWMTSProps): void { + protected refresh(prevProps?: RLayerWMTSProps): void { super.refresh(); if (prevProps?.url !== this.props.url || prevProps?.layer !== this.props.layer) { this.createSource().then(() => { diff --git a/src/layer/ROSM.tsx b/src/layer/ROSM.tsx index 01dc4166..9c5904d7 100644 --- a/src/layer/ROSM.tsx +++ b/src/layer/ROSM.tsx @@ -20,14 +20,14 @@ export interface ROSMProps extends RLayerRasterProps {} export default class ROSM extends LayerRaster { source: OSM; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.source = new OSM(); this.ol = new LayerTile({source: this.source}); this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: ROSMProps): void { + protected refresh(prevProps?: ROSMProps): void { super.refresh(prevProps); this.ol.setProperties({label: 'OpenStreetMap'}); } diff --git a/src/layer/ROSMWebGL.tsx b/src/layer/ROSMWebGL.tsx index dd8e6103..b42e3d5a 100644 --- a/src/layer/ROSMWebGL.tsx +++ b/src/layer/ROSMWebGL.tsx @@ -20,14 +20,14 @@ export interface ROSMWebGLProps extends RLayerWebGLProps {} export default class ROSMWebGL extends RLayerWebGL { source: OSM; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.source = new OSM(); this.ol = new LayerTileWebGL({source: this.source, cacheSize: props.cacheSize}); this.eventSources = [this.ol, this.source]; } - refresh(prevProps?: ROSMWebGLProps): void { + protected refresh(prevProps?: ROSMWebGLProps): void { super.refresh(prevProps); this.ol.setProperties({label: 'OpenStreetMap'}); } diff --git a/src/style/RBackground.tsx b/src/style/RBackground.tsx index eb1e8ebe..371b07d7 100644 --- a/src/style/RBackground.tsx +++ b/src/style/RBackground.tsx @@ -24,10 +24,10 @@ type Background = { * Provides an `RStyle` context - for `Fill` or `Stroke` */ export default class RBackground extends RBaseStyle { - static classProps = []; + protected static classProps = []; ol: Background; - create(props: RBackgroundProps): Background { + protected create(props: RBackgroundProps): Background { this.classProps = RBackground.classProps; const parent = this.context.style as unknown as Text; if (!parent.setBackgroundFill || !parent.setBackgroundStroke) diff --git a/src/style/RBaseStyle.tsx b/src/style/RBaseStyle.tsx index 551b10d2..061ec82b 100644 --- a/src/style/RBaseStyle.tsx +++ b/src/style/RBaseStyle.tsx @@ -17,23 +17,23 @@ export default class RBaseStyle

extends React.PureCom Record > { static contextType = RContext; - static classProps: string[] = []; + protected static classProps: string[] = []; classProps: string[]; ol: unknown; context: RContextType; - constructor(props: Readonly

, context: React.Context) { + constructor(props: Readonly

, context?: React.Context) { super(props, context); if (!this.context) throw new Error('A style property must be part of a style'); this.ol = this.create(props); } /* istanbul ignore next */ - create(props: P): unknown { + protected create(props: P): unknown { throw new Error('RBaseStyle is an abstract class'); } - refresh(prevProps?: P): void { + protected refresh(prevProps?: P): void { debug('refreshStyle', this); if (!prevProps) return; for (const p of this.classProps) { @@ -55,7 +55,7 @@ export default class RBaseStyle

extends React.PureCom } /* istanbul ignore next */ - set(ol: unknown): void { + protected set(ol: unknown): void { return; } diff --git a/src/style/RCircle.tsx b/src/style/RCircle.tsx index eaaf6afa..52056d64 100644 --- a/src/style/RCircle.tsx +++ b/src/style/RCircle.tsx @@ -20,10 +20,10 @@ export interface RCircleProps extends RRegularBaseProps { * Provides an `RStyle` context - for `Fill` or `Stroke` */ export default class RCircle extends RRegularBase { - static classProps = RRegularBase.classProps.concat(['radius']); + protected static classProps = RRegularBase.classProps.concat(['radius']); ol: Circle; - create(props: RCircleProps): Circle { + protected create(props: RCircleProps): Circle { this.classProps = RCircle.classProps; return new Circle({ ...props, diff --git a/src/style/RFill.tsx b/src/style/RFill.tsx index b9b2c312..ed6a0a8a 100644 --- a/src/style/RFill.tsx +++ b/src/style/RFill.tsx @@ -19,15 +19,15 @@ export interface RFillProps extends RBaseStyleProps { * Requires an `RStyle` context */ export default class RFill extends RBaseStyle { - static classProps = ['color']; + protected static classProps = ['color']; ol: Fill; - create(props: RFillProps): Fill { + protected create(props: RFillProps): Fill { this.classProps = RFill.classProps; return new Fill(props); } - set(ol: Fill): void { + protected set(ol: Fill): void { if (this.context.style.setFill) return this.context.style.setFill(ol); /* istanbul ignore next */ throw new Error('Parent element does not support a fill'); diff --git a/src/style/RIcon.tsx b/src/style/RIcon.tsx index cc380873..8a69bee7 100644 --- a/src/style/RIcon.tsx +++ b/src/style/RIcon.tsx @@ -68,7 +68,7 @@ export interface RIconProps extends RImageProps { * Requires an `RStyle` context */ export default class RIcon extends RImage { - static classProps = RImage.classProps.concat([ + protected static classProps = RImage.classProps.concat([ 'anchor', 'anchorXUnits', 'anchorYUnits', @@ -83,7 +83,7 @@ export default class RIcon extends RImage { ]); ol: Icon; - create(props: RIconProps): Icon { + protected create(props: RIconProps): Icon { this.classProps = RIcon.classProps; return new Icon(props); } diff --git a/src/style/RImage.tsx b/src/style/RImage.tsx index 1e86b241..85c8f71a 100644 --- a/src/style/RImage.tsx +++ b/src/style/RImage.tsx @@ -25,15 +25,21 @@ export interface RImageProps extends RBaseStyleProps { * An abstract class serving as base for all styles that render an image */ export default class RImage

extends RBaseStyle

{ - static classProps = ['opacity', 'rotateWithView', 'rotation', 'scale', 'displacement']; + protected static classProps = [ + 'opacity', + 'rotateWithView', + 'rotation', + 'scale', + 'displacement' + ]; ol: Image; /* istanbul ignore next */ - create(props: P): Image { + protected create(props: P): Image { throw new Error('RImage is an abstract class'); } - set(ol: Image): void { + protected set(ol: Image): void { if (!this.context.style.setImage) throw new Error('Parent element does not support an image'); this.context.style.setImage(ol); diff --git a/src/style/RRegularBase.tsx b/src/style/RRegularBase.tsx index 6ef9e266..7a80517e 100644 --- a/src/style/RRegularBase.tsx +++ b/src/style/RRegularBase.tsx @@ -15,17 +15,17 @@ export interface RRegularBaseProps extends RImageProps { /** Abstract class */ export default class RRegularBase

extends RImage

{ - static classProps = RImage.classProps.concat(['radius']); + protected static classProps = RImage.classProps.concat(['radius']); ol: Image; stroke: Stroke; fill: Fill; /* istanbul ignore next */ - create(props: P): Image { + protected create(props: P): Image { throw new Error('RImage is an abstract class'); } - setStroke(s: Stroke): void { + protected setStroke(s: Stroke): void { /* This a sneaky way around OpenLayers not supporting * setStroke/setFill on RegulaRBaseStyle-derived classes */ this.stroke = s; @@ -33,7 +33,7 @@ export default class RRegularBase

extends RImage

super.set(this.ol); } - setFill(f: Fill): void { + protected setFill(f: Fill): void { /* This a sneaky way around OpenLayers not supporting * setStroke/setFill on RegulaRBaseStyle-derived classes */ this.fill = f; diff --git a/src/style/RRegularShape.tsx b/src/style/RRegularShape.tsx index fdf22ff4..c60472bc 100644 --- a/src/style/RRegularShape.tsx +++ b/src/style/RRegularShape.tsx @@ -26,10 +26,15 @@ export interface RRegularShapeProps extends RRegularBaseProps { * Provides an `RStyle` context - for `Fill` or `Stroke` */ export default class RRegularShape extends RRegularBase { - static classProps = RRegularBase.classProps.concat(['radius1', 'radius2', 'points', 'angle']); + protected static classProps = RRegularBase.classProps.concat([ + 'radius1', + 'radius2', + 'points', + 'angle' + ]); ol: RegularShape; - create(props: RRegularShapeProps): RegularShape { + protected create(props: RRegularShapeProps): RegularShape { this.classProps = RRegularShape.classProps; return new RegularShape({ ...props, diff --git a/src/style/RStroke.tsx b/src/style/RStroke.tsx index e6f9e32e..27353a36 100644 --- a/src/style/RStroke.tsx +++ b/src/style/RStroke.tsx @@ -33,15 +33,15 @@ export interface RStrokeProps extends RBaseStyleProps { * Requires an `RStyle` context */ export default class RStroke extends RBaseStyle { - static classProps = ['color', 'width', 'lineCap', 'lineJoin', 'lineDash']; + protected static classProps = ['color', 'width', 'lineCap', 'lineJoin', 'lineDash']; ol: Stroke; - create(props: RStrokeProps): Stroke { + protected create(props: RStrokeProps): Stroke { this.classProps = RStroke.classProps; return new Stroke(props); } - set(ol: Stroke): void { + protected set(ol: Stroke): void { if (this.context.style.setStroke) return this.context.style.setStroke(ol); /* istanbul ignore next */ throw new Error('Parent element does not support a stroke'); diff --git a/src/style/RStyle.tsx b/src/style/RStyle.tsx index ad896050..530efbb7 100644 --- a/src/style/RStyle.tsx +++ b/src/style/RStyle.tsx @@ -48,12 +48,14 @@ export const createRStyle = (): RStyleRef => React.createRef(); * * It provides the special `RStyle` context */ -export default class RStyle extends RlayersBase> { +export default class RStyle extends React.PureComponent> { + static contextType = RContext; + context: RContextType; ol: StyleLike; childRefs: RStyleRef[]; cache: LRU; - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); if (props.render) this.ol = this.style; else this.ol = new Style({zIndex: props.zIndex}); @@ -81,12 +83,21 @@ export default class RStyle extends RlayersBase, + prev: Readonly, + snap: unknown + ): void { + if (this.props !== prevProps) { + debug('willRefresh', this, prevProps, this.props); + this.refresh(prevProps); + } + } + refresh(prevProps?: RStyleProps): void { - super.refresh(prevProps); if (!prevProps || prevProps?.render !== this.props.render) { if (this.context?.styleArray) { if (this.ol === this.style) diff --git a/src/style/RStyleArray.tsx b/src/style/RStyleArray.tsx index 1383a311..f394303c 100644 --- a/src/style/RStyleArray.tsx +++ b/src/style/RStyleArray.tsx @@ -22,7 +22,7 @@ import debug from '../debug'; * be supported rlayers either */ export default class RStyleArray extends RStyle { - constructor(props: Readonly, context: React.Context) { + constructor(props: Readonly, context?: React.Context) { super(props, context); this.childRefs = []; if (props.render) this.ol = this.style; diff --git a/src/style/RText.tsx b/src/style/RText.tsx index 673c006b..57bc6d6f 100644 --- a/src/style/RText.tsx +++ b/src/style/RText.tsx @@ -48,7 +48,7 @@ export interface RTextProps extends RBaseStyleProps { * Provides an `RStyle` context - for `Fill` or `Stroke` */ export default class RText extends RBaseStyle { - static classProps = [ + protected static classProps = [ 'text', 'font', 'offsetY', @@ -63,12 +63,12 @@ export default class RText extends RBaseStyle { ]; ol: Text; - create(props: RTextProps): Text { + protected create(props: RTextProps): Text { this.classProps = RText.classProps; return new Text(props); } - set(ol: Text): void { + protected set(ol: Text): void { if (!this.context.style.setText) /* istanbul ignore next */ throw new Error('Parent element does not support a text'); diff --git a/test/RFeature.test.tsx b/test/RFeature.test.tsx index adc36f0a..743b006c 100644 --- a/test/RFeature.test.tsx +++ b/test/RFeature.test.tsx @@ -3,8 +3,6 @@ import React from 'react'; import {fireEvent, render} from '@testing-library/react'; import {Polygon, Point} from 'ol/geom'; -import {Pixel} from 'ol/pixel'; -import {VectorTile} from 'ol/layer'; import {Feature} from 'ol'; import {RFeature, RLayerVector, RMap, RContext, ROverlay} from 'rlayers'; import * as common from './common'; @@ -174,11 +172,9 @@ describe('', () => { }); it('should relay map events to features', () => { - const map = React.createRef() as React.RefObject; - const ref = [ - React.createRef() as React.RefObject, - React.createRef() as React.RefObject - ]; + const map = React.createRef(); + const features = [React.createRef(), React.createRef()]; + const layer = React.createRef(); const mapEvents = ['Click', 'SingleClick', 'DblClick', 'PointerDrag', 'PointerMove']; const handlers = [ jest.fn(common.handlerCheckContext(RFeature, ['map'], [map])), @@ -194,15 +190,15 @@ describe('', () => { // First pass, test installing new handlers const {rerender} = render( - + ', () => { ); - if (map.current === null) throw new Error('map.current is null'); - const dummyLayer = new VectorTile(); - const dummyGeom = new Point([0, 0]); - map.current.ol.getSize = () => [common.mapProps.width, common.mapProps.height]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (map.current.ol as any).viewport_ = { - getBoundingClientRect: () => ({ - width: common.mapProps.width, - height: common.mapProps.height, - left: 0, - top: 0 - }) - }; - map.current.ol.forEachFeatureAtPixel = jest.fn((pixel: Pixel, cb) => { - if (ref[0].current === null || ref[1].current === null) - throw new Error('Referenced feature not found'); - if (pixel[0] === 10) return cb.call(this, ref[0].current.ol, dummyLayer, dummyGeom); - if (pixel[0] === 20) return cb.call(this, ref[1].current.ol, dummyLayer, dummyGeom); - throw new Error('unexpected'); - }); + common.installMapFeaturesInterceptors(map.current!.ol, [ + {pixel: [10, 10], layer: layer.current!.ol, feature: features[0].current!.ol}, + {pixel: [20, 20], layer: layer.current!.ol, feature: features[1].current!.ol} + ]); act(() => { for (const ev of mapEvents) { - map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, 10)); - map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, 20)); + map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, [10, 10])); + map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, [20, 20])); } }); expect(handlers[0]).toHaveBeenCalledTimes(mapEvents.length); @@ -247,13 +227,13 @@ describe('', () => { ', () => { act(() => { for (const ev of mapEvents) { - map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, 10)); - map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, 20)); + map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, [10, 10])); + map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, [20, 20])); } }); expect(handlers[0]).toHaveBeenCalledTimes(mapEvents.length); @@ -277,12 +257,12 @@ describe('', () => { @@ -292,8 +272,8 @@ describe('', () => { act(() => { for (const ev of mapEvents) { - map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, 10)); - map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, 20)); + map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, [10, 10])); + map.current?.ol.dispatchEvent(common.createEvent(ev, map.current.ol, [20, 20])); } }); expect(handlers[0]).toHaveBeenCalledTimes(mapEvents.length); @@ -302,8 +282,9 @@ describe('', () => { }); it('should generate pointerenter, pointerleave and pointerdragend', () => { - const map = React.createRef() as React.RefObject; - const ref = [0, 1, 2].map(() => React.createRef() as React.RefObject); + const map = React.createRef(); + const ref = [0, 1, 2].map(() => React.createRef()); + const layer = React.createRef(); const mapEvents = ['PointerEnter', 'PointerLeave', 'PointerDragEnd']; const handlerProps = mapEvents.reduce( (ac, a) => ({ @@ -314,7 +295,7 @@ describe('', () => { ); const {container} = render( - + ', () => { ); - if (map.current === null) throw new Error('map.current is null'); - const dummyLayer = new VectorTile(); - const dummyGeom = new Point([0, 0]); - map.current.ol.getSize = () => [common.mapProps.width, common.mapProps.height]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (map.current.ol as any).viewport_ = { - getBoundingClientRect: () => ({ - width: common.mapProps.width, - height: common.mapProps.height, - left: 0, - top: 0 - }) - }; - map.current.ol.forEachFeatureAtPixel = jest.fn((pixel: Pixel, cb) => { - if (ref[0].current === null || ref[1].current === null || ref[2].current === null) - throw new Error('Referenced feature not found'); - if (pixel[0] === 10) { - if (cb.call(this, ref[0].current.ol, dummyLayer, dummyGeom)) return; - return cb.call(this, ref[2].current.ol, dummyLayer, dummyGeom); - } - if (pixel[0] === 20) return cb.call(this, ref[1].current.ol, dummyLayer, dummyGeom); - return undefined; - }); + + common.installMapFeaturesInterceptors(map.current!.ol, [ + {pixel: [10, 10], layer: layer.current!.ol, feature: ref[0].current!.ol}, + {pixel: [20, 20], layer: layer.current!.ol, feature: ref[1].current!.ol}, + {pixel: [10, 10], layer: layer.current!.ol, feature: ref[2].current!.ol} + ]); act(() => { - if (map.current === null) throw new Error('map.current is null'); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 0)); + map.current!.ol.dispatchEvent( + common.createEvent('pointermove', map.current!.ol, [0, 0]) + ); }); expect(handlerProps['onPointerEnter']).toHaveBeenCalledTimes(0); act(() => { - if (map.current === null) throw new Error('map.current is null'); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 10)); + map.current!.ol.dispatchEvent( + common.createEvent('pointermove', map.current!.ol, [10, 10]) + ); }); expect(handlerProps['onPointerEnter']).toHaveBeenCalledTimes(2); expect(handlerProps['onPointerLeave']).toHaveBeenCalledTimes(0); act(() => { - if (map.current === null) throw new Error('map.current is null'); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 20)); + map.current!.ol.dispatchEvent( + common.createEvent('pointermove', map.current!.ol, [20, 20]) + ); }); expect(handlerProps['onPointerEnter']).toHaveBeenCalledTimes(2); expect(handlerProps['onPointerLeave']).toHaveBeenCalledTimes(2); act(() => { - if (map.current === null) throw new Error('map.current is null'); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 0)); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 10)); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 0)); + map.current!.ol.dispatchEvent( + common.createEvent('pointermove', map.current!.ol, [0, 0]) + ); + map.current!.ol.dispatchEvent( + common.createEvent('pointermove', map.current!.ol, [10, 10]) + ); + map.current!.ol.dispatchEvent( + common.createEvent('pointermove', map.current!.ol, [0, 0]) + ); }); expect(handlerProps['onPointerEnter']).toHaveBeenCalledTimes(4); expect(handlerProps['onPointerLeave']).toHaveBeenCalledTimes(4); act(() => { - if (map.current === null) throw new Error('map.current is null'); - map.current.ol.dispatchEvent( - common.createEvent('pointerdrag', map.current.ol, 10, true) + map.current!.ol.dispatchEvent( + common.createEvent('pointerdrag', map.current!.ol, [10, 10], true) + ); + map.current!.ol.dispatchEvent( + common.createEvent('pointermove', map.current!.ol, [0, 0]) ); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 0)); }); expect(handlerProps['onPointerEnter']).toHaveBeenCalledTimes(4); expect(handlerProps['onPointerLeave']).toHaveBeenCalledTimes(4); expect(handlerProps['onPointerDragEnd']).toHaveBeenCalledTimes(2); - expect(RFeature.lastFeaturesDragged.length).toBe(0); - expect(RFeature.lastFeaturesEntered.length).toBe(0); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((RFeature as any).lastFeaturesDragged.length).toBe(0); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((RFeature as any).lastFeaturesEntered.length).toBe(0); }); it('should throw an error without a Layer', () => { diff --git a/test/RMap.test.tsx b/test/RMap.test.tsx index 7cde77a9..5032636d 100644 --- a/test/RMap.test.tsx +++ b/test/RMap.test.tsx @@ -91,16 +91,25 @@ describe('', () => { 'RenderComplete', 'Change' ]; - const map = React.createRef() as React.RefObject; + const map = React.createRef(); const handler = jest.fn(common.handlerCheckContext(RMap, [], [])); const handlers = mapEvents.reduce((ac, a) => ({...ac, ['on' + a]: handler}), {}); + let testRan = false; expect( render( + { + expect(ol.map).toBe(map.current?.ol); + expect(rcomp.rMap).toBe(map.current); + testRan = true; + }} + /> ).container.innerHTML ).toMatchSnapshot(); + expect(testRan).toBeTruthy(); for (const evname of mapEvents) map.current?.ol.dispatchEvent(common.createEvent(evname, map.current.ol)); expect( diff --git a/test/ROverlay.test.tsx b/test/ROverlay.test.tsx index f67b5ae6..4acb028e 100644 --- a/test/ROverlay.test.tsx +++ b/test/ROverlay.test.tsx @@ -7,27 +7,46 @@ import * as common from './common'; describe('', () => { it('should support updating the props', async () => { - const comp = (trigger, text) => ( + const ref = React.createRef(); + const feature = React.createRef(); + let testRan = false; + const comp = (trigger, text, opts) => ( - +

{text}
+ { + expect(ol.feature).toBe(feature.current?.ol); + expect(rcomp.rFeature).toBe(feature.current); + testRan = true; + }} + /> ); - const {getByText, rerender, container, unmount} = render(comp('click', 'text1')); + const {getByText, rerender, container, unmount} = render(comp('click', 'text1', {})); expect(getByText('text1')).toBeInstanceOf(HTMLDivElement); expect(container.innerHTML).toMatchSnapshot(); - rerender(comp('trigger', 'text2')); + expect(ref.current?.ol.getPositioning()).toBe('top-left'); + expect(ref.current?.ol.getOffset()).toEqual([0, 0]); + expect(testRan).toBeTruthy(); + + testRan = false; + rerender(comp('trigger', 'text2', {positioning: 'bottom-right', offset: [-5, 5]})); expect(getByText('text2')).toBeInstanceOf(HTMLDivElement); + expect(ref.current?.ol.getPositioning()).toBe('bottom-right'); + expect(ref.current?.ol.getOffset()).toEqual([-5, 5]); expect(container.innerHTML).toMatchSnapshot(); + expect(testRan).toBeTruthy(); unmount(); }); @@ -58,7 +77,7 @@ describe('', () => { }); it('should support autoplacement', async () => { - const map = React.createRef() as React.RefObject; + const map = React.createRef(); const comp = (auto) => ( diff --git a/test/RPopup.test.tsx b/test/RPopup.test.tsx index c6299cd9..360c766a 100644 --- a/test/RPopup.test.tsx +++ b/test/RPopup.test.tsx @@ -7,13 +7,14 @@ import * as common from './common'; describe('', () => { it('should show a popup on click', async () => { - const map = React.createRef() as React.RefObject; - const feature = React.createRef() as React.RefObject; - const popup = React.createRef() as React.RefObject; + const map = React.createRef(); + const feature = React.createRef(); + const popup = React.createRef(); + const layer = React.createRef(); const comp = ( - + ', () => { ); const {container, rerender} = render(comp); - if (map.current === null) throw new Error('failed rendering map'); + + common.installMapFeaturesInterceptors(map.current!.ol, [ + {pixel: [10, 10], layer: layer.current!.ol, feature: feature.current!.ol} + ]); + expect(popup.current?.visible).toBeFalsy(); expect(container.innerHTML).toMatchSnapshot(); - feature.current?.ol.dispatchEvent(common.createEvent('click', map.current.ol)); + map.current!.ol.dispatchEvent(common.createEvent('click', map.current!.ol, [10, 10])); rerender(comp); expect(popup.current?.visible).toBeTruthy(); expect(container.innerHTML).toMatchSnapshot(); - feature.current?.ol.dispatchEvent(common.createEvent('click', map.current.ol)); + map.current!.ol.dispatchEvent(common.createEvent('click', map.current!.ol, [10, 10])); rerender(comp); expect(popup.current?.visible).toBeFalsy(); expect(container.innerHTML).toMatchSnapshot(); diff --git a/test/RVectorLayer.test.tsx b/test/RVectorLayer.test.tsx index 398303f9..1c6b68dc 100644 --- a/test/RVectorLayer.test.tsx +++ b/test/RVectorLayer.test.tsx @@ -80,24 +80,27 @@ describe('', () => { ref.current.source.loadFeatures( ref.current.source.getExtent(), 1000, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ref.current.source.getProjection()! ); unmount(); }); - it('should attach event handlers to features added after creation', async () => { - const map = React.createRef() as React.RefObject; - const ref = React.createRef() as React.RefObject; + it('should call event handlers on features added after creation', async () => { + const map = React.createRef(); + const ref = React.createRef(); const handler = jest.fn(common.handlerCheckContext(RLayerVector, ['map'], [map])); - const {container, unmount} = render( + const {unmount} = render( ); - if (map.current === null) throw new Error('failed rendering map'); + const f = new Feature(new Point([0, 0])); + common.installMapFeaturesInterceptors(map.current!.ol, [ + {pixel: [10, 10], layer: ref.current!.ol, feature: f} + ]); ref.current?.source.addFeature(f); - f.dispatchEvent(common.createEvent('click', map.current.ol)); + + map.current?.ol.dispatchEvent(common.createEvent('click', map.current.ol, [10, 10])); expect(handler).toHaveBeenCalledTimes(1); unmount(); }); @@ -118,7 +121,7 @@ describe('', () => { expect(addFeature).toHaveBeenCalledTimes(1); unmount(); }); - it('should load trigger addFeature/w multiple', async () => { + it('should load trigger addFeature/w multiple features', async () => { const addFeature = jest.fn(); const vector = React.createRef() as React.RefObject; const {container, unmount, rerender} = render( @@ -165,11 +168,18 @@ describe('', () => { ); - if (map.current === null) throw new Error('failed rendering map'); + common.installMapFeaturesInterceptors( + map.current!.ol, + layer + .current!.ol.getSource()! + .getFeatures() + .map((f, i) => ({pixel: [i, i], layer: layer.current!.ol, feature: f})) + ); expect(render1.container.innerHTML).toMatchSnapshot(); for (const evname of mapEvents) - for (const f of layer.current?.ol.getSource()?.getFeatures() || []) - f.dispatchEvent(common.createEvent(evname, map.current.ol)); + for (const f in layer.current?.ol.getSource()?.getFeatures() || []) { + map.current?.ol.dispatchEvent(common.createEvent(evname, map.current.ol, [+f, +f])); + } render1.unmount(); // unmount -> remount -> should render the same const comp = ( @@ -179,21 +189,26 @@ describe('', () => { ); const render2 = render(comp); expect(render2.container.innerHTML).toMatchSnapshot(); + common.installMapFeaturesInterceptors( + map.current!.ol, + layer + .current!.ol.getSource()! + .getFeatures() + .map((f, i) => ({pixel: [i, i], layer: layer.current!.ol, feature: f})) + ); for (const evname of mapEvents) - for (const f of layer.current?.ol.getSource()?.getFeatures() || []) { + for (const i in layer.current?.ol.getSource()?.getFeatures() || []) { + const f = (layer.current?.ol.getSource()?.getFeatures() || [])[i]; // do not lose handlers - f.dispatchEvent(common.createEvent(evname, map.current.ol)); - // do not leak handlers - expect((f.getListeners(evname.toLowerCase()) || []).length).toBe(1); + map.current?.ol.dispatchEvent(common.createEvent(evname, map.current.ol, [+i, +i])); } // rerender -> should render the same render2.rerender(comp); for (const evname of mapEvents) - for (const f of layer.current?.ol.getSource()?.getFeatures() || []) { + for (const i in layer.current?.ol.getSource()?.getFeatures() || []) { + const f = (layer.current?.ol.getSource()?.getFeatures() || [])[i]; // do not lose handlers - f.dispatchEvent(common.createEvent(evname, map.current.ol)); - // do not leak handlers - expect((f.getListeners(evname.toLowerCase()) || []).length).toBe(1); + map.current?.ol.dispatchEvent(common.createEvent(evname, map.current.ol, [+i, +i])); } expect(render2.container.innerHTML).toMatchSnapshot(); expect(handler).toHaveBeenCalledTimes(mapEvents.length * features.length * 3); diff --git a/test/RVectorTiles.test.tsx b/test/RVectorTiles.test.tsx index 552b9858..8488da22 100644 --- a/test/RVectorTiles.test.tsx +++ b/test/RVectorTiles.test.tsx @@ -3,10 +3,8 @@ import React from 'react'; import {fireEvent, render} from '@testing-library/react'; import {MVT} from 'ol/format'; -import {Pixel} from 'ol/pixel'; import {Style} from 'ol/style'; -import {Geometry, Point} from 'ol/geom'; -import RenderFeature from 'ol/render/Feature'; +import {Feature} from 'ol'; import {RLayerVectorTile, RMap} from 'rlayers'; import {RStyle, RCircle, RStroke} from 'rlayers/style'; import * as common from './common'; @@ -18,10 +16,6 @@ const props = { format: new MVT() }; -const dummyGeom = new Point([0, 0]); -const dummyFeat0 = {id: 0} as unknown as RenderFeature; -const dummyFeat1 = {id: 1} as unknown as RenderFeature; - describe('', () => { it('should create a vector tile layer', async () => { const ref = React.createRef() as React.RefObject; @@ -45,92 +39,72 @@ describe('', () => { // eslint-disable-next-line no-console console.error = err; }); - it('should attach event handlers to features ', async () => { - const mapEvents = ['Click', 'PointerMove']; + it('should relay OpenLayers events to features', async () => { + const mapEvents = ['Click', 'PointerMove'] as const; const handler = jest.fn(); const handlers = mapEvents.reduce((ac, a) => ({...ac, ['on' + a]: handler}), {}); - const map = React.createRef() as React.RefObject; - const layer = React.createRef() as React.RefObject; - const {container, unmount} = render( + const map = React.createRef(); + const layer = React.createRef(); + const {unmount} = render( ); - if (map.current === null || layer.current === null) throw new Error('failed rendering map'); - map.current.ol.getSize = () => [common.mapProps.width, common.mapProps.height]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (map.current.ol as any).viewport_ = { - getBoundingClientRect: () => ({ - width: common.mapProps.width, - height: common.mapProps.height, - left: 0, - top: 0 - }) - }; - map.current.ol.forEachFeatureAtPixel = jest.fn((pixel: Pixel, cb) => { - if (map.current === null || layer.current === null) - throw new Error('failed rendering map'); - if (pixel[0] === 10) return cb.call(this, dummyFeat0, layer.current.ol, dummyGeom); - return undefined; - }); - for (const ev of mapEvents) - map.current.ol.dispatchEvent(common.createEvent(ev, map.current.ol)); + common.installMapFeaturesInterceptors(map.current!.ol, [ + {pixel: [10, 10], layer: layer.current!.ol, feature: new Feature()} + ]); + + for (const ev of mapEvents) { + // This should trigger a callback + map.current!.ol.dispatchEvent(common.createEvent(ev, map.current!.ol, [10, 10])); + // This should not + map.current!.ol.dispatchEvent(common.createEvent(ev, map.current!.ol, [20, 20])); + } expect(handler).toHaveBeenCalledTimes(mapEvents.length); unmount(); }); - it('should generate events to features ', async () => { - const mapEvents = ['PointerEnter', 'PointerLeave']; - const handlers = {onPointerEnter: jest.fn(), onPointerLeave: jest.fn()}; - const map = React.createRef() as React.RefObject; - const layer = React.createRef() as React.RefObject; + it('should generate enter/leave events to features ', async () => { + const mapEvents = ['onPointerEnter', 'onPointerLeave'] as const; + const handlers = mapEvents.reduce((ac, a) => ({...ac, [a]: jest.fn()}), {}) as Record< + (typeof mapEvents)[number], + () => void + >; + const map = React.createRef(); + const layer = React.createRef(); const {container, unmount} = render( ); - if (map.current === null || layer.current === null) throw new Error('failed rendering map'); - map.current.ol.getSize = () => [common.mapProps.width, common.mapProps.height]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (map.current.ol as any).viewport_ = { - getBoundingClientRect: () => ({ - width: common.mapProps.width, - height: common.mapProps.height, - left: 0, - top: 0 - }) - }; - map.current.ol.forEachFeatureAtPixel = jest.fn((pixel: Pixel, cb) => { - if (map.current === null || layer.current === null) - throw new Error('failed rendering map'); - if (pixel[0] === 10) return cb.call(this, dummyFeat0, layer.current.ol, dummyGeom); - if (pixel[0] === 20) return cb.call(this, dummyFeat1, layer.current.ol, dummyGeom); - return undefined; - }); + common.installMapFeaturesInterceptors(map.current!.ol, [ + {pixel: [10, 10], layer: layer.current!.ol, feature: new Feature()}, + {pixel: [20, 20], layer: layer.current!.ol, feature: new Feature()} + ]); expect(handlers.onPointerEnter).toHaveBeenCalledTimes(0); expect(handlers.onPointerLeave).toHaveBeenCalledTimes(0); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 0)); + map.current!.ol.dispatchEvent(common.createEvent('pointermove', map.current!.ol, [0, 0])); expect(handlers.onPointerEnter).toHaveBeenCalledTimes(0); expect(handlers.onPointerLeave).toHaveBeenCalledTimes(0); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 10)); + map.current!.ol.dispatchEvent(common.createEvent('pointermove', map.current!.ol, [10, 10])); expect(handlers.onPointerEnter).toHaveBeenCalledTimes(1); expect(handlers.onPointerLeave).toHaveBeenCalledTimes(0); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 20)); + map.current!.ol.dispatchEvent(common.createEvent('pointermove', map.current!.ol, [20, 20])); expect(handlers.onPointerEnter).toHaveBeenCalledTimes(2); expect(handlers.onPointerLeave).toHaveBeenCalledTimes(1); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 0)); + map.current!.ol.dispatchEvent(common.createEvent('pointermove', map.current!.ol, [0, 0])); expect(handlers.onPointerEnter).toHaveBeenCalledTimes(2); expect(handlers.onPointerLeave).toHaveBeenCalledTimes(2); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 10)); + map.current!.ol.dispatchEvent(common.createEvent('pointermove', map.current!.ol, [10, 10])); expect(handlers.onPointerEnter).toHaveBeenCalledTimes(3); expect(handlers.onPointerLeave).toHaveBeenCalledTimes(2); - map.current.ol.dispatchEvent(common.createEvent('pointermove', map.current.ol, 0)); + map.current!.ol.dispatchEvent(common.createEvent('pointermove', map.current!.ol, [0, 0])); expect(handlers.onPointerEnter).toHaveBeenCalledTimes(3); expect(handlers.onPointerLeave).toHaveBeenCalledTimes(3); diff --git a/test/__snapshots__/RMap.test.tsx.snap b/test/__snapshots__/RMap.test.tsx.snap index 5373f997..d6c4975f 100644 --- a/test/__snapshots__/RMap.test.tsx.snap +++ b/test/__snapshots__/RMap.test.tsx.snap @@ -4,6 +4,6 @@ exports[` should display an OSM map 1`] = `"
    "`; -exports[` should handle Map events w/update 1`] = `"
      "`; +exports[` should handle Map events w/update 1`] = `"
        "`; exports[` should handle Map events w/update 2`] = `"
          "`; diff --git a/test/__snapshots__/ROverlay.test.tsx.snap b/test/__snapshots__/ROverlay.test.tsx.snap index f29ec25d..4c237b79 100644 --- a/test/__snapshots__/ROverlay.test.tsx.snap +++ b/test/__snapshots__/ROverlay.test.tsx.snap @@ -8,6 +8,6 @@ exports[` should support removing the layer 1`] = `"
            "`; -exports[` should support updating the props 1`] = `"
              "`; +exports[` should support updating the props 1`] = `"
                "`; -exports[` should support updating the props 2`] = `"
                  "`; +exports[` should support updating the props 2`] = `"
                    "`; diff --git a/test/__snapshots__/RVectorLayer.test.tsx.snap b/test/__snapshots__/RVectorLayer.test.tsx.snap index 7477197f..e03a4107 100644 --- a/test/__snapshots__/RVectorLayer.test.tsx.snap +++ b/test/__snapshots__/RVectorLayer.test.tsx.snap @@ -12,9 +12,9 @@ exports[` should load GeoJSON features 1`] = `"
                    marker "\\"\\""
                      "`; -exports[` should load trigger addFeature/w multiple 1`] = `"
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                      marker "\\"\\""
                        "`; +exports[` should load trigger addFeature/w multiple features 1`] = `"
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                        marker "\\"\\""
                          "`; -exports[` should load trigger addFeature/w multiple 2`] = `"
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                          marker "\\"\\""
                            "`; +exports[` should load trigger addFeature/w multiple features 2`] = `"
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                            marker "\\"\\""
                              "`; exports[` should support disabling of the AM wrapping 1`] = `"
                                "`; diff --git a/test/common.ts b/test/common.ts index 792585dd..381c0809 100644 --- a/test/common.ts +++ b/test/common.ts @@ -1,11 +1,16 @@ /* istanbul ignore file */ -import {Map} from 'ol'; +import {Feature, Map} from 'ol'; +import {Pixel} from 'ol/pixel'; +import {Layer} from 'ol/layer'; import {fromLonLat} from 'ol/proj'; import {Coordinate} from 'ol/coordinate'; import {Style, Stroke, Circle, Fill} from 'ol/style'; import {Listener, ListenerFunction, ListenerObject} from 'ol/events'; +import {Geometry, SimpleGeometry} from 'ol/geom'; +import {Source} from 'ol/source'; +import LayerRenderer from 'ol/renderer/Layer'; -import {MapBrowserEvent, RContextType, RlayersBase} from 'rlayers'; +import {MapBrowserEvent, RContextType, RlayersBase, useOL, useRLayersComponent} from 'rlayers'; import React from 'react'; export const mapProps = { @@ -17,10 +22,10 @@ export const mapProps = { export function createEvent( evname: string, map: Map, - coords?: number, + coords?: Pixel, dragging?: boolean ): MapBrowserEvent { - const event = {clientX: coords ?? 10, clientY: coords ?? 10} as unknown; + const event = {clientX: coords?.[0] ?? 10, clientY: coords?.[1] ?? 10} as unknown; return new MapBrowserEvent(evname.toLowerCase(), map, event as UIEvent, dragging); } @@ -92,3 +97,53 @@ export function handlerCheckContext( expect((this.context as RContextType)[cEl[i]]).toBe(cRef[i].current?.ol); }; } + +/** + * Install event interceptors on map that is not displayed + */ +export function installMapFeaturesInterceptors( + map: Map, + features: { + pixel: Pixel; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + layer: Layer>; + feature: Feature; + }[] +) { + map.getSize = () => [mapProps.width, mapProps.height]; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (map as any).viewport_ = { + getBoundingClientRect: () => ({ + width: mapProps.width, + height: mapProps.height, + left: 0, + top: 0 + }) + }; + map.forEachFeatureAtPixel = function (this: Map, pixel: Pixel, cb, options) { + for (const f of features) { + if (options?.layerFilter) { + if (!options.layerFilter(f.layer)) continue; + } + if (f.pixel[0] == pixel[0] && f.pixel[1] == pixel[1]) { + const stop = cb.call( + this, + f.feature, + f.layer, + f.feature.getGeometry() as SimpleGeometry + ); + if (stop) return stop; + } + } + }; +} + +export function CheckHooks(props: { + cb: (ol: ReturnType, rcomp: ReturnType) => void; +}): JSX.Element { + const ol = useOL(); + const rcomp = useRLayersComponent(); + + React.useEffect(() => props.cb(ol, rcomp)); + return React.createElement('div'); +}