diff --git a/examples/geographic.js b/examples/geographic.js index 1bebfee353..8f463ce348 100644 --- a/examples/geographic.js +++ b/examples/geographic.js @@ -1,22 +1,33 @@ import {useGeographic} from '../src/ol/proj.js'; -import Map from '../src/ol/Map.js'; -import View from '../src/ol/View.js'; -import TileLayer from '../src/ol/layer/Tile.js'; -import OSM from '../src/ol/source/OSM.js'; +import {Map, View, Feature} from '../src/ol/index.js'; +import {Point} from '../src/ol/geom.js'; +import {Vector as VectorLayer, Tile as TileLayer} from '../src/ol/layer.js'; +import {OSM, Vector as VectorSource} from '../src/ol/source.js'; useGeographic(); +const place = [-110, 45]; + +const point = new Point(place); + const map = new Map({ + target: 'map', + view: new View({ + center: place, + zoom: 8 + }), layers: [ new TileLayer({ source: new OSM() + }), + new VectorLayer({ + source: new VectorSource({ + features: [ + new Feature(point) + ] + }) }) - ], - target: 'map', - view: new View({ - center: [-110, 45], - zoom: 8 - }) + ] }); const info = document.getElementById('info'); diff --git a/rendering/cases/geometry-geographic/expected.png b/rendering/cases/geometry-geographic/expected.png new file mode 100644 index 0000000000..4bcb5636fd Binary files /dev/null and b/rendering/cases/geometry-geographic/expected.png differ diff --git a/rendering/cases/geometry-geographic/main.js b/rendering/cases/geometry-geographic/main.js new file mode 100644 index 0000000000..3c1cd68523 --- /dev/null +++ b/rendering/cases/geometry-geographic/main.js @@ -0,0 +1,43 @@ +import {Map, View, Feature} from '../../../src/ol/index.js'; +import {Point} from '../../../src/ol/geom.js'; +import {Tile as TileLayer, Vector as VectorLayer} from '../../../src/ol/layer.js'; +import {useGeographic} from '../../../src/ol/proj.js'; +import {XYZ, Vector as VectorSource} from '../../../src/ol/source.js'; +import {Style, Circle, Fill} from '../../../src/ol/style.js'; + +useGeographic(); + +const center = [8.6, 50.1]; + +const point = new Point(center); + +new Map({ + layers: [ + new TileLayer({ + source: new XYZ({ + url: '/data/tiles/satellite/{z}/{x}/{y}.jpg', + transition: 0 + }) + }), + new VectorLayer({ + source: new VectorSource({ + features: [ + new Feature(point) + ] + }), + style: new Style({ + image: new Circle({ + radius: 5, + fill: new Fill({color: 'red'}) + }) + }) + }) + ], + target: 'map', + view: new View({ + center: center, + zoom: 3 + }) +}); + +render(); diff --git a/src/ol/functions.js b/src/ol/functions.js index ce76868317..c61ac787a5 100644 --- a/src/ol/functions.js +++ b/src/ol/functions.js @@ -2,6 +2,8 @@ * @module ol/functions */ +import {equals as arrayEquals} from './array.js'; + /** * Always returns true. * @returns {boolean} true. @@ -24,3 +26,36 @@ export function FALSE() { * @return {void} Nothing. */ export function VOID() {} + +/** + * Wrap a function in another function that remembers the last return. If the + * returned function is called twice in a row with the same arguments and the same + * this object, it will return the value from the first call in the second call. + * + * @template ReturnType + * @template ThisType + * @param {function(this:ThisType, ...any):ReturnType} fn The function to memoize. + * @return {function(this:ThisType, ...any):ReturnType} The memoized function. + */ +export function memoizeOne(fn) { + let called = false; + + /** @type ReturnType */ + let lastResult; + + /** @type Array */ + let lastArgs; + + let lastThis; + + return function() { + const nextArgs = Array.prototype.slice.call(arguments); + if (!called || this !== lastThis || !arrayEquals(nextArgs, lastArgs)) { + called = true; + lastThis = this; + lastArgs = nextArgs; + lastResult = fn.apply(this, arguments); + } + return lastResult; + }; +} diff --git a/src/ol/geom/Geometry.js b/src/ol/geom/Geometry.js index df5d7e2285..b92c4455a6 100644 --- a/src/ol/geom/Geometry.js +++ b/src/ol/geom/Geometry.js @@ -5,10 +5,10 @@ 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} from '../proj.js'; +import {get as getProjection, getTransform, getTransformFromProjections} from '../proj.js'; import Units from '../proj/Units.js'; import {create as createTransform, compose as composeTransform} from '../transform.js'; - +import {memoizeOne} from '../functions.js'; /** * @type {import("../transform.js").Transform} @@ -63,6 +63,24 @@ class Geometry extends BaseObject { */ this.simplifiedGeometryRevision = 0; + /** + * 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. + * @return {Geometry} Simplified geometry. + */ + this.simplifyTransformed = memoizeOne(function(squaredTolerance, sourceProjection, destProjection) { + if (!sourceProjection || !destProjection) { + return this.getSimplifiedGeometry(squaredTolerance); + } + const transform = getTransformFromProjections(sourceProjection, destProjection); + const clone = this.clone(); + clone.applyTransform(transform); + return clone.getSimplifiedGeometry(squaredTolerance); + }); + } /** diff --git a/src/ol/render/Feature.js b/src/ol/render/Feature.js index 4bbc9b7d52..322d4a9b0e 100644 --- a/src/ol/render/Feature.js +++ b/src/ol/render/Feature.js @@ -200,6 +200,18 @@ class RenderFeature { return this; } + /** + * 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. + * @return {RenderFeature} Simplified geometry. + */ + simplifyTransformed(squaredTolerance, sourceProjection, destProjection) { + return this; + } + /** * Get the feature properties. * @return {Object} Feature properties. diff --git a/src/ol/renderer/canvas/VectorLayer.js b/src/ol/renderer/canvas/VectorLayer.js index 14abdcb628..cb19e846cb 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} from '../../proj.js'; +import {fromUserExtent, toUserExtent, getUserProjection} from '../../proj.js'; import CanvasBuilderGroup from '../../render/canvas/BuilderGroup.js'; import ExecutorGroup, {replayDeclutter} from '../../render/canvas/ExecutorGroup.js'; import CanvasLayerRenderer from './Layer.js'; @@ -304,7 +304,12 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { getRenderTolerance(resolution, pixelRatio), extent, resolution, pixelRatio, vectorLayer.getDeclutter()); - vectorSource.loadFeatures(extent, resolution, projection); + const userProjection = getUserProjection(); + if (userProjection) { + vectorSource.loadFeatures(toUserExtent(extent, projection), resolution, userProjection); + } else { + vectorSource.loadFeatures(extent, resolution, projection); + } const squaredTolerance = getSquaredRenderTolerance(resolution, pixelRatio); @@ -319,15 +324,16 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { styles = styleFunction(feature, resolution); } if (styles) { - const dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup); + const dirty = this.renderFeature(feature, squaredTolerance, styles, replayGroup, projection); this.dirty_ = this.dirty_ || dirty; } }.bind(this); + const userExtent = toUserExtent(extent, projection); if (vectorLayerRenderOrder) { /** @type {Array} */ const features = []; - vectorSource.forEachFeatureInExtent(extent, + vectorSource.forEachFeatureInExtent(userExtent, /** * @param {import("../../Feature.js").default} feature Feature. */ @@ -339,7 +345,7 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { render(features[i]); } } else { - vectorSource.forEachFeatureInExtent(extent, render); + vectorSource.forEachFeatureInExtent(userExtent, render); } const replayGroupInstructions = replayGroup.finish(); @@ -362,9 +368,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. * @return {boolean} `true` if an image is loading. */ - renderFeature(feature, squaredTolerance, styles, builderGroup) { + renderFeature(feature, squaredTolerance, styles, builderGroup, projection) { if (!styles) { return false; } @@ -373,12 +380,12 @@ class CanvasVectorLayerRenderer extends CanvasLayerRenderer { for (let i = 0, ii = styles.length; i < ii; ++i) { loading = renderFeature( builderGroup, feature, styles[i], squaredTolerance, - this.boundHandleStyleImageChange_) || loading; + this.boundHandleStyleImageChange_, projection) || loading; } } else { loading = renderFeature( builderGroup, feature, styles, squaredTolerance, - this.boundHandleStyleImageChange_); + this.boundHandleStyleImageChange_, projection); } return loading; } diff --git a/src/ol/renderer/vector.js b/src/ol/renderer/vector.js index 2ef0c2fd2e..a8b89f786a 100644 --- a/src/ol/renderer/vector.js +++ b/src/ol/renderer/vector.js @@ -5,6 +5,7 @@ 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'; /** @@ -92,10 +93,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. * @return {boolean} `true` if style is loading. * @template T */ -export function renderFeature(replayGroup, feature, style, squaredTolerance, listener) { +export function renderFeature(replayGroup, feature, style, squaredTolerance, listener, projection) { let loading = false; const imageStyle = style.getImage(); if (imageStyle) { @@ -111,7 +113,7 @@ export function renderFeature(replayGroup, feature, style, squaredTolerance, lis loading = true; } } - renderFeatureInternal(replayGroup, feature, style, squaredTolerance); + renderFeatureInternal(replayGroup, feature, style, squaredTolerance, projection); return loading; } @@ -122,13 +124,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. */ -function renderFeatureInternal(replayGroup, feature, style, squaredTolerance) { +function renderFeatureInternal(replayGroup, feature, style, squaredTolerance, projection) { const geometry = style.getGeometryFunction()(feature); if (!geometry) { return; } - const simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance); + const simplifiedGeometry = geometry.simplifyTransformed(squaredTolerance, getUserProjection(), projection); const renderer = style.getRenderer(); if (renderer) { renderGeometry(replayGroup, simplifiedGeometry, style, feature); diff --git a/test/spec/ol/functions.test.js b/test/spec/ol/functions.test.js new file mode 100644 index 0000000000..5db61b1813 --- /dev/null +++ b/test/spec/ol/functions.test.js @@ -0,0 +1,61 @@ +import {memoizeOne} from '../../../src/ol/functions.js'; + + +describe('ol/functions', function() { + + describe('memoizeOne()', function() { + it('returns the result from the first call when called a second time with the same args', function() { + const arg1 = {}; + const arg2 = {}; + const arg3 = {}; + function call(a1, a2, a3) { + return {}; + } + const memoized = memoizeOne(call); + const result = memoized(arg1, arg2, arg3); + expect(memoized(arg1, arg2, arg3)).to.be(result); + }); + + it('returns the result from the first call when called a second time with the same this object', function() { + const arg1 = {}; + const arg2 = {}; + const arg3 = {}; + function call(a1, a2, a3) { + return {}; + } + const memoized = memoizeOne(call); + + const thisObj = {}; + + const result = memoized.call(thisObj, arg1, arg2, arg3); + expect(memoized.call(thisObj, arg1, arg2, arg3)).to.be(result); + }); + + it('returns a different result when called a second time with the different args', function() { + const arg1 = {}; + const arg2 = {}; + const arg3 = {}; + function call(a1, a2, a3) { + return {}; + } + const memoized = memoizeOne(call); + const result = memoized(arg1, arg2, arg3); + expect(memoized(arg3, arg2, arg1)).not.to.be(result); + }); + + it('returns a different result when called a second time with a different this object', function() { + const arg1 = {}; + const arg2 = {}; + const arg3 = {}; + function call(a1, a2, a3) { + return {}; + } + const firstThis = {}; + const secondThis = {}; + const memoized = memoizeOne(call); + const result = memoized.call(firstThis, arg1, arg2, arg3); + expect(memoized.call(secondThis, arg1, arg2, arg3)).not.to.be(result); + }); + }); + +});