Merge pull request #3214 from tschaub/raster
Pixel manipulation with raster sources.
This commit is contained in:
3
Makefile
3
Makefile
@@ -19,7 +19,8 @@ BUILD_HOSTED := build/hosted/$(BRANCH)
|
|||||||
BUILD_HOSTED_EXAMPLES := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES))
|
BUILD_HOSTED_EXAMPLES := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES))
|
||||||
BUILD_HOSTED_EXAMPLES_JS := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES_JS))
|
BUILD_HOSTED_EXAMPLES_JS := $(addprefix $(BUILD_HOSTED)/,$(EXAMPLES_JS))
|
||||||
|
|
||||||
CHECK_EXAMPLE_TIMESTAMPS = $(patsubst examples/%.html,build/timestamps/check-%-timestamp,$(EXAMPLES_HTML))
|
UNPHANTOMABLE_EXAMPLES = examples/shaded-relief.html examples/raster.html examples/region-growing.html
|
||||||
|
CHECK_EXAMPLE_TIMESTAMPS = $(patsubst examples/%.html,build/timestamps/check-%-timestamp,$(filter-out $(UNPHANTOMABLE_EXAMPLES),$(EXAMPLES_HTML)))
|
||||||
|
|
||||||
TASKS_JS := $(shell find tasks -name '*.js')
|
TASKS_JS := $(shell find tasks -name '*.js')
|
||||||
|
|
||||||
|
|||||||
31
examples/raster.css
Normal file
31
examples/raster.css
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
31
examples/raster.html
Normal file
31
examples/raster.html
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
template: example.html
|
||||||
|
title: Raster Source
|
||||||
|
shortdesc: Demonstrates pixelwise operations with a raster source.
|
||||||
|
docs: >
|
||||||
|
<p>
|
||||||
|
This example uses a <code>ol.source.Raster</code> to generate data
|
||||||
|
based on another source. The raster source accepts any number of
|
||||||
|
input sources (tile or image based) and runs a pipeline of
|
||||||
|
operations on the input pixels. The return from the final
|
||||||
|
operation is used as the data for the output source.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In this case, a single tiled source of imagery is used as input.
|
||||||
|
For each pixel, the Vegetaion Greenness Index
|
||||||
|
(<a href="http://www.tandfonline.com/doi/abs/10.1080/10106040108542184#.Vb90ITBViko">VGI</a>)
|
||||||
|
is calculated from the input pixels. A second operation colors
|
||||||
|
those pixels based on a threshold value (values above the
|
||||||
|
threshold are green and those below are transparent).
|
||||||
|
</p>
|
||||||
|
tags: "raster, pixel"
|
||||||
|
resources:
|
||||||
|
- http://d3js.org/d3.v3.min.js
|
||||||
|
- raster.css
|
||||||
|
---
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span12 rel">
|
||||||
|
<div id="map" class="map"></div>
|
||||||
|
<div id="plot"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
200
examples/raster.js
Normal file
200
examples/raster.js
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
// NOCOMPILE
|
||||||
|
// this example uses d3 for which we don't have an externs file.
|
||||||
|
goog.require('ol.Map');
|
||||||
|
goog.require('ol.View');
|
||||||
|
goog.require('ol.layer.Image');
|
||||||
|
goog.require('ol.layer.Tile');
|
||||||
|
goog.require('ol.source.BingMaps');
|
||||||
|
goog.require('ol.source.Raster');
|
||||||
|
|
||||||
|
var minVgi = 0;
|
||||||
|
var maxVgi = 0.25;
|
||||||
|
var 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 {ol.raster.Pixel} pixel An array of [R, G, B, A] values.
|
||||||
|
* @return {number} The VGI value for the given pixel.
|
||||||
|
*/
|
||||||
|
function vgi(pixel) {
|
||||||
|
var r = pixel[0] / 255;
|
||||||
|
var g = pixel[1] / 255;
|
||||||
|
var 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) {
|
||||||
|
var min = counts.min;
|
||||||
|
var max = counts.max;
|
||||||
|
var num = counts.values.length;
|
||||||
|
if (value < min) {
|
||||||
|
// do nothing
|
||||||
|
} else if (value >= max) {
|
||||||
|
counts.values[num - 1] += 1;
|
||||||
|
} else {
|
||||||
|
var index = Math.floor((value - min) / counts.delta);
|
||||||
|
counts.values[index] += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use aerial imagery as the input data for the raster source.
|
||||||
|
*/
|
||||||
|
var bing = new ol.source.BingMaps({
|
||||||
|
key: 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3',
|
||||||
|
imagerySet: 'Aerial'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a raster source where pixels with VGI values above a threshold will
|
||||||
|
* be colored green.
|
||||||
|
*/
|
||||||
|
var raster = new ol.source.Raster({
|
||||||
|
sources: [bing],
|
||||||
|
operation: function(pixels, data) {
|
||||||
|
var pixel = pixels[0];
|
||||||
|
var 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.1);
|
||||||
|
|
||||||
|
function createCounts(min, max, num) {
|
||||||
|
var values = new Array(num);
|
||||||
|
for (var 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
var map = new ol.Map({
|
||||||
|
layers: [
|
||||||
|
new ol.layer.Tile({
|
||||||
|
source: bing
|
||||||
|
}),
|
||||||
|
new ol.layer.Image({
|
||||||
|
source: raster
|
||||||
|
})
|
||||||
|
],
|
||||||
|
target: 'map',
|
||||||
|
view: new ol.View({
|
||||||
|
center: [-9651695, 4937351],
|
||||||
|
zoom: 13,
|
||||||
|
minZoom: 12,
|
||||||
|
maxZoom: 19
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var timer = null;
|
||||||
|
function schedulePlot(resolution, counts, threshold) {
|
||||||
|
if (timer) {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = null;
|
||||||
|
}
|
||||||
|
timer = setTimeout(plot.bind(null, resolution, counts, threshold), 1000 / 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
var barWidth = 15;
|
||||||
|
var plotHeight = 150;
|
||||||
|
var chart = d3.select('#plot').append('svg')
|
||||||
|
.attr('width', barWidth * bins)
|
||||||
|
.attr('height', plotHeight);
|
||||||
|
|
||||||
|
var chartRect = chart[0][0].getBoundingClientRect();
|
||||||
|
|
||||||
|
var tip = d3.select(document.body).append('div')
|
||||||
|
.attr('class', 'tip');
|
||||||
|
|
||||||
|
function plot(resolution, counts, threshold) {
|
||||||
|
var yScale = d3.scale.linear()
|
||||||
|
.domain([0, d3.max(counts.values)])
|
||||||
|
.range([0, plotHeight]);
|
||||||
|
|
||||||
|
var bar = chart.selectAll('rect').data(counts.values);
|
||||||
|
|
||||||
|
bar.enter().append('rect');
|
||||||
|
|
||||||
|
bar.attr('class', function(count, index) {
|
||||||
|
var 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(count, index) {
|
||||||
|
var threshold = counts.min + (index * counts.delta);
|
||||||
|
if (raster.get('threshold') !== threshold) {
|
||||||
|
raster.set('threshold', threshold);
|
||||||
|
raster.changed();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
bar.on('mouseover', function(count, index) {
|
||||||
|
var area = 0;
|
||||||
|
for (var 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',
|
||||||
|
top: (d3.event.y - 60) + 'px',
|
||||||
|
opacity: 1
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
bar.on('mouseout', function() {
|
||||||
|
tip.transition().style('opacity', 0).each('end', function() {
|
||||||
|
tip.style('display', 'none');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function message(value, area) {
|
||||||
|
var acres = (area / 4046.86).toFixed(0).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||||
|
return acres + ' acres at<br>' + value.toFixed(2) + ' VGI or above';
|
||||||
|
}
|
||||||
4
examples/region-growing.css
Normal file
4
examples/region-growing.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
table.controls td {
|
||||||
|
min-width: 110px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
38
examples/region-growing.html
Normal file
38
examples/region-growing.html
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
template: example.html
|
||||||
|
title: Region Growing
|
||||||
|
shortdesc: Grow a region from a seed pixel
|
||||||
|
docs: >
|
||||||
|
<p>Click a region on the map. The computed region will be red.</p>
|
||||||
|
<p>
|
||||||
|
This example uses a <code>ol.source.Raster</code> to generate data
|
||||||
|
based on another source. The raster source accepts any number of
|
||||||
|
input sources (tile or image based) and runs a pipeline of
|
||||||
|
operations on the input data. The return from the final
|
||||||
|
operation is used as the data for the output source.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In this case, a single tiled source of imagery data is used as input.
|
||||||
|
The region is calculated in a single "image" operation using the "seed"
|
||||||
|
pixel provided by the user clicking on the map. The "threshold" value
|
||||||
|
determines whether a given contiguous pixel belongs to the "region" - the
|
||||||
|
difference between a candidate pixel's RGB values and the seed values must
|
||||||
|
be below the threshold.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This example also shows how an additional function can be made available
|
||||||
|
to the operation.
|
||||||
|
</p>
|
||||||
|
tags: "raster, region growing"
|
||||||
|
---
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span12">
|
||||||
|
<div id="map" class="map" style="cursor: pointer"></div>
|
||||||
|
<table class="controls">
|
||||||
|
<tr>
|
||||||
|
<td>Threshold: <span id="threshold-value"></span></td>
|
||||||
|
<td><input id="threshold" type="range" min="1" max="50" value="20"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
132
examples/region-growing.js
Normal file
132
examples/region-growing.js
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// NOCOMPILE
|
||||||
|
goog.require('ol.Map');
|
||||||
|
goog.require('ol.View');
|
||||||
|
goog.require('ol.layer.Image');
|
||||||
|
goog.require('ol.layer.Tile');
|
||||||
|
goog.require('ol.proj');
|
||||||
|
goog.require('ol.source.BingMaps');
|
||||||
|
goog.require('ol.source.Raster');
|
||||||
|
|
||||||
|
function growRegion(inputs, data) {
|
||||||
|
var image = inputs[0];
|
||||||
|
var seed = data.pixel;
|
||||||
|
var delta = parseInt(data.delta);
|
||||||
|
if (!seed) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
|
|
||||||
|
seed = seed.map(Math.round);
|
||||||
|
var width = image.width;
|
||||||
|
var height = image.height;
|
||||||
|
var inputData = image.data;
|
||||||
|
var outputData = new Uint8ClampedArray(inputData);
|
||||||
|
var seedIdx = (seed[1] * width + seed[0]) * 4;
|
||||||
|
var seedR = inputData[seedIdx];
|
||||||
|
var seedG = inputData[seedIdx + 1];
|
||||||
|
var seedB = inputData[seedIdx + 2];
|
||||||
|
var edge = [seed];
|
||||||
|
while (edge.length) {
|
||||||
|
var newedge = [];
|
||||||
|
for (var i = 0, ii = edge.length; i < ii; i++) {
|
||||||
|
// As noted in the Raster source constructor, this function is provided
|
||||||
|
// using the `lib` option. Other functions will NOT be visible unless
|
||||||
|
// provided using the `lib` option.
|
||||||
|
var next = nextEdges(edge[i]);
|
||||||
|
for (var j = 0, jj = next.length; j < jj; j++) {
|
||||||
|
var s = next[j][0], t = next[j][1];
|
||||||
|
if (s >= 0 && s < width && t >= 0 && t < height) {
|
||||||
|
var ci = (t * width + s) * 4;
|
||||||
|
var cr = inputData[ci];
|
||||||
|
var cg = inputData[ci + 1];
|
||||||
|
var cb = inputData[ci + 2];
|
||||||
|
var ca = inputData[ci + 3];
|
||||||
|
// if alpha is zero, carry on
|
||||||
|
if (ca === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (Math.abs(seedR - cr) < delta && Math.abs(seedG - cg) < delta &&
|
||||||
|
Math.abs(seedB - cb) < delta) {
|
||||||
|
outputData[ci] = 255;
|
||||||
|
outputData[ci + 1] = 0;
|
||||||
|
outputData[ci + 2] = 0;
|
||||||
|
outputData[ci + 3] = 255;
|
||||||
|
newedge.push([s, t]);
|
||||||
|
}
|
||||||
|
// mark as visited
|
||||||
|
inputData[ci + 3] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
edge = newedge;
|
||||||
|
}
|
||||||
|
return new ImageData(outputData, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
function next4Edges(edge) {
|
||||||
|
var x = edge[0], y = edge[1];
|
||||||
|
return [
|
||||||
|
[x + 1, y],
|
||||||
|
[x - 1, y],
|
||||||
|
[x, y + 1],
|
||||||
|
[x, y - 1]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = 'Ak-dzM4wZjSqTlzveKz5u0d4IQ4bRzVI309GxmkgSVr1ewS6iPSrOvOKhA-CJlm3';
|
||||||
|
|
||||||
|
var imagery = new ol.layer.Tile({
|
||||||
|
source: new ol.source.BingMaps({key: key, imagerySet: 'Aerial'})
|
||||||
|
});
|
||||||
|
|
||||||
|
var raster = new ol.source.Raster({
|
||||||
|
sources: [imagery.getSource()],
|
||||||
|
operationType: 'image',
|
||||||
|
operation: growRegion,
|
||||||
|
// Functions in the `lib` object will be available to the operation run in
|
||||||
|
// the web worker.
|
||||||
|
lib: {
|
||||||
|
nextEdges: next4Edges
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var rasterImage = new ol.layer.Image({
|
||||||
|
opacity: 0.7,
|
||||||
|
source: raster
|
||||||
|
});
|
||||||
|
|
||||||
|
var map = new ol.Map({
|
||||||
|
layers: [imagery, rasterImage],
|
||||||
|
target: 'map',
|
||||||
|
view: new ol.View({
|
||||||
|
center: ol.proj.fromLonLat([-119.07, 47.65]),
|
||||||
|
zoom: 11
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
var coordinate;
|
||||||
|
|
||||||
|
map.on('click', function(event) {
|
||||||
|
coordinate = event.coordinate;
|
||||||
|
raster.changed();
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('beforeoperations', function(event) {
|
||||||
|
// the event.data object will be passed to operations
|
||||||
|
var data = event.data;
|
||||||
|
data.delta = thresholdControl.value;
|
||||||
|
if (coordinate) {
|
||||||
|
data.pixel = map.getPixelFromCoordinate(coordinate);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var thresholdControl = document.getElementById('threshold');
|
||||||
|
|
||||||
|
function updateControlValue() {
|
||||||
|
document.getElementById('threshold-value').innerText = thresholdControl.value;
|
||||||
|
}
|
||||||
|
updateControlValue();
|
||||||
|
|
||||||
|
thresholdControl.addEventListener('input', function() {
|
||||||
|
updateControlValue();
|
||||||
|
raster.changed();
|
||||||
|
});
|
||||||
4
examples/shaded-relief.css
Normal file
4
examples/shaded-relief.css
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
table.controls td {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2px 5px;
|
||||||
|
}
|
||||||
46
examples/shaded-relief.html
Normal file
46
examples/shaded-relief.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
template: example.html
|
||||||
|
title: Shaded Relief
|
||||||
|
shortdesc: Calculate shaded relief from elevation data
|
||||||
|
docs: >
|
||||||
|
<p>
|
||||||
|
This example uses a <code>ol.source.Raster</code> to generate data
|
||||||
|
based on another source. The raster source accepts any number of
|
||||||
|
input sources (tile or image based) and runs a pipeline of
|
||||||
|
operations on the input data. The return from the final
|
||||||
|
operation is used as the data for the output source.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
In this case, a single tiled source of elevation data is used as input.
|
||||||
|
The shaded relief is calculated in a single "image" operation. By setting
|
||||||
|
<code>operationType: 'image'</code> on the raster source, operations are
|
||||||
|
called with an <code>ImageData</code> object for each of the input sources.
|
||||||
|
Operations are also called with a general purpose <code>data</code> object.
|
||||||
|
In this example, the sun elevation and azimuth data from the inputs above
|
||||||
|
are assigned to this <code>data</code> object and accessed in the shading
|
||||||
|
operation. The shading operation returns an array of <code>ImageData</code>
|
||||||
|
objects. When the raster source is used by an image layer, the first
|
||||||
|
<code>ImageData</code> object returned by the last operation in the pipeline
|
||||||
|
is used for rendering.
|
||||||
|
</p>
|
||||||
|
tags: "raster, shaded relief"
|
||||||
|
---
|
||||||
|
<div class="row-fluid">
|
||||||
|
<div class="span12">
|
||||||
|
<div id="map" class="map"></div>
|
||||||
|
<table class="controls">
|
||||||
|
<tr>
|
||||||
|
<td>vertical exaggeration: <span id="vertOut"></span>x</td>
|
||||||
|
<td><input id="vert" type="range" min="1" max="5" value="1"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>sun elevation: <span id="sunElOut"></span>°</td>
|
||||||
|
<td><input id="sunEl" type="range" min="0" max="90" value="45"/></td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>sun azimuth: <span id="sunAzOut"></span>°</td>
|
||||||
|
<td><input id="sunAz" type="range" min="0" max="360" value="45"/></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
158
examples/shaded-relief.js
Normal file
158
examples/shaded-relief.js
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
// NOCOMPILE
|
||||||
|
goog.require('ol.Map');
|
||||||
|
goog.require('ol.View');
|
||||||
|
goog.require('ol.layer.Image');
|
||||||
|
goog.require('ol.layer.Tile');
|
||||||
|
goog.require('ol.source.Raster');
|
||||||
|
goog.require('ol.source.TileJSON');
|
||||||
|
goog.require('ol.source.XYZ');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates a shaded relief image given elevation data. Uses a 3x3
|
||||||
|
* neighborhood for determining slope and aspect.
|
||||||
|
* @param {Array.<ImageData>} inputs Array of input images.
|
||||||
|
* @param {Object} data Data added in the "beforeoperations" event.
|
||||||
|
* @return {Array.<ImageData>} Output images (only the first is rendered).
|
||||||
|
*/
|
||||||
|
function shade(inputs, data) {
|
||||||
|
var elevationImage = inputs[0];
|
||||||
|
var width = elevationImage.width;
|
||||||
|
var height = elevationImage.height;
|
||||||
|
var elevationData = elevationImage.data;
|
||||||
|
var shadeData = new Uint8ClampedArray(elevationData.length);
|
||||||
|
var dp = data.resolution * 2;
|
||||||
|
var maxX = width - 1;
|
||||||
|
var maxY = height - 1;
|
||||||
|
var pixel = [0, 0, 0, 0];
|
||||||
|
var twoPi = 2 * Math.PI;
|
||||||
|
var halfPi = Math.PI / 2;
|
||||||
|
var sunEl = Math.PI * data.sunEl / 180;
|
||||||
|
var sunAz = Math.PI * data.sunAz / 180;
|
||||||
|
var cosSunEl = Math.cos(sunEl);
|
||||||
|
var sinSunEl = Math.sin(sunEl);
|
||||||
|
var pixelX, pixelY, x0, x1, y0, y1, offset,
|
||||||
|
z0, z1, dzdx, dzdy, slope, aspect, cosIncidence, scaled;
|
||||||
|
for (pixelY = 0; pixelY <= maxY; ++pixelY) {
|
||||||
|
y0 = pixelY === 0 ? 0 : pixelY - 1;
|
||||||
|
y1 = pixelY === maxY ? maxY : pixelY + 1;
|
||||||
|
for (pixelX = 0; pixelX <= maxX; ++pixelX) {
|
||||||
|
x0 = pixelX === 0 ? 0 : pixelX - 1;
|
||||||
|
x1 = pixelX === maxX ? maxX : pixelX + 1;
|
||||||
|
|
||||||
|
// determine elevation for (x0, pixelY)
|
||||||
|
offset = (pixelY * width + x0) * 4;
|
||||||
|
pixel[0] = elevationData[offset];
|
||||||
|
pixel[1] = elevationData[offset + 1];
|
||||||
|
pixel[2] = elevationData[offset + 2];
|
||||||
|
pixel[3] = elevationData[offset + 3];
|
||||||
|
z0 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3);
|
||||||
|
|
||||||
|
// determine elevation for (x1, pixelY)
|
||||||
|
offset = (pixelY * width + x1) * 4;
|
||||||
|
pixel[0] = elevationData[offset];
|
||||||
|
pixel[1] = elevationData[offset + 1];
|
||||||
|
pixel[2] = elevationData[offset + 2];
|
||||||
|
pixel[3] = elevationData[offset + 3];
|
||||||
|
z1 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3);
|
||||||
|
|
||||||
|
dzdx = (z1 - z0) / dp;
|
||||||
|
|
||||||
|
// determine elevation for (pixelX, y0)
|
||||||
|
offset = (y0 * width + pixelX) * 4;
|
||||||
|
pixel[0] = elevationData[offset];
|
||||||
|
pixel[1] = elevationData[offset + 1];
|
||||||
|
pixel[2] = elevationData[offset + 2];
|
||||||
|
pixel[3] = elevationData[offset + 3];
|
||||||
|
z0 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3);
|
||||||
|
|
||||||
|
// determine elevation for (pixelX, y1)
|
||||||
|
offset = (y1 * width + pixelX) * 4;
|
||||||
|
pixel[0] = elevationData[offset];
|
||||||
|
pixel[1] = elevationData[offset + 1];
|
||||||
|
pixel[2] = elevationData[offset + 2];
|
||||||
|
pixel[3] = elevationData[offset + 3];
|
||||||
|
z1 = data.vert * (pixel[0] + pixel[1] * 2 + pixel[2] * 3);
|
||||||
|
|
||||||
|
dzdy = (z1 - z0) / dp;
|
||||||
|
|
||||||
|
slope = Math.atan(Math.sqrt(dzdx * dzdx + dzdy * dzdy));
|
||||||
|
|
||||||
|
aspect = Math.atan2(dzdy, -dzdx);
|
||||||
|
if (aspect < 0) {
|
||||||
|
aspect = halfPi - aspect;
|
||||||
|
} else if (aspect > halfPi) {
|
||||||
|
aspect = twoPi - aspect + halfPi;
|
||||||
|
} else {
|
||||||
|
aspect = halfPi - aspect;
|
||||||
|
}
|
||||||
|
|
||||||
|
cosIncidence = sinSunEl * Math.cos(slope) +
|
||||||
|
cosSunEl * Math.sin(slope) * Math.cos(sunAz - aspect);
|
||||||
|
|
||||||
|
offset = (pixelY * width + pixelX) * 4;
|
||||||
|
scaled = 255 * cosIncidence;
|
||||||
|
shadeData[offset] = scaled;
|
||||||
|
shadeData[offset + 1] = scaled;
|
||||||
|
shadeData[offset + 2] = scaled;
|
||||||
|
shadeData[offset + 3] = elevationData[offset + 3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ImageData(shadeData, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
var elevation = new ol.source.XYZ({
|
||||||
|
url: 'https://{a-d}.tiles.mapbox.com/v3/aj.sf-dem/{z}/{x}/{y}.png',
|
||||||
|
crossOrigin: 'anonymous'
|
||||||
|
});
|
||||||
|
|
||||||
|
var raster = new ol.source.Raster({
|
||||||
|
sources: [elevation],
|
||||||
|
operationType: 'image',
|
||||||
|
operation: shade
|
||||||
|
});
|
||||||
|
|
||||||
|
var map = new ol.Map({
|
||||||
|
target: 'map',
|
||||||
|
layers: [
|
||||||
|
new ol.layer.Tile({
|
||||||
|
source: new ol.source.TileJSON({
|
||||||
|
url: 'http://api.tiles.mapbox.com/v3/tschaub.miapgppd.jsonp'
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
new ol.layer.Image({
|
||||||
|
opacity: 0.3,
|
||||||
|
source: raster
|
||||||
|
})
|
||||||
|
],
|
||||||
|
view: new ol.View({
|
||||||
|
extent: [-13675026, 4439648, -13580856, 4580292],
|
||||||
|
center: [-13615645, 4497969],
|
||||||
|
minZoom: 10,
|
||||||
|
maxZoom: 16,
|
||||||
|
zoom: 13
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
var controlIds = ['vert', 'sunEl', 'sunAz'];
|
||||||
|
var controls = {};
|
||||||
|
controlIds.forEach(function(id) {
|
||||||
|
var control = document.getElementById(id);
|
||||||
|
var output = document.getElementById(id + 'Out');
|
||||||
|
control.addEventListener('input', function() {
|
||||||
|
output.innerText = control.value;
|
||||||
|
raster.changed();
|
||||||
|
});
|
||||||
|
output.innerText = control.value;
|
||||||
|
controls[id] = control;
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('beforeoperations', function(event) {
|
||||||
|
// the event.data object will be passed to operations
|
||||||
|
var data = event.data;
|
||||||
|
data.resolution = event.resolution;
|
||||||
|
for (var id in controls) {
|
||||||
|
data[id] = Number(controls[id].value);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -266,6 +266,30 @@ oli.source.ImageEvent = function() {};
|
|||||||
oli.source.ImageEvent.prototype.image;
|
oli.source.ImageEvent.prototype.image;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @interface
|
||||||
|
*/
|
||||||
|
oli.source.RasterEvent = function() {};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {ol.Extent}
|
||||||
|
*/
|
||||||
|
oli.source.RasterEvent.prototype.extent;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
oli.source.RasterEvent.prototype.resolution;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Object}
|
||||||
|
*/
|
||||||
|
oli.source.RasterEvent.prototype.data;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @interface
|
* @interface
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4501,6 +4501,65 @@ olx.source.ImageVectorOptions.prototype.source;
|
|||||||
olx.source.ImageVectorOptions.prototype.style;
|
olx.source.ImageVectorOptions.prototype.style;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{sources: Array.<ol.source.Source>,
|
||||||
|
* operation: (ol.raster.Operation|undefined),
|
||||||
|
* lib: (Object|undefined),
|
||||||
|
* threads: (number|undefined),
|
||||||
|
* operationType: (ol.raster.OperationType|undefined)}}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
olx.source.RasterOptions;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Input sources.
|
||||||
|
* @type {Array.<ol.source.Source>}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
olx.source.RasterOptions.prototype.sources;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raster operation. The operation will be called with data from input sources
|
||||||
|
* and the output will be assigned to the raster source.
|
||||||
|
* @type {ol.raster.Operation|undefined}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
olx.source.RasterOptions.prototype.operation;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Functions that will be made available to operations run in a worker.
|
||||||
|
* @type {Object|undefined}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
olx.source.RasterOptions.prototype.lib;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default, operations will be run in a single worker thread. To avoid using
|
||||||
|
* workers altogether, set `threads: 0`. For pixel operations, operations can
|
||||||
|
* be run in multiple worker threads. Note that there is additional overhead in
|
||||||
|
* transferring data to multiple workers, and that depending on the user's
|
||||||
|
* system, it may not be possible to parallelize the work.
|
||||||
|
* @type {number|undefined}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
olx.source.RasterOptions.prototype.threads;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Operation type. Supported values are `'pixel'` and `'image'`. By default,
|
||||||
|
* `'pixel'` operations are assumed, and operations will be called with an
|
||||||
|
* array of pixels from input sources. If set to `'image'`, operations will
|
||||||
|
* be called with an array of ImageData objects from input sources.
|
||||||
|
* @type {ol.raster.OperationType|undefined}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
olx.source.RasterOptions.prototype.operationType;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {{attributions: (Array.<ol.Attribution>|undefined),
|
* @typedef {{attributions: (Array.<ol.Attribution>|undefined),
|
||||||
* crossOrigin: (null|string|undefined),
|
* crossOrigin: (null|string|undefined),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"async": "0.9.0",
|
"async": "0.9.0",
|
||||||
|
"browserify": "9.0.3",
|
||||||
"closure-util": "1.5.0",
|
"closure-util": "1.5.0",
|
||||||
"fs-extra": "0.12.0",
|
"fs-extra": "0.12.0",
|
||||||
"glob": "5.0.3",
|
"glob": "5.0.3",
|
||||||
@@ -36,11 +37,11 @@
|
|||||||
"metalsmith": "1.6.0",
|
"metalsmith": "1.6.0",
|
||||||
"metalsmith-templates": "0.7.0",
|
"metalsmith-templates": "0.7.0",
|
||||||
"nomnom": "1.8.0",
|
"nomnom": "1.8.0",
|
||||||
|
"pixelworks": "1.0.0",
|
||||||
"rbush": "1.3.5",
|
"rbush": "1.3.5",
|
||||||
"temp": "0.8.1",
|
"temp": "0.8.1",
|
||||||
"walk": "2.3.4",
|
"walk": "2.3.4",
|
||||||
"wrench": "1.5.8",
|
"wrench": "1.5.8"
|
||||||
"browserify": "9.0.3"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"clean-css": "2.2.16",
|
"clean-css": "2.2.16",
|
||||||
@@ -60,6 +61,7 @@
|
|||||||
"slimerjs-edge": "0.10.0-pre-2"
|
"slimerjs-edge": "0.10.0-pre-2"
|
||||||
},
|
},
|
||||||
"ext": [
|
"ext": [
|
||||||
"rbush"
|
"rbush",
|
||||||
|
{"module": "pixelworks", "browserify": true}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
goog.provide('ol.ImageCanvas');
|
goog.provide('ol.ImageCanvas');
|
||||||
|
|
||||||
|
goog.require('goog.asserts');
|
||||||
goog.require('ol.ImageBase');
|
goog.require('ol.ImageBase');
|
||||||
goog.require('ol.ImageState');
|
goog.require('ol.ImageState');
|
||||||
|
|
||||||
@@ -13,12 +14,23 @@ goog.require('ol.ImageState');
|
|||||||
* @param {number} pixelRatio Pixel ratio.
|
* @param {number} pixelRatio Pixel ratio.
|
||||||
* @param {Array.<ol.Attribution>} attributions Attributions.
|
* @param {Array.<ol.Attribution>} attributions Attributions.
|
||||||
* @param {HTMLCanvasElement} canvas Canvas.
|
* @param {HTMLCanvasElement} canvas Canvas.
|
||||||
|
* @param {ol.ImageCanvasLoader=} opt_loader Optional loader function to
|
||||||
|
* support asynchronous canvas drawing.
|
||||||
*/
|
*/
|
||||||
ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions,
|
ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions,
|
||||||
canvas) {
|
canvas, opt_loader) {
|
||||||
|
|
||||||
goog.base(this, extent, resolution, pixelRatio, ol.ImageState.LOADED,
|
/**
|
||||||
attributions);
|
* Optional canvas loader function.
|
||||||
|
* @type {?ol.ImageCanvasLoader}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.loader_ = goog.isDef(opt_loader) ? opt_loader : null;
|
||||||
|
|
||||||
|
var state = goog.isDef(opt_loader) ?
|
||||||
|
ol.ImageState.IDLE : ol.ImageState.LOADED;
|
||||||
|
|
||||||
|
goog.base(this, extent, resolution, pixelRatio, state, attributions);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @private
|
||||||
@@ -26,13 +38,68 @@ ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions,
|
|||||||
*/
|
*/
|
||||||
this.canvas_ = canvas;
|
this.canvas_ = canvas;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {Error}
|
||||||
|
*/
|
||||||
|
this.error_ = null;
|
||||||
|
|
||||||
};
|
};
|
||||||
goog.inherits(ol.ImageCanvas, ol.ImageBase);
|
goog.inherits(ol.ImageCanvas, ol.ImageBase);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get any error associated with asynchronous rendering.
|
||||||
|
* @return {Error} Any error that occurred during rendering.
|
||||||
|
*/
|
||||||
|
ol.ImageCanvas.prototype.getError = function() {
|
||||||
|
return this.error_;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle async drawing complete.
|
||||||
|
* @param {Error} err Any error during drawing.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.ImageCanvas.prototype.handleLoad_ = function(err) {
|
||||||
|
if (err) {
|
||||||
|
this.error_ = err;
|
||||||
|
this.state = ol.ImageState.ERROR;
|
||||||
|
} else {
|
||||||
|
this.state = ol.ImageState.LOADED;
|
||||||
|
}
|
||||||
|
this.changed();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger drawing on canvas.
|
||||||
|
*/
|
||||||
|
ol.ImageCanvas.prototype.load = function() {
|
||||||
|
if (this.state == ol.ImageState.IDLE) {
|
||||||
|
goog.asserts.assert(!goog.isNull(this.loader_));
|
||||||
|
this.state = ol.ImageState.LOADING;
|
||||||
|
this.changed();
|
||||||
|
this.loader_(goog.bind(this.handleLoad_, this));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @inheritDoc
|
* @inheritDoc
|
||||||
*/
|
*/
|
||||||
ol.ImageCanvas.prototype.getImage = function(opt_context) {
|
ol.ImageCanvas.prototype.getImage = function(opt_context) {
|
||||||
return this.canvas_;
|
return this.canvas_;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that is called to trigger asynchronous canvas drawing. It is
|
||||||
|
* called with a "done" callback that should be called when drawing is done.
|
||||||
|
* If any error occurs during drawing, the "done" callback should be called with
|
||||||
|
* that error.
|
||||||
|
*
|
||||||
|
* @typedef {function(function(Error))}
|
||||||
|
*/
|
||||||
|
ol.ImageCanvasLoader;
|
||||||
|
|||||||
32
src/ol/raster/operation.js
Normal file
32
src/ol/raster/operation.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
goog.provide('ol.raster.Operation');
|
||||||
|
goog.provide('ol.raster.OperationType');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Raster operation type. Supported values are `'pixel'` and `'image'`.
|
||||||
|
* @enum {string}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
ol.raster.OperationType = {
|
||||||
|
PIXEL: 'pixel',
|
||||||
|
IMAGE: 'image'
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A function that takes an array of input data, performs some operation, and
|
||||||
|
* returns an array of ouput data. For `'pixel'` type operations, functions
|
||||||
|
* will be called with an array of {@link ol.raster.Pixel} data and should
|
||||||
|
* return an array of the same. For `'image'` type operations, functions will
|
||||||
|
* be called with an array of {@link ImageData
|
||||||
|
* https://developer.mozilla.org/en-US/docs/Web/API/ImageData} and should return
|
||||||
|
* an array of the same. The operations are called with a second "data"
|
||||||
|
* argument, which can be used for storage. The data object is accessible
|
||||||
|
* from raster events, where it can be initialized in "beforeoperations" and
|
||||||
|
* accessed again in "afteroperations".
|
||||||
|
*
|
||||||
|
* @typedef {function((Array.<ol.raster.Pixel>|Array.<ImageData>), Object):
|
||||||
|
* (Array.<ol.raster.Pixel>|Array.<ImageData>)}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
ol.raster.Operation;
|
||||||
9
src/ol/raster/pixel.js
Normal file
9
src/ol/raster/pixel.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
goog.provide('ol.raster.Pixel');
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of numbers representing pixel values.
|
||||||
|
* @typedef {Array.<number>} ol.raster.Pixel
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
ol.raster.Pixel;
|
||||||
@@ -113,7 +113,6 @@ ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) {
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @protected
|
|
||||||
* @return {ol.layer.Layer} Layer.
|
* @return {ol.layer.Layer} Layer.
|
||||||
*/
|
*/
|
||||||
ol.renderer.Layer.prototype.getLayer = function() {
|
ol.renderer.Layer.prototype.getLayer = function() {
|
||||||
|
|||||||
535
src/ol/source/rastersource.js
Normal file
535
src/ol/source/rastersource.js
Normal file
@@ -0,0 +1,535 @@
|
|||||||
|
goog.provide('ol.source.Raster');
|
||||||
|
goog.provide('ol.source.RasterEvent');
|
||||||
|
goog.provide('ol.source.RasterEventType');
|
||||||
|
|
||||||
|
goog.require('goog.asserts');
|
||||||
|
goog.require('goog.events');
|
||||||
|
goog.require('goog.events.Event');
|
||||||
|
goog.require('goog.events.EventType');
|
||||||
|
goog.require('goog.functions');
|
||||||
|
goog.require('goog.object');
|
||||||
|
goog.require('goog.vec.Mat4');
|
||||||
|
goog.require('ol.ImageCanvas');
|
||||||
|
goog.require('ol.TileQueue');
|
||||||
|
goog.require('ol.dom');
|
||||||
|
goog.require('ol.ext.pixelworks');
|
||||||
|
goog.require('ol.extent');
|
||||||
|
goog.require('ol.layer.Image');
|
||||||
|
goog.require('ol.layer.Tile');
|
||||||
|
goog.require('ol.raster.OperationType');
|
||||||
|
goog.require('ol.renderer.canvas.ImageLayer');
|
||||||
|
goog.require('ol.renderer.canvas.TileLayer');
|
||||||
|
goog.require('ol.source.Image');
|
||||||
|
goog.require('ol.source.State');
|
||||||
|
goog.require('ol.source.Tile');
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @classdesc
|
||||||
|
* A source that transforms data from any number of input sources using an array
|
||||||
|
* of {@link ol.raster.Operation} functions to transform input pixel values into
|
||||||
|
* output pixel values.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @extends {ol.source.Image}
|
||||||
|
* @param {olx.source.RasterOptions} options Options.
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
ol.source.Raster = function(options) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {*}
|
||||||
|
*/
|
||||||
|
this.worker_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {ol.raster.OperationType}
|
||||||
|
*/
|
||||||
|
this.operationType_ = goog.isDef(options.operationType) ?
|
||||||
|
options.operationType : ol.raster.OperationType.PIXEL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
this.threads_ = goog.isDef(options.threads) ? options.threads : 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {Array.<ol.renderer.canvas.Layer>}
|
||||||
|
*/
|
||||||
|
this.renderers_ = ol.source.Raster.createRenderers_(options.sources);
|
||||||
|
|
||||||
|
for (var r = 0, rr = this.renderers_.length; r < rr; ++r) {
|
||||||
|
goog.events.listen(this.renderers_[r], goog.events.EventType.CHANGE,
|
||||||
|
this.changed, false, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {CanvasRenderingContext2D}
|
||||||
|
*/
|
||||||
|
this.canvasContext_ = ol.dom.createCanvasContext2D();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {ol.TileQueue}
|
||||||
|
*/
|
||||||
|
this.tileQueue_ = new ol.TileQueue(
|
||||||
|
goog.functions.constant(1),
|
||||||
|
goog.bind(this.changed, this));
|
||||||
|
|
||||||
|
var layerStatesArray = ol.source.Raster.getLayerStatesArray_(this.renderers_);
|
||||||
|
var layerStates = {};
|
||||||
|
for (var i = 0, ii = layerStatesArray.length; i < ii; ++i) {
|
||||||
|
layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most recently rendered state.
|
||||||
|
* @type {?ol.source.Raster.RenderedState}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.renderedState_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most recently rendered image canvas.
|
||||||
|
* @type {ol.ImageCanvas}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this.renderedImageCanvas_ = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @type {olx.FrameState}
|
||||||
|
*/
|
||||||
|
this.frameState_ = {
|
||||||
|
animate: false,
|
||||||
|
attributions: {},
|
||||||
|
coordinateToPixelMatrix: goog.vec.Mat4.createNumber(),
|
||||||
|
extent: null,
|
||||||
|
focus: null,
|
||||||
|
index: 0,
|
||||||
|
layerStates: layerStates,
|
||||||
|
layerStatesArray: layerStatesArray,
|
||||||
|
logos: {},
|
||||||
|
pixelRatio: 1,
|
||||||
|
pixelToCoordinateMatrix: goog.vec.Mat4.createNumber(),
|
||||||
|
postRenderFunctions: [],
|
||||||
|
size: [0, 0],
|
||||||
|
skippedFeatureUids: {},
|
||||||
|
tileQueue: this.tileQueue_,
|
||||||
|
time: Date.now(),
|
||||||
|
usedTiles: {},
|
||||||
|
viewState: /** @type {olx.ViewState} */ ({
|
||||||
|
rotation: 0
|
||||||
|
}),
|
||||||
|
viewHints: [],
|
||||||
|
wantedTiles: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
goog.base(this, {});
|
||||||
|
|
||||||
|
if (goog.isDef(options.operation)) {
|
||||||
|
this.setOperation(options.operation, options.lib);
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
goog.inherits(ol.source.Raster, ol.source.Image);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the operation.
|
||||||
|
* @param {ol.raster.Operation} operation New operation.
|
||||||
|
* @param {Object=} opt_lib Functions that will be available to operations run
|
||||||
|
* in a worker.
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
ol.source.Raster.prototype.setOperation = function(operation, opt_lib) {
|
||||||
|
this.worker_ = new ol.ext.pixelworks.Processor({
|
||||||
|
operation: operation,
|
||||||
|
imageOps: this.operationType_ === ol.raster.OperationType.IMAGE,
|
||||||
|
queue: 1,
|
||||||
|
lib: opt_lib,
|
||||||
|
threads: this.threads_
|
||||||
|
});
|
||||||
|
this.changed();
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the stored frame state.
|
||||||
|
* @param {ol.Extent} extent The view extent (in map units).
|
||||||
|
* @param {number} resolution The view resolution.
|
||||||
|
* @param {ol.proj.Projection} projection The view projection.
|
||||||
|
* @return {olx.FrameState} The updated frame state.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.prototype.updateFrameState_ =
|
||||||
|
function(extent, resolution, projection) {
|
||||||
|
|
||||||
|
var frameState = /** @type {olx.FrameState} */ (
|
||||||
|
goog.object.clone(this.frameState_));
|
||||||
|
|
||||||
|
frameState.viewState = /** @type {olx.ViewState} */ (
|
||||||
|
goog.object.clone(frameState.viewState));
|
||||||
|
|
||||||
|
var center = ol.extent.getCenter(extent);
|
||||||
|
var width = Math.round(ol.extent.getWidth(extent) / resolution);
|
||||||
|
var height = Math.round(ol.extent.getHeight(extent) / resolution);
|
||||||
|
|
||||||
|
frameState.extent = extent;
|
||||||
|
frameState.focus = ol.extent.getCenter(extent);
|
||||||
|
frameState.size[0] = width;
|
||||||
|
frameState.size[1] = height;
|
||||||
|
|
||||||
|
var viewState = frameState.viewState;
|
||||||
|
viewState.center = center;
|
||||||
|
viewState.projection = projection;
|
||||||
|
viewState.resolution = resolution;
|
||||||
|
return frameState;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the most recently rendered image canvas is dirty.
|
||||||
|
* @param {ol.Extent} extent The requested extent.
|
||||||
|
* @param {number} resolution The requested resolution.
|
||||||
|
* @return {boolean} The image is dirty.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.prototype.isDirty_ = function(extent, resolution) {
|
||||||
|
var state = this.renderedState_;
|
||||||
|
return !state ||
|
||||||
|
this.getRevision() !== state.revision ||
|
||||||
|
resolution !== state.resolution ||
|
||||||
|
!ol.extent.equals(extent, state.extent);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritDoc
|
||||||
|
*/
|
||||||
|
ol.source.Raster.prototype.getImage =
|
||||||
|
function(extent, resolution, pixelRatio, projection) {
|
||||||
|
|
||||||
|
if (!this.allSourcesReady_()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isDirty_(extent, resolution)) {
|
||||||
|
return this.renderedImageCanvas_;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = this.canvasContext_;
|
||||||
|
var canvas = context.canvas;
|
||||||
|
|
||||||
|
var width = Math.round(ol.extent.getWidth(extent) / resolution);
|
||||||
|
var height = Math.round(ol.extent.getHeight(extent) / resolution);
|
||||||
|
|
||||||
|
if (width !== canvas.width ||
|
||||||
|
height !== canvas.height) {
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
var frameState = this.updateFrameState_(extent, resolution, projection);
|
||||||
|
|
||||||
|
var imageCanvas = new ol.ImageCanvas(
|
||||||
|
extent, resolution, 1, this.getAttributions(), canvas,
|
||||||
|
this.composeFrame_.bind(this, frameState));
|
||||||
|
|
||||||
|
this.renderedImageCanvas_ = imageCanvas;
|
||||||
|
|
||||||
|
this.renderedState_ = {
|
||||||
|
extent: extent,
|
||||||
|
resolution: resolution,
|
||||||
|
revision: this.getRevision()
|
||||||
|
};
|
||||||
|
|
||||||
|
return imageCanvas;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if all sources are ready.
|
||||||
|
* @return {boolean} All sources are ready.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.prototype.allSourcesReady_ = function() {
|
||||||
|
var ready = true;
|
||||||
|
var source;
|
||||||
|
for (var i = 0, ii = this.renderers_.length; i < ii; ++i) {
|
||||||
|
source = this.renderers_[i].getLayer().getSource();
|
||||||
|
if (source.getState() !== ol.source.State.READY) {
|
||||||
|
ready = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ready;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose the frame. This renders data from all sources, runs pixel-wise
|
||||||
|
* operations, and renders the result to the stored canvas context.
|
||||||
|
* @param {olx.FrameState} frameState The frame state.
|
||||||
|
* @param {function(Error)} callback Called when composition is complete.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) {
|
||||||
|
var len = this.renderers_.length;
|
||||||
|
var imageDatas = new Array(len);
|
||||||
|
for (var i = 0; i < len; ++i) {
|
||||||
|
var imageData = ol.source.Raster.getImageData_(
|
||||||
|
this.renderers_[i], frameState, frameState.layerStatesArray[i]);
|
||||||
|
if (imageData) {
|
||||||
|
imageDatas[i] = imageData;
|
||||||
|
} else {
|
||||||
|
// image not yet ready
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = {};
|
||||||
|
this.dispatchEvent(new ol.source.RasterEvent(
|
||||||
|
ol.source.RasterEventType.BEFOREOPERATIONS, frameState, data));
|
||||||
|
|
||||||
|
this.worker_.process(imageDatas, data,
|
||||||
|
this.onWorkerComplete_.bind(this, frameState, callback));
|
||||||
|
|
||||||
|
frameState.tileQueue.loadMoreTiles(16, 16);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when pixel processing is complete.
|
||||||
|
* @param {olx.FrameState} frameState The frame state.
|
||||||
|
* @param {function(Error)} callback Called when rendering is complete.
|
||||||
|
* @param {Error} err Any error during processing.
|
||||||
|
* @param {ImageData} output The output image data.
|
||||||
|
* @param {Object} data The user data.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.prototype.onWorkerComplete_ =
|
||||||
|
function(frameState, callback, err, output, data) {
|
||||||
|
if (err) {
|
||||||
|
callback(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (goog.isNull(output)) {
|
||||||
|
// job aborted
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new ol.source.RasterEvent(
|
||||||
|
ol.source.RasterEventType.AFTEROPERATIONS, frameState, data));
|
||||||
|
|
||||||
|
var resolution = frameState.viewState.resolution / frameState.pixelRatio;
|
||||||
|
if (!this.isDirty_(frameState.extent, resolution)) {
|
||||||
|
this.canvasContext_.putImageData(output, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
callback(null);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get image data from a renderer.
|
||||||
|
* @param {ol.renderer.canvas.Layer} renderer Layer renderer.
|
||||||
|
* @param {olx.FrameState} frameState The frame state.
|
||||||
|
* @param {ol.layer.LayerState} layerState The layer state.
|
||||||
|
* @return {ImageData} The image data.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.getImageData_ = function(renderer, frameState, layerState) {
|
||||||
|
renderer.prepareFrame(frameState, layerState);
|
||||||
|
// We should be able to call renderer.composeFrame(), but this is inefficient
|
||||||
|
// for tiled sources (we've already rendered to an intermediate canvas in the
|
||||||
|
// prepareFrame call and we don't need to render again to the output canvas).
|
||||||
|
// TODO: make all canvas renderers render to a single canvas
|
||||||
|
var image = renderer.getImage();
|
||||||
|
if (!image) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
var imageTransform = renderer.getImageTransform();
|
||||||
|
var dx = Math.round(goog.vec.Mat4.getElement(imageTransform, 0, 3));
|
||||||
|
var dy = Math.round(goog.vec.Mat4.getElement(imageTransform, 1, 3));
|
||||||
|
var width = frameState.size[0];
|
||||||
|
var height = frameState.size[1];
|
||||||
|
if (image instanceof Image) {
|
||||||
|
if (!ol.source.Raster.context_) {
|
||||||
|
ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height);
|
||||||
|
} else {
|
||||||
|
var canvas = ol.source.Raster.context_.canvas;
|
||||||
|
if (canvas.width !== width || canvas.height !== height) {
|
||||||
|
ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height);
|
||||||
|
} else {
|
||||||
|
ol.source.Raster.context_.clearRect(0, 0, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var dw = Math.round(
|
||||||
|
image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0));
|
||||||
|
var dh = Math.round(
|
||||||
|
image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1));
|
||||||
|
ol.source.Raster.context_.drawImage(image, dx, dy, dw, dh);
|
||||||
|
return ol.source.Raster.context_.getImageData(0, 0, width, height);
|
||||||
|
} else {
|
||||||
|
return image.getContext('2d').getImageData(-dx, -dy, width, height);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A reusable canvas context.
|
||||||
|
* @type {CanvasRenderingContext2D}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.context_ = null;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of layer states from a list of renderers.
|
||||||
|
* @param {Array.<ol.renderer.canvas.Layer>} renderers Layer renderers.
|
||||||
|
* @return {Array.<ol.layer.LayerState>} The layer states.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.getLayerStatesArray_ = function(renderers) {
|
||||||
|
return renderers.map(function(renderer) {
|
||||||
|
return renderer.getLayer().getLayerState();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create renderers for all sources.
|
||||||
|
* @param {Array.<ol.source.Source>} sources The sources.
|
||||||
|
* @return {Array.<ol.renderer.canvas.Layer>} Array of layer renderers.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.createRenderers_ = function(sources) {
|
||||||
|
var len = sources.length;
|
||||||
|
var renderers = new Array(len);
|
||||||
|
for (var i = 0; i < len; ++i) {
|
||||||
|
renderers[i] = ol.source.Raster.createRenderer_(sources[i]);
|
||||||
|
}
|
||||||
|
return renderers;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a renderer for the provided source.
|
||||||
|
* @param {ol.source.Source} source The source.
|
||||||
|
* @return {ol.renderer.canvas.Layer} The renderer.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.createRenderer_ = function(source) {
|
||||||
|
var renderer = null;
|
||||||
|
if (source instanceof ol.source.Tile) {
|
||||||
|
renderer = ol.source.Raster.createTileRenderer_(
|
||||||
|
/** @type {ol.source.Tile} */ (source));
|
||||||
|
} else if (source instanceof ol.source.Image) {
|
||||||
|
renderer = ol.source.Raster.createImageRenderer_(
|
||||||
|
/** @type {ol.source.Image} */ (source));
|
||||||
|
} else {
|
||||||
|
goog.asserts.fail('Unsupported source type: ' + source);
|
||||||
|
}
|
||||||
|
return renderer;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an image renderer for the provided source.
|
||||||
|
* @param {ol.source.Image} source The source.
|
||||||
|
* @return {ol.renderer.canvas.Layer} The renderer.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.createImageRenderer_ = function(source) {
|
||||||
|
var layer = new ol.layer.Image({source: source});
|
||||||
|
return new ol.renderer.canvas.ImageLayer(layer);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a tile renderer for the provided source.
|
||||||
|
* @param {ol.source.Tile} source The source.
|
||||||
|
* @return {ol.renderer.canvas.Layer} The renderer.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
ol.source.Raster.createTileRenderer_ = function(source) {
|
||||||
|
var layer = new ol.layer.Tile({source: source});
|
||||||
|
return new ol.renderer.canvas.TileLayer(layer);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {{revision: number,
|
||||||
|
* resolution: number,
|
||||||
|
* extent: ol.Extent}}
|
||||||
|
*/
|
||||||
|
ol.source.Raster.RenderedState;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @classdesc
|
||||||
|
* Events emitted by {@link ol.source.Raster} instances are instances of this
|
||||||
|
* type.
|
||||||
|
*
|
||||||
|
* @constructor
|
||||||
|
* @extends {goog.events.Event}
|
||||||
|
* @implements {oli.source.RasterEvent}
|
||||||
|
* @param {string} type Type.
|
||||||
|
* @param {olx.FrameState} frameState The frame state.
|
||||||
|
* @param {Object} data An object made available to operations.
|
||||||
|
*/
|
||||||
|
ol.source.RasterEvent = function(type, frameState, data) {
|
||||||
|
goog.base(this, type);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The raster extent.
|
||||||
|
* @type {ol.Extent}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
this.extent = frameState.extent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pixel resolution (map units per pixel).
|
||||||
|
* @type {number}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
this.resolution = frameState.viewState.resolution / frameState.pixelRatio;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object made available to all operations. This can be used by operations
|
||||||
|
* as a storage object (e.g. for calculating statistics).
|
||||||
|
* @type {Object}
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
this.data = data;
|
||||||
|
|
||||||
|
};
|
||||||
|
goog.inherits(ol.source.RasterEvent, goog.events.Event);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
ol.source.RasterEventType = {
|
||||||
|
/**
|
||||||
|
* Triggered before operations are run.
|
||||||
|
* @event ol.source.RasterEvent#beforeoperations
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
BEFOREOPERATIONS: 'beforeoperations',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Triggered after operations are run.
|
||||||
|
* @event ol.source.RasterEvent#afteroperations
|
||||||
|
* @api
|
||||||
|
*/
|
||||||
|
AFTEROPERATIONS: 'afteroperations'
|
||||||
|
};
|
||||||
307
test/spec/ol/source/rastersource.test.js
Normal file
307
test/spec/ol/source/rastersource.test.js
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
goog.provide('ol.test.source.RasterSource');
|
||||||
|
|
||||||
|
var red = 'data:image/gif;base64,R0lGODlhAQABAPAAAP8AAP///yH5BAAAAAAALAAAAAA' +
|
||||||
|
'BAAEAAAICRAEAOw==';
|
||||||
|
|
||||||
|
var green = 'data:image/gif;base64,R0lGODlhAQABAPAAAAD/AP///yH5BAAAAAAALAAAA' +
|
||||||
|
'AABAAEAAAICRAEAOw==';
|
||||||
|
|
||||||
|
var blue = 'data:image/gif;base64,R0lGODlhAQABAPAAAAAA/////yH5BAAAAAAALAAAAA' +
|
||||||
|
'ABAAEAAAICRAEAOw==';
|
||||||
|
|
||||||
|
function itNoPhantom() {
|
||||||
|
if (window.mochaPhantomJS) {
|
||||||
|
return xit.apply(this, arguments);
|
||||||
|
} else {
|
||||||
|
return it.apply(this, arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('ol.source.Raster', function() {
|
||||||
|
|
||||||
|
var target, map, redSource, greenSource, blueSource;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
target = document.createElement('div');
|
||||||
|
|
||||||
|
var style = target.style;
|
||||||
|
style.position = 'absolute';
|
||||||
|
style.left = '-1000px';
|
||||||
|
style.top = '-1000px';
|
||||||
|
style.width = '2px';
|
||||||
|
style.height = '2px';
|
||||||
|
document.body.appendChild(target);
|
||||||
|
|
||||||
|
var extent = [-1, -1, 1, 1];
|
||||||
|
|
||||||
|
redSource = new ol.source.ImageStatic({
|
||||||
|
url: red,
|
||||||
|
imageExtent: extent
|
||||||
|
});
|
||||||
|
|
||||||
|
greenSource = new ol.source.ImageStatic({
|
||||||
|
url: green,
|
||||||
|
imageExtent: extent
|
||||||
|
});
|
||||||
|
|
||||||
|
blueSource = new ol.source.ImageStatic({
|
||||||
|
url: blue,
|
||||||
|
imageExtent: extent
|
||||||
|
});
|
||||||
|
|
||||||
|
raster = new ol.source.Raster({
|
||||||
|
threads: 0,
|
||||||
|
sources: [redSource, greenSource, blueSource],
|
||||||
|
operation: function(inputs) {
|
||||||
|
return inputs[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
map = new ol.Map({
|
||||||
|
target: target,
|
||||||
|
view: new ol.View({
|
||||||
|
resolutions: [1],
|
||||||
|
projection: new ol.proj.Projection({
|
||||||
|
code: 'image',
|
||||||
|
units: 'pixels',
|
||||||
|
extent: extent
|
||||||
|
})
|
||||||
|
}),
|
||||||
|
layers: [
|
||||||
|
new ol.layer.Image({
|
||||||
|
source: raster
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
goog.dispose(map);
|
||||||
|
document.body.removeChild(target);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('constructor', function() {
|
||||||
|
|
||||||
|
it('returns a tile source', function() {
|
||||||
|
var source = new ol.source.Raster({
|
||||||
|
threads: 0,
|
||||||
|
sources: [new ol.source.Tile({})]
|
||||||
|
});
|
||||||
|
expect(source).to.be.a(ol.source.Source);
|
||||||
|
expect(source).to.be.a(ol.source.Raster);
|
||||||
|
});
|
||||||
|
|
||||||
|
itNoPhantom('defaults to "pixel" operation', function(done) {
|
||||||
|
|
||||||
|
var log = [];
|
||||||
|
|
||||||
|
raster = new ol.source.Raster({
|
||||||
|
threads: 0,
|
||||||
|
sources: [redSource, greenSource, blueSource],
|
||||||
|
operation: function(inputs) {
|
||||||
|
log.push(inputs);
|
||||||
|
return inputs[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('afteroperations', function() {
|
||||||
|
expect(log.length).to.equal(4);
|
||||||
|
var inputs = log[0];
|
||||||
|
var pixel = inputs[0];
|
||||||
|
expect(pixel).to.be.an('array');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
map.getLayers().item(0).setSource(raster);
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
itNoPhantom('allows operation type to be set to "image"', function(done) {
|
||||||
|
|
||||||
|
var log = [];
|
||||||
|
|
||||||
|
raster = new ol.source.Raster({
|
||||||
|
operationType: ol.raster.OperationType.IMAGE,
|
||||||
|
threads: 0,
|
||||||
|
sources: [redSource, greenSource, blueSource],
|
||||||
|
operation: function(inputs) {
|
||||||
|
log.push(inputs);
|
||||||
|
return inputs[0];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('afteroperations', function() {
|
||||||
|
expect(log.length).to.equal(1);
|
||||||
|
var inputs = log[0];
|
||||||
|
expect(inputs[0]).to.be.an(ImageData);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
map.getLayers().item(0).setSource(raster);
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#setOperation()', function() {
|
||||||
|
|
||||||
|
itNoPhantom('allows operation to be set', function(done) {
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
raster.setOperation(function(pixels) {
|
||||||
|
++count;
|
||||||
|
var redPixel = pixels[0];
|
||||||
|
var greenPixel = pixels[1];
|
||||||
|
var bluePixel = pixels[2];
|
||||||
|
expect(redPixel).to.eql([255, 0, 0, 255]);
|
||||||
|
expect(greenPixel).to.eql([0, 255, 0, 255]);
|
||||||
|
expect(bluePixel).to.eql([0, 0, 255, 255]);
|
||||||
|
return pixels[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
raster.on('afteroperations', function(event) {
|
||||||
|
expect(count).to.equal(4);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
itNoPhantom('updates and re-runs the operation', function(done) {
|
||||||
|
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
raster.on('afteroperations', function(event) {
|
||||||
|
++count;
|
||||||
|
if (count === 1) {
|
||||||
|
raster.setOperation(function(inputs) {
|
||||||
|
return inputs[0];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('beforeoperations', function() {
|
||||||
|
|
||||||
|
itNoPhantom('gets called before operations are run', function(done) {
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
raster.setOperation(function(inputs) {
|
||||||
|
++count;
|
||||||
|
return inputs[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('beforeoperations', function(event) {
|
||||||
|
expect(count).to.equal(0);
|
||||||
|
expect(!!event).to.be(true);
|
||||||
|
expect(event.extent).to.be.an('array');
|
||||||
|
expect(event.resolution).to.be.a('number');
|
||||||
|
expect(event.data).to.be.an('object');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
itNoPhantom('allows data to be set for the operation', function(done) {
|
||||||
|
|
||||||
|
raster.setOperation(function(inputs, data) {
|
||||||
|
++data.count;
|
||||||
|
return inputs[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('beforeoperations', function(event) {
|
||||||
|
event.data.count = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('afteroperations', function(event) {
|
||||||
|
expect(event.data.count).to.equal(4);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('afteroperations', function() {
|
||||||
|
|
||||||
|
itNoPhantom('gets called after operations are run', function(done) {
|
||||||
|
|
||||||
|
var count = 0;
|
||||||
|
raster.setOperation(function(inputs) {
|
||||||
|
++count;
|
||||||
|
return inputs[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('afteroperations', function(event) {
|
||||||
|
expect(count).to.equal(4);
|
||||||
|
expect(!!event).to.be(true);
|
||||||
|
expect(event.extent).to.be.an('array');
|
||||||
|
expect(event.resolution).to.be.a('number');
|
||||||
|
expect(event.data).to.be.an('object');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
itNoPhantom('receives data set by the operation', function(done) {
|
||||||
|
|
||||||
|
raster.setOperation(function(inputs, data) {
|
||||||
|
data.message = 'hello world';
|
||||||
|
return inputs[0];
|
||||||
|
});
|
||||||
|
|
||||||
|
raster.on('afteroperations', function(event) {
|
||||||
|
expect(event.data.message).to.equal('hello world');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
var view = map.getView();
|
||||||
|
view.setCenter([0, 0]);
|
||||||
|
view.setZoom(0);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
goog.require('ol.Map');
|
||||||
|
goog.require('ol.View');
|
||||||
|
goog.require('ol.layer.Image');
|
||||||
|
goog.require('ol.proj.Projection');
|
||||||
|
goog.require('ol.raster.OperationType');
|
||||||
|
goog.require('ol.source.Image');
|
||||||
|
goog.require('ol.source.ImageStatic');
|
||||||
|
goog.require('ol.source.Raster');
|
||||||
|
goog.require('ol.source.Source');
|
||||||
|
goog.require('ol.source.Tile');
|
||||||
Reference in New Issue
Block a user