Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ staticMode = false;

`geojsonData` is required, and should be of type "FeatureCollection" or "Feature". The default is an empty geojson object so that we can initialize a VectorLayer & VectorSource regardless. This is currently optimized for geojson containing a single polygon feature.

`geojsonColor` & `geojsonBuffer` are optional style properties. Color sets the stroke of the displayed data and buffer is used to fit the map view to the extent of the geojson features. `geojsonBuffer` corresponds to "value" param in OL documentation [here](https://openlayers.org/en/latest/apidoc/module-ol_extent.html).
`geojsonColor` & `geojsonBuffer` are optional style properties. Color sets the stroke of the displayed data and buffer is used to fit the map view to the extent of the geojson features. `geojsonBuffer` corresponds to "value" param in OL documentation [here](https://openlayers.org/en/latest/apidoc/module-ol_extent.html#.buffer).

`hideResetControl` hides the `↻` button, which when clicked would re-center your map if you've zoomed or panned away from the default view. `staticMode` additionally hides the `+/-` buttons, and disables mouse and keyboard zoom and pan/drag interactions.

Expand Down Expand Up @@ -135,7 +135,17 @@ featureBuffer = 40;

Set `showFeaturesAtPoint` to true. `osFeaturesApiKey`, `latitude`, and `longitude` are each required to query the OS Features API for features that contain this point.

`featureColor` & `featureBuffer` are optional style properties. Color sets the stroke of the displayed data and buffer is used to fit the map view to the extent of the features. `featureBuffer` corresponds to "value" param in OL documentation [here](https://openlayers.org/en/latest/apidoc/module-ol_extent.html).
`featureColor` & `featureBuffer` are optional style properties. Color sets the stroke of the displayed data and buffer is used to fit the map view to the extent of the features. `featureBuffer` corresponds to "value" param in OL documentation [here](https://openlayers.org/en/latest/apidoc/module-ol_extent.html#.buffer).

#### Example: click to expand or deselect the highlighted feature

Extends prior example by making the map interactive and listening for single click events. Currently only possible when `showFeaturesAtPoint` is also enabled.

```html
<my-map showFeaturesAtPoint clickFeatures ... />
```

Set `clickFeatures` to true, this will begin listening for single click events. New features will be highlighted or de-selected as you click. If the selected features share borders, the highlight border will appear as a merged single feature.

## Running Locally

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"prepublishOnly": "rm -rf dist types && npm run build"
},
"dependencies": {
"@turf/union": "^6.5.0",
"lit": "^2.0.0-rc.2",
"ol": "^6.6.1",
"ol-mapbox-style": "^6.4.1",
Expand Down
30 changes: 30 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 34 additions & 30 deletions src/my-map.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators.js";
import { Control, defaults as defaultControls } from "ol/control";
import { buffer } from "ol/extent";
import { GeoJSON } from "ol/format";
import { defaults as defaultInteractions } from "ol/interaction";
import { Vector as VectorLayer } from "ol/layer";
Expand All @@ -15,11 +14,11 @@ import { last } from "rambda";
import { draw, drawingLayer, drawingSource, modify, snap } from "./draw";
import {
makeFeatureLayer,
featureSource,
outlineSource,
getFeaturesAtPoint,
} from "./os-features";
import { makeOsVectorTileBaseMap, makeRasterBaseMap } from "./os-layers";
import { AreaUnitEnum, formatArea } from "./utils";
import { AreaUnitEnum, fitToData, formatArea } from "./utils";

@customElement("my-map")
export class MyMap extends LitElement {
Expand Down Expand Up @@ -72,6 +71,9 @@ export class MyMap extends LitElement {
@property({ type: Boolean })
showFeaturesAtPoint = false;

@property({ type: Boolean })
clickFeatures = false;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Been meaning to review this PR properly but in the meantime just wanted to mention this. These boolean arguments make me worry about hitting a boolean trap. It doesn't seem to be the case quite yet, as there seems to be no intersection between the existing arguments, but I can see how we might see the existing code, and follow existing pattern, and mindlessly add more boolean arguments that do create traps. Just thinking out loud.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for looking out! I agree this is a tricky balance - for now, I am trying to minimally/thoughtfully introduce new boolean properties, and sticking to a rule of thumb that they all be false by default until a user enables them.

Something I see often in other map libraries is the concept of "modes", where mode could be a string property and we'd enable/specify one mode at a time - like "draw", "showFeature", "clickFeatures", "showGeojson" etc. But the tricky nature of this project is our requirement to mix & match between these various modes & allow the functionalities to co-exist -- hence how I settled on the booleans in the first place.

But definitley a good point to chew on as we think about how to best stabilize our API going forward!


@property({ type: String })
featureColor = "#0000ff";

Expand Down Expand Up @@ -155,11 +157,9 @@ export class MyMap extends LitElement {

const handleReset = () => {
if (this.showFeaturesAtPoint) {
const extent = featureSource.getExtent();
map.getView().fit(buffer(extent, this.featureBuffer));
} else if (outlineSource.getFeatures().length > 0) {
const extent = outlineSource.getExtent();
map.getView().fit(buffer(extent, this.geojsonBuffer));
fitToData(map, outlineSource, this.featureBuffer);
} else if (geojsonSource.getFeatures().length > 0) {
fitToData(map, geojsonSource, this.geojsonBuffer);
} else {
map.getView().setCenter(fromLonLat([this.longitude, this.latitude]));
map.getView().setZoom(this.zoom);
Expand Down Expand Up @@ -194,22 +194,22 @@ export class MyMap extends LitElement {
});

// add a vector layer to display static geojson if features are provided
const outlineSource = new VectorSource();
const geojsonSource = new VectorSource();

if (this.geojsonData.type === "FeatureCollection") {
let features = new GeoJSON().readFeatures(this.geojsonData, {
featureProjection: "EPSG:3857",
});
outlineSource.addFeatures(features);
geojsonSource.addFeatures(features);
} else if (this.geojsonData.type === "Feature") {
let feature = new GeoJSON().readFeature(this.geojsonData, {
featureProjection: "EPSG:3857",
});
outlineSource.addFeature(feature);
geojsonSource.addFeature(feature);
}

const outlineLayer = new VectorLayer({
source: outlineSource,
const geojsonLayer = new VectorLayer({
source: geojsonSource,
style: new Style({
stroke: new Stroke({
color: this.geojsonColor,
Expand All @@ -218,15 +218,14 @@ export class MyMap extends LitElement {
}),
});

map.addLayer(outlineLayer);
map.addLayer(geojsonLayer);

if (outlineSource.getFeatures().length > 0) {
if (geojsonSource.getFeatures().length > 0) {
// fit map to extent of geojson features, overriding default zoom & center
const extent = outlineSource.getExtent();
map.getView().fit(buffer(extent, this.geojsonBuffer));
fitToData(map, geojsonSource, this.geojsonBuffer);

// log total area of first feature (assumes geojson is a single polygon for now)
const data = outlineSource.getFeatures()[0].getGeometry();
const data = geojsonSource.getFeatures()[0].getGeometry();
console.log("geojsonData total area:", formatArea(data, this.areaUnit));
}

Expand Down Expand Up @@ -262,28 +261,33 @@ export class MyMap extends LitElement {
});
}

if (this.showFeaturesAtPoint) {
if (this.showFeaturesAtPoint && Boolean(this.osFeaturesApiKey)) {
getFeaturesAtPoint(
fromLonLat([this.longitude, this.latitude]),
this.osFeaturesApiKey
);

const featureLayer = makeFeatureLayer(this.featureColor);
map.addLayer(featureLayer);
if (this.clickFeatures) {
map.on("singleclick", (e) => {
getFeaturesAtPoint(e.coordinate, this.osFeaturesApiKey);
});
}

const outlineLayer = makeFeatureLayer(this.featureColor);
map.addLayer(outlineLayer);

// ensure getFeatures has fetched successfully
featureSource.on("change", () => {
// ensure getFeaturesAtPoint has fetched successfully
outlineSource.on("change", () => {
if (
featureSource.getState() === "ready" &&
featureSource.getFeatures().length > 0
outlineSource.getState() === "ready" &&
outlineSource.getFeatures().length > 0
) {
// fit map to extent of features
const extent = featureSource.getExtent();
map.getView().fit(buffer(extent, this.featureBuffer));
fitToData(map, outlineSource, this.featureBuffer);

// log total area of feature
const data = featureSource.getFeatures()[0].getGeometry();
console.log("feature total area:", formatArea(data, this.areaUnit));
// log total area of feature or merged features
const data = outlineSource.getFeatures()[0].getGeometry();
console.log("feature(s) total area:", formatArea(data, this.areaUnit));
}
});
}
Expand Down
33 changes: 27 additions & 6 deletions src/os-features.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import union from "@turf/union";
import { GeoJSON } from "ol/format";
import { Vector as VectorLayer } from "ol/layer";
import { toLonLat } from "ol/proj";
Expand All @@ -6,11 +7,13 @@ import { Stroke, Style } from "ol/style";

const featureServiceUrl = "https://api.os.uk/features/v1/wfs";

export const featureSource = new VectorSource();
const featureSource = new VectorSource();

export const outlineSource = new VectorSource();

export function makeFeatureLayer(color: string) {
return new VectorLayer({
source: featureSource,
source: outlineSource,
style: new Style({
stroke: new Stroke({
width: 3,
Expand Down Expand Up @@ -59,8 +62,6 @@ export function getFeaturesAtPoint(coord: Array<number>, apiKey: any) {
fetch(getUrl(wfsParams))
.then((response) => response.json())
.then((data) => {
console.log("features at this point:", data);

if (!data.features.length) return;

const properties = data.features[0].properties,
Expand All @@ -76,8 +77,28 @@ export function getFeaturesAtPoint(coord: Array<number>, apiKey: any) {
featureProjection: "EPSG:3857",
});

featureSource.clear();
featureSource.addFeatures(features);
features.forEach((feature) => {
const id = feature.getProperties().TOID;
const existingFeature = featureSource.getFeatureById(id);

if (existingFeature) {
featureSource.removeFeature(existingFeature);
} else {
feature.setId(id);
featureSource.addFeature(feature);
}
});

outlineSource.clear();
outlineSource.addFeature(
// Merge all of the features into a single feature
geojson.readFeature(
featureSource.getFeatures().reduce((acc: any, curr) => {
const toMerge = geojson.writeFeatureObject(curr).geometry;
return acc ? union(acc, toMerge) : toMerge;
}, null)
)
);
})
.catch((error) => console.log(error));
}
Expand Down
19 changes: 19 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { buffer } from "ol/extent";
import Geometry from "ol/geom/Geometry";
import Map from "ol/Map";
import { Vector } from "ol/source";
import { getArea } from "ol/sphere";

export type AreaUnitEnum = "m2" | "ha";
Expand All @@ -24,3 +27,19 @@ export function formatArea(polygon: Geometry, unit: AreaUnitEnum) {

return output;
}

/**
* Fit map view to extent of data features, overriding default zoom & center
* @param olMap - an OpenLayers map
* @param olSource - an OpenLayers vector source
* @param bufferValue - amount to buffer extent by, refer to https://openlayers.org/en/latest/apidoc/module-ol_extent.html#.buffer
* @returns - a map view
*/
export function fitToData(
olMap: Map,
olSource: Vector<Geometry>,
bufferValue: number
) {
const extent = olSource.getExtent();
return olMap.getView().fit(buffer(extent, bufferValue));
}