diff --git a/examples/immediate-geographic.html b/examples/immediate-geographic.html new file mode 100644 index 0000000000..2683857e27 --- /dev/null +++ b/examples/immediate-geographic.html @@ -0,0 +1,14 @@ +--- +layout: example.html +title: Immediate Rendering (Geographic) +shortdesc: Using the immediate rendering API with geometries in geographic coordinates. +docs: > + This example uses the "immediate" rendering API with geometries in geographic coordinates. + The immediate rendering API lets you draw styled geometries without adding them to a layer first. + Use the `getVectorContext` function to create a rendering context from a render event. Using the + `context.drawGeometry()` and `context.setStyle()` methods on this rendering context, you can draw + any geometry on each render frame. The `useGeographic` function is used in this example so that + geometries can be in geographic coordinates. +tags: "immediate, geographic" +--- +
diff --git a/examples/immediate-geographic.js b/examples/immediate-geographic.js new file mode 100644 index 0000000000..7c92cf0459 --- /dev/null +++ b/examples/immediate-geographic.js @@ -0,0 +1,64 @@ +import {Map, View} from '../src/ol/index.js'; +import {Point} from '../src/ol/geom.js'; +import TileLayer from '../src/ol/layer/Tile.js'; +import Stamen from '../src/ol/source/Stamen.js'; +import {Circle, Fill, Style} from '../src/ol/style.js'; +import {getVectorContext} from '../src/ol/render.js'; +import {useGeographic} from '../src/ol/proj.js'; +import {upAndDown} from '../src/ol/easing.js'; + +useGeographic(); + +const layer = new TileLayer({ + source: new Stamen({ + layer: 'toner' + }) +}); + +const map = new Map({ + layers: [layer], + target: 'map', + view: new View({ + center: [0, 0], + zoom: 2 + }) +}); + +const image = new Circle({ + radius: 8, + fill: new Fill({color: 'rgb(255, 153, 0)'}) +}); + +const style = new Style({ + image: image +}); + +const n = 1000; +const geometries = new Array(n); + +for (let i = 0; i < n; ++i) { + const lon = 360 * Math.random() - 180; + const lat = 180 * Math.random() - 90; + geometries[i] = new Point([lon, lat]); +} + +layer.on('postrender', function(event) { + const vectorContext = getVectorContext(event); + + for (let i = 0; i < n; ++i) { + const importance = upAndDown(Math.pow((n - i) / n, 0.15)); + if (importance < 0.1) { + continue; + } + image.setOpacity(importance); + image.setScale(importance); + vectorContext.setStyle(style); + vectorContext.drawGeometry(geometries[i]); + } + + const lon = 360 * Math.random() - 180; + const lat = 180 * Math.random() - 90; + geometries.push(new Point([lon, lat])); + geometries.shift(); + map.render(); +}); diff --git a/src/ol/geom/Geometry.js b/src/ol/geom/Geometry.js index 00c18a7c04..bab5f4f68b 100644 --- a/src/ol/geom/Geometry.js +++ b/src/ol/geom/Geometry.js @@ -5,7 +5,7 @@ import {abstract} from '../util.js'; import BaseObject from '../Object.js'; import {createEmpty, getHeight, returnOrUpdate} from '../extent.js'; import {transform2D} from './flat/transform.js'; -import {get as getProjection, getTransform, getTransformFromProjections} from '../proj.js'; +import {get as getProjection, getTransform} from '../proj.js'; import Units from '../proj/Units.js'; import {create as createTransform, compose as composeTransform} from '../transform.js'; import {memoizeOne} from '../functions.js'; @@ -62,17 +62,15 @@ class Geometry extends BaseObject { * @abstract * @param {number} revision The geometry revision. * @param {number} squaredTolerance Squared tolerance. - * @param {import("../proj/Projection.js").default} [sourceProjection] The source projection. - * @param {import("../proj/Projection.js").default} [destProjection] The destination projection. + * @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function. * @return {Geometry} Simplified geometry. */ - this.simplifyTransformedInternal = memoizeOne(function(revision, squaredTolerance, sourceProjection, destProjection) { - if (!sourceProjection || !destProjection) { + this.simplifyTransformedInternal = memoizeOne(function(revision, squaredTolerance, opt_transform) { + if (!opt_transform) { return this.getSimplifiedGeometry(squaredTolerance); } - const transform = getTransformFromProjections(sourceProjection, destProjection); const clone = this.clone(); - clone.applyTransform(transform); + clone.applyTransform(opt_transform); return clone.getSimplifiedGeometry(squaredTolerance); }); @@ -82,12 +80,11 @@ class Geometry extends BaseObject { * Get a transformed and simplified version of the geometry. * @abstract * @param {number} squaredTolerance Squared tolerance. - * @param {import("../proj/Projection.js").default} sourceProjection The source projection. - * @param {import("../proj/Projection.js").default} destProjection The destination projection. + * @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function. * @return {Geometry} Simplified geometry. */ - simplifyTransformed(squaredTolerance, sourceProjection, destProjection) { - return this.simplifyTransformedInternal(this.getRevision(), squaredTolerance, sourceProjection, destProjection); + simplifyTransformed(squaredTolerance, opt_transform) { + return this.simplifyTransformedInternal(this.getRevision(), squaredTolerance, opt_transform); } /** diff --git a/src/ol/render.js b/src/ol/render.js index fb48e303bd..9b62d933d1 100644 --- a/src/ol/render.js +++ b/src/ol/render.js @@ -9,6 +9,8 @@ import { scale as scaleTransform } from './transform.js'; import CanvasImmediateRenderer from './render/canvas/Immediate.js'; +import {getSquaredTolerance} from './renderer/vector.js'; +import {getUserProjection, getTransformFromProjections} from './proj.js'; /** @@ -92,9 +94,15 @@ export function toContext(context, opt_options) { export function getVectorContext(event) { const frameState = event.frameState; const transform = multiplyTransform(event.inversePixelTransform.slice(), frameState.coordinateToPixelTransform); + const squaredTolerance = getSquaredTolerance(frameState.viewState.resolution, frameState.pixelRatio); + let userTransform; + const userProjection = getUserProjection(); + if (userProjection) { + userTransform = getTransformFromProjections(userProjection, frameState.viewState.projection); + } return new CanvasImmediateRenderer( event.context, frameState.pixelRatio, frameState.extent, transform, - frameState.viewState.rotation); + frameState.viewState.rotation, squaredTolerance, userTransform); } /** diff --git a/src/ol/render/Feature.js b/src/ol/render/Feature.js index 322d4a9b0e..7d758130bf 100644 --- a/src/ol/render/Feature.js +++ b/src/ol/render/Feature.js @@ -204,11 +204,10 @@ class RenderFeature { * Get a transformed and simplified version of the geometry. * @abstract * @param {number} squaredTolerance Squared tolerance. - * @param {import("../proj/Projection.js").default} sourceProjection The source projection. - * @param {import("../proj/Projection.js").default} destProjection The destination projection. + * @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function. * @return {RenderFeature} Simplified geometry. */ - simplifyTransformed(squaredTolerance, sourceProjection, destProjection) { + simplifyTransformed(squaredTolerance, opt_transform) { return this; } diff --git a/src/ol/render/canvas/Immediate.js b/src/ol/render/canvas/Immediate.js index 871b41adb6..2f0894b4f1 100644 --- a/src/ol/render/canvas/Immediate.js +++ b/src/ol/render/canvas/Immediate.js @@ -31,8 +31,10 @@ class CanvasImmediateRenderer extends VectorContext { * @param {import("../../extent.js").Extent} extent Extent. * @param {import("../../transform.js").Transform} transform Transform. * @param {number} viewRotation View rotation. + * @param {number=} opt_squaredTolerance Optional squared tolerance for simplification. + * @param {import("../../proj.js").TransformFunction=} opt_userTransform Transform from user to view projection. */ - constructor(context, pixelRatio, extent, transform, viewRotation) { + constructor(context, pixelRatio, extent, transform, viewRotation, opt_squaredTolerance, opt_userTransform) { super(); /** @@ -65,6 +67,18 @@ class CanvasImmediateRenderer extends VectorContext { */ this.viewRotation_ = viewRotation; + /** + * @private + * @type {number} + */ + this.squaredTolerance_ = opt_squaredTolerance; + + /** + * @private + * @type {import("../../proj.js").TransformFunction} + */ + this.userTransform_ = opt_userTransform; + /** * @private * @type {?import("../canvas.js").FillState} @@ -505,6 +519,9 @@ class CanvasImmediateRenderer extends VectorContext { * @override */ drawPoint(geometry) { + if (this.squaredTolerance_) { + geometry = /** @type {import("../../geom/Point.js").default} */ (geometry.simplifyTransformed(this.squaredTolerance_, this.userTransform_)); + } const flatCoordinates = geometry.getFlatCoordinates(); const stride = geometry.getStride(); if (this.image_) { @@ -523,6 +540,9 @@ class CanvasImmediateRenderer extends VectorContext { * @override */ drawMultiPoint(geometry) { + if (this.squaredTolerance_) { + geometry = /** @type {import("../../geom/MultiPoint.js").default} */ (geometry.simplifyTransformed(this.squaredTolerance_, this.userTransform_)); + } const flatCoordinates = geometry.getFlatCoordinates(); const stride = geometry.getStride(); if (this.image_) { @@ -541,6 +561,9 @@ class CanvasImmediateRenderer extends VectorContext { * @override */ drawLineString(geometry) { + if (this.squaredTolerance_) { + geometry = /** @type {import("../../geom/LineString.js").default} */ (geometry.simplifyTransformed(this.squaredTolerance_, this.userTransform_)); + } if (!intersects(this.extent_, geometry.getExtent())) { return; } @@ -567,6 +590,9 @@ class CanvasImmediateRenderer extends VectorContext { * @override */ drawMultiLineString(geometry) { + if (this.squaredTolerance_) { + geometry = /** @type {import("../../geom/MultiLineString.js").default} */ (geometry.simplifyTransformed(this.squaredTolerance_, this.userTransform_)); + } const geometryExtent = geometry.getExtent(); if (!intersects(this.extent_, geometryExtent)) { return; @@ -598,6 +624,9 @@ class CanvasImmediateRenderer extends VectorContext { * @override */ drawPolygon(geometry) { + if (this.squaredTolerance_) { + geometry = /** @type {import("../../geom/Polygon.js").default} */ (geometry.simplifyTransformed(this.squaredTolerance_, this.userTransform_)); + } if (!intersects(this.extent_, geometry.getExtent())) { return; } @@ -632,6 +661,9 @@ class CanvasImmediateRenderer extends VectorContext { * @override */ drawMultiPolygon(geometry) { + if (this.squaredTolerance_) { + geometry = /** @type {import("../../geom/MultiPolygon.js").default} */ (geometry.simplifyTransformed(this.squaredTolerance_, this.userTransform_)); + } if (!intersects(this.extent_, geometry.getExtent())) { return; } diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index ee9110e5e1..4cb11e29da 100644 --- a/src/ol/renderer/canvas/VectorLayer.js +++ b/src/ol/renderer/canvas/VectorLayer.js @@ -4,7 +4,7 @@ import {getUid} from '../../util.js'; import ViewHint from '../../ViewHint.js'; import {buffer, createEmpty, containsExtent, getWidth, intersects as intersectsExtent} from '../../extent.js'; -import {fromUserExtent, toUserExtent, getUserProjection} from '../../proj.js'; +import {fromUserExtent, toUserExtent, getUserProjection, getTransformFromProjections} from '../../proj.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import CanvasLayerRenderer from './Layer.js'; @@ -307,8 +307,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { pixelRatio, vectorLayer.getDeclutter()); const userProjection = getUserProjection(); + let userTransform; if (userProjection) { vectorSource.loadFeatures(toUserExtent(extent, projection), resolution, userProjection); + userTransform = getTransformFromProjections(userProjection, projection); } else { vectorSource.loadFeatures(extent, resolution, projection); } @@ -326,7 +328,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { styles = styleFunction(feature, resolution); } if (styles) { - const dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup, projection); + const dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup, userTransform); this.dirty_ = this.dirty_ || dirty; } }.bind(this); @@ -370,10 +372,10 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { * @param {number} squaredTolerance Squared render tolerance. * @param {import("../../style/Style.js").default|Array} styles The style or array of styles. * @param {import("../../render/canvas/BuilderGroup.js").default} builderGroup Builder group. - * @param {import("../../proj/Projection.js").default} projection The view projection. + * @param {import("../../proj.js").TransformFunction} opt_transform Transform from user to view projection. * @return {boolean} `true` if an image is loading. */ - renderFeature(feature, squaredTolerance, styles, builderGroup, projection) { + renderFeature(feature, squaredTolerance, styles, builderGroup, opt_transform) { if (!styles) { return false; } @@ -382,12 +384,12 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { for (let i = 0, ii = styles.length; i < ii; ++i) { loading = renderFeature( builderGroup, feature, styles[i], squaredTolerance, - this.boundHandleStyleImageChange_, projection) || loading; + this.boundHandleStyleImageChange_, opt_transform) || loading; } } else { loading = renderFeature( builderGroup, feature, styles, squaredTolerance, - this.boundHandleStyleImageChange_, projection); + this.boundHandleStyleImageChange_, opt_transform); } return loading; } diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 0c1d23a3bb..ec8c008538 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -5,7 +5,6 @@ import {getUid} from '../util.js'; import ImageState from '../ImageState.js'; import GeometryType from '../geom/GeometryType.js'; import BuilderType from '../render/canvas/BuilderType.js'; -import {getUserProjection} from '../proj.js'; /** @@ -93,11 +92,11 @@ function renderCircleGeometry(builderGroup, geometry, style, feature) { * @param {import("../style/Style.js").default} style Style. * @param {number} squaredTolerance Squared tolerance. * @param {function(import("../events/Event.js").default): void} listener Listener function. - * @param {import("../proj/Projection.js").default} [projection] The view projection. + * @param {import("../proj.js").TransformFunction} [opt_transform] Transform from user to view projection. * @return {boolean} `true` if style is loading. * @template T */ -export function renderFeature(replayGroup, feature, style, squaredTolerance, listener, projection) { +export function renderFeature(replayGroup, feature, style, squaredTolerance, listener, opt_transform) { let loading = false; const imageStyle = style.getImage(); if (imageStyle) { @@ -113,7 +112,7 @@ export function renderFeature(replayGroup, feature, style, squaredTolerance, lis loading = true; } } - renderFeatureInternal(replayGroup, feature, style, squaredTolerance, projection); + renderFeatureInternal(replayGroup, feature, style, squaredTolerance, opt_transform); return loading; } @@ -124,14 +123,14 @@ export function renderFeature(replayGroup, feature, style, squaredTolerance, lis * @param {import("../Feature.js").FeatureLike} feature Feature. * @param {import("../style/Style.js").default} style Style. * @param {number} squaredTolerance Squared tolerance. - * @param {import("../proj/Projection.js").default} [projection] The view projection. + * @param {import("../proj.js").TransformFunction} [opt_transform] Optional transform function. */ -function renderFeatureInternal(replayGroup, feature, style, squaredTolerance, projection) { +function renderFeatureInternal(replayGroup, feature, style, squaredTolerance, opt_transform) { const geometry = style.getGeometryFunction()(feature); if (!geometry) { return; } - const simplifiedGeometry = geometry.simplifyTransformed(squaredTolerance, getUserProjection(), projection); + const simplifiedGeometry = geometry.simplifyTransformed(squaredTolerance, opt_transform); const renderer = style.getRenderer(); if (renderer) { renderGeometry(replayGroup, simplifiedGeometry, style, feature); diff --git a/test/spec/ol/geom/point.test.js b/test/spec/ol/geom/point.test.js index 67a3edf232..88d77ca855 100644 --- a/test/spec/ol/geom/point.test.js +++ b/test/spec/ol/geom/point.test.js @@ -1,5 +1,5 @@ import Point from '../../../../src/ol/geom/Point.js'; -import {get as getProjection} from '../../../../src/ol/proj.js'; +import {get as getProjection, getTransformFromProjections} from '../../../../src/ol/proj.js'; describe('ol.geom.Point', function() { @@ -161,9 +161,10 @@ describe('ol.geom.Point', function() { const geom = new Point([1, 2]); const source = getProjection('EPSG:4326'); const dest = getProjection('EPSG:3857'); + const transform = getTransformFromProjections(source, dest); const squaredTolerance = 0.5; - const first = geom.simplifyTransformed(squaredTolerance, source, dest); - const second = geom.simplifyTransformed(squaredTolerance, source, dest); + const first = geom.simplifyTransformed(squaredTolerance, transform); + const second = geom.simplifyTransformed(squaredTolerance, transform); expect(second).to.be(first); }); @@ -171,9 +172,10 @@ describe('ol.geom.Point', function() { const geom = new Point([1, 2]); const source = getProjection('EPSG:4326'); const dest = getProjection('EPSG:3857'); + const transform = getTransformFromProjections(source, dest); const squaredTolerance = 0.5; - const first = geom.simplifyTransformed(squaredTolerance, source, dest); - const second = geom.simplifyTransformed(squaredTolerance * 2, source, dest); + const first = geom.simplifyTransformed(squaredTolerance, transform); + const second = geom.simplifyTransformed(squaredTolerance * 2, transform); expect(second).not.to.be(first); }); @@ -181,11 +183,12 @@ describe('ol.geom.Point', function() { const geom = new Point([1, 2]); const source = getProjection('EPSG:4326'); const dest = getProjection('EPSG:3857'); + const transform = getTransformFromProjections(source, dest); const squaredTolerance = 0.5; - const first = geom.simplifyTransformed(squaredTolerance, source, dest); + const first = geom.simplifyTransformed(squaredTolerance, transform); geom.setCoordinates([3, 4]); - const second = geom.simplifyTransformed(squaredTolerance * 2, source, dest); + const second = geom.simplifyTransformed(squaredTolerance * 2, transform); expect(second).not.to.be(first); }); diff --git a/test/spec/ol/renderer/vector.test.js b/test/spec/ol/renderer/vector.test.js index 246ea491e7..2e6bdcd553 100644 --- a/test/spec/ol/renderer/vector.test.js +++ b/test/spec/ol/renderer/vector.test.js @@ -17,7 +17,7 @@ import Feature from '../../../../src/ol/Feature.js'; describe('ol.renderer.vector', function() { describe('#renderFeature', function() { let builderGroup; - let feature, iconStyle, style, squaredTolerance, listener, listenerThis; + let feature, iconStyle, style, squaredTolerance, listener; let iconStyleLoadSpy; beforeEach(function() { @@ -33,7 +33,6 @@ describe('ol.renderer.vector', function() { }); squaredTolerance = 1; listener = function() {}; - listenerThis = {}; iconStyleLoadSpy = sinon.stub(iconStyle, 'load').callsFake(function() { iconStyle.iconImage_.imageState_ = 1; // LOADING }); @@ -49,16 +48,14 @@ describe('ol.renderer.vector', function() { let listeners; // call #1 - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(iconStyleLoadSpy.calledOnce).to.be.ok(); listeners = iconStyle.iconImage_.listeners_['change']; expect(listeners.length).to.eql(1); // call #2 - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(iconStyleLoadSpy.calledOnce).to.be.ok(); listeners = iconStyle.iconImage_.listeners_['change']; @@ -75,8 +72,7 @@ describe('ol.renderer.vector', function() { style.getZIndex(), 'Image'); const setImageStyleSpy = sinon.spy(imageReplay, 'setImageStyle'); const drawPointSpy = sinon.stub(imageReplay, 'drawPoint').callsFake(VOID); - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(setImageStyleSpy.called).to.be(false); setImageStyleSpy.restore(); drawPointSpy.restore(); @@ -88,8 +84,7 @@ describe('ol.renderer.vector', function() { style.getZIndex(), 'Image'); const setImageStyleSpy = sinon.spy(imageReplay, 'setImageStyle'); const drawMultiPointSpy = sinon.stub(imageReplay, 'drawMultiPoint').callsFake(VOID); - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(setImageStyleSpy.called).to.be(false); setImageStyleSpy.restore(); drawMultiPointSpy.restore(); @@ -102,8 +97,7 @@ describe('ol.renderer.vector', function() { const setFillStrokeStyleSpy = sinon.spy(lineStringReplay, 'setFillStrokeStyle'); const drawLineStringSpy = sinon.stub(lineStringReplay, 'drawLineString').callsFake(VOID); - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawLineStringSpy.called).to.be(true); setFillStrokeStyleSpy.restore(); @@ -117,8 +111,7 @@ describe('ol.renderer.vector', function() { const setFillStrokeStyleSpy = sinon.spy(lineStringReplay, 'setFillStrokeStyle'); const drawMultiLineStringSpy = sinon.stub(lineStringReplay, 'drawMultiLineString').callsFake(VOID); - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawMultiLineStringSpy.called).to.be(true); setFillStrokeStyleSpy.restore(); @@ -133,8 +126,7 @@ describe('ol.renderer.vector', function() { const setFillStrokeStyleSpy = sinon.spy(polygonReplay, 'setFillStrokeStyle'); const drawPolygonSpy = sinon.stub(polygonReplay, 'drawPolygon').callsFake(VOID); - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawPolygonSpy.called).to.be(true); setFillStrokeStyleSpy.restore(); @@ -149,8 +141,7 @@ describe('ol.renderer.vector', function() { const setFillStrokeStyleSpy = sinon.spy(polygonReplay, 'setFillStrokeStyle'); const drawMultiPolygonSpy = sinon.stub(polygonReplay, 'drawMultiPolygon').callsFake(VOID); - renderFeature(builderGroup, feature, - style, squaredTolerance, listener, listenerThis); + renderFeature(builderGroup, feature, style, squaredTolerance, listener); expect(setFillStrokeStyleSpy.called).to.be(true); expect(drawMultiPolygonSpy.called).to.be(true); setFillStrokeStyleSpy.restore();