Merge pull request #8923 from tschaub/composite
Use the composite renderer
This commit is contained in:
@@ -2,15 +2,46 @@
|
||||
|
||||
### Next version
|
||||
|
||||
#### Backwards incompatible changes
|
||||
|
||||
Breaking change: The `OverviewMap` control now cannot be instantiated without a list of layers.
|
||||
##### Removal of the "vector" render mode for vector tile layers
|
||||
|
||||
Breaking change: layers can no longer be shared between several `Map` objects.
|
||||
If you were previously using `VectorTile` layers with `renderMode: 'vector'`, you have to remove this configuration option. That mode was removed. `'hybrid'` (default) and `'image'` are still available.
|
||||
|
||||
Breaking change: the `Graticule` control has been replaced by a layer also called `Graticule`, found in `ol/layer/Graticule`.
|
||||
The API remains similar.
|
||||
##### New `prerender` and `postrender` layer events replace old `precompose` and `postcompose` events
|
||||
|
||||
#### Breaking change: drop of support for most of WebGL features
|
||||
If you were previously registering for `precompose` and `postcompose` events, you should now register for `prerender` and `postrender` events on layers. Layers are no longer composed to a single Canvas element. Instead, they are added to the map viewport as individual elements.
|
||||
|
||||
##### New `getVectorContext` function provides access to the immediate vector rendering API
|
||||
|
||||
Previously, render events included a `vectorContext` property that allowed you to render features or geometries directly to the map. This is still possible, but you now have to explicitly create a vector context with the `getVectorContext` function. This change makes the immediate rendering API an explicit dependency if your application uses it. If you don't use this API, your application bundle will not include the vector rendering modules (as it did before).
|
||||
|
||||
Here is an abbreviated example of how to use the `getVectorContext` function:
|
||||
|
||||
```js
|
||||
import {getVectorContext} from 'ol/render';
|
||||
|
||||
// construct your map and layers as usual
|
||||
|
||||
layer.on('postrender', function(event) {
|
||||
const vectorContext = getVectorContext(event);
|
||||
// use any of the drawing methods on the vector context
|
||||
});
|
||||
```
|
||||
|
||||
##### Layers can only be added to a single map
|
||||
|
||||
Previously, it was possible to render a single layer in two maps. Now, each layer can only belong to a single map (in the same way that a single DOM element can only have one parent).
|
||||
|
||||
##### The `OverviewMap` requires a list of layers.
|
||||
|
||||
Due to the constraint above (layers can only be added to a single map), the overview map needs to be constructed with a list of layers.
|
||||
|
||||
##### The `ol/Graticule` has been replaced by `ol/layer/Graticule`
|
||||
|
||||
Previously, a graticule was not a layer. Now it is. See the graticule example for details on how to add a graticule layer to your map.
|
||||
|
||||
##### Drop of support for the experimental WebGL renderer
|
||||
|
||||
The WebGL map and layers renderers are gone, replaced by a `WebGLHelper` function that provides a lightweight,
|
||||
low-level access to the WebGL API. This is implemented in a new `WebGLPointsLayer` which does simple rendering of large number
|
||||
@@ -31,7 +62,6 @@ The removed classes and components are:
|
||||
* The shader build process using `mustache` and the `Makefile` at the root
|
||||
|
||||
|
||||
|
||||
### v5.3.0
|
||||
|
||||
#### The `getUid` function returns string
|
||||
@@ -42,7 +72,7 @@ The `getUid` function from the `ol/util` module now returns a string instead of
|
||||
|
||||
When a map contains a layer from a `ol/source/OSM` source, the `ol/control/Attribution` control will be shown with the `collapsible: false` behavior.
|
||||
|
||||
To get the previous behavior, configure the `ol/control/Attribution` control with `collapsible: true`.
|
||||
To get the previous behavior, configure the `ol/control/Attribution` control with `collapsible: true`.
|
||||
|
||||
### v5.2.0
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ Features for `updates` must have an id set by the feature reader or `ol.Feature#
|
||||
|
||||
### 28
|
||||
|
||||
`renderMode` must be `'image'`, `'hybrid'` or `'vector'`.
|
||||
`renderMode` must be `'image'` or `'hybrid'`.
|
||||
|
||||
### 29
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
.map{
|
||||
background-repeat: repeat;
|
||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAN1wAADdcBQiibeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAApSURBVBiVY7x///5/BjSgqKjIiC7GhC6ACwygQgxHMzAwMGDz4FDwDAD5/wevjSk4mwAAAABJRU5ErkJggg==);
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
layout: example.html
|
||||
title: Blend Modes
|
||||
shortdesc: Shows how to change the canvas compositing / blending mode in post- and precompose eventhandlers.
|
||||
docs: >
|
||||
<p>This example shows how to change the canvas compositing / blending mode in
|
||||
post- and precompose event handlers. The Canvas 2D API provides the property
|
||||
<code>globalCompositeOperation</code> with which one can influence which
|
||||
composition operation will be used when drawing on the canvas. The various
|
||||
options are well described on the <a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation">MDN
|
||||
documentation page</a>.</p>
|
||||
|
||||
<p>In this example three circles on the corners of an equilateral triangle are
|
||||
drawn with red, green or blue styles respectively. By setting the
|
||||
<code>globalCompositeOperation</code> you can change how these colors turn out
|
||||
when they are combined on the map.</p>
|
||||
|
||||
<p>You can select an operation in the select-field and you can also control
|
||||
which layers will be affected by the chosen operation through the layer
|
||||
checkboxes.</p>
|
||||
tags: "blendmode, blend-mode, blend mode, blendingmode, blending-mode, blending mode, composition, compositing, canvas, vector"
|
||||
---
|
||||
<div id="map" class="map"></div>
|
||||
<form class="form-horizontal">
|
||||
<label>
|
||||
<select id="blend-mode" class="form-control">
|
||||
<option value="source-over">source-over (default)</option>
|
||||
<option>source-in</option>
|
||||
<option>source-out</option>
|
||||
<option>source-atop</option>
|
||||
<option>destination-over</option>
|
||||
<option>destination-in</option>
|
||||
<option>destination-out</option>
|
||||
<option>destination-atop</option>
|
||||
<option>lighter</option>
|
||||
<option>copy</option>
|
||||
<option>xor</option>
|
||||
<option>multiply</option>
|
||||
<option>screen</option>
|
||||
<option>overlay</option>
|
||||
<option>darken</option>
|
||||
<option>lighten</option>
|
||||
<option>color-dodge</option>
|
||||
<option>color-burn</option>
|
||||
<option>hard-light</option>
|
||||
<option>soft-light</option>
|
||||
<option selected>difference</option>
|
||||
<option>exclusion</option>
|
||||
<option>hue</option>
|
||||
<option>saturation</option>
|
||||
<option>color</option>
|
||||
<option>luminosity</option>
|
||||
</select>
|
||||
Canvas compositing / blending mode
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="affect-red" checked>
|
||||
Red circle affected
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="affect-green" checked>
|
||||
Green circle affected
|
||||
</label>
|
||||
<label>
|
||||
<input type="checkbox" id="affect-blue" checked>
|
||||
Blue circle affected
|
||||
</label>
|
||||
</form>
|
||||
@@ -1,173 +0,0 @@
|
||||
import Feature from '../src/ol/Feature.js';
|
||||
import Map from '../src/ol/Map.js';
|
||||
import View from '../src/ol/View.js';
|
||||
import Point from '../src/ol/geom/Point.js';
|
||||
import VectorLayer from '../src/ol/layer/Vector.js';
|
||||
import VectorSource from '../src/ol/source/Vector.js';
|
||||
import {Circle as CircleStyle, Fill, Stroke, Style} from '../src/ol/style.js';
|
||||
|
||||
|
||||
// Create separate layers for red, green an blue circles.
|
||||
//
|
||||
// Every layer has one feature that is styled with a circle, together the
|
||||
// features form the corners of an equilateral triangle and their styles overlap
|
||||
const redLayer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [new Feature(new Point([0, 0]))]
|
||||
}),
|
||||
style: new Style({
|
||||
image: new CircleStyle({
|
||||
fill: new Fill({
|
||||
color: 'rgba(255,0,0,0.8)'
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgb(255,0,0)',
|
||||
width: 15
|
||||
}),
|
||||
radius: 120
|
||||
})
|
||||
})
|
||||
});
|
||||
const greenLayer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
// 433.013 is roughly 250 * Math.sqrt(3)
|
||||
features: [new Feature(new Point([250, 433.013]))]
|
||||
}),
|
||||
style: new Style({
|
||||
image: new CircleStyle({
|
||||
fill: new Fill({
|
||||
color: 'rgba(0,255,0,0.8)'
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgb(0,255,0)',
|
||||
width: 15
|
||||
}),
|
||||
radius: 120
|
||||
})
|
||||
})
|
||||
});
|
||||
const blueLayer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [new Feature(new Point([500, 0]))]
|
||||
}),
|
||||
style: new Style({
|
||||
image: new CircleStyle({
|
||||
fill: new Fill({
|
||||
color: 'rgba(0,0,255,0.8)'
|
||||
}),
|
||||
stroke: new Stroke({
|
||||
color: 'rgb(0,0,255)',
|
||||
width: 15
|
||||
}),
|
||||
radius: 120
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
// Create the map, the view is centered on the triangle. Zooming and panning is
|
||||
// restricted to a sane area
|
||||
const map = new Map({
|
||||
layers: [
|
||||
redLayer,
|
||||
greenLayer,
|
||||
blueLayer
|
||||
],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: [250, 220],
|
||||
extent: [0, 0, 500, 500],
|
||||
resolution: 4,
|
||||
minResolution: 2,
|
||||
maxResolution: 32
|
||||
})
|
||||
});
|
||||
|
||||
// Get the form elements and bind the listeners
|
||||
const select = document.getElementById('blend-mode');
|
||||
const affectRed = document.getElementById('affect-red');
|
||||
const affectGreen = document.getElementById('affect-green');
|
||||
const affectBlue = document.getElementById('affect-blue');
|
||||
|
||||
|
||||
/**
|
||||
* This method sets the globalCompositeOperation to the value of the select
|
||||
* field and it is bound to the precompose event of the layers.
|
||||
*
|
||||
* @param {module:ol/render/Event~RenderEvent} evt The render event.
|
||||
*/
|
||||
const setBlendModeFromSelect = function(evt) {
|
||||
evt.context.globalCompositeOperation = select.value;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* This method resets the globalCompositeOperation to the default value of
|
||||
* 'source-over' and it is bound to the postcompose event of the layers.
|
||||
*
|
||||
* @param {module:ol/render/Event~RenderEvent} evt The render event.
|
||||
*/
|
||||
const resetBlendModeFromSelect = function(evt) {
|
||||
evt.context.globalCompositeOperation = 'source-over';
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Bind the pre- and postcompose handlers to the passed layer.
|
||||
*
|
||||
* @param {module:ol/layer/Vector} layer The layer to bind the handlers to.
|
||||
*/
|
||||
const bindLayerListeners = function(layer) {
|
||||
layer.on('precompose', setBlendModeFromSelect);
|
||||
layer.on('postcompose', resetBlendModeFromSelect);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Unind the pre- and postcompose handlers to the passed layers.
|
||||
*
|
||||
* @param {module:ol/layer/Vector} layer The layer to unbind the handlers from.
|
||||
*/
|
||||
const unbindLayerListeners = function(layer) {
|
||||
layer.un('precompose', setBlendModeFromSelect);
|
||||
layer.un('postcompose', resetBlendModeFromSelect);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handler for the click event of the 'affect-XXX' checkboxes.
|
||||
*
|
||||
* @this {HTMLInputElement}
|
||||
*/
|
||||
const affectLayerClicked = function() {
|
||||
let layer;
|
||||
if (this.id == 'affect-red') {
|
||||
layer = redLayer;
|
||||
} else if (this.id == 'affect-green') {
|
||||
layer = greenLayer;
|
||||
} else {
|
||||
layer = blueLayer;
|
||||
}
|
||||
if (this.checked) {
|
||||
bindLayerListeners(layer);
|
||||
} else {
|
||||
unbindLayerListeners(layer);
|
||||
}
|
||||
map.render();
|
||||
};
|
||||
|
||||
|
||||
// Rerender map when blend mode changes
|
||||
select.addEventListener('change', function() {
|
||||
map.render();
|
||||
});
|
||||
|
||||
// Unbind / bind listeners depending on the checked state when the checkboxes
|
||||
// are clicked
|
||||
affectRed.addEventListener('click', affectLayerClicked);
|
||||
affectGreen.addEventListener('click', affectLayerClicked);
|
||||
affectBlue.addEventListener('click', affectLayerClicked);
|
||||
|
||||
// Initially bind listeners
|
||||
bindLayerListeners(redLayer);
|
||||
bindLayerListeners(greenLayer);
|
||||
bindLayerListeners(blueLayer);
|
||||
2
examples/d3.js
vendored
2
examples/d3.js
vendored
@@ -1,4 +1,4 @@
|
||||
import Map from '../src/ol/CompositeMap.js';
|
||||
import Map from '../src/ol/Map.js';
|
||||
import View from '../src/ol/View.js';
|
||||
import {getWidth, getCenter} from '../src/ol/extent.js';
|
||||
import {Layer, Tile as TileLayer} from '../src/ol/layer.js';
|
||||
|
||||
@@ -4,14 +4,14 @@ import {MultiPoint, Point} from '../src/ol/geom.js';
|
||||
import TileLayer from '../src/ol/layer/Tile.js';
|
||||
import OSM from '../src/ol/source/OSM.js';
|
||||
import {Circle as CircleStyle, Fill, Stroke, Style} from '../src/ol/style.js';
|
||||
import {getVectorContext} from '../src/ol/render.js';
|
||||
|
||||
const tileLayer = new TileLayer({
|
||||
source: new OSM()
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM()
|
||||
})
|
||||
],
|
||||
layers: [tileLayer],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: [0, 0],
|
||||
@@ -46,8 +46,8 @@ const omegaTheta = 30000; // Rotation period in ms
|
||||
const R = 7e6;
|
||||
const r = 2e6;
|
||||
const p = 2e6;
|
||||
map.on('postcompose', function(event) {
|
||||
const vectorContext = event.vectorContext;
|
||||
tileLayer.on('postrender', function(event) {
|
||||
const vectorContext = getVectorContext(event);
|
||||
const frameState = event.frameState;
|
||||
const theta = 2 * Math.PI * frameState.time / omegaTheta;
|
||||
const coordinates = [];
|
||||
|
||||
@@ -3,7 +3,7 @@ layout: example.html
|
||||
title: Custom Animation
|
||||
shortdesc: Demonstrates how to animate features.
|
||||
docs: >
|
||||
This example shows how to use <b>postcompose</b> and <b>vectorContext</b> to
|
||||
This example shows how to use <b>postrender</b> and <b>vectorContext</b> to
|
||||
animate features. Here we choose to do a flash animation each time a feature
|
||||
is added to the layer.
|
||||
tags: "animation, vector, feature, flash"
|
||||
|
||||
@@ -8,16 +8,16 @@ import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
|
||||
import {fromLonLat} from '../src/ol/proj.js';
|
||||
import {OSM, Vector as VectorSource} from '../src/ol/source.js';
|
||||
import {Circle as CircleStyle, Stroke, Style} from '../src/ol/style.js';
|
||||
import {getVectorContext} from '../src/ol/render.js';
|
||||
|
||||
const tileLayer = new TileLayer({
|
||||
source: new OSM({
|
||||
wrapX: false
|
||||
})
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM({
|
||||
wrapX: false
|
||||
})
|
||||
})
|
||||
],
|
||||
layers: [tileLayer],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: [0, 0],
|
||||
@@ -44,10 +44,10 @@ function addRandomFeature() {
|
||||
const duration = 3000;
|
||||
function flash(feature) {
|
||||
const start = new Date().getTime();
|
||||
const listenerKey = map.on('postcompose', animate);
|
||||
const listenerKey = tileLayer.on('postrender', animate);
|
||||
|
||||
function animate(event) {
|
||||
const vectorContext = event.vectorContext;
|
||||
const vectorContext = getVectorContext(event);
|
||||
const frameState = event.frameState;
|
||||
const flashGeom = feature.getGeometry().clone();
|
||||
const elapsed = frameState.time - start;
|
||||
@@ -72,7 +72,7 @@ function flash(feature) {
|
||||
unByKey(listenerKey);
|
||||
return;
|
||||
}
|
||||
// tell OpenLayers to continue postcompose animation
|
||||
// tell OpenLayers to continue postrender animation
|
||||
map.render();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ layout: example.html
|
||||
title: Marker Animation
|
||||
shortdesc: Demonstrates how to move a feature along a line.
|
||||
docs: >
|
||||
This example shows how to use <b>postcompose</b> and <b>vectorContext</b> to
|
||||
animate a (marker) feature along a line. In this example an encoded polyline
|
||||
This example shows how to use <b>postrender</b> events and a <b>vector context</b> to
|
||||
animate a marker feature along a line. In this example an encoded polyline
|
||||
is being used.
|
||||
tags: "animation, feature, postcompose, polyline"
|
||||
tags: "animation, feature, postrender, polyline"
|
||||
cloak:
|
||||
- key: As1HiMj1PvLPlqc_gtM7AqZfBL8ZL3VrjaS3zIb22Uvb9WKhuJObROC-qUpa81U5
|
||||
value: Your Bing Maps Key from http://www.bingmapsportal.com/ here
|
||||
|
||||
@@ -7,6 +7,7 @@ import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
|
||||
import BingMaps from '../src/ol/source/BingMaps.js';
|
||||
import VectorSource from '../src/ol/source/Vector.js';
|
||||
import {Circle as CircleStyle, Fill, Icon, Stroke, Style} from '../src/ol/style.js';
|
||||
import {getVectorContext} from '../src/ol/render.js';
|
||||
|
||||
// This long string is placed here due to jsFiddle limitations.
|
||||
// It is usually loaded with AJAX.
|
||||
@@ -142,7 +143,7 @@ const map = new Map({
|
||||
});
|
||||
|
||||
const moveFeature = function(event) {
|
||||
const vectorContext = event.vectorContext;
|
||||
const vectorContext = getVectorContext(event);
|
||||
const frameState = event.frameState;
|
||||
|
||||
if (animating) {
|
||||
@@ -160,7 +161,7 @@ const moveFeature = function(event) {
|
||||
const feature = new Feature(currentPoint);
|
||||
vectorContext.drawFeature(feature, styles.geoMarker);
|
||||
}
|
||||
// tell OpenLayers to continue the postcompose animation
|
||||
// tell OpenLayers to continue the postrender animation
|
||||
map.render();
|
||||
};
|
||||
|
||||
@@ -176,7 +177,7 @@ function startAnimation() {
|
||||
geoMarker.setStyle(null);
|
||||
// just in case you pan somewhere else
|
||||
map.getView().setCenter(center);
|
||||
map.on('postcompose', moveFeature);
|
||||
vectorLayer.on('postrender', moveFeature);
|
||||
map.render();
|
||||
}
|
||||
}
|
||||
@@ -194,7 +195,7 @@ function stopAnimation(ended) {
|
||||
/** @type {module:ol/geom/Point~Point} */ (geoMarker.getGeometry())
|
||||
.setCoordinates(coord);
|
||||
//remove listener
|
||||
map.un('postcompose', moveFeature);
|
||||
vectorLayer.un('postrender', moveFeature);
|
||||
}
|
||||
|
||||
startButton.addEventListener('click', startAnimation, false);
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
---
|
||||
layout: example.html
|
||||
title: Flight Animation
|
||||
shortdesc: Demonstrates how to animate flights with ´postcompose´.
|
||||
shortdesc: Demonstrates how to animate flights with ´postrender´.
|
||||
docs: >
|
||||
This example shows how to use <b>postcompose</b> and <b>vectorContext</b> to
|
||||
This example shows how to use <b>postrender</b> and <b>vectorContext</b> to
|
||||
animate flights. A great circle arc between two airports is calculated using
|
||||
<a href="https://github.com/springmeyer/arc.js">arc.js</a> and then the flight
|
||||
paths are animated with <b>postcompose</b>. The flight data is provided by
|
||||
paths are animated with <b>postrender</b>. The flight data is provided by
|
||||
<a href="http://openflights.org/data.html">OpenFlights</a> (a simplified data
|
||||
set from the <a href="https://www.mapbox.com/mapbox.js/example/v1.0.0/animating-flight-paths/">
|
||||
Mapbox.js documentation</a> is used).
|
||||
|
||||
@@ -6,14 +6,17 @@ import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
|
||||
import Stamen from '../src/ol/source/Stamen.js';
|
||||
import VectorSource from '../src/ol/source/Vector.js';
|
||||
import {Stroke, Style} from '../src/ol/style.js';
|
||||
import {getVectorContext} from '../src/ol/render.js';
|
||||
|
||||
const tileLayer = new TileLayer({
|
||||
source: new Stamen({
|
||||
layer: 'toner'
|
||||
})
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new Stamen({
|
||||
layer: 'toner'
|
||||
})
|
||||
})
|
||||
tileLayer
|
||||
],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
@@ -63,7 +66,7 @@ const flightsSource = new VectorSource({
|
||||
addLater(feature, i * 50);
|
||||
}
|
||||
}
|
||||
map.on('postcompose', animateFlights);
|
||||
tileLayer.on('postrender', animateFlights);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -85,7 +88,7 @@ map.addLayer(flightsLayer);
|
||||
|
||||
const pointsPerMs = 0.1;
|
||||
function animateFlights(event) {
|
||||
const vectorContext = event.vectorContext;
|
||||
const vectorContext = getVectorContext(event);
|
||||
const frameState = event.frameState;
|
||||
vectorContext.setStyle(style);
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ const view = new View({
|
||||
zoom: 19
|
||||
});
|
||||
|
||||
const tileLayer = new TileLayer({
|
||||
source: new OSM()
|
||||
});
|
||||
|
||||
// creating the map
|
||||
const map = new Map({
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new OSM()
|
||||
})
|
||||
],
|
||||
layers: [tileLayer],
|
||||
target: 'map',
|
||||
view: view
|
||||
});
|
||||
@@ -155,7 +155,7 @@ const geolocateBtn = document.getElementById('geolocate');
|
||||
geolocateBtn.addEventListener('click', function() {
|
||||
geolocation.setTracking(true); // Start position tracking
|
||||
|
||||
map.on('postcompose', updateView);
|
||||
tileLayer.on('postrender', updateView);
|
||||
map.render();
|
||||
|
||||
disableButtons();
|
||||
@@ -197,7 +197,7 @@ simulateBtn.addEventListener('click', function() {
|
||||
}
|
||||
geolocate();
|
||||
|
||||
map.on('postcompose', updateView);
|
||||
tileLayer.on('postrender', updateView);
|
||||
map.render();
|
||||
|
||||
disableButtons();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../src/ol/CompositeMap.js';
|
||||
import Map from '../src/ol/Map.js';
|
||||
import View from '../src/ol/View.js';
|
||||
import KML from '../src/ol/format/KML.js';
|
||||
import {Heatmap as HeatmapLayer, Tile as TileLayer} from '../src/ol/layer.js';
|
||||
|
||||
@@ -7,6 +7,7 @@ import {Tile as TileLayer, Vector as VectorLayer} from '../src/ol/layer.js';
|
||||
import OSM, {ATTRIBUTION} from '../src/ol/source/OSM.js';
|
||||
import VectorSource from '../src/ol/source/Vector.js';
|
||||
import {Circle as CircleStyle, Fill, Stroke, Style} from '../src/ol/style.js';
|
||||
import {getVectorContext} from '../src/ol/render.js';
|
||||
|
||||
|
||||
const colors = {
|
||||
@@ -73,6 +74,10 @@ vectorSource.on('addfeature', function(event) {
|
||||
time.duration = time.stop - time.start;
|
||||
});
|
||||
|
||||
const vectorLayer = new VectorLayer({
|
||||
source: vectorSource,
|
||||
style: styleFunction
|
||||
});
|
||||
|
||||
const map = new Map({
|
||||
layers: [
|
||||
@@ -86,10 +91,7 @@ const map = new Map({
|
||||
'?apikey=0e6fc415256d4fbb9b5166a718591d71'
|
||||
})
|
||||
}),
|
||||
new VectorLayer({
|
||||
source: vectorSource,
|
||||
style: styleFunction
|
||||
})
|
||||
vectorLayer
|
||||
],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
@@ -153,8 +155,8 @@ const style = new Style({
|
||||
stroke: stroke
|
||||
})
|
||||
});
|
||||
map.on('postcompose', function(evt) {
|
||||
const vectorContext = evt.vectorContext;
|
||||
vectorLayer.on('postrender', function(evt) {
|
||||
const vectorContext = getVectorContext(evt);
|
||||
vectorContext.setStyle(style);
|
||||
if (point !== null) {
|
||||
vectorContext.drawGeometry(point);
|
||||
|
||||
@@ -3,9 +3,9 @@ layout: example.html
|
||||
title: Image Filters
|
||||
shortdesc: Apply a filter to imagery
|
||||
docs: >
|
||||
<p>Layer rendering can be manipulated in <code>precompose</code> and <code>postcompose</code> event listeners.
|
||||
<p>Layer rendering can be manipulated in <code>prerender</code> and <code>postrender</code> event listeners.
|
||||
These listeners get an event with a reference to the Canvas rendering context.
|
||||
In this example, the <code>postcompose</code> listener applies a filter to the image data.</p>
|
||||
In this example, the <code>postrender</code> listener applies a filter to the image data.</p>
|
||||
tags: "filter, image manipulation"
|
||||
cloak:
|
||||
- key: As1HiMj1PvLPlqc_gtM7AqZfBL8ZL3VrjaS3zIb22Uvb9WKhuJObROC-qUpa81U5
|
||||
|
||||
@@ -90,9 +90,9 @@ select.onchange = function() {
|
||||
|
||||
|
||||
/**
|
||||
* Apply a filter on "postcompose" events.
|
||||
* Apply a filter on "postrender" events.
|
||||
*/
|
||||
imagery.on('postcompose', function(event) {
|
||||
imagery.on('postrender', function(event) {
|
||||
convolve(event.context, selectedKernel);
|
||||
});
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ const map = new Map({
|
||||
})
|
||||
});
|
||||
|
||||
osm.on('precompose', function(event) {
|
||||
osm.on('prerender', function(event) {
|
||||
const ctx = event.context;
|
||||
ctx.save();
|
||||
const pixelRatio = event.frameState.pixelRatio;
|
||||
@@ -38,7 +38,7 @@ osm.on('precompose', function(event) {
|
||||
ctx.translate(-size[0] / 2 * pixelRatio, -size[1] / 2 * pixelRatio);
|
||||
});
|
||||
|
||||
osm.on('postcompose', function(event) {
|
||||
osm.on('postrender', function(event) {
|
||||
const ctx = event.context;
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
@@ -3,9 +3,9 @@ layout: example.html
|
||||
title: Layer Spy
|
||||
shortdesc: View a portion of one layer over another
|
||||
docs: >
|
||||
<p>Layer rendering can be manipulated in <code>precompose</code> and <code>postcompose</code> event listeners.
|
||||
<p>Layer rendering can be manipulated in <code>prerender</code> and <code>postrender</code> event listeners.
|
||||
These listeners get an event with a reference to the Canvas rendering context.
|
||||
In this example, the <code>precompose</code> listener sets a clipping mask around the most
|
||||
In this example, the <code>prerender</code> listener sets a clipping mask around the most
|
||||
recent mouse position, giving you a spyglass effect for viewing one layer over another.</p>
|
||||
<p>Move around the map to see the effect. Use the ↑ up and ↓ down arrow keys to adjust the spyglass size.</p>
|
||||
tags: "spy, image manipulation"
|
||||
|
||||
@@ -52,7 +52,7 @@ container.addEventListener('mouseout', function() {
|
||||
});
|
||||
|
||||
// before rendering the layer, do some clipping
|
||||
imagery.on('precompose', function(event) {
|
||||
imagery.on('prerender', function(event) {
|
||||
const ctx = event.context;
|
||||
const pixelRatio = event.frameState.pixelRatio;
|
||||
ctx.save();
|
||||
@@ -69,7 +69,7 @@ imagery.on('precompose', function(event) {
|
||||
});
|
||||
|
||||
// after rendering the layer, restore the canvas context
|
||||
imagery.on('postcompose', function(event) {
|
||||
imagery.on('postrender', function(event) {
|
||||
const ctx = event.context;
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ const map = new Map({
|
||||
|
||||
const swipe = document.getElementById('swipe');
|
||||
|
||||
bing.on('precompose', function(event) {
|
||||
bing.on('prerender', function(event) {
|
||||
const ctx = event.context;
|
||||
const width = ctx.canvas.width * (swipe.value / 100);
|
||||
|
||||
@@ -35,7 +35,7 @@ bing.on('precompose', function(event) {
|
||||
ctx.clip();
|
||||
});
|
||||
|
||||
bing.on('postcompose', function(event) {
|
||||
bing.on('postrender', function(event) {
|
||||
const ctx = event.context;
|
||||
ctx.restore();
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@ layout: example.html
|
||||
title: Magnify
|
||||
shortdesc: Show a magnified version of imager under the pointer
|
||||
docs: >
|
||||
<p>This example makes use of the <code>postcompose</code> event listener to
|
||||
<p>This example makes use of the <code>postrender</code> event listener to
|
||||
oversample imagery in a circle around the pointer location. Listeners for this event have access to the Canvas context and can manipulate image data.</p>
|
||||
<p>Move around the map to see the effect. Use the ↑ up and ↓ down arrow keys to adjust the magnified circle size.</p>
|
||||
tags: "magnify, image manipulation"
|
||||
|
||||
@@ -3,6 +3,7 @@ import View from '../src/ol/View.js';
|
||||
import TileLayer from '../src/ol/layer/Tile.js';
|
||||
import {fromLonLat} from '../src/ol/proj.js';
|
||||
import BingMaps from '../src/ol/source/BingMaps.js';
|
||||
import {getPixelFromPixel} from '../src/ol/render.js';
|
||||
|
||||
const key = 'As1HiMj1PvLPlqc_gtM7AqZfBL8ZL3VrjaS3zIb22Uvb9WKhuJObROC-qUpa81U5';
|
||||
|
||||
@@ -48,16 +49,17 @@ container.addEventListener('mouseout', function() {
|
||||
});
|
||||
|
||||
// after rendering the layer, show an oversampled version around the pointer
|
||||
imagery.on('postcompose', function(event) {
|
||||
imagery.on('postrender', function(event) {
|
||||
if (mousePosition) {
|
||||
const pixel = getPixelFromPixel(event, mousePosition);
|
||||
const offset = getPixelFromPixel(event, [mousePosition[0] + radius, mousePosition[1]]);
|
||||
const half = Math.sqrt(Math.pow(offset[0] - pixel[0], 2) + Math.pow(offset[1] - pixel[1], 2));
|
||||
const context = event.context;
|
||||
const pixelRatio = event.frameState.pixelRatio;
|
||||
const half = radius * pixelRatio;
|
||||
const centerX = mousePosition[0] * pixelRatio;
|
||||
const centerY = mousePosition[1] * pixelRatio;
|
||||
const centerX = pixel[0];
|
||||
const centerY = pixel[1];
|
||||
const originX = centerX - half;
|
||||
const originY = centerY - half;
|
||||
const size = 2 * half + 1;
|
||||
const size = Math.round(2 * half + 1);
|
||||
const sourceData = context.getImageData(originX, originY, size, size).data;
|
||||
const dest = context.createImageData(size, size);
|
||||
const destData = dest.data;
|
||||
@@ -82,7 +84,7 @@ imagery.on('postcompose', function(event) {
|
||||
}
|
||||
context.beginPath();
|
||||
context.arc(centerX, centerY, half, 0, 2 * Math.PI);
|
||||
context.lineWidth = 3 * pixelRatio;
|
||||
context.lineWidth = 3 * half / radius;
|
||||
context.strokeStyle = 'rgba(255,255,255,0.5)';
|
||||
context.putImageData(dest, originX, originY);
|
||||
context.stroke();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../src/ol/CompositeMap.js';
|
||||
import Map from '../src/ol/Map.js';
|
||||
import View from '../src/ol/View.js';
|
||||
import Layer from '../src/ol/layer/Layer';
|
||||
import {assign} from '../src/ol/obj';
|
||||
|
||||
@@ -2,7 +2,7 @@ import Map from '../src/ol/Map.js';
|
||||
import View from '../src/ol/View.js';
|
||||
import {Image as ImageLayer, Tile as TileLayer} from '../src/ol/layer.js';
|
||||
import {fromLonLat} from '../src/ol/proj.js';
|
||||
import RasterSource from '../src/ol/source/Raster.js';
|
||||
import {Raster as RasterSource, TileJSON} from '../src/ol/source.js';
|
||||
import XYZ from '../src/ol/source/XYZ.js';
|
||||
|
||||
function flood(pixels, data) {
|
||||
@@ -37,8 +37,9 @@ const map = new Map({
|
||||
target: 'map',
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new XYZ({
|
||||
url: 'https://api.mapbox.com/styles/v1/tschaub/ciutc102t00c62js5fqd47kqw/tiles/256/{z}/{x}/{y}?access_token=' + key
|
||||
source: new TileJSON({
|
||||
url: 'https://api.tiles.mapbox.com/v3/mapbox.world-light.json?secure',
|
||||
crossOrigin: 'anonymous'
|
||||
})
|
||||
}),
|
||||
new ImageLayer({
|
||||
|
||||
@@ -5,6 +5,7 @@ import {LineString, Point} from '../src/ol/geom.js';
|
||||
import VectorLayer from '../src/ol/layer/Vector.js';
|
||||
import VectorSource from '../src/ol/source/Vector.js';
|
||||
import {Circle as CircleStyle, Fill, Stroke, Style} from '../src/ol/style.js';
|
||||
import {getVectorContext} from '../src/ol/render.js';
|
||||
|
||||
|
||||
const count = 20000;
|
||||
@@ -104,8 +105,8 @@ const style = new Style({
|
||||
})
|
||||
});
|
||||
|
||||
map.on('postcompose', function(evt) {
|
||||
const vectorContext = evt.vectorContext;
|
||||
vector.on('postrender', function(evt) {
|
||||
const vectorContext = getVectorContext(evt);
|
||||
vectorContext.setStyle(style);
|
||||
if (point !== null) {
|
||||
vectorContext.drawGeometry(point);
|
||||
|
||||
@@ -152,6 +152,9 @@ ExampleBuilder.prototype.render = async function(dir, chunk) {
|
||||
// add in script tag
|
||||
const jsName = `${name}.js`;
|
||||
let jsSource = getJsSource(chunk, path.join('.', jsName));
|
||||
if (!jsSource) {
|
||||
throw new Error(`No .js source for ${jsName}`);
|
||||
}
|
||||
jsSource = jsSource.replace(/'\.\.\/src\//g, '\'');
|
||||
if (data.cloak) {
|
||||
for (const entry of data.cloak) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 38 KiB |
@@ -1,47 +0,0 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import TileLayer from '../../../src/ol/layer/Tile.js';
|
||||
import {fromLonLat} from '../../../src/ol/proj';
|
||||
import XYZ from '../../../src/ol/source/XYZ';
|
||||
import {getSize} from '../../../src/ol/extent';
|
||||
|
||||
const center = fromLonLat([8.6, 50.1]);
|
||||
|
||||
const map = new Map({
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: center,
|
||||
zoom: 3
|
||||
})
|
||||
});
|
||||
|
||||
const layerExtent = centerExtent();
|
||||
|
||||
map.addLayer(
|
||||
new TileLayer({
|
||||
source: new XYZ({
|
||||
url: '/data/tiles/satellite/{z}/{x}/{y}.jpg'
|
||||
}),
|
||||
extent: layerExtent
|
||||
}),
|
||||
);
|
||||
|
||||
map.addLayer(
|
||||
new TileLayer({
|
||||
source: new XYZ({
|
||||
url: '/data/tiles/stamen-labels/{z}/{x}/{y}.png'
|
||||
}),
|
||||
extent: layerExtent
|
||||
})
|
||||
);
|
||||
|
||||
render();
|
||||
|
||||
function centerExtent() {
|
||||
const c = map.getView().calculateExtent([256, 256]);
|
||||
const qw = getSize(c)[0] / 4;
|
||||
const qh = getSize(c)[1] / 4;
|
||||
return [c[0] + qw, c[1] + qh, c[2] - qw, c[3] - qh];
|
||||
}
|
||||
|
||||
|
||||
BIN
rendering/cases/layer-tile-extent/expected.png
Normal file
BIN
rendering/cases/layer-tile-extent/expected.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
40
rendering/cases/layer-tile-extent/main.js
Normal file
40
rendering/cases/layer-tile-extent/main.js
Normal file
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* Tile layers get clipped to their extent.
|
||||
*/
|
||||
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import TileLayer from '../../../src/ol/layer/Tile.js';
|
||||
import {fromLonLat} from '../../../src/ol/proj';
|
||||
import {transformExtent} from '../../../src/ol/proj.js';
|
||||
import XYZ from '../../../src/ol/source/XYZ';
|
||||
|
||||
const center = fromLonLat([7, 50]);
|
||||
const extent = transformExtent([2, 47, 10, 53], 'EPSG:4326', 'EPSG:3857');
|
||||
|
||||
new Map({
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: center,
|
||||
zoom: 3
|
||||
}),
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new XYZ({
|
||||
url: '/data/tiles/satellite/{z}/{x}/{y}.jpg',
|
||||
maxZoom: 3
|
||||
}),
|
||||
extent: extent
|
||||
}),
|
||||
new TileLayer({
|
||||
source: new XYZ({
|
||||
url: '/data/tiles/stamen-labels/{z}/{x}/{y}.png',
|
||||
minZoom: 3,
|
||||
maxZoom: 5
|
||||
}),
|
||||
extent: extent
|
||||
})
|
||||
]
|
||||
});
|
||||
|
||||
render();
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import TileLayer from '../../../src/ol/layer/Tile.js';
|
||||
import {fromLonLat} from '../../../src/ol/proj';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import TileLayer from '../../../src/ol/layer/Tile.js';
|
||||
import {fromLonLat} from '../../../src/ol/proj';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import VectorTileSource from '../../../src/ol/source/VectorTile';
|
||||
import MVT from '../../../src/ol/format/MVT';
|
||||
@@ -23,4 +23,7 @@ new Map({
|
||||
})
|
||||
});
|
||||
|
||||
render({message: 'Vector tile layer renders'});
|
||||
render({
|
||||
message: 'Vector tile layer renders',
|
||||
tolerance: 0.02
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import Feature from '../../../src/ol/Feature.js';
|
||||
import LineString from '../../../src/ol/geom/LineString.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import Feature from '../../../src/ol/Feature.js';
|
||||
import LineString from '../../../src/ol/geom/LineString.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import Feature from '../../../src/ol/Feature.js';
|
||||
import LineString from '../../../src/ol/geom/LineString.js';
|
||||
@@ -67,4 +67,4 @@ new Map({
|
||||
})
|
||||
});
|
||||
|
||||
render();
|
||||
render({tolerance: 0.02});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import Map from '../../../src/ol/CompositeMap.js';
|
||||
import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import Feature from '../../../src/ol/Feature.js';
|
||||
import LineString from '../../../src/ol/geom/LineString.js';
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 101 KiB |
@@ -2,33 +2,34 @@ import Map from '../../../src/ol/Map.js';
|
||||
import View from '../../../src/ol/View.js';
|
||||
import {Vector as VectorLayer, Tile as TileLayer} from '../../../src/ol/layer.js';
|
||||
import {Vector as VectorSource, XYZ} from '../../../src/ol/source.js';
|
||||
import Point from '../../../src/ol/geom/Point.js';
|
||||
import Feature from '../../../src/ol/Feature.js';
|
||||
import {fromLonLat} from '../../../src/ol/proj.js';
|
||||
|
||||
const center = fromLonLat([-111, 45.7]);
|
||||
import GeoJSON from '../../../src/ol/format/GeoJSON.js';
|
||||
import {Style, Stroke} from '../../../src/ol/style.js';
|
||||
|
||||
new Map({
|
||||
layers: [
|
||||
new TileLayer({
|
||||
source: new XYZ({
|
||||
url: '/data/tiles/satellite/{z}/{x}/{y}.jpg'
|
||||
url: '/data/tiles/satellite/{z}/{x}/{y}.jpg',
|
||||
maxZoom: 3
|
||||
})
|
||||
}),
|
||||
new VectorLayer({
|
||||
style: new Style({
|
||||
stroke: new Stroke({
|
||||
color: 'rgba(255,255,255,0.5)',
|
||||
width: 0.75
|
||||
})
|
||||
}),
|
||||
source: new VectorSource({
|
||||
features: [
|
||||
new Feature(
|
||||
new Point(center)
|
||||
)
|
||||
]
|
||||
url: '/data/countries.json',
|
||||
format: new GeoJSON()
|
||||
})
|
||||
})
|
||||
],
|
||||
target: 'map',
|
||||
view: new View({
|
||||
center: center,
|
||||
zoom: 3
|
||||
center: [0, 0],
|
||||
zoom: 2
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
181
rendering/data/countries.json
Normal file
181
rendering/data/countries.json
Normal file
File diff suppressed because one or more lines are too long
@@ -182,24 +182,30 @@ async function copyActualToExpected(entry) {
|
||||
async function renderEach(page, entries, options) {
|
||||
let fail = false;
|
||||
for (const entry of entries) {
|
||||
const config = await renderPage(page, entry, options);
|
||||
const message = config.message !== undefined ? config.message : entry;
|
||||
const tolerance = config.tolerance !== undefined ? config.tolerance : 0;
|
||||
const {tolerance = 0, message = ''} = await renderPage(page, entry, options);
|
||||
|
||||
if (options.fix) {
|
||||
await copyActualToExpected(entry);
|
||||
continue;
|
||||
}
|
||||
|
||||
const {error, mismatch} = await getScreenshotsMismatch(entry);
|
||||
if (error) {
|
||||
options.log.error(error);
|
||||
fail = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
let detail = `case ${entry}`;
|
||||
if (message) {
|
||||
detail = `${detail} (${message})`;
|
||||
}
|
||||
|
||||
if (mismatch > tolerance) {
|
||||
options.log.error(`checking '${message}': mismatch ${mismatch.toFixed(3)}`);
|
||||
options.log.error(`${detail}': mismatch ${mismatch.toFixed(3)}`);
|
||||
fail = true;
|
||||
} else {
|
||||
options.log.info(`checking '${message}': ok`);
|
||||
options.log.info(`${detail}': ok`);
|
||||
await touch(getPassFilePath(entry));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
/**
|
||||
* @module ol/CompositeMap
|
||||
*/
|
||||
import PluggableMap from './PluggableMap.js';
|
||||
import {defaults as defaultControls} from './control/util.js';
|
||||
import {defaults as defaultInteractions} from './interaction.js';
|
||||
import {assign} from './obj.js';
|
||||
import CompositeMapRenderer from './renderer/Composite.js';
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* The map is the core component of OpenLayers. For a map to render, a view,
|
||||
* one or more layers, and a target container are needed:
|
||||
*
|
||||
* import Map from 'ol/Map';
|
||||
* import View from 'ol/View';
|
||||
* import TileLayer from 'ol/layer/Tile';
|
||||
* import OSM from 'ol/source/OSM';
|
||||
*
|
||||
* var map = new Map({
|
||||
* view: new View({
|
||||
* center: [0, 0],
|
||||
* zoom: 1
|
||||
* }),
|
||||
* layers: [
|
||||
* new TileLayer({
|
||||
* source: new OSM()
|
||||
* })
|
||||
* ],
|
||||
* target: 'map'
|
||||
* });
|
||||
*
|
||||
* The above snippet creates a map using a {@link module:ol/layer/Tile} to
|
||||
* display {@link module:ol/source/OSM~OSM} OSM data and render it to a DOM
|
||||
* element with the id `map`.
|
||||
*
|
||||
* The constructor places a viewport container (with CSS class name
|
||||
* `ol-viewport`) in the target element (see `getViewport()`), and then two
|
||||
* further elements within the viewport: one with CSS class name
|
||||
* `ol-overlaycontainer-stopevent` for controls and some overlays, and one with
|
||||
* CSS class name `ol-overlaycontainer` for other overlays (see the `stopEvent`
|
||||
* option of {@link module:ol/Overlay~Overlay} for the difference). The map
|
||||
* itself is placed in a further element within the viewport.
|
||||
*
|
||||
* Layers are stored as a {@link module:ol/Collection~Collection} in
|
||||
* layerGroups. A top-level group is provided by the library. This is what is
|
||||
* accessed by `getLayerGroup` and `setLayerGroup`. Layers entered in the
|
||||
* options are added to this group, and `addLayer` and `removeLayer` change the
|
||||
* layer collection in the group. `getLayers` is a convenience function for
|
||||
* `getLayerGroup().getLayers()`. Note that {@link module:ol/layer/Group~Group}
|
||||
* is a subclass of {@link module:ol/layer/Base}, so layers entered in the
|
||||
* options or added with `addLayer` can be groups, which can contain further
|
||||
* groups, and so on.
|
||||
*
|
||||
* @fires import("./MapBrowserEvent.js").MapBrowserEvent
|
||||
* @fires import("./MapEvent.js").MapEvent
|
||||
* @fires module:ol/render/Event~RenderEvent#postcompose
|
||||
* @fires module:ol/render/Event~RenderEvent#precompose
|
||||
* @api
|
||||
*/
|
||||
class CompositeMap extends PluggableMap {
|
||||
|
||||
/**
|
||||
* @param {import("./PluggableMap.js").MapOptions} options Map options.
|
||||
*/
|
||||
constructor(options) {
|
||||
options = assign({}, options);
|
||||
if (!options.controls) {
|
||||
options.controls = defaultControls();
|
||||
}
|
||||
if (!options.interactions) {
|
||||
options.interactions = defaultInteractions();
|
||||
}
|
||||
|
||||
super(options);
|
||||
}
|
||||
|
||||
createRenderer() {
|
||||
return new CompositeMapRenderer(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default CompositeMap;
|
||||
@@ -5,7 +5,7 @@ import PluggableMap from './PluggableMap.js';
|
||||
import {defaults as defaultControls} from './control/util.js';
|
||||
import {defaults as defaultInteractions} from './interaction.js';
|
||||
import {assign} from './obj.js';
|
||||
import CanvasMapRenderer from './renderer/canvas/Map.js';
|
||||
import CompositeMapRenderer from './renderer/Composite.js';
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
@@ -76,7 +76,7 @@ class Map extends PluggableMap {
|
||||
}
|
||||
|
||||
createRenderer() {
|
||||
return new CanvasMapRenderer(this);
|
||||
return new CompositeMapRenderer(this);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -623,8 +623,7 @@ class PluggableMap extends BaseObject {
|
||||
const hitTolerance = options.hitTolerance !== undefined ?
|
||||
opt_options.hitTolerance * this.frameState_.pixelRatio : 0;
|
||||
const layerFilter = options.layerFilter || TRUE;
|
||||
return this.renderer_.forEachLayerAtPixel(
|
||||
pixel, this.frameState_, hitTolerance, callback, null, layerFilter, null);
|
||||
return this.renderer_.forEachLayerAtPixel(pixel, this.frameState_, hitTolerance, callback, layerFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,7 @@ import Tile from './Tile.js';
|
||||
import TileState from './TileState.js';
|
||||
import {createCanvasContext2D} from './dom.js';
|
||||
import {listen, unlistenByKey} from './events.js';
|
||||
import {getHeight, getIntersection, getWidth} from './extent.js';
|
||||
import {containsExtent, getHeight, getIntersection, getWidth} from './extent.js';
|
||||
import EventType from './events/EventType.js';
|
||||
import {loadFeaturesXhr} from './featureloader.js';
|
||||
import {VOID} from './functions.js';
|
||||
@@ -148,26 +148,44 @@ class VectorImageTile extends Tile {
|
||||
this.finishLoading_();
|
||||
}
|
||||
|
||||
if (zoom <= tileCoord[0] && this.state != TileState.LOADED) {
|
||||
while (zoom > tileGrid.getMinZoom()) {
|
||||
const tile = new VectorImageTile(tileCoord, state, sourceRevision,
|
||||
format, tileLoadFunction, urlTileCoord, tileUrlFunction,
|
||||
sourceTileGrid, tileGrid, sourceTiles, pixelRatio, projection,
|
||||
tileClass, VOID, --zoom);
|
||||
if (tile.state == TileState.LOADED) {
|
||||
this.createInterimTile_ = function() {
|
||||
if (this.getState() !== TileState.LOADED && !useLoadedOnly) {
|
||||
let bestZoom = -1;
|
||||
for (const key in sourceTiles) {
|
||||
const sourceTile = sourceTiles[key];
|
||||
if (sourceTile.getState() === TileState.LOADED) {
|
||||
const sourceTileCoord = sourceTile.tileCoord;
|
||||
const sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord);
|
||||
if (containsExtent(sourceTileExtent, extent) && sourceTileCoord[0] > bestZoom) {
|
||||
bestZoom = sourceTileCoord[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
if (bestZoom !== -1) {
|
||||
const tile = new VectorImageTile(tileCoord, state, sourceRevision,
|
||||
format, tileLoadFunction, urlTileCoord, tileUrlFunction,
|
||||
sourceTileGrid, tileGrid, sourceTiles, pixelRatio, projection,
|
||||
tileClass, VOID, bestZoom);
|
||||
this.interimTile = tile;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getInterimTile() {
|
||||
if (!this.interimTile) {
|
||||
this.createInterimTile_();
|
||||
}
|
||||
return super.getInterimTile();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
disposeInternal() {
|
||||
delete this.createInterimTile_;
|
||||
this.state = TileState.ABORT;
|
||||
this.changed();
|
||||
if (this.interimTile) {
|
||||
@@ -312,6 +330,7 @@ class VectorImageTile extends Tile {
|
||||
if (loaded == this.tileKeys.length) {
|
||||
this.loadListenerKeys_.forEach(unlistenByKey);
|
||||
this.loadListenerKeys_.length = 0;
|
||||
this.sourceTilesLoaded = true;
|
||||
this.setState(TileState.LOADED);
|
||||
} else {
|
||||
this.setState(empty == this.tileKeys.length ? TileState.EMPTY : TileState.ERROR);
|
||||
|
||||
@@ -94,9 +94,8 @@ class VectorTileLayer extends BaseVectorLayer {
|
||||
let renderMode = options.renderMode || VectorTileRenderType.HYBRID;
|
||||
assert(renderMode == undefined ||
|
||||
renderMode == VectorTileRenderType.IMAGE ||
|
||||
renderMode == VectorTileRenderType.HYBRID ||
|
||||
renderMode == VectorTileRenderType.VECTOR,
|
||||
28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'`
|
||||
renderMode == VectorTileRenderType.HYBRID,
|
||||
28); // `renderMode` must be `'image'` or `'hybrid'`
|
||||
|
||||
if (options.declutter && renderMode == VectorTileRenderType.IMAGE) {
|
||||
renderMode = VectorTileRenderType.HYBRID;
|
||||
|
||||
@@ -17,6 +17,5 @@
|
||||
*/
|
||||
export default {
|
||||
IMAGE: 'image',
|
||||
HYBRID: 'hybrid',
|
||||
VECTOR: 'vector'
|
||||
HYBRID: 'hybrid'
|
||||
};
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
* @module ol/render
|
||||
*/
|
||||
import {DEVICE_PIXEL_RATIO} from './has.js';
|
||||
import {create as createTransform, scale as scaleTransform} from './transform.js';
|
||||
import {
|
||||
apply as applyTransform,
|
||||
create as createTransform,
|
||||
invert as invertTransform,
|
||||
multiply as multiplyTransform,
|
||||
scale as scaleTransform
|
||||
} from './transform.js';
|
||||
import CanvasImmediateRenderer from './render/canvas/Immediate.js';
|
||||
|
||||
|
||||
@@ -77,3 +83,31 @@ export function toContext(context, opt_options) {
|
||||
const transform = scaleTransform(createTransform(), pixelRatio, pixelRatio);
|
||||
return new CanvasImmediateRenderer(context, pixelRatio, extent, transform, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a vector context for drawing to the event's canvas.
|
||||
* @param {import("./render/Event.js").default} event Render event.
|
||||
* @returns {CanvasImmediateRenderer} Vector context.
|
||||
* @api
|
||||
*/
|
||||
export function getVectorContext(event) {
|
||||
const frameState = event.frameState;
|
||||
const transform = multiplyTransform(invertTransform(event.pixelTransform.slice()), frameState.coordinateToPixelTransform);
|
||||
return new CanvasImmediateRenderer(
|
||||
event.context, frameState.pixelRatio, frameState.extent, transform,
|
||||
frameState.viewState.rotation);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the pixel of the event's canvas context from the map viewport's css pixel
|
||||
* @param {import("./render/Event.js").default} event Render event.
|
||||
* @param {import("./pixel.js").Pixel} pixel Css pixel relative to the top-left
|
||||
* corner of the map viewport.
|
||||
* @returns {import("./pixel.js").Pixel} Pixel on the event's canvas context.
|
||||
* @api
|
||||
*/
|
||||
export function getPixelFromPixel(event, pixel) {
|
||||
const result = pixel.slice(0);
|
||||
applyTransform(invertTransform(event.pixelTransform.slice()), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -8,21 +8,22 @@ class RenderEvent extends Event {
|
||||
|
||||
/**
|
||||
* @param {import("./EventType.js").default} type Type.
|
||||
* @param {import("./VectorContext.js").default=} opt_vectorContext Vector context.
|
||||
* @param {import("../transform.js").Transform=} opt_pixelTransform Transform.
|
||||
* @param {import("../PluggableMap.js").FrameState=} opt_frameState Frame state.
|
||||
* @param {?CanvasRenderingContext2D=} opt_context Context.
|
||||
* @param {?import("../webgl/Helper.js").default=} opt_glContext WebGL Context.
|
||||
*/
|
||||
constructor(type, opt_vectorContext, opt_frameState, opt_context, opt_glContext) {
|
||||
constructor(type, opt_pixelTransform, opt_frameState, opt_context, opt_glContext) {
|
||||
|
||||
super(type);
|
||||
|
||||
/**
|
||||
* For canvas, this is an instance of {@link module:ol/render/canvas/Immediate}.
|
||||
* @type {import("./VectorContext.js").default|undefined}
|
||||
* Transform from css pixels (relative to the top-left corner of the map viewport)
|
||||
* to render pixel on this event's `context`.
|
||||
* @type {import("../transform.js").Transform|undefined}
|
||||
* @api
|
||||
*/
|
||||
this.vectorContext = opt_vectorContext;
|
||||
this.pixelTransform = opt_pixelTransform;
|
||||
|
||||
/**
|
||||
* An object representing the current render frame state.
|
||||
|
||||
@@ -6,21 +6,41 @@
|
||||
* @enum {string}
|
||||
*/
|
||||
export default {
|
||||
|
||||
/**
|
||||
* @event module:ol/render/Event~RenderEvent#postcompose
|
||||
* Triggered before a layer is rendered.
|
||||
* @event module:ol/render/Event~RenderEvent#prerender
|
||||
* @api
|
||||
*/
|
||||
POSTCOMPOSE: 'postcompose',
|
||||
/**
|
||||
* @event module:ol/render/Event~RenderEvent#precompose
|
||||
* @api
|
||||
*/
|
||||
PRECOMPOSE: 'precompose',
|
||||
PRERENDER: 'prerender',
|
||||
|
||||
/**
|
||||
* @event module:ol/render/Event~RenderEvent#render
|
||||
* @api
|
||||
*/
|
||||
RENDER: 'render',
|
||||
|
||||
/**
|
||||
* Triggered after a layer is rendered.
|
||||
* @event module:ol/render/Event~RenderEvent#postrender
|
||||
* @api
|
||||
*/
|
||||
POSTRENDER: 'postrender',
|
||||
|
||||
/**
|
||||
* Triggered before layers are rendered.
|
||||
* @event module:ol/render/Event~RenderEvent#precompose
|
||||
* @api
|
||||
*/
|
||||
PRECOMPOSE: 'precompose',
|
||||
|
||||
/**
|
||||
* Triggered after all layers are rendered.
|
||||
* @event module:ol/render/Event~RenderEvent#postcompose
|
||||
* @api
|
||||
*/
|
||||
POSTCOMPOSE: 'postcompose',
|
||||
|
||||
/**
|
||||
* Triggered when rendering is complete, i.e. all sources and tiles have
|
||||
* finished loading for the current viewport, and all tiles are faded in.
|
||||
@@ -28,4 +48,5 @@ export default {
|
||||
* @api
|
||||
*/
|
||||
RENDERCOMPLETE: 'rendercomplete'
|
||||
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/**
|
||||
* @module ol/renderer/canvas/Map
|
||||
*/
|
||||
import {apply as applyTransform} from '../transform.js';
|
||||
import {stableSort} from '../array.js';
|
||||
import {CLASS_UNSELECTABLE} from '../css.js';
|
||||
import {visibleAtResolution} from '../layer/Layer.js';
|
||||
@@ -31,9 +30,6 @@ class CompositeMapRenderer extends MapRenderer {
|
||||
*/
|
||||
this.element_ = document.createElement('div');
|
||||
const style = this.element_.style;
|
||||
style.display = 'flex';
|
||||
style.alignItems = 'center';
|
||||
style.justifyContent = 'center';
|
||||
style.width = '100%';
|
||||
style.height = '100%';
|
||||
|
||||
@@ -114,28 +110,27 @@ class CompositeMapRenderer extends MapRenderer {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) {
|
||||
let result;
|
||||
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
|
||||
const viewState = frameState.viewState;
|
||||
const viewResolution = viewState.resolution;
|
||||
|
||||
const layerStates = frameState.layerStatesArray;
|
||||
const numLayers = layerStates.length;
|
||||
|
||||
const coordinate = applyTransform(
|
||||
frameState.pixelToCoordinateTransform, pixel.slice());
|
||||
|
||||
for (let i = numLayers - 1; i >= 0; --i) {
|
||||
const layerState = layerStates[i];
|
||||
const layer = layerState.layer;
|
||||
if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) {
|
||||
if (visibleAtResolution(layerState, viewResolution) && layerFilter(layer)) {
|
||||
const layerRenderer = this.getLayerRenderer(layer);
|
||||
if (!layerRenderer) {
|
||||
continue;
|
||||
}
|
||||
result = layerRenderer.forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg);
|
||||
if (result) {
|
||||
return result;
|
||||
const data = layerRenderer.getDataAtPixel(pixel, frameState, hitTolerance);
|
||||
if (data) {
|
||||
const result = callback(layer, data);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,16 +92,14 @@ class LayerRenderer extends Observable {
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @param {import("../coordinate.js").Coordinate} coordinate Coordinate.
|
||||
* @param {import("../pixel.js").Pixel} pixel Pixel.
|
||||
* @param {import("../PluggableMap.js").FrameState} frameState FrameState.
|
||||
* @param {number} hitTolerance Hit tolerance in pixels.
|
||||
* @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer
|
||||
* callback.
|
||||
* @param {S} thisArg Value to use as `this` when executing `callback`.
|
||||
* @return {T|undefined} Callback result.
|
||||
* @template S,T
|
||||
* @return {Uint8ClampedArray|Uint8Array} The result. If there is no data at the pixel
|
||||
* location, null will be returned. If there is data, but pixel values cannot be
|
||||
* returned, and empty array will be returned.
|
||||
*/
|
||||
forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) {
|
||||
getDataAtPixel(pixel, frameState, hitTolerance) {
|
||||
return abstract();
|
||||
}
|
||||
|
||||
@@ -158,7 +156,7 @@ class LayerRenderer extends Observable {
|
||||
renderIfReadyAndVisible() {
|
||||
const layer = this.getLayer();
|
||||
if (layer.getVisible() && layer.getSourceState() == SourceState.READY) {
|
||||
this.changed();
|
||||
layer.changed();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,16 +161,14 @@ class MapRenderer extends Disposable {
|
||||
* @param {number} hitTolerance Hit tolerance in pixels.
|
||||
* @param {function(this: S, import("../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer
|
||||
* callback.
|
||||
* @param {S} thisArg Value to use as `this` when executing `callback`.
|
||||
* @param {function(this: U, import("../layer/Layer.js").default): boolean} layerFilter Layer filter
|
||||
* function, only layers which are visible and for which this function
|
||||
* returns `true` will be tested for features. By default, all visible
|
||||
* layers will be tested.
|
||||
* @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
|
||||
* @return {T|undefined} Callback result.
|
||||
* @template S,T,U
|
||||
*/
|
||||
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) {
|
||||
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, layerFilter) {
|
||||
return abstract();
|
||||
}
|
||||
|
||||
|
||||
@@ -3,16 +3,17 @@
|
||||
*/
|
||||
import {ENABLE_RASTER_REPROJECTION} from '../../reproj/common.js';
|
||||
import ViewHint from '../../ViewHint.js';
|
||||
import {containsExtent, intersects} from '../../extent.js';
|
||||
import {getIntersection, isEmpty} from '../../extent.js';
|
||||
import IntermediateCanvasRenderer from './IntermediateCanvas.js';
|
||||
import {create as createTransform, compose as composeTransform} from '../../transform.js';
|
||||
import CanvasLayerRenderer from './Layer.js';
|
||||
import {compose as composeTransform, toString as transformToString} from '../../transform.js';
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* Canvas renderer for image layers.
|
||||
* @api
|
||||
*/
|
||||
class CanvasImageLayerRenderer extends IntermediateCanvasRenderer {
|
||||
class CanvasImageLayerRenderer extends CanvasLayerRenderer {
|
||||
|
||||
/**
|
||||
* @param {import("../../layer/Image.js").default} imageLayer Image layer.
|
||||
@@ -25,13 +26,6 @@ class CanvasImageLayerRenderer extends IntermediateCanvasRenderer {
|
||||
* @type {?import("../../ImageBase.js").default}
|
||||
*/
|
||||
this.image_ = null;
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {import("../../transform.js").Transform}
|
||||
*/
|
||||
this.imageTransform_ = createTransform();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,26 +35,14 @@ class CanvasImageLayerRenderer extends IntermediateCanvasRenderer {
|
||||
return !this.image_ ? null : this.image_.getImage();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
getImageTransform() {
|
||||
return this.imageTransform_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
prepareFrame(frameState, layerState) {
|
||||
|
||||
this.clear(frameState);
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const size = frameState.size;
|
||||
const viewState = frameState.viewState;
|
||||
const viewCenter = viewState.center;
|
||||
const viewResolution = viewState.resolution;
|
||||
|
||||
let image;
|
||||
const imageLayer = /** @type {import("../../layer/Image.js").default} */ (this.getLayer());
|
||||
const imageSource = /** @type {import("../../source/Image.js").default} */ (imageLayer.getSource());
|
||||
|
||||
@@ -71,8 +53,7 @@ class CanvasImageLayerRenderer extends IntermediateCanvasRenderer {
|
||||
renderedExtent = getIntersection(renderedExtent, layerState.extent);
|
||||
}
|
||||
|
||||
if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] &&
|
||||
!isEmpty(renderedExtent)) {
|
||||
if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && !isEmpty(renderedExtent)) {
|
||||
let projection = viewState.projection;
|
||||
if (!ENABLE_RASTER_REPROJECTION) {
|
||||
const sourceProjection = imageSource.getProjection();
|
||||
@@ -86,29 +67,95 @@ class CanvasImageLayerRenderer extends IntermediateCanvasRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
if (this.image_) {
|
||||
image = this.image_;
|
||||
const imageExtent = image.getExtent();
|
||||
const imageResolution = image.getResolution();
|
||||
const imagePixelRatio = image.getPixelRatio();
|
||||
const scale = pixelRatio * imageResolution /
|
||||
(viewResolution * imagePixelRatio);
|
||||
const transform = composeTransform(this.imageTransform_,
|
||||
pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
|
||||
scale, scale,
|
||||
0,
|
||||
imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution,
|
||||
imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution);
|
||||
composeTransform(this.coordinateToCanvasPixelTransform,
|
||||
pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5],
|
||||
pixelRatio / viewResolution, -pixelRatio / viewResolution,
|
||||
0,
|
||||
-viewCenter[0], -viewCenter[1]);
|
||||
return !!this.image_;
|
||||
}
|
||||
|
||||
this.renderedResolution = imageResolution * pixelRatio / imagePixelRatio;
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
renderFrame(frameState, layerState) {
|
||||
const image = this.image_;
|
||||
const imageExtent = image.getExtent();
|
||||
const imageResolution = image.getResolution();
|
||||
const imagePixelRatio = image.getPixelRatio();
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const viewState = frameState.viewState;
|
||||
const viewCenter = viewState.center;
|
||||
const viewResolution = viewState.resolution;
|
||||
const size = frameState.size;
|
||||
const scale = pixelRatio * imageResolution / (viewResolution * imagePixelRatio);
|
||||
|
||||
let width = Math.round(size[0] * pixelRatio);
|
||||
let height = Math.round(size[1] * pixelRatio);
|
||||
const rotation = viewState.rotation;
|
||||
if (rotation) {
|
||||
const size = Math.round(Math.sqrt(width * width + height * height));
|
||||
width = height = size;
|
||||
}
|
||||
|
||||
return !!this.image_;
|
||||
const context = this.context;
|
||||
const canvas = context.canvas;
|
||||
|
||||
const pixelTransform = composeTransform(this.pixelTransform_,
|
||||
frameState.size[0] / 2, frameState.size[1] / 2,
|
||||
1 / pixelRatio, 1 / pixelRatio,
|
||||
rotation,
|
||||
-width / 2, -height / 2
|
||||
);
|
||||
const canvasTransform = transformToString(pixelTransform);
|
||||
|
||||
if (canvas.width != width || canvas.height != height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else {
|
||||
context.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
// clipped rendering if layer extent is set
|
||||
const extent = layerState.extent;
|
||||
const clipped = extent !== undefined &&
|
||||
!containsExtent(extent, frameState.extent) &&
|
||||
intersects(extent, frameState.extent);
|
||||
if (clipped) {
|
||||
this.clip(context, frameState, extent);
|
||||
}
|
||||
|
||||
const img = image.getImage();
|
||||
|
||||
const transform = composeTransform(this.tempTransform_,
|
||||
width / 2, height / 2,
|
||||
scale, scale,
|
||||
0,
|
||||
imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution,
|
||||
imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution);
|
||||
|
||||
this.renderedResolution = imageResolution * pixelRatio / imagePixelRatio;
|
||||
|
||||
const dx = transform[4];
|
||||
const dy = transform[5];
|
||||
const dw = img.width * transform[0];
|
||||
const dh = img.height * transform[3];
|
||||
|
||||
if (dw >= 0.5 && dh >= 0.5) {
|
||||
this.context.drawImage(img, 0, 0, +img.width, +img.height,
|
||||
Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
|
||||
}
|
||||
|
||||
if (clipped) {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
const opacity = layerState.opacity;
|
||||
if (opacity !== canvas.style.opacity) {
|
||||
canvas.style.opacity = opacity;
|
||||
}
|
||||
|
||||
if (canvasTransform !== canvas.style.transform) {
|
||||
canvas.style.transform = canvasTransform;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,204 +0,0 @@
|
||||
/**
|
||||
* @module ol/renderer/canvas/IntermediateCanvas
|
||||
*/
|
||||
import {abstract} from '../../util.js';
|
||||
import {scale as scaleCoordinate} from '../../coordinate.js';
|
||||
import {createCanvasContext2D} from '../../dom.js';
|
||||
import {containsExtent, intersects} from '../../extent.js';
|
||||
import CanvasLayerRenderer from './Layer.js';
|
||||
import {create as createTransform, apply as applyTransform} from '../../transform.js';
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
*/
|
||||
class IntermediateCanvasRenderer extends CanvasLayerRenderer {
|
||||
|
||||
/**
|
||||
* @param {import("../../layer/Layer.js").default} layer Layer.
|
||||
* @param {boolean=} opt_noContext Skip the context creation.
|
||||
*/
|
||||
constructor(layer, opt_noContext) {
|
||||
|
||||
super(layer);
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.context = opt_noContext ? null : createCanvasContext2D();
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {import("../../transform.js").Transform}
|
||||
*/
|
||||
this.coordinateToCanvasPixelTransform = createTransform();
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.hitCanvasContext_ = null;
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.layerContext = createCanvasContext2D();
|
||||
|
||||
const canvas = this.layerContext.canvas;
|
||||
canvas.style.position = 'absolute';
|
||||
canvas.className = this.getLayer().getClassName();
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
composeFrame(frameState, layerState, context) {
|
||||
|
||||
this.preCompose(context, frameState);
|
||||
|
||||
const image = this.getImage();
|
||||
if (image) {
|
||||
|
||||
// clipped rendering if layer extent is set
|
||||
const extent = layerState.extent;
|
||||
const clipped = extent !== undefined &&
|
||||
!containsExtent(extent, frameState.extent) &&
|
||||
intersects(extent, frameState.extent);
|
||||
if (clipped) {
|
||||
this.clip(context, frameState, extent);
|
||||
}
|
||||
|
||||
const imageTransform = this.getImageTransform();
|
||||
// for performance reasons, context.save / context.restore is not used
|
||||
// to save and restore the transformation matrix and the opacity.
|
||||
// see http://jsperf.com/context-save-restore-versus-variable
|
||||
const alpha = context.globalAlpha;
|
||||
context.globalAlpha = layerState.opacity;
|
||||
|
||||
// for performance reasons, context.setTransform is only used
|
||||
// when the view is rotated. see http://jsperf.com/canvas-transform
|
||||
const dx = imageTransform[4];
|
||||
const dy = imageTransform[5];
|
||||
const dw = image.width * imageTransform[0];
|
||||
const dh = image.height * imageTransform[3];
|
||||
if (dw >= 0.5 && dh >= 0.5) {
|
||||
context.drawImage(image, 0, 0, +image.width, +image.height,
|
||||
Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
|
||||
}
|
||||
context.globalAlpha = alpha;
|
||||
|
||||
if (clipped) {
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
|
||||
this.postCompose(context, frameState, layerState);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
renderFrame(frameState, layerState) {
|
||||
|
||||
this.preRender(frameState);
|
||||
const image = this.getImage();
|
||||
if (image) {
|
||||
|
||||
// clipped rendering if layer extent is set
|
||||
const extent = layerState.extent;
|
||||
const clipped = extent !== undefined &&
|
||||
!containsExtent(extent, frameState.extent) &&
|
||||
intersects(extent, frameState.extent);
|
||||
if (clipped) {
|
||||
this.clip(this.layerContext, frameState, extent);
|
||||
}
|
||||
|
||||
const imageTransform = this.getImageTransform();
|
||||
|
||||
// for performance reasons, context.setTransform is only used
|
||||
// when the view is rotated. see http://jsperf.com/canvas-transform
|
||||
const dx = imageTransform[4];
|
||||
const dy = imageTransform[5];
|
||||
const dw = image.width * imageTransform[0];
|
||||
const dh = image.height * imageTransform[3];
|
||||
|
||||
if (dw >= 0.5 && dh >= 0.5) {
|
||||
this.clear(frameState);
|
||||
this.layerContext.drawImage(image, 0, 0, +image.width, +image.height,
|
||||
Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
|
||||
}
|
||||
|
||||
if (clipped) {
|
||||
this.layerContext.restore();
|
||||
}
|
||||
}
|
||||
|
||||
this.postRender(frameState, layerState);
|
||||
return this.layerContext.canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {HTMLCanvasElement|HTMLVideoElement|HTMLImageElement} Canvas.
|
||||
*/
|
||||
getImage() {
|
||||
return abstract();
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @return {!import("../../transform.js").Transform} Image transform.
|
||||
*/
|
||||
getImageTransform() {
|
||||
return abstract();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
*/
|
||||
clear(frameState) {
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const canvas = this.layerContext.canvas;
|
||||
const width = Math.round(frameState.size[0] * pixelRatio);
|
||||
const height = Math.round(frameState.size[1] * pixelRatio);
|
||||
|
||||
if (canvas.width != width || canvas.height != height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.style.width = (width / pixelRatio) + 'px';
|
||||
canvas.style.height = (height / pixelRatio) + 'px';
|
||||
} else {
|
||||
this.layerContext.clearRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) {
|
||||
if (!this.getImage()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const pixel = applyTransform(this.coordinateToCanvasPixelTransform, coordinate.slice());
|
||||
scaleCoordinate(pixel, frameState.viewState.resolution / this.renderedResolution);
|
||||
|
||||
if (!this.hitCanvasContext_) {
|
||||
this.hitCanvasContext_ = createCanvasContext2D(1, 1);
|
||||
}
|
||||
|
||||
this.hitCanvasContext_.clearRect(0, 0, 1, 1);
|
||||
this.hitCanvasContext_.drawImage(this.getImage(), pixel[0], pixel[1], 1, 1, 0, 0, 1, 1);
|
||||
|
||||
const imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data;
|
||||
if (imageData[3] > 0) {
|
||||
return callback.call(thisArg, this.getLayer(), imageData);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default IntermediateCanvasRenderer;
|
||||
@@ -1,15 +1,13 @@
|
||||
/**
|
||||
* @module ol/renderer/canvas/Layer
|
||||
*/
|
||||
import {abstract} from '../../util.js';
|
||||
import {getBottomLeft, getBottomRight, getTopLeft, getTopRight} from '../../extent.js';
|
||||
import {TRUE} from '../../functions.js';
|
||||
import {createCanvasContext2D} from '../../dom.js';
|
||||
import RenderEvent from '../../render/Event.js';
|
||||
import RenderEventType from '../../render/EventType.js';
|
||||
import {rotateAtOffset} from '../../render/canvas.js';
|
||||
import CanvasImmediateRenderer from '../../render/canvas/Immediate.js';
|
||||
import LayerRenderer from '../Layer.js';
|
||||
import {create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js';
|
||||
import {invert as invertTransform, create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js';
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
@@ -30,11 +28,29 @@ class CanvasLayerRenderer extends LayerRenderer {
|
||||
this.renderedResolution;
|
||||
|
||||
/**
|
||||
* A temporary transform.
|
||||
* @private
|
||||
* @type {import("../../transform.js").Transform}
|
||||
*/
|
||||
this.transform_ = createTransform();
|
||||
this.tempTransform_ = createTransform();
|
||||
|
||||
/**
|
||||
* The transform for rendered pixels to viewport CSS pixels.
|
||||
* @private
|
||||
* @type {import("../../transform.js").Transform}
|
||||
*/
|
||||
this.pixelTransform_ = createTransform();
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.context = createCanvasContext2D();
|
||||
|
||||
const canvas = this.context.canvas;
|
||||
canvas.style.position = 'absolute';
|
||||
canvas.style.transformOrigin = 'top left';
|
||||
canvas.className = this.getLayer().getClassName();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,61 +85,63 @@ class CanvasLayerRenderer extends LayerRenderer {
|
||||
rotateAtOffset(context, rotation, halfWidth, halfHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasRenderingContext2D} context Context.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../extent.js").Extent} extent Clip extent.
|
||||
* @protected
|
||||
*/
|
||||
clipUnrotated(context, frameState, extent) {
|
||||
const topLeft = getTopLeft(extent);
|
||||
const topRight = getTopRight(extent);
|
||||
const bottomRight = getBottomRight(extent);
|
||||
const bottomLeft = getBottomLeft(extent);
|
||||
|
||||
applyTransform(frameState.coordinateToPixelTransform, topLeft);
|
||||
applyTransform(frameState.coordinateToPixelTransform, topRight);
|
||||
applyTransform(frameState.coordinateToPixelTransform, bottomRight);
|
||||
applyTransform(frameState.coordinateToPixelTransform, bottomLeft);
|
||||
|
||||
const inverted = invertTransform(this.pixelTransform_.slice());
|
||||
|
||||
applyTransform(inverted, topLeft);
|
||||
applyTransform(inverted, topRight);
|
||||
applyTransform(inverted, bottomRight);
|
||||
applyTransform(inverted, bottomLeft);
|
||||
|
||||
context.save();
|
||||
context.beginPath();
|
||||
context.moveTo(Math.round(topLeft[0]), Math.round(topLeft[1]));
|
||||
context.lineTo(Math.round(topRight[0]), Math.round(topRight[1]));
|
||||
context.lineTo(Math.round(bottomRight[0]), Math.round(bottomRight[1]));
|
||||
context.lineTo(Math.round(bottomLeft[0]), Math.round(bottomLeft[1]));
|
||||
context.clip();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../render/EventType.js").default} type Event type.
|
||||
* @param {CanvasRenderingContext2D} context Context.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../transform.js").Transform=} opt_transform Transform.
|
||||
* @param {import("../../transform.js").Transform} pixelTransform Transform.
|
||||
* @private
|
||||
*/
|
||||
dispatchComposeEvent_(type, context, frameState, opt_transform) {
|
||||
dispatchComposeEvent_(type, context, frameState, pixelTransform) {
|
||||
const layer = this.getLayer();
|
||||
if (layer.hasListener(type)) {
|
||||
const halfWidth = (frameState.size[0] * frameState.pixelRatio) / 2;
|
||||
const halfHeight = (frameState.size[1] * frameState.pixelRatio) / 2;
|
||||
const rotation = frameState.viewState.rotation;
|
||||
rotateAtOffset(context, -rotation, halfWidth, halfHeight);
|
||||
const transform = opt_transform !== undefined ?
|
||||
opt_transform : this.getTransform(frameState, 0);
|
||||
const render = new CanvasImmediateRenderer(
|
||||
context, frameState.pixelRatio, frameState.extent, transform,
|
||||
frameState.viewState.rotation);
|
||||
const composeEvent = new RenderEvent(type, render, frameState,
|
||||
const composeEvent = new RenderEvent(type, pixelTransform, frameState,
|
||||
context, null);
|
||||
layer.dispatchEvent(composeEvent);
|
||||
rotateAtOffset(context, rotation, halfWidth, halfHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../coordinate.js").Coordinate} coordinate Coordinate.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState FrameState.
|
||||
* @param {number} hitTolerance Hit tolerance in pixels.
|
||||
* @param {function(this: S, import("../../layer/Layer.js").default, (Uint8ClampedArray|Uint8Array)): T} callback Layer
|
||||
* callback.
|
||||
* @param {S} thisArg Value to use as `this` when executing `callback`.
|
||||
* @return {T|undefined} Callback result.
|
||||
* @template S,T,U
|
||||
*/
|
||||
forEachLayerAtCoordinate(coordinate, frameState, hitTolerance, callback, thisArg) {
|
||||
const hasFeature = this.forEachFeatureAtCoordinate(coordinate, frameState, hitTolerance, TRUE);
|
||||
|
||||
if (hasFeature) {
|
||||
return callback.call(thisArg, this.getLayer(), null);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasRenderingContext2D} context Context.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../layer/Layer.js").State} layerState Layer state.
|
||||
* @param {import("../../transform.js").Transform=} opt_transform Transform.
|
||||
* @protected
|
||||
*/
|
||||
postCompose(context, frameState, layerState, opt_transform) {
|
||||
this.dispatchComposeEvent_(RenderEventType.POSTCOMPOSE, context, frameState, opt_transform);
|
||||
preRender(context, frameState, opt_transform) {
|
||||
this.dispatchComposeEvent_(RenderEventType.PRERENDER, context, frameState, opt_transform);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,27 +150,8 @@ class CanvasLayerRenderer extends LayerRenderer {
|
||||
* @param {import("../../transform.js").Transform=} opt_transform Transform.
|
||||
* @protected
|
||||
*/
|
||||
preCompose(context, frameState, opt_transform) {
|
||||
this.dispatchComposeEvent_(RenderEventType.PRECOMPOSE, context, frameState, opt_transform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../transform.js").Transform=} opt_transform Transform.
|
||||
* @protected
|
||||
*/
|
||||
preRender(frameState, opt_transform) {
|
||||
// TODO: pre-render event
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../layer/Layer.js").State} layerState Layer state.
|
||||
* @param {import("../../transform.js").Transform=} opt_transform Transform.
|
||||
* @protected
|
||||
*/
|
||||
postRender(frameState, layerState, opt_transform) {
|
||||
// TODO: pre-render event
|
||||
postRender(context, frameState, opt_transform) {
|
||||
this.dispatchComposeEvent_(RenderEventType.POSTRENDER, context, frameState, opt_transform);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -165,25 +164,6 @@ class CanvasLayerRenderer extends LayerRenderer {
|
||||
this.dispatchComposeEvent_(RenderEventType.RENDER, context, frameState, opt_transform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {number} offsetX Offset on the x-axis in view coordinates.
|
||||
* @protected
|
||||
* @return {!import("../../transform.js").Transform} Transform.
|
||||
*/
|
||||
getTransform(frameState, offsetX) {
|
||||
const viewState = frameState.viewState;
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const dx1 = pixelRatio * frameState.size[0] / 2;
|
||||
const dy1 = pixelRatio * frameState.size[1] / 2;
|
||||
const sx = pixelRatio / viewState.resolution;
|
||||
const sy = -sx;
|
||||
const angle = -viewState.rotation;
|
||||
const dx2 = -viewState.center[0] + offsetX;
|
||||
const dy2 = -viewState.center[1];
|
||||
return composeTransform(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a transform for rendering to an element that will be rotated after rendering.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
@@ -202,17 +182,36 @@ class CanvasLayerRenderer extends LayerRenderer {
|
||||
const sy = -sx;
|
||||
const dx2 = -viewState.center[0] + offsetX;
|
||||
const dy2 = -viewState.center[1];
|
||||
return composeTransform(this.transform_, dx1, dy1, sx, sy, 0, dx2, dy2);
|
||||
return composeTransform(this.tempTransform_, dx1, dy1, sx, sy, -viewState.rotation, dx2, dy2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @abstract
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../layer/Layer.js").State} layerState Layer state.
|
||||
* @param {CanvasRenderingContext2D} context Context.
|
||||
* @param {import("../../pixel.js").Pixel} pixel Pixel.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState FrameState.
|
||||
* @param {number} hitTolerance Hit tolerance in pixels.
|
||||
* @return {Uint8ClampedArray|Uint8Array} The result. If there is no data at the pixel
|
||||
* location, null will be returned. If there is data, but pixel values cannot be
|
||||
* returned, and empty array will be returned.
|
||||
*/
|
||||
composeFrame(frameState, layerState, context) {
|
||||
abstract();
|
||||
getDataAtPixel(pixel, frameState, hitTolerance) {
|
||||
const renderPixel = applyTransform(invertTransform(this.pixelTransform_.slice()), pixel.slice());
|
||||
const context = this.context;
|
||||
|
||||
let data;
|
||||
try {
|
||||
data = context.getImageData(Math.round(renderPixel[0]), Math.round(renderPixel[1]), 1, 1).data;
|
||||
} catch (err) {
|
||||
if (err.name === 'SecurityError') {
|
||||
// tainted canvas, we assume there is data at the given pixel (although there might not be)
|
||||
return new Uint8Array();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data[3] === 0) {
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
/**
|
||||
* @module ol/renderer/canvas/Map
|
||||
*/
|
||||
import {create as createTransform, apply as applyTransform, compose as composeTransform} from '../../transform.js';
|
||||
import {stableSort} from '../../array.js';
|
||||
import {CLASS_UNSELECTABLE} from '../../css.js';
|
||||
import {createCanvasContext2D} from '../../dom.js';
|
||||
import {visibleAtResolution} from '../../layer/Layer.js';
|
||||
import RenderEvent from '../../render/Event.js';
|
||||
import RenderEventType from '../../render/EventType.js';
|
||||
import {rotateAtOffset} from '../../render/canvas.js';
|
||||
import CanvasImmediateRenderer from '../../render/canvas/Immediate.js';
|
||||
import MapRenderer, {sortByZIndex} from '../Map.js';
|
||||
import SourceState from '../../source/State.js';
|
||||
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* Canvas map renderer.
|
||||
* @api
|
||||
*/
|
||||
class CanvasMapRenderer extends MapRenderer {
|
||||
|
||||
/**
|
||||
* @param {import("../../PluggableMap.js").default} map Map.
|
||||
*/
|
||||
constructor(map) {
|
||||
super(map);
|
||||
|
||||
const container = map.getViewport();
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.context_ = createCanvasContext2D();
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {HTMLCanvasElement}
|
||||
*/
|
||||
this.canvas_ = this.context_.canvas;
|
||||
|
||||
this.canvas_.style.width = '100%';
|
||||
this.canvas_.style.height = '100%';
|
||||
this.canvas_.style.display = 'block';
|
||||
this.canvas_.className = CLASS_UNSELECTABLE;
|
||||
container.insertBefore(this.canvas_, container.childNodes[0] || null);
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {boolean}
|
||||
*/
|
||||
this.renderedVisible_ = true;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {import("../../transform.js").Transform}
|
||||
*/
|
||||
this.transform_ = createTransform();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../render/EventType.js").default} type Event type.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
*/
|
||||
dispatchRenderEvent(type, frameState) {
|
||||
const map = this.getMap();
|
||||
const context = this.context_;
|
||||
if (map.hasListener(type)) {
|
||||
const extent = frameState.extent;
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const viewState = frameState.viewState;
|
||||
const rotation = viewState.rotation;
|
||||
|
||||
const transform = this.getTransform(frameState);
|
||||
|
||||
const vectorContext = new CanvasImmediateRenderer(context, pixelRatio,
|
||||
extent, transform, rotation);
|
||||
const composeEvent = new RenderEvent(type, vectorContext,
|
||||
frameState, context, null);
|
||||
map.dispatchEvent(composeEvent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @protected
|
||||
* @return {!import("../../transform.js").Transform} Transform.
|
||||
*/
|
||||
getTransform(frameState) {
|
||||
const viewState = frameState.viewState;
|
||||
const dx1 = this.canvas_.width / 2;
|
||||
const dy1 = this.canvas_.height / 2;
|
||||
const sx = frameState.pixelRatio / viewState.resolution;
|
||||
const sy = -sx;
|
||||
const angle = -viewState.rotation;
|
||||
const dx2 = -viewState.center[0];
|
||||
const dy2 = -viewState.center[1];
|
||||
return composeTransform(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
renderFrame(frameState) {
|
||||
|
||||
if (!frameState) {
|
||||
if (this.renderedVisible_) {
|
||||
this.canvas_.style.display = 'none';
|
||||
this.renderedVisible_ = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const context = this.context_;
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const width = Math.round(frameState.size[0] * pixelRatio);
|
||||
const height = Math.round(frameState.size[1] * pixelRatio);
|
||||
if (this.canvas_.width != width || this.canvas_.height != height) {
|
||||
this.canvas_.width = width;
|
||||
this.canvas_.height = height;
|
||||
} else {
|
||||
context.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
const rotation = frameState.viewState.rotation;
|
||||
|
||||
this.calculateMatrices2D(frameState);
|
||||
|
||||
this.dispatchRenderEvent(RenderEventType.PRECOMPOSE, frameState);
|
||||
|
||||
const layerStatesArray = frameState.layerStatesArray;
|
||||
stableSort(layerStatesArray, sortByZIndex);
|
||||
|
||||
if (rotation) {
|
||||
context.save();
|
||||
rotateAtOffset(context, rotation, width / 2, height / 2);
|
||||
}
|
||||
|
||||
const viewResolution = frameState.viewState.resolution;
|
||||
let i, ii;
|
||||
for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
|
||||
const layerState = layerStatesArray[i];
|
||||
const layer = layerState.layer;
|
||||
const layerRenderer = /** @type {import("./Layer.js").default} */ (this.getLayerRenderer(layer));
|
||||
if (!visibleAtResolution(layerState, viewResolution) ||
|
||||
layerState.sourceState != SourceState.READY) {
|
||||
continue;
|
||||
}
|
||||
if (layerRenderer.prepareFrame(frameState, layerState)) {
|
||||
layerRenderer.composeFrame(frameState, layerState, context);
|
||||
}
|
||||
}
|
||||
|
||||
if (rotation) {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
this.dispatchRenderEvent(RenderEventType.POSTCOMPOSE, frameState);
|
||||
|
||||
if (!this.renderedVisible_) {
|
||||
this.canvas_.style.display = '';
|
||||
this.renderedVisible_ = true;
|
||||
}
|
||||
|
||||
this.scheduleRemoveUnusedLayerRenderers(frameState);
|
||||
this.scheduleExpireIconCache(frameState);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
forEachLayerAtPixel(pixel, frameState, hitTolerance, callback, thisArg, layerFilter, thisArg2) {
|
||||
let result;
|
||||
const viewState = frameState.viewState;
|
||||
const viewResolution = viewState.resolution;
|
||||
|
||||
const layerStates = frameState.layerStatesArray;
|
||||
const numLayers = layerStates.length;
|
||||
|
||||
const coordinate = applyTransform(
|
||||
frameState.pixelToCoordinateTransform, pixel.slice());
|
||||
|
||||
let i;
|
||||
for (i = numLayers - 1; i >= 0; --i) {
|
||||
const layerState = layerStates[i];
|
||||
const layer = layerState.layer;
|
||||
if (visibleAtResolution(layerState, viewResolution) && layerFilter.call(thisArg2, layer)) {
|
||||
const layerRenderer = /** @type {import("./Layer.js").default} */ (this.getLayerRenderer(layer));
|
||||
result = layerRenderer.forEachLayerAtCoordinate(
|
||||
coordinate, frameState, hitTolerance, callback, thisArg);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export default CanvasMapRenderer;
|
||||
@@ -4,25 +4,22 @@
|
||||
import {getUid} from '../../util.js';
|
||||
import TileRange from '../../TileRange.js';
|
||||
import TileState from '../../TileState.js';
|
||||
import ViewHint from '../../ViewHint.js';
|
||||
import {containsExtent, createEmpty, equals, getIntersection, isEmpty} from '../../extent.js';
|
||||
import IntermediateCanvasRenderer from './IntermediateCanvas.js';
|
||||
import {create as createTransform, compose as composeTransform} from '../../transform.js';
|
||||
import {createEmpty, getIntersection, getTopLeft} from '../../extent.js';
|
||||
import CanvasLayerRenderer from './Layer.js';
|
||||
import {compose as composeTransform, toString as transformToString} from '../../transform.js';
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
* Canvas renderer for tile layers.
|
||||
* @api
|
||||
*/
|
||||
class CanvasTileLayerRenderer extends IntermediateCanvasRenderer {
|
||||
class CanvasTileLayerRenderer extends CanvasLayerRenderer {
|
||||
|
||||
/**
|
||||
* @param {import("../../layer/Tile.js").default|import("../../layer/VectorTile.js").default} tileLayer Tile layer.
|
||||
* @param {boolean=} opt_noContext Skip the context creation.
|
||||
*/
|
||||
constructor(tileLayer, opt_noContext) {
|
||||
|
||||
super(tileLayer, opt_noContext);
|
||||
constructor(tileLayer) {
|
||||
super(tileLayer);
|
||||
|
||||
/**
|
||||
* @private
|
||||
@@ -66,18 +63,11 @@ class CanvasTileLayerRenderer extends IntermediateCanvasRenderer {
|
||||
*/
|
||||
this.tmpTileRange_ = new TileRange(0, 0, 0, 0);
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {import("../../transform.js").Transform}
|
||||
*/
|
||||
this.imageTransform_ = createTransform();
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @type {number}
|
||||
*/
|
||||
this.zDirection = 0;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,13 +115,25 @@ class CanvasTileLayerRenderer extends IntermediateCanvasRenderer {
|
||||
* @inheritDoc
|
||||
*/
|
||||
prepareFrame(frameState, layerState) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const size = frameState.size;
|
||||
/**
|
||||
* TODO: File a TypeScript issue about inheritDoc not being followed
|
||||
* all the way. Without this explicit return type, the VectorTileLayer
|
||||
* renderFrame function does not pass.
|
||||
*
|
||||
* @inheritDoc
|
||||
* @returns {HTMLElement} The rendered element.
|
||||
*/
|
||||
renderFrame(frameState, layerState) {
|
||||
const context = this.context;
|
||||
const viewState = frameState.viewState;
|
||||
const projection = viewState.projection;
|
||||
const viewResolution = viewState.resolution;
|
||||
const viewCenter = viewState.center;
|
||||
const rotation = viewState.rotation;
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
|
||||
const tileLayer = /** @type {import("../../layer/Tile.js").default} */ (this.getLayer());
|
||||
const tileSource = /** @type {import("../../source/Tile.js").default} */ (tileLayer.getSource());
|
||||
@@ -139,44 +141,59 @@ class CanvasTileLayerRenderer extends IntermediateCanvasRenderer {
|
||||
const tileGrid = tileSource.getTileGridForProjection(projection);
|
||||
const z = tileGrid.getZForResolution(viewResolution, this.zDirection);
|
||||
const tileResolution = tileGrid.getResolution(z);
|
||||
let oversampling = Math.round(viewResolution / tileResolution) || 1;
|
||||
let extent = frameState.extent;
|
||||
|
||||
if (layerState.extent !== undefined) {
|
||||
if (layerState.extent) {
|
||||
extent = getIntersection(extent, layerState.extent);
|
||||
}
|
||||
if (isEmpty(extent)) {
|
||||
// Return false to prevent the rendering of the layer.
|
||||
return false;
|
||||
}
|
||||
|
||||
const tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
|
||||
const imageExtent = tileGrid.getTileRangeExtent(z, tileRange);
|
||||
|
||||
const tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio);
|
||||
|
||||
// desired dimensions of the canvas in pixels
|
||||
let width = Math.round(frameState.size[0] * tilePixelRatio);
|
||||
let height = Math.round(frameState.size[1] * tilePixelRatio);
|
||||
if (tileResolution < viewResolution) {
|
||||
// scale canvas so it covers the viewport until new tiles come in
|
||||
let scale;
|
||||
if (z <= tileGrid.minZoom) {
|
||||
scale = Math.round(viewResolution / tileResolution);
|
||||
} else {
|
||||
scale = 1.5; // rely on lower z tiles
|
||||
}
|
||||
width *= scale;
|
||||
height *= scale;
|
||||
}
|
||||
|
||||
if (rotation) {
|
||||
const size = Math.round(Math.sqrt(width * width + height * height));
|
||||
width = height = size;
|
||||
}
|
||||
|
||||
const dx = tileResolution * width / 2 / tilePixelRatio;
|
||||
const dy = tileResolution * height / 2 / tilePixelRatio;
|
||||
const canvasExtent = [
|
||||
viewCenter[0] - dx,
|
||||
viewCenter[1] - dy,
|
||||
viewCenter[0] + dx,
|
||||
viewCenter[1] + dy
|
||||
];
|
||||
|
||||
const tileRange = tileGrid.getTileRangeForExtentAndZ(canvasExtent, z);
|
||||
|
||||
/**
|
||||
* @type {Object<number, Object<string, import("../../Tile.js").default>>}
|
||||
*/
|
||||
const tilesToDrawByZ = {};
|
||||
tilesToDrawByZ[z] = {};
|
||||
|
||||
const findLoadedTiles = this.createLoadedTileFinder(
|
||||
tileSource, projection, tilesToDrawByZ);
|
||||
|
||||
const hints = frameState.viewHints;
|
||||
const animatingOrInteracting = hints[ViewHint.ANIMATING] || hints[ViewHint.INTERACTING];
|
||||
const findLoadedTiles = this.createLoadedTileFinder(tileSource, projection, tilesToDrawByZ);
|
||||
|
||||
const tmpExtent = this.tmpExtent;
|
||||
const tmpTileRange = this.tmpTileRange_;
|
||||
this.newTiles_ = false;
|
||||
let tile, x, y;
|
||||
for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
|
||||
for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
|
||||
if (Date.now() - frameState.time > 16 && animatingOrInteracting) {
|
||||
continue;
|
||||
}
|
||||
tile = this.getTile(z, x, y, pixelRatio, projection);
|
||||
for (let x = tileRange.minX; x <= tileRange.maxX; ++x) {
|
||||
for (let y = tileRange.minY; y <= tileRange.maxY; ++y) {
|
||||
const tile = this.getTile(z, x, y, pixelRatio, projection);
|
||||
if (this.isDrawableTile_(tile)) {
|
||||
const uid = getUid(this);
|
||||
if (tile.getState() == TileState.LOADED) {
|
||||
@@ -192,105 +209,104 @@ class CanvasTileLayerRenderer extends IntermediateCanvasRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
const childTileRange = tileGrid.getTileCoordChildTileRange(
|
||||
tile.tileCoord, tmpTileRange, tmpExtent);
|
||||
const childTileRange = tileGrid.getTileCoordChildTileRange(tile.tileCoord, tmpTileRange, tmpExtent);
|
||||
|
||||
let covered = false;
|
||||
if (childTileRange) {
|
||||
covered = findLoadedTiles(z + 1, childTileRange);
|
||||
}
|
||||
if (!covered) {
|
||||
tileGrid.forEachTileCoordParentTileRange(
|
||||
tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
|
||||
tileGrid.forEachTileCoordParentTileRange(tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
const renderedResolution = tileResolution * pixelRatio / tilePixelRatio * oversampling;
|
||||
if (!(this.renderedResolution && Date.now() - frameState.time > 16 && animatingOrInteracting) && (
|
||||
this.newTiles_ ||
|
||||
!(this.renderedExtent_ && containsExtent(this.renderedExtent_, extent)) ||
|
||||
this.renderedRevision != sourceRevision ||
|
||||
oversampling != this.oversampling_ ||
|
||||
!animatingOrInteracting && renderedResolution != this.renderedResolution
|
||||
)) {
|
||||
const canvas = context.canvas;
|
||||
const canvasScale = tileResolution / frameState.viewState.resolution / tilePixelRatio;
|
||||
const pixelTransform = composeTransform(this.pixelTransform_,
|
||||
frameState.size[0] / 2, frameState.size[1] / 2,
|
||||
canvasScale, canvasScale,
|
||||
rotation,
|
||||
-width / 2, -height / 2
|
||||
);
|
||||
const canvasTransform = transformToString(pixelTransform);
|
||||
|
||||
const context = this.context;
|
||||
if (context) {
|
||||
const tilePixelSize = tileSource.getTilePixelSize(z, pixelRatio, projection);
|
||||
const width = Math.round(tileRange.getWidth() * tilePixelSize[0] / oversampling);
|
||||
const height = Math.round(tileRange.getHeight() * tilePixelSize[1] / oversampling);
|
||||
const canvas = context.canvas;
|
||||
if (canvas.width != width || canvas.height != height) {
|
||||
this.oversampling_ = oversampling;
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else {
|
||||
if (this.renderedExtent_ && !equals(imageExtent, this.renderedExtent_)) {
|
||||
context.clearRect(0, 0, width, height);
|
||||
}
|
||||
oversampling = this.oversampling_;
|
||||
}
|
||||
}
|
||||
|
||||
this.renderedTiles.length = 0;
|
||||
/** @type {Array<number>} */
|
||||
const zs = Object.keys(tilesToDrawByZ).map(Number);
|
||||
zs.sort(function(a, b) {
|
||||
if (a === z) {
|
||||
return 1;
|
||||
} else if (b === z) {
|
||||
return -1;
|
||||
} else {
|
||||
return a > b ? 1 : a < b ? -1 : 0;
|
||||
}
|
||||
});
|
||||
let currentResolution, currentScale, currentTilePixelSize, currentZ, i, ii;
|
||||
let tileExtent, tileGutter, tilesToDraw, w, h;
|
||||
for (i = 0, ii = zs.length; i < ii; ++i) {
|
||||
currentZ = zs[i];
|
||||
currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection);
|
||||
currentResolution = tileGrid.getResolution(currentZ);
|
||||
currentScale = currentResolution / tileResolution;
|
||||
tileGutter = tilePixelRatio * tileSource.getGutterForProjection(projection);
|
||||
tilesToDraw = tilesToDrawByZ[currentZ];
|
||||
for (const tileCoordKey in tilesToDraw) {
|
||||
tile = tilesToDraw[tileCoordKey];
|
||||
tileExtent = tileGrid.getTileCoordExtent(tile.getTileCoord(), tmpExtent);
|
||||
x = (tileExtent[0] - imageExtent[0]) / tileResolution * tilePixelRatio / oversampling;
|
||||
y = (imageExtent[3] - tileExtent[3]) / tileResolution * tilePixelRatio / oversampling;
|
||||
w = currentTilePixelSize[0] * currentScale / oversampling;
|
||||
h = currentTilePixelSize[1] * currentScale / oversampling;
|
||||
this.drawTileImage(tile, frameState, layerState, x, y, w, h, tileGutter, z === currentZ);
|
||||
this.renderedTiles.push(tile);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderedRevision = sourceRevision;
|
||||
this.renderedResolution = tileResolution * pixelRatio / tilePixelRatio * oversampling;
|
||||
this.renderedExtent_ = imageExtent;
|
||||
if (canvas.width != width || canvas.height != height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
} else {
|
||||
context.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
const scale = this.renderedResolution / viewResolution;
|
||||
const transform = composeTransform(this.imageTransform_,
|
||||
pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
|
||||
scale, scale,
|
||||
0,
|
||||
(this.renderedExtent_[0] - viewCenter[0]) / this.renderedResolution * pixelRatio,
|
||||
(viewCenter[1] - this.renderedExtent_[3]) / this.renderedResolution * pixelRatio);
|
||||
composeTransform(this.coordinateToCanvasPixelTransform,
|
||||
pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5],
|
||||
pixelRatio / viewResolution, -pixelRatio / viewResolution,
|
||||
0,
|
||||
-viewCenter[0], -viewCenter[1]);
|
||||
if (layerState.extent) {
|
||||
this.clipUnrotated(context, frameState, layerState.extent);
|
||||
}
|
||||
|
||||
this.preRender(context, frameState, pixelTransform);
|
||||
|
||||
this.renderedTiles.length = 0;
|
||||
/** @type {Array<number>} */
|
||||
const zs = Object.keys(tilesToDrawByZ).map(Number);
|
||||
zs.sort(function(a, b) {
|
||||
if (a === z) {
|
||||
return 1;
|
||||
} else if (b === z) {
|
||||
return -1;
|
||||
} else {
|
||||
return a > b ? 1 : a < b ? -1 : 0;
|
||||
}
|
||||
});
|
||||
|
||||
for (let i = 0, ii = zs.length; i < ii; ++i) {
|
||||
const currentZ = zs[i];
|
||||
const currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection);
|
||||
const currentResolution = tileGrid.getResolution(currentZ);
|
||||
const currentScale = currentResolution / tileResolution;
|
||||
const w = currentTilePixelSize[0] * currentScale;
|
||||
const h = currentTilePixelSize[1] * currentScale;
|
||||
const originTileCoord = tileGrid.getTileCoordForCoordAndZ(getTopLeft(canvasExtent), currentZ);
|
||||
const originTileExtent = tileGrid.getTileCoordExtent(originTileCoord);
|
||||
const originX = Math.round(tilePixelRatio * (originTileExtent[0] - canvasExtent[0]) / tileResolution);
|
||||
const originY = Math.round(tilePixelRatio * (canvasExtent[3] - originTileExtent[3]) / tileResolution);
|
||||
const tileGutter = tilePixelRatio * tileSource.getGutterForProjection(projection);
|
||||
const tilesToDraw = tilesToDrawByZ[currentZ];
|
||||
for (const tileCoordKey in tilesToDraw) {
|
||||
const tile = tilesToDraw[tileCoordKey];
|
||||
const tileCoord = tile.tileCoord;
|
||||
const x = originX - (originTileCoord[1] - tileCoord[1]) * w;
|
||||
const y = originY + (originTileCoord[2] - tileCoord[2]) * h;
|
||||
this.drawTileImage(tile, frameState, layerState, x, y, w, h, tileGutter, z === currentZ);
|
||||
this.renderedTiles.push(tile);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this.renderedRevision = sourceRevision;
|
||||
this.renderedResolution = tileResolution;
|
||||
this.renderedExtent_ = canvasExtent;
|
||||
|
||||
this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
|
||||
this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio,
|
||||
projection, extent, z, tileLayer.getPreload());
|
||||
this.scheduleExpireCache(frameState, tileSource);
|
||||
|
||||
return this.renderedTiles.length > 0;
|
||||
this.postRender(context, frameState, pixelTransform);
|
||||
|
||||
if (layerState.extent) {
|
||||
context.restore();
|
||||
}
|
||||
|
||||
const opacity = layerState.opacity;
|
||||
if (opacity !== canvas.style.opacity) {
|
||||
canvas.style.opacity = opacity;
|
||||
}
|
||||
|
||||
if (canvasTransform !== canvas.style.transform) {
|
||||
canvas.style.transform = canvasTransform;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -342,13 +358,6 @@ class CanvasTileLayerRenderer extends IntermediateCanvasRenderer {
|
||||
return context ? context.canvas : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
getImageTransform() {
|
||||
return this.imageTransform_;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the image from a tile.
|
||||
* @param {import("../../Tile.js").default} tile Tile.
|
||||
@@ -358,6 +367,7 @@ class CanvasTileLayerRenderer extends IntermediateCanvasRenderer {
|
||||
getTileImage(tile) {
|
||||
return /** @type {import("../../ImageTile.js").default} */ (tile).getImage();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import {equals} from '../../array.js';
|
||||
import {getHeight, getWidth, isEmpty} from '../../extent.js';
|
||||
import {assign} from '../../obj.js';
|
||||
import CanvasImageLayerRenderer from './ImageLayer.js';
|
||||
import {compose as composeTransform} from '../../transform.js';
|
||||
import CanvasVectorLayerRenderer from './VectorLayer.js';
|
||||
|
||||
/**
|
||||
@@ -49,9 +48,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
|
||||
*/
|
||||
prepareFrame(frameState, layerState) {
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const size = frameState.size;
|
||||
const viewState = frameState.viewState;
|
||||
const viewCenter = viewState.center;
|
||||
const viewResolution = viewState.resolution;
|
||||
|
||||
const hints = frameState.viewHints;
|
||||
@@ -77,7 +74,7 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
|
||||
!equals(skippedFeatures, newSkippedFeatures))) {
|
||||
context.canvas.width = imageFrameState.size[0] * pixelRatio;
|
||||
context.canvas.height = imageFrameState.size[1] * pixelRatio;
|
||||
vectorRenderer.compose(context, imageFrameState, layerState);
|
||||
vectorRenderer.renderFrame(imageFrameState, layerState);
|
||||
skippedFeatures = newSkippedFeatures;
|
||||
callback();
|
||||
}
|
||||
@@ -90,23 +87,8 @@ class CanvasVectorImageLayerRenderer extends CanvasImageLayerRenderer {
|
||||
|
||||
if (this.image_) {
|
||||
const image = this.image_;
|
||||
const imageExtent = image.getExtent();
|
||||
const imageResolution = image.getResolution();
|
||||
const imagePixelRatio = image.getPixelRatio();
|
||||
const scale = pixelRatio * imageResolution /
|
||||
(viewResolution * imagePixelRatio);
|
||||
const transform = composeTransform(this.imageTransform_,
|
||||
pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
|
||||
scale, scale,
|
||||
0,
|
||||
imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution,
|
||||
imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution);
|
||||
composeTransform(this.coordinateToCanvasPixelTransform,
|
||||
pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5],
|
||||
pixelRatio / viewResolution, -pixelRatio / viewResolution,
|
||||
0,
|
||||
-viewCenter[0], -viewCenter[1]);
|
||||
|
||||
this.renderedResolution = imageResolution * pixelRatio / imagePixelRatio;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,17 @@
|
||||
*/
|
||||
import {getUid} from '../../util.js';
|
||||
import ViewHint from '../../ViewHint.js';
|
||||
import {createCanvasContext2D} from '../../dom.js';
|
||||
import {listen, unlisten} from '../../events.js';
|
||||
import EventType from '../../events/EventType.js';
|
||||
import rbush from 'rbush';
|
||||
import {buffer, createEmpty, containsExtent, getWidth} from '../../extent.js';
|
||||
import RenderEventType from '../../render/EventType.js';
|
||||
import {labelCache, rotateAtOffset} from '../../render/canvas.js';
|
||||
import {labelCache} from '../../render/canvas.js';
|
||||
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
|
||||
import InstructionsGroupExecutor from '../../render/canvas/ExecutorGroup.js';
|
||||
import CanvasLayerRenderer from './Layer.js';
|
||||
import {defaultOrder as defaultRenderOrder, getTolerance as getRenderTolerance, getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js';
|
||||
import {toString as transformToString} from '../../transform.js';
|
||||
|
||||
/**
|
||||
* @classdesc
|
||||
@@ -77,15 +77,6 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
|
||||
*/
|
||||
this.replayGroupChanged = true;
|
||||
|
||||
/**
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.context = createCanvasContext2D();
|
||||
|
||||
const canvas = this.context.canvas;
|
||||
canvas.style.position = 'absolute';
|
||||
canvas.className = this.getLayer().getClassName();
|
||||
|
||||
listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this);
|
||||
}
|
||||
|
||||
@@ -97,151 +88,43 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
|
||||
super.disposeInternal();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasRenderingContext2D} context Context.
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../layer/Layer.js").State} layerState Layer state.
|
||||
*/
|
||||
compose(context, frameState, layerState) {
|
||||
const extent = frameState.extent;
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const skippedFeatureUids = layerState.managed ?
|
||||
frameState.skippedFeatureUids : {};
|
||||
const viewState = frameState.viewState;
|
||||
const projection = viewState.projection;
|
||||
const rotation = viewState.rotation;
|
||||
const projectionExtent = projection.getExtent();
|
||||
const vectorSource = /** @type {import("../../source/Vector.js").default} */ (this.getLayer().getSource());
|
||||
|
||||
let transform = this.getTransform(frameState, 0);
|
||||
|
||||
// clipped rendering if layer extent is set
|
||||
const clipExtent = layerState.extent;
|
||||
const clipped = clipExtent !== undefined;
|
||||
if (clipped) {
|
||||
this.clip(context, frameState, clipExtent);
|
||||
}
|
||||
const replayGroup = this.replayGroup_;
|
||||
if (replayGroup && !replayGroup.isEmpty()) {
|
||||
if (this.declutterTree_) {
|
||||
this.declutterTree_.clear();
|
||||
}
|
||||
const layer = /** @type {import("../../layer/Vector.js").default} */ (this.getLayer());
|
||||
let drawOffsetX = 0;
|
||||
let drawOffsetY = 0;
|
||||
let replayContext;
|
||||
const transparentLayer = layerState.opacity !== 1;
|
||||
const hasRenderListeners = layer.hasListener(RenderEventType.RENDER);
|
||||
if (transparentLayer || hasRenderListeners) {
|
||||
let drawWidth = context.canvas.width;
|
||||
let drawHeight = context.canvas.height;
|
||||
if (rotation) {
|
||||
const drawSize = Math.round(Math.sqrt(drawWidth * drawWidth + drawHeight * drawHeight));
|
||||
drawOffsetX = (drawSize - drawWidth) / 2;
|
||||
drawOffsetY = (drawSize - drawHeight) / 2;
|
||||
drawWidth = drawHeight = drawSize;
|
||||
}
|
||||
// resize and clear
|
||||
this.context.canvas.width = drawWidth;
|
||||
this.context.canvas.height = drawHeight;
|
||||
replayContext = this.context;
|
||||
} else {
|
||||
replayContext = context;
|
||||
}
|
||||
|
||||
const alpha = replayContext.globalAlpha;
|
||||
if (!transparentLayer) {
|
||||
// for performance reasons, context.save / context.restore is not used
|
||||
// to save and restore the transformation matrix and the opacity.
|
||||
// see http://jsperf.com/context-save-restore-versus-variable
|
||||
replayContext.globalAlpha = layerState.opacity;
|
||||
}
|
||||
|
||||
if (replayContext != context) {
|
||||
replayContext.translate(drawOffsetX, drawOffsetY);
|
||||
}
|
||||
|
||||
const viewHints = frameState.viewHints;
|
||||
const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);
|
||||
const halfWidth = (frameState.size[0] * pixelRatio) / 2;
|
||||
const halfHeight = (frameState.size[1] * pixelRatio) / 2;
|
||||
rotateAtOffset(replayContext, -rotation, halfWidth, halfHeight);
|
||||
replayGroup.execute(replayContext, transform, rotation, skippedFeatureUids, snapToPixel);
|
||||
if (vectorSource.getWrapX() && projection.canWrapX() &&
|
||||
!containsExtent(projectionExtent, extent)) {
|
||||
let startX = extent[0];
|
||||
const worldWidth = getWidth(projectionExtent);
|
||||
let world = 0;
|
||||
let offsetX;
|
||||
while (startX < projectionExtent[0]) {
|
||||
--world;
|
||||
offsetX = worldWidth * world;
|
||||
transform = this.getTransform(frameState, offsetX);
|
||||
replayGroup.execute(replayContext, transform, rotation, skippedFeatureUids, snapToPixel);
|
||||
startX += worldWidth;
|
||||
}
|
||||
world = 0;
|
||||
startX = extent[2];
|
||||
while (startX > projectionExtent[2]) {
|
||||
++world;
|
||||
offsetX = worldWidth * world;
|
||||
transform = this.getTransform(frameState, offsetX);
|
||||
replayGroup.execute(replayContext, transform, rotation, skippedFeatureUids, snapToPixel);
|
||||
startX -= worldWidth;
|
||||
}
|
||||
}
|
||||
rotateAtOffset(replayContext, rotation, halfWidth, halfHeight);
|
||||
|
||||
if (hasRenderListeners) {
|
||||
this.dispatchRenderEvent(replayContext, frameState, transform);
|
||||
}
|
||||
if (replayContext != context) {
|
||||
if (transparentLayer) {
|
||||
const mainContextAlpha = context.globalAlpha;
|
||||
context.globalAlpha = layerState.opacity;
|
||||
context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY);
|
||||
context.globalAlpha = mainContextAlpha;
|
||||
} else {
|
||||
context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY);
|
||||
}
|
||||
replayContext.translate(-drawOffsetX, -drawOffsetY);
|
||||
}
|
||||
|
||||
if (!transparentLayer) {
|
||||
replayContext.globalAlpha = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
if (clipped) {
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
composeFrame(frameState, layerState, context) {
|
||||
const transform = this.getTransform(frameState, 0);
|
||||
this.preCompose(context, frameState, transform);
|
||||
this.compose(context, frameState, layerState);
|
||||
this.postCompose(context, frameState, layerState, transform);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../PluggableMap.js").FrameState} frameState Frame state.
|
||||
* @param {import("../../layer/Layer.js").State} layerState Layer state.
|
||||
*/
|
||||
render(frameState, layerState) {
|
||||
const replayGroup = this.replayGroup_;
|
||||
if (!replayGroup || replayGroup.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderFrame(frameState, layerState) {
|
||||
const context = this.context;
|
||||
const canvas = context.canvas;
|
||||
|
||||
const extent = frameState.extent;
|
||||
const replayGroup = this.replayGroup_;
|
||||
if (!replayGroup || replayGroup.isEmpty()) {
|
||||
if (canvas.width > 0) {
|
||||
canvas.width = 0;
|
||||
}
|
||||
return canvas;
|
||||
}
|
||||
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
|
||||
// a scale transform (we could add a createScale function in ol/transform)
|
||||
const pixelTransform = [1 / pixelRatio, 0, 0, 1 / pixelRatio, 0, 0];
|
||||
|
||||
// resize and clear
|
||||
const width = Math.round(frameState.size[0] * pixelRatio);
|
||||
const height = Math.round(frameState.size[1] * pixelRatio);
|
||||
if (canvas.width != width || canvas.height != height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const canvasTransform = transformToString(pixelTransform);
|
||||
if (canvas.style.transform !== canvasTransform) {
|
||||
canvas.style.transform = canvasTransform;
|
||||
}
|
||||
} else {
|
||||
context.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
this.preRender(context, frameState, pixelTransform);
|
||||
|
||||
const extent = frameState.extent;
|
||||
const viewState = frameState.viewState;
|
||||
const projection = viewState.projection;
|
||||
const rotation = viewState.rotation;
|
||||
@@ -259,26 +142,11 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
|
||||
this.declutterTree_.clear();
|
||||
}
|
||||
|
||||
// resize and clear
|
||||
let width = Math.round(frameState.size[0] * pixelRatio);
|
||||
let height = Math.round(frameState.size[1] * pixelRatio);
|
||||
if (rotation) {
|
||||
const size = Math.round(Math.sqrt(width * width + height * height));
|
||||
width = height = size;
|
||||
}
|
||||
if (canvas.width != width || canvas.height != height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
canvas.style.width = (width / pixelRatio) + 'px';
|
||||
canvas.style.height = (height / pixelRatio) + 'px';
|
||||
} else {
|
||||
context.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
const viewHints = frameState.viewHints;
|
||||
const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);
|
||||
|
||||
let transform = this.getRenderTransform(frameState, width, height, 0);
|
||||
const transform = this.getRenderTransform(frameState, width, height, 0);
|
||||
const skippedFeatureUids = layerState.managed ? frameState.skippedFeatureUids : {};
|
||||
replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel);
|
||||
|
||||
@@ -290,7 +158,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
|
||||
while (startX < projectionExtent[0]) {
|
||||
--world;
|
||||
offsetX = worldWidth * world;
|
||||
transform = this.getRenderTransform(frameState, width, height, offsetX);
|
||||
const transform = this.getRenderTransform(frameState, width, height, offsetX);
|
||||
replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel);
|
||||
startX += worldWidth;
|
||||
}
|
||||
@@ -299,41 +167,27 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
|
||||
while (startX > projectionExtent[2]) {
|
||||
++world;
|
||||
offsetX = worldWidth * world;
|
||||
transform = this.getRenderTransform(frameState, width, height, offsetX);
|
||||
const transform = this.getRenderTransform(frameState, width, height, offsetX);
|
||||
replayGroup.execute(context, transform, rotation, skippedFeatureUids, snapToPixel);
|
||||
startX -= worldWidth;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.getLayer().hasListener(RenderEventType.RENDER)) {
|
||||
this.dispatchRenderEvent(context, frameState, transform);
|
||||
this.dispatchRenderEvent(context, frameState, pixelTransform);
|
||||
}
|
||||
|
||||
if (clipped) {
|
||||
context.restore();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
renderFrame(frameState, layerState) {
|
||||
this.preRender(frameState);
|
||||
this.render(frameState, layerState);
|
||||
this.postRender(frameState, layerState);
|
||||
const canvas = this.context.canvas;
|
||||
this.postRender(context, frameState, pixelTransform);
|
||||
|
||||
const opacity = layerState.opacity;
|
||||
if (opacity !== canvas.style.opacity) {
|
||||
canvas.style.opacity = opacity;
|
||||
}
|
||||
|
||||
const rotation = frameState.viewState.rotation;
|
||||
const transform = 'rotate(' + rotation + 'rad)';
|
||||
if (transform !== canvas.style.transform) {
|
||||
canvas.style.transform = transform;
|
||||
}
|
||||
|
||||
return canvas;
|
||||
}
|
||||
|
||||
@@ -447,7 +301,9 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer {
|
||||
const replayGroup = new CanvasBuilderGroup(
|
||||
getRenderTolerance(resolution, pixelRatio), extent, resolution,
|
||||
pixelRatio, vectorSource.getOverlaps(), this.declutterTree_, vectorLayer.getRenderBuffer());
|
||||
|
||||
vectorSource.loadFeatures(extent, resolution, projection);
|
||||
|
||||
/**
|
||||
* @param {import("../../Feature.js").default} feature Feature.
|
||||
* @this {CanvasVectorLayerRenderer}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* @module ol/renderer/canvas/VectorTileLayer
|
||||
*/
|
||||
import {getUid} from '../../util.js';
|
||||
import {createCanvasContext2D} from '../../dom.js';
|
||||
import TileState from '../../TileState.js';
|
||||
import ViewHint from '../../ViewHint.js';
|
||||
import {listen, unlisten} from '../../events.js';
|
||||
@@ -12,17 +13,20 @@ import VectorTileRenderType from '../../layer/VectorTileRenderType.js';
|
||||
import {equivalent as equivalentProjection} from '../../proj.js';
|
||||
import Units from '../../proj/Units.js';
|
||||
import ReplayType from '../../render/ReplayType.js';
|
||||
import {labelCache, rotateAtOffset} from '../../render/canvas.js';
|
||||
import {labelCache} from '../../render/canvas.js';
|
||||
import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js';
|
||||
import {ORDER} from '../../render/replay.js';
|
||||
import CanvasTileLayerRenderer from './TileLayer.js';
|
||||
import {getSquaredTolerance as getSquaredRenderTolerance, renderFeature} from '../vector.js';
|
||||
import {
|
||||
apply as applyTransform,
|
||||
create as createTransform,
|
||||
compose as composeTransform,
|
||||
invert as invertTransform,
|
||||
reset as resetTransform,
|
||||
scale as scaleTransform,
|
||||
translate as translateTransform
|
||||
translate as translateTransform,
|
||||
toString as transformToString
|
||||
} from '../../transform.js';
|
||||
import CanvasExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js';
|
||||
|
||||
@@ -58,9 +62,41 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
|
||||
* @param {import("../../layer/VectorTile.js").default} layer VectorTile layer.
|
||||
*/
|
||||
constructor(layer) {
|
||||
super(layer);
|
||||
|
||||
const renderMode = layer.getRenderMode();
|
||||
super(layer, renderMode === VectorTileRenderType.VECTOR);
|
||||
const baseCanvas = this.context.canvas;
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {CanvasRenderingContext2D}
|
||||
*/
|
||||
this.overlayContext_ = createCanvasContext2D();
|
||||
|
||||
const overlayCanvas = this.overlayContext_.canvas;
|
||||
overlayCanvas.style.position = 'absolute';
|
||||
overlayCanvas.style.transformOrigin = 'top left';
|
||||
|
||||
const container = document.createElement('div');
|
||||
const style = container.style;
|
||||
style.position = 'absolute';
|
||||
style.width = '100%';
|
||||
style.height = '100%';
|
||||
|
||||
container.appendChild(baseCanvas);
|
||||
container.appendChild(overlayCanvas);
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {HTMLElement}
|
||||
*/
|
||||
this.container_ = container;
|
||||
|
||||
/**
|
||||
* The transform for rendered pixels to viewport CSS pixels for the overlay canvas.
|
||||
* @private
|
||||
* @type {import("../../transform.js").Transform}
|
||||
*/
|
||||
this.overlayPixelTransform_ = createTransform();
|
||||
|
||||
/**
|
||||
* Declutter tree.
|
||||
@@ -86,9 +122,8 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
|
||||
*/
|
||||
this.tmpTransform_ = createTransform();
|
||||
|
||||
// Use lower resolution for pure vector rendering. Closest resolution otherwise.
|
||||
this.zDirection = renderMode === VectorTileRenderType.VECTOR ? 1 : 0;
|
||||
|
||||
// Use closest resolution.
|
||||
this.zDirection = 0;
|
||||
|
||||
listen(labelCache, EventType.CLEAR, this.handleFontsChanged_, this);
|
||||
|
||||
@@ -227,9 +262,9 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
|
||||
render.call(this, feature);
|
||||
}
|
||||
}
|
||||
const replayGroupInstructions = builderGroup.finish();
|
||||
const executorGroupInstructions = builderGroup.finish();
|
||||
const renderingReplayGroup = new CanvasExecutorGroup(0, sharedExtent, resolution,
|
||||
pixelRatio, source.getOverlaps(), this.declutterTree_, replayGroupInstructions, layer.getRenderBuffer());
|
||||
pixelRatio, source.getOverlaps(), this.declutterTree_, executorGroupInstructions, layer.getRenderBuffer());
|
||||
sourceTile.setExecutorGroup(layer, tile.tileCoord.toString(), renderingReplayGroup);
|
||||
}
|
||||
builderState.renderedRevision = revision;
|
||||
@@ -332,101 +367,117 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
postCompose(context, frameState, layerState) {
|
||||
renderFrame(frameState, layerState) {
|
||||
super.renderFrame(frameState, layerState);
|
||||
|
||||
const layer = /** @type {import("../../layer/VectorTile.js").default} */ (this.getLayer());
|
||||
const renderMode = layer.getRenderMode();
|
||||
if (renderMode != VectorTileRenderType.IMAGE) {
|
||||
const declutterReplays = layer.getDeclutter() ? {} : null;
|
||||
const source = /** @type {import("../../source/VectorTile.js").default} */ (layer.getSource());
|
||||
const replayTypes = VECTOR_REPLAYS[renderMode];
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const rotation = frameState.viewState.rotation;
|
||||
const size = frameState.size;
|
||||
let offsetX, offsetY;
|
||||
if (rotation) {
|
||||
offsetX = Math.round(pixelRatio * size[0] / 2);
|
||||
offsetY = Math.round(pixelRatio * size[1] / 2);
|
||||
rotateAtOffset(context, -rotation, offsetX, offsetY);
|
||||
if (renderMode === VectorTileRenderType.IMAGE) {
|
||||
return this.container_;
|
||||
}
|
||||
|
||||
const context = this.overlayContext_;
|
||||
const declutterReplays = layer.getDeclutter() ? {} : null;
|
||||
const source = /** @type {import("../../source/VectorTile.js").default} */ (layer.getSource());
|
||||
const replayTypes = VECTOR_REPLAYS[renderMode];
|
||||
const pixelRatio = frameState.pixelRatio;
|
||||
const rotation = frameState.viewState.rotation;
|
||||
const size = frameState.size;
|
||||
|
||||
// resize and clear
|
||||
const canvas = context.canvas;
|
||||
const width = Math.round(size[0] * pixelRatio);
|
||||
const height = Math.round(size[1] * pixelRatio);
|
||||
this.overlayPixelTransform_[0] = 1 / pixelRatio;
|
||||
this.overlayPixelTransform_[3] = 1 / pixelRatio;
|
||||
if (canvas.width != width || canvas.height != height) {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
const canvasTransform = transformToString(this.overlayPixelTransform_);
|
||||
if (canvas.style.transform !== canvasTransform) {
|
||||
canvas.style.transform = canvasTransform;
|
||||
}
|
||||
if (declutterReplays) {
|
||||
this.declutterTree_.clear();
|
||||
} else {
|
||||
context.clearRect(0, 0, width, height);
|
||||
}
|
||||
|
||||
if (declutterReplays) {
|
||||
this.declutterTree_.clear();
|
||||
}
|
||||
const viewHints = frameState.viewHints;
|
||||
const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);
|
||||
const tiles = this.renderedTiles;
|
||||
const tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
|
||||
const clips = [];
|
||||
const zs = [];
|
||||
for (let i = tiles.length - 1; i >= 0; --i) {
|
||||
const tile = /** @type {import("../../VectorImageTile.js").default} */ (tiles[i]);
|
||||
if (tile.getState() == TileState.ABORT) {
|
||||
continue;
|
||||
}
|
||||
const viewHints = frameState.viewHints;
|
||||
const snapToPixel = !(viewHints[ViewHint.ANIMATING] || viewHints[ViewHint.INTERACTING]);
|
||||
const tiles = this.renderedTiles;
|
||||
const tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
|
||||
const clips = [];
|
||||
const zs = [];
|
||||
for (let i = tiles.length - 1; i >= 0; --i) {
|
||||
const tile = /** @type {import("../../VectorImageTile.js").default} */ (tiles[i]);
|
||||
if (tile.getState() == TileState.ABORT) {
|
||||
const tileCoord = tile.tileCoord;
|
||||
const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tile.extent[0];
|
||||
const transform = this.getRenderTransform(frameState, width, height, worldOffset);
|
||||
for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
|
||||
const sourceTile = tile.getTile(tile.tileKeys[t]);
|
||||
if (sourceTile.getState() != TileState.LOADED) {
|
||||
continue;
|
||||
}
|
||||
const tileCoord = tile.tileCoord;
|
||||
const worldOffset = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent)[0] - tile.extent[0];
|
||||
let transform = undefined;
|
||||
for (let t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
|
||||
const sourceTile = tile.getTile(tile.tileKeys[t]);
|
||||
if (sourceTile.getState() != TileState.LOADED) {
|
||||
continue;
|
||||
}
|
||||
const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tileCoord.toString()));
|
||||
if (!executorGroup || !executorGroup.hasExecutors(replayTypes)) {
|
||||
// sourceTile was not yet loaded when this.createReplayGroup_() was
|
||||
// called, or it has no replays of the types we want to render
|
||||
continue;
|
||||
}
|
||||
if (!transform) {
|
||||
transform = this.getTransform(frameState, worldOffset);
|
||||
}
|
||||
const currentZ = sourceTile.tileCoord[0];
|
||||
const currentClip = executorGroup.getClipCoords(transform);
|
||||
context.save();
|
||||
context.globalAlpha = layerState.opacity;
|
||||
// Create a clip mask for regions in this low resolution tile that are
|
||||
// already filled by a higher resolution tile
|
||||
for (let j = 0, jj = clips.length; j < jj; ++j) {
|
||||
const clip = clips[j];
|
||||
if (currentZ < zs[j]) {
|
||||
context.beginPath();
|
||||
// counter-clockwise (outer ring) for current tile
|
||||
context.moveTo(currentClip[0], currentClip[1]);
|
||||
context.lineTo(currentClip[2], currentClip[3]);
|
||||
context.lineTo(currentClip[4], currentClip[5]);
|
||||
context.lineTo(currentClip[6], currentClip[7]);
|
||||
// clockwise (inner ring) for higher resolution tile
|
||||
context.moveTo(clip[6], clip[7]);
|
||||
context.lineTo(clip[4], clip[5]);
|
||||
context.lineTo(clip[2], clip[3]);
|
||||
context.lineTo(clip[0], clip[1]);
|
||||
context.clip();
|
||||
}
|
||||
}
|
||||
executorGroup.execute(context, transform, rotation, {}, snapToPixel, replayTypes, declutterReplays);
|
||||
context.restore();
|
||||
clips.push(currentClip);
|
||||
zs.push(currentZ);
|
||||
const executorGroup = /** @type {CanvasExecutorGroup} */ (sourceTile.getExecutorGroup(layer, tileCoord.toString()));
|
||||
if (!executorGroup || !executorGroup.hasExecutors(replayTypes)) {
|
||||
// sourceTile was not yet loaded when this.createReplayGroup_() was
|
||||
// called, or it has no replays of the types we want to render
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (declutterReplays) {
|
||||
replayDeclutter(declutterReplays, context, rotation, snapToPixel);
|
||||
}
|
||||
if (rotation) {
|
||||
rotateAtOffset(context, rotation,
|
||||
/** @type {number} */ (offsetX), /** @type {number} */ (offsetY));
|
||||
const currentZ = sourceTile.tileCoord[0];
|
||||
const currentClip = executorGroup.getClipCoords(transform);
|
||||
context.save();
|
||||
|
||||
// Create a clip mask for regions in this low resolution tile that are
|
||||
// already filled by a higher resolution tile
|
||||
for (let j = 0, jj = clips.length; j < jj; ++j) {
|
||||
const clip = clips[j];
|
||||
if (currentZ < zs[j]) {
|
||||
context.beginPath();
|
||||
// counter-clockwise (outer ring) for current tile
|
||||
context.moveTo(currentClip[0], currentClip[1]);
|
||||
context.lineTo(currentClip[2], currentClip[3]);
|
||||
context.lineTo(currentClip[4], currentClip[5]);
|
||||
context.lineTo(currentClip[6], currentClip[7]);
|
||||
// clockwise (inner ring) for higher resolution tile
|
||||
context.moveTo(clip[6], clip[7]);
|
||||
context.lineTo(clip[4], clip[5]);
|
||||
context.lineTo(clip[2], clip[3]);
|
||||
context.lineTo(clip[0], clip[1]);
|
||||
context.clip();
|
||||
}
|
||||
}
|
||||
executorGroup.execute(context, transform, rotation, {}, snapToPixel, replayTypes, declutterReplays);
|
||||
context.restore();
|
||||
clips.push(currentClip);
|
||||
zs.push(currentZ);
|
||||
}
|
||||
}
|
||||
super.postCompose(context, frameState, layerState);
|
||||
if (declutterReplays) {
|
||||
replayDeclutter(declutterReplays, context, rotation, snapToPixel);
|
||||
}
|
||||
|
||||
const opacity = layerState.opacity;
|
||||
if (opacity !== canvas.style.opacity) {
|
||||
canvas.style.opacity = opacity;
|
||||
}
|
||||
|
||||
return this.container_;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import("../../Feature.js").FeatureLike} feature Feature.
|
||||
* @param {number} squaredTolerance Squared tolerance.
|
||||
* @param {import("../../style/Style.js").default|Array<import("../../style/Style.js").default>} styles The style or array of styles.
|
||||
* @param {import("../../render/canvas/BuilderGroup.js").default} replayGroup Replay group.
|
||||
* @param {import("../../render/canvas/BuilderGroup.js").default} executorGroup Replay group.
|
||||
* @return {boolean} `true` if an image is loading.
|
||||
*/
|
||||
renderFeature(feature, squaredTolerance, styles, replayGroup) {
|
||||
renderFeature(feature, squaredTolerance, styles, executorGroup) {
|
||||
if (!styles) {
|
||||
return false;
|
||||
}
|
||||
@@ -434,12 +485,12 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
|
||||
if (Array.isArray(styles)) {
|
||||
for (let i = 0, ii = styles.length; i < ii; ++i) {
|
||||
loading = renderFeature(
|
||||
replayGroup, feature, styles[i], squaredTolerance,
|
||||
executorGroup, feature, styles[i], squaredTolerance,
|
||||
this.handleStyleImageChange_, this) || loading;
|
||||
}
|
||||
} else {
|
||||
loading = renderFeature(
|
||||
replayGroup, feature, styles, squaredTolerance,
|
||||
executorGroup, feature, styles, squaredTolerance,
|
||||
this.handleStyleImageChange_, this);
|
||||
}
|
||||
return loading;
|
||||
@@ -483,6 +534,35 @@ class CanvasVectorTileLayerRenderer extends CanvasTileLayerRenderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
getDataAtPixel(pixel, frameState, hitTolerance) {
|
||||
let data = super.getDataAtPixel(pixel, frameState, hitTolerance);
|
||||
if (data) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const renderPixel = applyTransform(invertTransform(this.overlayPixelTransform_.slice()), pixel.slice());
|
||||
const context = this.overlayContext_;
|
||||
|
||||
try {
|
||||
data = context.getImageData(Math.round(renderPixel[0]), Math.round(renderPixel[1]), 1, 1).data;
|
||||
} catch (err) {
|
||||
if (err.name === 'SecurityError') {
|
||||
// tainted canvas, we assume there is data at the given pixel (although there might not be)
|
||||
return new Uint8Array();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
if (data[3] === 0) {
|
||||
return null;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -169,26 +169,23 @@ class RasterSource extends ImageSource {
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {Array<import("../renderer/canvas/Layer.js").default>}
|
||||
* @type {Array<import("../layer/Layer.js").default>}
|
||||
*/
|
||||
this.renderers_ = createRenderers(options.sources);
|
||||
this.layers_ = createLayers(options.sources);
|
||||
|
||||
for (let r = 0, rr = this.renderers_.length; r < rr; ++r) {
|
||||
listen(this.renderers_[r], EventType.CHANGE,
|
||||
this.changed, this);
|
||||
for (let i = 0, ii = this.layers_.length; i < ii; ++i) {
|
||||
listen(this.layers_[i], EventType.CHANGE, this.changed, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* @private
|
||||
* @type {import("../TileQueue.js").default}
|
||||
*/
|
||||
this.tileQueue_ = new TileQueue(
|
||||
function() {
|
||||
return 1;
|
||||
},
|
||||
this.changed.bind(this));
|
||||
this.tileQueue_ = new TileQueue(function() {
|
||||
return 1;
|
||||
}, this.changed.bind(this));
|
||||
|
||||
const layerStatesArray = getLayerStatesArray(this.renderers_);
|
||||
const layerStatesArray = getLayerStatesArray(this.layers_);
|
||||
|
||||
/**
|
||||
* @type {Object<string, import("../layer/Layer.js").State>}
|
||||
@@ -307,8 +304,8 @@ class RasterSource extends ImageSource {
|
||||
allSourcesReady_() {
|
||||
let ready = true;
|
||||
let source;
|
||||
for (let i = 0, ii = this.renderers_.length; i < ii; ++i) {
|
||||
source = this.renderers_[i].getLayer().getSource();
|
||||
for (let i = 0, ii = this.layers_.length; i < ii; ++i) {
|
||||
source = this.layers_[i].getSource();
|
||||
if (source.getState() !== SourceState.READY) {
|
||||
ready = false;
|
||||
break;
|
||||
@@ -356,11 +353,10 @@ class RasterSource extends ImageSource {
|
||||
*/
|
||||
processSources_() {
|
||||
const frameState = this.requestedFrameState_;
|
||||
const len = this.renderers_.length;
|
||||
const len = this.layers_.length;
|
||||
const imageDatas = new Array(len);
|
||||
for (let i = 0; i < len; ++i) {
|
||||
const imageData = getImageData(
|
||||
this.renderers_[i], frameState, frameState.layerStatesArray[i]);
|
||||
const imageData = getImageData(this.layers_[i], frameState, frameState.layerStatesArray[i]);
|
||||
if (imageData) {
|
||||
imageDatas[i] = imageData;
|
||||
} else {
|
||||
@@ -429,18 +425,32 @@ let sharedContext = null;
|
||||
|
||||
|
||||
/**
|
||||
* Get image data from a renderer.
|
||||
* @param {import("../renderer/canvas/Layer.js").default} renderer Layer renderer.
|
||||
* Get image data from a layer.
|
||||
* @param {import("../layer/Layer.js").default} layer Layer to render.
|
||||
* @param {import("../PluggableMap.js").FrameState} frameState The frame state.
|
||||
* @param {import("../layer/Layer.js").State} layerState The layer state.
|
||||
* @return {ImageData} The image data.
|
||||
*/
|
||||
function getImageData(renderer, frameState, layerState) {
|
||||
function getImageData(layer, frameState, layerState) {
|
||||
const renderer = layer.getRenderer();
|
||||
if (!renderer) {
|
||||
throw new Error('Unsupported layer type: ' + layer);
|
||||
}
|
||||
|
||||
if (!renderer.prepareFrame(frameState, layerState)) {
|
||||
return null;
|
||||
}
|
||||
const width = frameState.size[0];
|
||||
const height = frameState.size[1];
|
||||
const element = renderer.renderFrame(frameState, layerState);
|
||||
if (!(element instanceof HTMLCanvasElement)) {
|
||||
throw new Error('Unsupported rendered element: ' + element);
|
||||
}
|
||||
if (element.width === width && element.height === height) {
|
||||
const context = element.getContext('2d');
|
||||
return context.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
||||
if (!sharedContext) {
|
||||
sharedContext = createCanvasContext2D(width, height);
|
||||
} else {
|
||||
@@ -451,44 +461,44 @@ function getImageData(renderer, frameState, layerState) {
|
||||
sharedContext.clearRect(0, 0, width, height);
|
||||
}
|
||||
}
|
||||
renderer.composeFrame(frameState, layerState, sharedContext);
|
||||
sharedContext.drawImage(element, 0, 0, width, height);
|
||||
return sharedContext.getImageData(0, 0, width, height);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a list of layer states from a list of renderers.
|
||||
* @param {Array<import("../renderer/canvas/Layer.js").default>} renderers Layer renderers.
|
||||
* Get a list of layer states from a list of layers.
|
||||
* @param {Array<import("../layer/Layer.js").default>} layers Layers.
|
||||
* @return {Array<import("../layer/Layer.js").State>} The layer states.
|
||||
*/
|
||||
function getLayerStatesArray(renderers) {
|
||||
return renderers.map(function(renderer) {
|
||||
return renderer.getLayer().getLayerState();
|
||||
function getLayerStatesArray(layers) {
|
||||
return layers.map(function(layer) {
|
||||
return layer.getLayerState();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create renderers for all sources.
|
||||
* Create layers for all sources.
|
||||
* @param {Array<import("./Source.js").default|import("../layer/Layer.js").default>} sources The sources.
|
||||
* @return {Array<import("../renderer/canvas/Layer.js").default>} Array of layer renderers.
|
||||
* @return {Array<import("../layer/Layer.js").default>} Array of layers.
|
||||
*/
|
||||
function createRenderers(sources) {
|
||||
function createLayers(sources) {
|
||||
const len = sources.length;
|
||||
const renderers = new Array(len);
|
||||
const layers = new Array(len);
|
||||
for (let i = 0; i < len; ++i) {
|
||||
renderers[i] = createRenderer(sources[i]);
|
||||
layers[i] = createLayer(sources[i]);
|
||||
}
|
||||
return renderers;
|
||||
return layers;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a renderer for the provided source.
|
||||
* Create a layer for the provided source.
|
||||
* @param {import("./Source.js").default|import("../layer/Layer.js").default} layerOrSource The layer or source.
|
||||
* @return {import("../renderer/canvas/Layer.js").default} The renderer.
|
||||
* @return {import("../layer/Layer.js").default} The layer.
|
||||
*/
|
||||
function createRenderer(layerOrSource) {
|
||||
function createLayer(layerOrSource) {
|
||||
// @type {import("../layer/Layer.js").default}
|
||||
let layer;
|
||||
if (layerOrSource instanceof Source) {
|
||||
@@ -500,7 +510,7 @@ function createRenderer(layerOrSource) {
|
||||
} else {
|
||||
layer = layerOrSource;
|
||||
}
|
||||
return layer ? /** @type {import("../renderer/canvas/Layer.js").default} */ (layer.createRenderer()) : null;
|
||||
return layer;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -236,3 +236,13 @@ export function invert(transform) {
|
||||
export function determinant(mat) {
|
||||
return mat[0] * mat[3] - mat[1] * mat[2];
|
||||
}
|
||||
|
||||
/**
|
||||
* A string version of the transform. This can be used
|
||||
* for CSS transforms.
|
||||
* @param {!Transform} mat Matrix.
|
||||
* @return {string} The transform as a string.
|
||||
*/
|
||||
export function toString(mat) {
|
||||
return 'matrix(' + mat.join(', ') + ')';
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// require all modules ending in ".test.js" from the
|
||||
// current directory and all subdirectories
|
||||
const testsContext = require.context('.', true, /\.test\.js$/);
|
||||
const testsContext = require.context('./spec', true, /\.test\.js$/);
|
||||
|
||||
testsContext.keys().forEach(testsContext);
|
||||
|
||||
@@ -8,6 +8,7 @@ describe('ol.control.ScaleLine', function() {
|
||||
let map;
|
||||
beforeEach(function() {
|
||||
const target = document.createElement('div');
|
||||
target.style.height = '256px';
|
||||
document.body.appendChild(target);
|
||||
map = new Map({
|
||||
target: target
|
||||
|
||||
@@ -39,10 +39,10 @@ describe('ol.layer.VectorTile', function() {
|
||||
describe('constructor (options)', function() {
|
||||
it('works with options', function() {
|
||||
let layer = new VectorTileLayer({
|
||||
renderMode: 'vector',
|
||||
renderMode: 'hybrid',
|
||||
source: new VectorTileSource({})
|
||||
});
|
||||
expect(layer.getRenderMode()).to.be('vector');
|
||||
expect(layer.getRenderMode()).to.be('hybrid');
|
||||
layer = new VectorTileLayer({
|
||||
renderMode: 'image',
|
||||
source: new VectorTileSource({})
|
||||
|
||||
@@ -18,7 +18,7 @@ import PinchZoom from '../../../src/ol/interaction/PinchZoom.js';
|
||||
import ImageLayer from '../../../src/ol/layer/Image.js';
|
||||
import TileLayer from '../../../src/ol/layer/Tile.js';
|
||||
import VectorLayer from '../../../src/ol/layer/Vector.js';
|
||||
import IntermediateCanvasRenderer from '../../../src/ol/renderer/canvas/IntermediateCanvas.js';
|
||||
import TileLayerRenderer from '../../../src/ol/renderer/canvas/TileLayer.js';
|
||||
import ImageStatic from '../../../src/ol/source/ImageStatic.js';
|
||||
import VectorSource from '../../../src/ol/source/Vector.js';
|
||||
import XYZ from '../../../src/ol/source/XYZ.js';
|
||||
@@ -325,9 +325,9 @@ describe('ol.Map', function() {
|
||||
|
||||
beforeEach(function(done) {
|
||||
log = [];
|
||||
original = IntermediateCanvasRenderer.prototype.forEachLayerAtCoordinate;
|
||||
IntermediateCanvasRenderer.prototype.forEachLayerAtCoordinate = function(coordinate) {
|
||||
log.push(coordinate.slice());
|
||||
original = TileLayerRenderer.prototype.getDataAtPixel;
|
||||
TileLayerRenderer.prototype.getDataAtPixel = function(pixel) {
|
||||
log.push(pixel.slice());
|
||||
};
|
||||
|
||||
target = document.createElement('div');
|
||||
@@ -364,13 +364,13 @@ describe('ol.Map', function() {
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
IntermediateCanvasRenderer.prototype.forEachLayerAtCoordinate = original;
|
||||
TileLayerRenderer.prototype.getDataAtPixel = original;
|
||||
map.dispose();
|
||||
document.body.removeChild(target);
|
||||
log = null;
|
||||
});
|
||||
|
||||
it('calls each layer renderer with the same coordinate', function() {
|
||||
it('calls each layer renderer with the same pixel', function() {
|
||||
const pixel = [10, 20];
|
||||
map.forEachLayerAtPixel(pixel, function() {});
|
||||
expect(log.length).to.equal(3);
|
||||
|
||||
@@ -13,6 +13,8 @@ describe('ol.render.Box', function() {
|
||||
box = new RenderBox('test-box');
|
||||
|
||||
target = document.createElement('div');
|
||||
target.style.height = '256px';
|
||||
|
||||
document.body.appendChild(target);
|
||||
|
||||
map = new Map({
|
||||
|
||||
@@ -95,18 +95,18 @@ describe('ol.renderer.canvas.ImageLayer', function() {
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('dispatches precompose and postcompose events on the vector layer', function(done) {
|
||||
let precompose = 0;
|
||||
let postcompose = 0;
|
||||
layer.on('precompose', function() {
|
||||
++precompose;
|
||||
it('dispatches prerender and postrender events on the vector layer', function(done) {
|
||||
let prerender = 0;
|
||||
let postrender = 0;
|
||||
layer.on('prerender', function() {
|
||||
++prerender;
|
||||
});
|
||||
layer.on('postcompose', function() {
|
||||
++postcompose;
|
||||
layer.on('postrender', function() {
|
||||
++postrender;
|
||||
});
|
||||
map.once('postrender', function() {
|
||||
expect(precompose).to.be(1);
|
||||
expect(postcompose).to.be(1);
|
||||
expect(prerender).to.be(1);
|
||||
expect(postrender).to.be(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,110 +0,0 @@
|
||||
import {create as createTransform, scale} from '../../../../../src/ol/transform.js';
|
||||
import ImageLayer from '../../../../../src/ol/layer/Image.js';
|
||||
import MapRenderer from '../../../../../src/ol/renderer/Map.js';
|
||||
import IntermediateCanvasRenderer from '../../../../../src/ol/renderer/canvas/IntermediateCanvas.js';
|
||||
|
||||
|
||||
describe('ol.renderer.canvas.IntermediateCanvas', function() {
|
||||
|
||||
describe('#composeFrame()', function() {
|
||||
let renderer, frameState, layerState, context;
|
||||
beforeEach(function() {
|
||||
const layer = new ImageLayer({
|
||||
extent: [1, 2, 3, 4]
|
||||
});
|
||||
renderer = new IntermediateCanvasRenderer(layer);
|
||||
const image = new Image();
|
||||
image.width = 3;
|
||||
image.height = 3;
|
||||
renderer.getImage = function() {
|
||||
return image;
|
||||
};
|
||||
frameState = {
|
||||
viewState: {
|
||||
center: [2, 3],
|
||||
resolution: 1,
|
||||
rotation: 0
|
||||
},
|
||||
size: [10, 10],
|
||||
pixelRatio: 1,
|
||||
coordinateToPixelTransform: createTransform(),
|
||||
pixelToCoordinateTransform: createTransform()
|
||||
};
|
||||
renderer.getImageTransform = function() {
|
||||
return createTransform();
|
||||
};
|
||||
MapRenderer.prototype.calculateMatrices2D(frameState);
|
||||
layerState = layer.getLayerState();
|
||||
context = {
|
||||
save: sinon.spy(),
|
||||
restore: sinon.spy(),
|
||||
translate: sinon.spy(),
|
||||
rotate: sinon.spy(),
|
||||
beginPath: sinon.spy(),
|
||||
moveTo: sinon.spy(),
|
||||
lineTo: sinon.spy(),
|
||||
clip: sinon.spy(),
|
||||
drawImage: sinon.spy()
|
||||
};
|
||||
});
|
||||
|
||||
it('clips to layer extent and draws image', function() {
|
||||
frameState.extent = [0, 1, 4, 5];
|
||||
|
||||
renderer.composeFrame(frameState, layerState, context);
|
||||
expect(context.save.callCount).to.be(1);
|
||||
expect(context.translate.callCount).to.be(0);
|
||||
expect(context.rotate.callCount).to.be(0);
|
||||
expect(context.beginPath.callCount).to.be(1);
|
||||
expect(context.moveTo.firstCall.args).to.eql([4, 4]);
|
||||
expect(context.lineTo.firstCall.args).to.eql([6, 4]);
|
||||
expect(context.lineTo.secondCall.args).to.eql([6, 6]);
|
||||
expect(context.lineTo.thirdCall.args).to.eql([4, 6]);
|
||||
expect(context.clip.callCount).to.be(1);
|
||||
expect(context.drawImage.firstCall.args).to.eql(
|
||||
[renderer.getImage(), 0, 0, 3, 3, 0, 0, 3, 3]);
|
||||
expect(context.restore.callCount).to.be(1);
|
||||
});
|
||||
|
||||
it('does not clip if frame extent does not intersect layer extent', function() {
|
||||
frameState.extent = [1.1, 2.1, 2.9, 3.9];
|
||||
|
||||
renderer.composeFrame(frameState, layerState, context);
|
||||
expect(context.save.callCount).to.be(0);
|
||||
expect(context.translate.callCount).to.be(0);
|
||||
expect(context.rotate.callCount).to.be(0);
|
||||
expect(context.beginPath.callCount).to.be(0);
|
||||
expect(context.clip.callCount).to.be(0);
|
||||
expect(context.drawImage.firstCall.args).to.eql(
|
||||
[renderer.getImage(), 0, 0, 3, 3, 0, 0, 3, 3]);
|
||||
expect(context.restore.callCount).to.be(0);
|
||||
});
|
||||
|
||||
it('does not clip if frame extent is outside of layer extent', function() {
|
||||
frameState.extent = [10, 20, 30, 40];
|
||||
|
||||
renderer.composeFrame(frameState, layerState, context);
|
||||
expect(context.save.callCount).to.be(0);
|
||||
expect(context.translate.callCount).to.be(0);
|
||||
expect(context.rotate.callCount).to.be(0);
|
||||
expect(context.beginPath.callCount).to.be(0);
|
||||
expect(context.clip.callCount).to.be(0);
|
||||
expect(context.drawImage.firstCall.args).to.eql(
|
||||
[renderer.getImage(), 0, 0, 3, 3, 0, 0, 3, 3]);
|
||||
expect(context.restore.callCount).to.be(0);
|
||||
});
|
||||
|
||||
it('does not draw image with width or height < 0.5', function() {
|
||||
frameState.extent = [10, 20, 30, 40];
|
||||
renderer.getImageTransform = function() {
|
||||
return scale(createTransform(), 0.1, 0.1);
|
||||
};
|
||||
|
||||
renderer.composeFrame(frameState, layerState, context);
|
||||
|
||||
expect(context.drawImage.notCalled).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,271 +0,0 @@
|
||||
import {getUid} from '../../../../../src/ol/util.js';
|
||||
import Feature from '../../../../../src/ol/Feature.js';
|
||||
import Map from '../../../../../src/ol/Map.js';
|
||||
import View from '../../../../../src/ol/View.js';
|
||||
import Point from '../../../../../src/ol/geom/Point.js';
|
||||
import TileLayer from '../../../../../src/ol/layer/Tile.js';
|
||||
import VectorLayer from '../../../../../src/ol/layer/Vector.js';
|
||||
import CanvasLayerRenderer from '../../../../../src/ol/renderer/canvas/Layer.js';
|
||||
import CanvasMapRenderer from '../../../../../src/ol/renderer/canvas/Map.js';
|
||||
import VectorSource from '../../../../../src/ol/source/Vector.js';
|
||||
import Icon from '../../../../../src/ol/style/Icon.js';
|
||||
import Style from '../../../../../src/ol/style/Style.js';
|
||||
|
||||
describe('ol.renderer.canvas.Map', function() {
|
||||
|
||||
describe('constructor', function() {
|
||||
|
||||
it('creates a new instance', function() {
|
||||
const map = new Map({
|
||||
target: document.createElement('div')
|
||||
});
|
||||
const renderer = new CanvasMapRenderer(map);
|
||||
expect(renderer).to.be.a(CanvasMapRenderer);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('#forEachFeatureAtCoordinate', function() {
|
||||
|
||||
let layer, map, target;
|
||||
|
||||
beforeEach(function(done) {
|
||||
target = document.createElement('div');
|
||||
target.style.width = '100px';
|
||||
target.style.height = '100px';
|
||||
document.body.appendChild(target);
|
||||
map = new Map({
|
||||
pixelRatio: 1,
|
||||
target: target,
|
||||
view: new View({
|
||||
center: [0, 0],
|
||||
zoom: 0
|
||||
})
|
||||
});
|
||||
|
||||
// 1 x 1 pixel black icon
|
||||
const img = document.createElement('img');
|
||||
img.onload = function() {
|
||||
done();
|
||||
};
|
||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
|
||||
|
||||
layer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [
|
||||
new Feature({
|
||||
geometry: new Point([0, 0])
|
||||
})
|
||||
]
|
||||
}),
|
||||
style: new Style({
|
||||
image: new Icon({
|
||||
img: img,
|
||||
imgSize: [1, 1]
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
map.setTarget(null);
|
||||
document.body.removeChild(target);
|
||||
});
|
||||
|
||||
it('calls callback with layer for managed layers', function() {
|
||||
map.addLayer(layer);
|
||||
map.renderSync();
|
||||
const cb = sinon.spy();
|
||||
map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb);
|
||||
expect(cb).to.be.called();
|
||||
expect(cb.firstCall.args[1]).to.be(layer);
|
||||
});
|
||||
|
||||
it('calls callback with null for unmanaged layers', function() {
|
||||
layer.setMap(map);
|
||||
map.renderSync();
|
||||
const cb = sinon.spy();
|
||||
map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb);
|
||||
expect(cb).to.be.called();
|
||||
expect(cb.firstCall.args[1]).to.be(null);
|
||||
});
|
||||
|
||||
it('calls callback with main layer when skipped feature on unmanaged layer', function() {
|
||||
const feature = layer.getSource().getFeatures()[0];
|
||||
const managedLayer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [feature]
|
||||
})
|
||||
});
|
||||
map.addLayer(managedLayer);
|
||||
map.skipFeature(feature);
|
||||
layer.setMap(map);
|
||||
map.renderSync();
|
||||
const cb = sinon.spy();
|
||||
map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb);
|
||||
expect(cb.callCount).to.be(1);
|
||||
expect(cb.firstCall.args[1]).to.be(managedLayer);
|
||||
});
|
||||
|
||||
it('filters managed layers', function() {
|
||||
map.addLayer(layer);
|
||||
map.renderSync();
|
||||
const cb = sinon.spy();
|
||||
map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]), cb, {
|
||||
layerFilter: function() {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
expect(cb).to.not.be.called();
|
||||
});
|
||||
|
||||
it('doesn\'t fail with layer with no source', function() {
|
||||
map.addLayer(new TileLayer());
|
||||
map.renderSync();
|
||||
expect(function() {
|
||||
map.forEachFeatureAtPixel(map.getPixelFromCoordinate([0, 0]),
|
||||
function() {});
|
||||
}).to.not.throwException();
|
||||
});
|
||||
|
||||
it('calls callback for clicks inside of the hitTolerance', function() {
|
||||
map.addLayer(layer);
|
||||
map.renderSync();
|
||||
const cb1 = sinon.spy();
|
||||
const cb2 = sinon.spy();
|
||||
|
||||
const pixel = map.getPixelFromCoordinate([0, 0]);
|
||||
|
||||
const pixelsInside = [
|
||||
[pixel[0] + 9, pixel[1]],
|
||||
[pixel[0] - 9, pixel[1]],
|
||||
[pixel[0], pixel[1] + 9],
|
||||
[pixel[0], pixel[1] - 9]
|
||||
];
|
||||
|
||||
const pixelsOutside = [
|
||||
[pixel[0] + 9, pixel[1] + 9],
|
||||
[pixel[0] - 9, pixel[1] + 9],
|
||||
[pixel[0] + 9, pixel[1] - 9],
|
||||
[pixel[0] - 9, pixel[1] - 9]
|
||||
];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
map.forEachFeatureAtPixel(pixelsInside[i], cb1, {hitTolerance: 10});
|
||||
}
|
||||
expect(cb1.callCount).to.be(4);
|
||||
expect(cb1.firstCall.args[1]).to.be(layer);
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
map.forEachFeatureAtPixel(pixelsOutside[j], cb2, {hitTolerance: 10});
|
||||
}
|
||||
expect(cb2).not.to.be.called();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#forEachLayerAtCoordinate', function() {
|
||||
|
||||
let layer, map, target;
|
||||
|
||||
beforeEach(function(done) {
|
||||
target = document.createElement('div');
|
||||
target.style.width = '100px';
|
||||
target.style.height = '100px';
|
||||
document.body.appendChild(target);
|
||||
map = new Map({
|
||||
pixelRatio: 1,
|
||||
target: target,
|
||||
view: new View({
|
||||
center: [0, 0],
|
||||
zoom: 0
|
||||
})
|
||||
});
|
||||
|
||||
// 1 x 1 pixel black icon
|
||||
const img = document.createElement('img');
|
||||
img.onload = function() {
|
||||
done();
|
||||
};
|
||||
img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVR4nGNiAAAABgADNjd8qAAAAABJRU5ErkJggg==';
|
||||
|
||||
layer = new VectorLayer({
|
||||
source: new VectorSource({
|
||||
features: [
|
||||
new Feature({
|
||||
geometry: new Point([0, 0])
|
||||
})
|
||||
]
|
||||
}),
|
||||
style: new Style({
|
||||
image: new Icon({
|
||||
img: img,
|
||||
imgSize: [1, 1]
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
map.setTarget(null);
|
||||
document.body.removeChild(target);
|
||||
});
|
||||
|
||||
it('calls callback for clicks inside of the hitTolerance', function() {
|
||||
map.addLayer(layer);
|
||||
map.renderSync();
|
||||
const cb1 = sinon.spy();
|
||||
const cb2 = sinon.spy();
|
||||
|
||||
const pixel = map.getPixelFromCoordinate([0, 0]);
|
||||
|
||||
const pixelsInside = [
|
||||
[pixel[0] + 9, pixel[1]],
|
||||
[pixel[0] - 9, pixel[1]],
|
||||
[pixel[0], pixel[1] + 9],
|
||||
[pixel[0], pixel[1] - 9]
|
||||
];
|
||||
|
||||
const pixelsOutside = [
|
||||
[pixel[0] + 9, pixel[1] + 9],
|
||||
[pixel[0] - 9, pixel[1] + 9],
|
||||
[pixel[0] + 9, pixel[1] - 9],
|
||||
[pixel[0] - 9, pixel[1] - 9]
|
||||
];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
map.forEachLayerAtPixel(pixelsInside[i], cb1, {hitTolerance: 10});
|
||||
}
|
||||
expect(cb1.callCount).to.be(4);
|
||||
expect(cb1.firstCall.args[0]).to.be(layer);
|
||||
|
||||
for (let j = 0; j < 4; j++) {
|
||||
map.forEachLayerAtPixel(pixelsOutside[j], cb2, {hitTolerance: 10});
|
||||
}
|
||||
expect(cb2).not.to.be.called();
|
||||
});
|
||||
});
|
||||
|
||||
describe('#renderFrame()', function() {
|
||||
let layer, map, renderer;
|
||||
|
||||
beforeEach(function() {
|
||||
map = new Map({});
|
||||
map.on('postcompose', function() {});
|
||||
layer = new VectorLayer({
|
||||
source: new VectorSource({wrapX: true})
|
||||
});
|
||||
renderer = map.getRenderer();
|
||||
renderer.layerRenderers_ = {};
|
||||
const layerRenderer = new CanvasLayerRenderer(layer);
|
||||
layerRenderer.prepareFrame = function() {
|
||||
return true;
|
||||
};
|
||||
layerRenderer.getImage = function() {
|
||||
return null;
|
||||
};
|
||||
renderer.layerRenderers_[getUid(layer)] = layerRenderer;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
@@ -1,12 +1,7 @@
|
||||
import Map from '../../../../../src/ol/Map.js';
|
||||
import View from '../../../../../src/ol/View.js';
|
||||
import TileLayer from '../../../../../src/ol/layer/Tile.js';
|
||||
import {get as getProjection} from '../../../../../src/ol/proj.js';
|
||||
import MapRenderer from '../../../../../src/ol/renderer/Map.js';
|
||||
import CanvasTileLayerRenderer from '../../../../../src/ol/renderer/canvas/TileLayer.js';
|
||||
import TileWMS from '../../../../../src/ol/source/TileWMS.js';
|
||||
import XYZ from '../../../../../src/ol/source/XYZ.js';
|
||||
import {create as createTransform} from '../../../../../src/ol/transform.js';
|
||||
|
||||
|
||||
describe('ol.renderer.canvas.TileLayer', function() {
|
||||
@@ -54,70 +49,4 @@ describe('ol.renderer.canvas.TileLayer', function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe('#composeFrame()', function() {
|
||||
|
||||
let img = null;
|
||||
beforeEach(function(done) {
|
||||
img = new Image(1, 1);
|
||||
img.onload = function() {
|
||||
done();
|
||||
};
|
||||
img.src = 'data:image/gif;base64,' +
|
||||
'R0lGODlhAQABAPAAAP8AAP///yH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==';
|
||||
});
|
||||
afterEach(function() {
|
||||
img = null;
|
||||
});
|
||||
|
||||
it('uses correct draw scale when rotating (HiDPI)', function() {
|
||||
const layer = new TileLayer({
|
||||
source: new XYZ({
|
||||
tileSize: 1
|
||||
})
|
||||
});
|
||||
const renderer = new CanvasTileLayerRenderer(layer);
|
||||
renderer.renderedTiles = [];
|
||||
const frameState = {
|
||||
viewHints: [],
|
||||
time: Date.now(),
|
||||
viewState: {
|
||||
center: [10, 5],
|
||||
projection: getProjection('EPSG:3857'),
|
||||
resolution: 1,
|
||||
rotation: Math.PI
|
||||
},
|
||||
extent: [0, 0, 20, 10],
|
||||
size: [20, 10],
|
||||
pixelRatio: 2,
|
||||
coordinateToPixelTransform: createTransform(),
|
||||
pixelToCoordinateTransform: createTransform(),
|
||||
usedTiles: {},
|
||||
wantedTiles: {}
|
||||
};
|
||||
renderer.getImageTransform = function() {
|
||||
return createTransform();
|
||||
};
|
||||
MapRenderer.prototype.calculateMatrices2D(frameState);
|
||||
const layerState = layer.getLayerState();
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 200;
|
||||
canvas.height = 100;
|
||||
const context = {
|
||||
canvas: canvas,
|
||||
drawImage: sinon.spy()
|
||||
};
|
||||
renderer.renderedTiles = [{
|
||||
getTileCoord: function() {
|
||||
return [0, 0, 0];
|
||||
},
|
||||
getImage: function() {
|
||||
return img;
|
||||
}
|
||||
}];
|
||||
renderer.prepareFrame(frameState, layerState);
|
||||
renderer.composeFrame(frameState, layerState, context);
|
||||
expect(context.drawImage.firstCall.args[0].width).to.be(17);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -296,22 +296,22 @@ describe('ol.renderer.canvas.VectorLayer', function() {
|
||||
expect(renderer.replayGroupChanged).to.be(false);
|
||||
});
|
||||
|
||||
it('dispatches a render event when rendering to own context', function(done) {
|
||||
it('dispatches a postrender event when rendering', function(done) {
|
||||
const layer = renderer.getLayer();
|
||||
layer.getSource().addFeature(new Feature(new Point([0, 0])));
|
||||
layer.once('render', function() {
|
||||
layer.once('postrender', function() {
|
||||
expect(true);
|
||||
done();
|
||||
});
|
||||
frameState.extent = [-10000, -10000, 10000, 10000];
|
||||
frameState.size = [100, 100];
|
||||
frameState.viewState.center = [0, 0];
|
||||
let composed = false;
|
||||
let rendered = false;
|
||||
if (renderer.prepareFrame(frameState, {})) {
|
||||
composed = true;
|
||||
renderer.compose(renderer.context, frameState, layer.getLayerState);
|
||||
rendered = true;
|
||||
renderer.renderFrame(frameState, layer.getLayerState());
|
||||
}
|
||||
expect(composed).to.be(true);
|
||||
expect(rendered).to.be(true);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -75,6 +75,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
|
||||
tileGrid: createXYZ()
|
||||
});
|
||||
source.getTile = function() {
|
||||
arguments[1] = TileState.LOADED;
|
||||
const tile = VectorTileSource.prototype.getTile.apply(source, arguments);
|
||||
tile.setState(TileState.LOADED);
|
||||
return tile;
|
||||
@@ -97,31 +98,6 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
|
||||
expect(renderer.zDirection).to.be(0);
|
||||
});
|
||||
|
||||
it('uses lower resolution for pure vector rendering', function() {
|
||||
const testLayer = new VectorTileLayer({
|
||||
renderMode: VectorTileRenderType.VECTOR,
|
||||
source: source,
|
||||
style: layerStyle
|
||||
});
|
||||
const renderer = new CanvasVectorTileLayerRenderer(testLayer);
|
||||
expect(renderer.zDirection).to.be(1);
|
||||
});
|
||||
|
||||
it('does not render images for pure vector rendering', function() {
|
||||
const testLayer = new VectorTileLayer({
|
||||
renderMode: VectorTileRenderType.VECTOR,
|
||||
source: source,
|
||||
style: layerStyle
|
||||
});
|
||||
map.removeLayer(layer);
|
||||
map.addLayer(testLayer);
|
||||
const spy = sinon.spy(CanvasVectorTileLayerRenderer.prototype,
|
||||
'renderTileImage_');
|
||||
map.renderSync();
|
||||
expect(spy.callCount).to.be(0);
|
||||
spy.restore();
|
||||
});
|
||||
|
||||
it('does not render replays for pure image rendering', function() {
|
||||
const testLayer = new VectorTileLayer({
|
||||
renderMode: VectorTileRenderType.IMAGE,
|
||||
@@ -131,7 +107,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
|
||||
map.removeLayer(layer);
|
||||
map.addLayer(testLayer);
|
||||
const spy = sinon.spy(CanvasVectorTileLayerRenderer.prototype,
|
||||
'getTransform');
|
||||
'getRenderTransform');
|
||||
map.renderSync();
|
||||
expect(spy.callCount).to.be(0);
|
||||
spy.restore();
|
||||
@@ -139,7 +115,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
|
||||
|
||||
it('renders both replays and images for hybrid rendering', function() {
|
||||
const spy1 = sinon.spy(CanvasVectorTileLayerRenderer.prototype,
|
||||
'getTransform');
|
||||
'getRenderTransform');
|
||||
const spy2 = sinon.spy(CanvasVectorTileLayerRenderer.prototype,
|
||||
'renderTileImage_');
|
||||
map.renderSync();
|
||||
@@ -154,7 +130,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
|
||||
renderer: function() {}
|
||||
}));
|
||||
const spy = sinon.spy(CanvasVectorTileLayerRenderer.prototype,
|
||||
'getTransform');
|
||||
'getRenderTransform');
|
||||
map.renderSync();
|
||||
expect(spy.callCount).to.be(1);
|
||||
spy.restore();
|
||||
@@ -267,7 +243,7 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
|
||||
return document.createElement('canvas');
|
||||
};
|
||||
const tile = new VectorImageTile([0, 0, 0], undefined, undefined, undefined,
|
||||
undefined, undefined, undefined, undefined, undefined, undefined, undefined,
|
||||
undefined, [0, 0, 0], undefined, createXYZ(), createXYZ(), undefined, undefined,
|
||||
undefined, undefined, undefined, 0);
|
||||
tile.transition_ = 0;
|
||||
tile.wrappedTileCoord = [0, 0, 0];
|
||||
@@ -294,13 +270,13 @@ describe('ol.renderer.canvas.VectorTileLayer', function() {
|
||||
usedTiles: {},
|
||||
wantedTiles: {}
|
||||
};
|
||||
renderer.prepareFrame(frameState, {});
|
||||
renderer.renderFrame(frameState, {});
|
||||
const replayState = renderer.renderedTiles[0].getReplayState(layer);
|
||||
const revision = replayState.renderedTileRevision;
|
||||
renderer.prepareFrame(frameState, {});
|
||||
renderer.renderFrame(frameState, {});
|
||||
expect(replayState.renderedTileRevision).to.be(revision);
|
||||
layer.changed();
|
||||
renderer.prepareFrame(frameState, {});
|
||||
renderer.renderFrame(frameState, {});
|
||||
expect(replayState.renderedTileRevision).to.be(revision + 1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user