Skip to content

Commit d35959b

Browse files
committed
Make CropLegendControl work
1 parent 3936d85 commit d35959b

3 files changed

Lines changed: 127 additions & 59 deletions

File tree

map/crop/CropLegendControl.js

Lines changed: 90 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,29 @@
11
import Control from 'ol/control/Control';
2+
import VectorTile from 'ol/layer/VectorTile';
3+
import {toGeometry} from "ol/render/Feature";
4+
import TileState from "ol/TileState";
5+
import {intersects} from "ol/extent";
6+
import debounce from "lodash.debounce";
7+
8+
function forFeaturesInExtent(vt, extent, callback) {
9+
const renderer = vt.getRenderer();
10+
const tileCache = renderer.getTileCache();
11+
if (tileCache.getCount() === 0) return;
12+
13+
const tileGrid = renderer.getLayer().getSource().tileGrid;
14+
const z = tileGrid.getZForResolution(renderer.renderedResolution);
15+
tileCache.forEach((tile) => {
16+
if (tile.tileCoord[0] !== z || tile.getState() !== TileState.LOADED) return;
17+
for (const sourceTile of tile.getSourceTiles()) {
18+
const tileCoord = sourceTile.tileCoord;
19+
if (intersects(extent, tileGrid.getTileCoordExtent(tileCoord))) {
20+
for (const candidate of sourceTile.getFeatures()) {
21+
if (intersects(extent, candidate.getExtent())) callback(candidate);
22+
}
23+
}
24+
}
25+
});
26+
}
227

