原文链接及内容

运行界面

本例使用ol/source/Raster类基于另一个数据源对象来生成数据。栅格数据源接受任意数量的输入数据源(基于切片或图像),并在输入像素上运行一个操作管道,最终操作的返回值用作输出数据源的数据。
在这种情况下,使用单个切片图像数据源作为输入,对于每个像素,从输入像素计算植被绿色指数(VGI)。第二个操作基于一个阈值为这些像素上色(高于阈值的值为绿色,低于阈值的值为透明)。

main.js代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
import Map from 'ol/Map.js';
import RasterSource from 'ol/source/Raster.js';
import View from 'ol/View.js';
import XYZ from 'ol/source/XYZ.js';
import {Image as ImageLayer, Tile as TileLayer} from 'ol/layer.js';

const minVgi = 0;
const maxVgi = 0.5;
const bins = 10;

/**
* Calculate the Vegetation Greenness Index (VGI) from an input pixel. This
* is a rough estimate assuming that pixel values correspond to reflectance.
* @param {Array<number>} pixel An array of [R, G, B, A] values.
* @return {number} The VGI value for the given pixel.
*/
function vgi(pixel) {
const r = pixel[0] / 255;
const g = pixel[1] / 255;
const b = pixel[2] / 255;
return (2 * g - r - b) / (2 * g + r + b);
}

/**
* Summarize values for a histogram.
* @param {numver} value A VGI value.
* @param {Object} counts An object for keeping track of VGI counts.
*/
function summarize(value, counts) {
const min = counts.min;
const max = counts.max;
const num = counts.values.length;
if (value < min) {
// do nothing
} else if (value >= max) {
counts.values[num - 1] += 1;
} else {
const index = Math.floor((value - min) / counts.delta);
counts.values[index] += 1;
}
}

/**
* Use aerial imagery as the input data for the raster source.
*/

const key = 'Get your own API key at https://www.maptiler.com/cloud/';
const attributions =
'<a href="https://www.maptiler.com/copyright/" target="_blank">&copy; MapTiler</a> ' +
'<a href="https://www.openstreetmap.org/copyright" target="_blank">&copy; OpenStreetMap contributors</a>';

const aerial = new XYZ({
attributions: attributions,
url: 'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key,
maxZoom: 20,
crossOrigin: '',
});

/**
* Create a raster source where pixels with VGI values above a threshold will
* be colored green.
*/
const raster = new RasterSource({
sources: [aerial],
/**
* Run calculations on pixel data.
* @param {Array} pixels List of pixels (one per source).
* @param {Object} data User data object.
* @return {Array} The output pixel.
*/
operation: function (pixels, data) {
const pixel = pixels[0];
const value = vgi(pixel);
summarize(value, data.counts);
if (value >= data.threshold) {
pixel[0] = 0;
pixel[1] = 255;
pixel[2] = 0;
pixel[3] = 128;
} else {
pixel[3] = 0;
}
return pixel;
},
lib: {
vgi: vgi,
summarize: summarize,
},
});
raster.set('threshold', 0.25);

function createCounts(min, max, num) {
const values = new Array(num);
for (let i = 0; i < num; ++i) {
values[i] = 0;
}
return {
min: min,
max: max,
values: values,
delta: (max - min) / num,
};
}

raster.on('beforeoperations', function (event) {
event.data.counts = createCounts(minVgi, maxVgi, bins);
event.data.threshold = raster.get('threshold');
});

raster.on('afteroperations', function (event) {
schedulePlot(event.resolution, event.data.counts, event.data.threshold);
});

const map = new Map({
layers: [
new TileLayer({
source: aerial,
}),
new ImageLayer({
source: raster,
}),
],
target: 'map',
view: new View({
center: [-9651695, 4937351],
zoom: 13,
minZoom: 12,
maxZoom: 19,
}),
});

let timer = null;
function schedulePlot(resolution, counts, threshold) {
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(plot.bind(null, resolution, counts, threshold), 1000 / 60);
}

const barWidth = 15;
const plotHeight = 150;
const chart = d3
.select('#plot')
.append('svg')
.attr('width', barWidth * bins)
.attr('height', plotHeight);

const chartRect = chart.node().getBoundingClientRect();

const tip = d3.select(document.body).append('div').attr('class', 'tip');

function plot(resolution, counts, threshold) {
const yScale = d3
.scaleLinear()
.domain([0, d3.max(counts.values)])
.range([0, plotHeight]);

const bar = chart.selectAll('rect').data(counts.values);

bar.enter().append('rect');

bar
.attr('class', function (count, index) {
const value = counts.min + index * counts.delta;
return 'bar' + (value >= threshold ? ' selected' : '');
})
.attr('width', barWidth - 2);

bar
.transition()
.attr('transform', function (value, index) {
return (
'translate(' +
index * barWidth +
', ' +
(plotHeight - yScale(value)) +
')'
);
})
.attr('height', yScale);

bar.on('mousemove', function () {
const index = bar.nodes().indexOf(this);
const threshold = counts.min + index * counts.delta;
if (raster.get('threshold') !== threshold) {
raster.set('threshold', threshold);
raster.changed();
}
});

bar.on('mouseover', function (event) {
const index = bar.nodes().indexOf(this);
let area = 0;
for (let i = counts.values.length - 1; i >= index; --i) {
area += resolution * resolution * counts.values[i];
}
tip.html(message(counts.min + index * counts.delta, area));
tip.style('display', 'block');
tip
.transition()
.style('left', chartRect.left + index * barWidth + barWidth / 2 + 'px')
.style('top', event.y - 60 + 'px')
.style('opacity', 1);
});

bar.on('mouseout', function () {
tip
.transition()
.style('opacity', 0)
.on('end', function () {
tip.style('display', 'none');
});
});
}

function message(value, area) {
const acres = (area / 4046.86)
.toFixed(0)
.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
return acres + ' acres at<br>' + value.toFixed(2) + ' VGI or above';
}

界面布局文件index.html代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Raster Source</title>
<link rel="stylesheet" href="node_modules/ol/ol.css">
<style>
.map {
width: 100%;
height: 400px;
}

.rel {
position: relative
}

#plot {
pointer-events: none;
position: absolute;
bottom: 10px;
left: 10px;
}

.bar {
pointer-events: auto;
fill: #AFAFB9;
}

.bar.selected {
fill: green;
}

.tip {
position: absolute;
background: black;
color: white;
padding: 6px;
font-size: 12px;
border-radius: 4px;
margin-bottom: 10px;
display: none;
opacity: 0;
}
</style>
</head>
<body>
<div class="rel">
<div id="map" class="map"></div>
<div id="plot"></div>
</div>
<!-- Pointer events polyfill for old browsers, see https://caniuse.com/#feat=pointer -->
<script src="https://cdn.jsdelivr.net/npm/elm-pep@1.0.6/dist/elm-pep.js"></script>
<script src="https://cdn.jsdelivr.net/npm/d3@7.4.4/dist/d3.min.js"></script>
<script type="module" src="main.js"></script>
</body>
</html>