Skip to content

Commit

Permalink
Vector feature typing through TS generics (#198)
Browse files Browse the repository at this point in the history
* support using TS generics to specify the feature geometry type

* compatibility with older versions
  • Loading branch information
mmomtchev authored Nov 20, 2023
1 parent 963412a commit 7870d91
Show file tree
Hide file tree
Showing 10 changed files with 102 additions and 38 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
#

- Support OpenLayers 8.2.0
- Support using TypeScript generics to specify the feature geometry type for vector layers

# [2.1.0] 2023-09-13

Expand Down
25 changes: 18 additions & 7 deletions examples/Features.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import React, {useCallback} from 'react';
import {Feature} from 'ol';
import {fromLonLat} from 'ol/proj';
import GeoJSON from 'ol/format/GeoJSON';
import {Point} from 'ol/geom';
import 'ol/ol.css';

import {RMap, ROSM, RLayerVector, RStyle} from 'rlayers';

/**
* Including data from a static file included at bundling time
* webpack will do everything necessary
* (this won't work in CodePen)
*/
import geojsonFeatures from './data/geo.json';

export default function Features(): JSX.Element {
Expand All @@ -13,13 +20,16 @@ export default function Features(): JSX.Element {
<div className='d-flex flex-row'>
<RMap className='example-map' initial={{center: fromLonLat([2.364, 48.82]), zoom: 11}}>
<ROSM />
{/* From a static file included at bundling time */}
{/* (this won't work in CodePen) */}
<RLayerVector
{/* When using TypeScript you can (optionally) specify the type of the features */}
<RLayerVector<Feature<Point>>
zIndex={10}
features={new GeoJSON({featureProjection: 'EPSG:3857'}).readFeatures(
geojsonFeatures
)}
/* Input data will have to be typed too */
features={
new GeoJSON({featureProjection: 'EPSG:3857'}).readFeatures(
geojsonFeatures
) as Feature<Point>[]
}
/* The type will be propagated to all callbacks */
onClick={useCallback(
(e) => {
setFlow([...flow, e.target.get('en')].slice(-16));
Expand All @@ -33,9 +43,10 @@ export default function Features(): JSX.Element {
</RStyle.RCircle>
</RStyle.RStyle>
</RLayerVector>
{/* From an URL */}
{/* Without any type, the features will be assumed to be a of a generic Geometry type */}
<RLayerVector
zIndex={5}
/* This layer will be getting its data from an URL */
format={new GeoJSON({featureProjection: 'EPSG:3857'})}
url='https://raw.githubusercontent.com/gregoiredavid/france-geojson/master/departements.geojson'
onPointerEnter={useCallback(
Expand Down
2 changes: 1 addition & 1 deletion src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export interface RContextType {
/** The current RLayer component */
readonly rLayer?: RLayer<RLayerProps>;
/** The current RLayerVector component */
readonly rLayerVector?: RLayerBaseVector<RLayerBaseVectorProps>;
readonly rLayerVector?: RLayerBaseVector<OLFeatureClass, RLayerBaseVectorProps>;
/** The current RLayerVectorTile component */
readonly rLayerVectorTile?: RLayerVectorTile;
/** The current RFeature component */
Expand Down
52 changes: 37 additions & 15 deletions src/layer/RLayerBaseVector.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import {Feature, MapBrowserEvent} from 'ol';
import {Feature} from 'ol';
import {LoadingStrategy, VectorSourceEvent} from 'ol/source/Vector';
import RenderEvent from 'ol/render/Event';
import BaseVector from 'ol/layer/BaseVector';
Expand All @@ -9,7 +9,6 @@ import CanvasVectorImageLayerRenderer from 'ol/renderer/canvas/VectorImageLayer'
import WebGLPointsLayerRenderer from 'ol/renderer/webgl/PointsLayer';
import {Vector as SourceVector} from 'ol/source';
import FeatureFormat from 'ol/format/Feature';
import {FeatureLike} from 'ol/Feature';
import {FeatureLoader, FeatureUrlFunction} from 'ol/featureloader';
import Geometry from 'ol/geom/Geometry';
import BaseObject from 'ol/Object';
Expand All @@ -21,14 +20,28 @@ import {default as RStyle, RStyleLike} from '../style/RStyle';
import {OLEvent, RlayersBase} from '../REvent';

import debug from '../debug';
import RenderFeature from 'ol/render/Feature';
import JSONFeature from 'ol/format/JSONFeature';

export const featureHandlersSymbol = '_rlayers_feature_handlers';
export type FeatureHandlers = Record<OLEvent, number>;

// This is very hackish, maybe it is time to drop older OpenLayers versions
type OLFeatureType<F extends OLFeatureClass> = RenderFeature extends ReturnType<
JSONFeature['readFeatures']
>[0]
? // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
F
: // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
Feature<F>;

/**
* @propsfor RLayerBaseVector
*/
export interface RLayerBaseVectorProps extends RLayerProps {
export interface RLayerBaseVectorProps<F extends OLFeatureClass = OLFeatureClass>
extends RLayerProps {
/** URL for loading features can be a function of type `FeatureUrlFunction`, requires `format` */
url?: string | FeatureUrlFunction;
/**
Expand All @@ -43,7 +56,7 @@ export interface RLayerBaseVectorProps extends RLayerProps {
*
* this property currently does not support dynamic updates
*/
features?: Feature<Geometry>[];
features?: OLFeatureType<F>[];
/** Format of the features when `url` is used
*
* this property currently does not support dynamic updates
Expand All @@ -61,21 +74,24 @@ export interface RLayerBaseVectorProps extends RLayerProps {
*/
wrapX?: boolean;
/** Default onClick handler for loaded features */
onClick?: (this: RLayerBaseVector<RLayerBaseVectorProps>, e: RFeatureUIEvent) => boolean | void;
onClick?: (
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: RFeatureUIEvent
) => boolean | void;
/** Called when a feature is added, not called for features
* already present at creation, ie loaded via `features` or `url`
*
* use onFeaturesLoadEnd for features loaded via `url`
*/
onAddFeature?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: VectorSourceEvent<OLFeatureClass>
) => boolean | void;
/**
* Called upon initiating the request for new features
*/
onFeaturesLoadStart?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: VectorSourceEvent<OLFeatureClass>
) => boolean | void;
/**
Expand All @@ -86,44 +102,50 @@ export interface RLayerBaseVectorProps extends RLayerProps {
* This callback is invoked before the features are loaded
*/
onFeaturesLoadEnd?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: VectorSourceEvent<OLFeatureClass>
) => boolean | void;
/**
* Called on failure while loading features
*/
onFeaturesLoadError?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: VectorSourceEvent<OLFeatureClass>
) => boolean | void;
/** onPointerMove handler for all loaded features */
onPointerMove?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: RFeatureUIEvent
) => boolean | void;
/** onPointerEnter handler for all loaded features */
onPointerEnter?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: RFeatureUIEvent
) => boolean | void;
/** onPointerLeave handler for all loaded features */
onPointerLeave?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: RFeatureUIEvent
) => boolean | void;
onPostRender?: (
this: RLayerBaseVector<RLayerBaseVectorProps>,
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: RenderEvent
) => boolean | void;
onPreRender?: (
this: RLayerBaseVector<F, RLayerBaseVectorProps<F>>,
e: RenderEvent
) => boolean | void;
onPreRender?: (this: RLayerBaseVector<RLayerBaseVectorProps>, e: RenderEvent) => boolean | void;
}

/**
* An abstract class used for grouping code common to all Vector layers
*
* Meant to be extended
*/
export default class RLayerBaseVector<P extends RLayerBaseVectorProps> extends RLayer<P> {
export default class RLayerBaseVector<
F extends OLFeatureClass,
P extends RLayerBaseVectorProps<F>
> extends RLayer<P> {
ol: BaseVector<
SourceVector<OLFeatureClass>,
| CanvasVectorLayerRenderer
Expand Down
12 changes: 8 additions & 4 deletions src/layer/RLayerCluster.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ import {default as RStyle} from '../style/RStyle';
/**
* @propsfor RLayerCluster
*/
export interface RLayerClusterProps extends RLayerBaseVectorProps {
export interface RLayerClusterProps<F extends OLFeatureClass = OLFeatureClass>
extends RLayerBaseVectorProps<F> {
/** Clustering distance */
distance?: number;
}

/** A vector layer that clusters its RFeatures
/**
* A vector layer that clusters its RFeatures
*
* Compatible with RLayerVector
*
* Requires an `RMap` context
*
* Not compatible with a vector layer context for JSX-declared RFeatures
*/
export default class RLayerCluster extends RLayerBaseVector<RLayerClusterProps> {
export default class RLayerCluster<
F extends OLFeatureClass = OLFeatureClass
> extends RLayerBaseVector<F, RLayerClusterProps<F>> {
ol: LayerVector<SourceCluster>;
source: SourceCluster;
cluster: SourceVector<OLFeatureClass>;
Expand All @@ -48,7 +52,7 @@ export default class RLayerCluster extends RLayerBaseVector<RLayerClusterProps>
return [this.ol, this.source, this.cluster];
}

protected refresh(prev?: RLayerClusterProps): void {
protected refresh(prev?: RLayerClusterProps<F>): void {
super.refresh(prev);
if (prev?.distance !== this.props.distance) this.source.setDistance(this.props.distance);
if (prev?.url !== this.props.url) {
Expand Down
4 changes: 2 additions & 2 deletions src/layer/RLayerHeatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type OLFeaturePoint = RenderFeature extends ReturnType<JSONFeature['readFeatures
/**
* @propsfor RLayerHeatmap
*/
export interface RLayerHeatmapProps extends RLayerBaseVectorProps {
export interface RLayerHeatmapProps extends RLayerBaseVectorProps<OLFeaturePoint> {
/** Blurring */
blur?: number;
/** Radius */
Expand All @@ -40,7 +40,7 @@ export interface RLayerHeatmapProps extends RLayerBaseVectorProps {
*
* Provides a vector layer for JSX-declared RFeatures
*/
export default class RLayerHeatmap extends RLayerBaseVector<RLayerHeatmapProps> {
export default class RLayerHeatmap extends RLayerBaseVector<OLFeaturePoint, RLayerHeatmapProps> {
ol: LayerHeatmap;
source: SourceVector<OLFeaturePoint>;

Expand Down
13 changes: 7 additions & 6 deletions src/layer/RLayerVector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React from 'react';
import {Feature, Map as Map} from 'ol';
import {Vector as LayerVector} from 'ol/layer';
import {Vector as SourceVector} from 'ol/source';
import Geometry from 'ol/geom/Geometry';

import {OLFeatureClass, RContextType} from '../context';
import {default as RLayerBaseVector, RLayerBaseVectorProps} from './RLayerBaseVector';
Expand All @@ -19,11 +18,13 @@ import debug from '../debug';
*
* Provides a vector layer context for JSX-declared `RFeature`s
*/
export default class RLayerVector extends RLayerBaseVector<RLayerBaseVectorProps> {
ol: LayerVector<SourceVector<OLFeatureClass>>;
source: SourceVector<OLFeatureClass>;
export default class RLayerVector<
F extends OLFeatureClass = OLFeatureClass
> extends RLayerBaseVector<F, RLayerBaseVectorProps<F>> {
ol: LayerVector<SourceVector<F>>;
source: SourceVector<F>;

protected createSource(props: Readonly<RLayerBaseVectorProps>): BaseObject[] {
protected createSource(props: Readonly<RLayerBaseVectorProps<F>>): BaseObject[] {
this.source = new SourceVector({
features: this.props.features,
url: this.props.url,
Expand All @@ -40,7 +41,7 @@ export default class RLayerVector extends RLayerBaseVector<RLayerBaseVectorProps
return [this.ol, this.source];
}

protected refresh(prevProps?: RLayerBaseVectorProps): void {
protected refresh(prevProps?: RLayerBaseVectorProps<F>): void {
super.refresh(prevProps);
if (prevProps?.url !== this.props.url) {
this.source.setUrl(this.props.url);
Expand Down
9 changes: 6 additions & 3 deletions src/layer/RLayerVectorImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {Feature, Map as Map} from 'ol';
import {VectorImage as LayerVectorImage} from 'ol/layer';
import {Vector as SourceVector} from 'ol/source';
import Geometry from 'ol/geom/Geometry';
import {FeatureLike} from 'ol/Feature';

import {OLFeatureClass, RContextType} from '../context';
import {default as RLayerBaseVector, RLayerBaseVectorProps} from './RLayerBaseVector';
Expand All @@ -19,11 +20,13 @@ import debug from '../debug';
*
* Provides a vector layer context for JSX-declared `RFeature`s
*/
export default class RLayerVectorImage extends RLayerBaseVector<RLayerBaseVectorProps> {
export default class RLayerVectorImage<
F extends OLFeatureClass = OLFeatureClass
> extends RLayerBaseVector<F, RLayerBaseVectorProps<F>> {
ol: LayerVectorImage<SourceVector<OLFeatureClass>>;
source: SourceVector<OLFeatureClass>;

protected createSource(props: Readonly<RLayerBaseVectorProps>): BaseObject[] {
protected createSource(props: Readonly<RLayerBaseVectorProps<F>>): BaseObject[] {
this.source = new SourceVector({
features: this.props.features,
url: this.props.url,
Expand All @@ -40,7 +43,7 @@ export default class RLayerVectorImage extends RLayerBaseVector<RLayerBaseVector
return [this.ol, this.source];
}

protected refresh(prevProps?: RLayerBaseVectorProps): void {
protected refresh(prevProps?: RLayerBaseVectorProps<F>): void {
super.refresh(prevProps);
if (prevProps?.url !== this.props.url) {
this.source.setUrl(this.props.url);
Expand Down
20 changes: 20 additions & 0 deletions test/RVectorLayer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@ import {fireEvent, render} from '@testing-library/react';
import {GeoJSON} from 'ol/format';
import {Feature} from 'ol';
import {Geometry, Point} from 'ol/geom';
import RenderFeature from 'ol/render/Feature';
import JSONFeature from 'ol/format/JSONFeature';
import SourceVector from 'ol/source/Vector';

import {RFeature, RLayerVector, RContext, RMap, RLayerVectorImage} from 'rlayers';
import * as common from './common';

Expand Down Expand Up @@ -34,6 +38,22 @@ describe('<RLayerVector>', () => {
expect(refMap.current?.ol);
unmount();
});
it('should support feature typing through generics', async () => {
type OLFeaturePoint = RenderFeature extends ReturnType<JSONFeature['readFeatures']>[0]
? Feature<Point>
: Point;

const ref = React.createRef<RLayerVector<OLFeaturePoint>>();

const {container, unmount} = render(
<RMap {...common.mapProps}>
<RLayerVector<OLFeaturePoint> ref={ref} />
</RMap>
);
expect(container.innerHTML).toMatchSnapshot();
expect(ref.current?.source).toBeInstanceOf(SourceVector<OLFeaturePoint>);
unmount();
});
it('should throw an error without a Map', () => {
// eslint-disable-next-line no-console
const err = console.error;
Expand Down
Loading

0 comments on commit 7870d91

Please sign in to comment.