328
/**
429
* A custom Legend Control for OpenLayers
@@ -14,13 +39,77 @@ export class CropLegendControl extends Control {
1439
target: options.target,
1540
});
1641

42+
this.attribute = options.attribute;
43+
this.mapping = options.mapping || {};
1744
this.legendItems = [];
1845
this.render();
46+
this.tileLoadEnd = this.tileLoadEnd.bind(this);
47+
this.changeSource = this.changeSource.bind(this);
48+
this.updateFeatureCount = debounce(this.updateFeatureCount, 1000).bind(this);
49+
}
50+
updateFeatureCount(e) {
51+
const map = this.getMap();
52+
const vectortiles = map.getLayers().getArray().filter(f => f instanceof VectorTile);
53+
const extent = map.getView().calculateExtentInternal();
54+
let totalArea = 0, count = 0, cropArea = {}, cropCount = {};
55+
for (const vt of vectortiles) {
56+
forFeaturesInExtent(vt, extent, (feature) => {
57+
const area = feature.properties_["area"] || toGeometry(feature).getArea();
58+
const crop = feature.properties_[this.attribute];
59+
count++;
60+
totalArea += area;
61+
cropCount[crop] = (cropCount[crop] || 0) + 1;
62+
cropArea[crop] = (cropArea[crop] || 0) + area;
63+
});
64+
}
65+
const topCrops = Object.entries(cropArea)
66+
.map(([crop, area]) => ({ crop, area }))
67+
.sort((a, b) => b.area - a.area) // Descending order
68+
.slice(0, 5);
69+
70+
this.legendItems = topCrops.map(({crop, area}) =>
71+
{
72+
const c = this.mapping[crop];
73+
const percent = (area / totalArea) * 100;
74+
return {
75+
label: c.name.replaceAll("_", " "),
76+
color: c.color || "#99bbccaa",
77+
percent: percent.toFixed(2) + "%",
78+
area: area.toFixed(2),
79+
}
80+
}
81+
)
82+
this.render();
83+
}
84+
85+
tileLoadEnd(e) {
86+
const features = e.tile.getFeatures();
87+
for (const feature of features) {
88+
const p = feature.getProperties();
89+
p.color = this.mapping[p[this.attribute]]?.color || "#99bbccaa";
90+
}
91+
this.updateFeatureCount(e)
92+
}
93+
94+
changeSource(e) {
95+
if (!(e.element instanceof VectorTile)) return;
96+
if (e.type === "add") {
97+
e.element.getSource().on('tileloadend', this.tileLoadEnd);
98+
} else {
99+
e.element.getSource().un('tileloadend', this.tileLoadEnd);
100+
}
19101
}
20102

21103
setMap(map) {
22104
super.setMap(map);
23-
105+
const layers = map.getLayers();
106+
if (map) {
107+
map.on("moveend", this.updateFeatureCount);
108+
layers.on(["add", "remove"], this.changeSource)
109+
} else {
110+
map.un("moveend", this.updateFeatureCount);
111+
layers.un(["add", "remove"], this.changeSource)
112+
}
24113
}
25114
render() {
26115
const element = this.element;

map/crop/crop.js

Lines changed: 4 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,6 @@ import {hcat} from "./codes";
44
import VectorTile from "ol/layer/VectorTile";
55
import {PMTilesVectorSource} from "ol-pmtiles";
66
const cropExtension = "https://fiboa.github.io/hcat-extension/v0.1.0/schema.yaml";
7-
import debounce from "lodash.debounce";
8-
import TileState from 'ol/TileState';
9-
import {intersects} from "ol/extent";
10-
import {toGeometry} from "ol/render/Feature";
117
import {CropLegendControl} from "./CropLegendControl";
128

139
const CROP_ATTRIBUTE = "ec:hcat_code";
@@ -16,56 +12,15 @@ const fieldStyle = {
1612
"stroke-width": 0.5,
1713
"fill-color": ['get', 'color']
1814
}
19-
const mapping = Object.fromEntries(hcat.map(c => [c.code, c.color]));
20-
21-
function forFeaturesInExtent(vt, extent, callback) {
22-
const renderer = vt.getRenderer();
23-
const tileCache = renderer.getTileCache();
24-
if (tileCache.getCount() === 0) return;
25-
26-
const tileGrid = renderer.getLayer().getSource().tileGrid;
27-
const z = tileGrid.getZForResolution(renderer.renderedResolution);
28-
tileCache.forEach((tile) => {
29-
if (tile.tileCoord[0] !== z || tile.getState() !== TileState.LOADED) return;
30-
for (const sourceTile of tile.getSourceTiles()) {
31-
const tileCoord = sourceTile.tileCoord;
32-
if (intersects(extent, tileGrid.getTileCoordExtent(tileCoord))) {
33-
for (const candidate of sourceTile.getFeatures()) {
34-
if (intersects(extent, candidate.getExtent())) callback(candidate);
35-
}
36-
}
37-
}
38-
});
39-
}
15+
const mapping = Object.fromEntries(hcat.map(c => [c.code, c]));
4016

4117
class CropMap extends FiboaMap {
4218
constructor() {
4319
super();
4420
this.fieldStyle = fieldStyle;
45-
this.vectortiles = [];
46-
this.updateFeatureCount = debounce(this.updateFeatureCount, 1000).bind(this);
47-
this.map.on("moveend", this.updateFeatureCount)
48-
this.map.addControl(new CropLegendControl());
49-
}
50-
updateFeatureCount(e) {
51-
// const extent = this.map.getView().calculateExtent();
52-
const extent = this.map.getView().calculateExtentInternal();
53-
const result = {
54-
area: 0,
55-
count: 0,
56-
perCrop: {}
57-
}
58-
for (const vt of this.vectortiles) {
59-
forFeaturesInExtent(vt, extent, (feature) => {
60-
const area = feature.properties_["area"] || toGeometry(feature).getArea();
61-
const crop = feature.properties_[CROP_ATTRIBUTE];
62-
result.count++;
63-
result.area += area;
64-
result[crop] = (result[crop] || 0) + area;
65-
});
66-
}
67-
console.log(extent, result);
21+
this.map.addControl(new CropLegendControl({attribute: CROP_ATTRIBUTE, mapping}));
6822
}
23+
6924
addPMTilesLayer(c, options) {
7025
const source = new PMTilesVectorSource(options);
7126
const fields = new VectorTile({
@@ -78,15 +33,7 @@ class CropMap extends FiboaMap {
7833
});
7934
// TODO, Count the features in view and display a legend explaining the top 5 crops
8035
// TODO, Add a filter based on crops. This will not be perfect on high zoom levels (lossy vector tiles)
81-
this.vectortiles.push(fields);
82-
source.on('tileloadend', e => {
83-
const features = e.tile.getFeatures();
84-
for (const feature of features) {
85-
const p = feature.getProperties();
86-
p.color = mapping[p[CROP_ATTRIBUTE]] || "#99bbccaa";
87-
}
88-
this.updateFeatureCount(e)
89-
})
36+
9037
this.map.addLayer(fields);
9138
}
9239
}

map/style.css

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,36 @@ form {
102102
.ol-crop-legend {
103103
right: .5em;
104104
bottom: .5em;
105-
}
105+
106+
background-color: rgba(255, 255, 255, 0.85);
107+
border: 1px solid #ccc;
108+
padding: 10px;
109+
font-size: 12px;
110+
line-height: 1.5;
111+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.3);
112+
}
113+
114+
.ol-crop-legend .legend-title {
115+
font-weight: bold;
116+
margin-bottom: 5px;
117+
}
118+
119+
.ol-crop-legend .legend-list {
120+
list-style: none;
121+
margin: 0;
122+
padding: 0;
123+
}
124+
125+
.ol-crop-legend .legend-list li {
126+
display: flex;
127+
align-items: center;
128+
margin-bottom: 5px;
129+
}
130+
131+
.ol-crop-legend .legend-color {
132+
width: 20px;
133+
height: 20px;
134+
flex-shrink: 0;
135+
margin-right: 10px;
136+
border: 1px solid #000000;
137+
}

0 commit comments

Comments
 (0)