From 9d8609dd08c0992a321c5793c5b4b307e7e439a8 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Tue, 26 Nov 2019 10:25:50 +0000 Subject: [PATCH 001/636] Modify and snap to circle in user coordinates Correct modify interaction at center and at drawn circle circumference Correct snap interaction at drawn circle circumference Test circle geometry in a user projection --- src/ol/interaction/Modify.js | 47 +++++++-- src/ol/interaction/Snap.js | 22 +++- test/spec/ol/interaction/modify.test.js | 128 +++++++++++++++++++++++- test/spec/ol/interaction/snap.test.js | 27 ++++- 4 files changed, 210 insertions(+), 14 deletions(-) diff --git a/src/ol/interaction/Modify.js b/src/ol/interaction/Modify.js index fe81a771ee..dffdf8e707 100644 --- a/src/ol/interaction/Modify.js +++ b/src/ol/interaction/Modify.js @@ -14,13 +14,14 @@ import {always, primaryAction, altKeyOnly, singleClick} from '../events/conditio import {boundingExtent, buffer as bufferExtent, createOrUpdateFromCoordinate as createExtent} from '../extent.js'; import GeometryType from '../geom/GeometryType.js'; import Point from '../geom/Point.js'; +import {fromCircle} from '../geom/Polygon.js'; import PointerInteraction from './Pointer.js'; import VectorLayer from '../layer/Vector.js'; import VectorSource from '../source/Vector.js'; import VectorEventType from '../source/VectorEventType.js'; import RBush from '../structs/RBush.js'; import {createEditingStyle} from '../style/Style.js'; -import {fromUserExtent, toUserExtent, fromUserCoordinate, toUserCoordinate} from '../proj.js'; +import {getUserProjection, fromUserExtent, toUserExtent, fromUserCoordinate, toUserCoordinate} from '../proj.js'; /** @@ -657,7 +658,14 @@ class Modify extends PointerInteraction { centerSegmentData.featureSegments = featureSegments; circumferenceSegmentData.featureSegments = featureSegments; this.rBush_.insert(createExtent(coordinates), centerSegmentData); - this.rBush_.insert(geometry.getExtent(), circumferenceSegmentData); + let circleGeometry = /** @type {import("../geom/Geometry.js").default} */ (geometry); + const userProjection = getUserProjection(); + if (userProjection && this.getMap()) { + const projection = this.getMap().getView().getProjection(); + circleGeometry = circleGeometry.clone().transform(userProjection, projection); + circleGeometry = fromCircle(/** @type {import("../geom/Circle.js").default} */ (circleGeometry)).transform(projection, userProjection); + } + this.rBush_.insert(circleGeometry.getExtent(), circumferenceSegmentData); } /** @@ -785,7 +793,16 @@ class Modify extends PointerInteraction { this.changingFeature_ = false; } else { // We're dragging the circle's circumference: this.changingFeature_ = true; - geometry.setRadius(coordinateDistance(geometry.getCenter(), vertex)); + const projection = evt.map.getView().getProjection(); + let radius = coordinateDistance(fromUserCoordinate(geometry.getCenter(), projection), + fromUserCoordinate(vertex, projection)); + const userProjection = getUserProjection(); + if (userProjection) { + const circleGeometry = geometry.clone().transform(userProjection, projection); + circleGeometry.setRadius(radius); + radius = circleGeometry.transform(projection, userProjection).getRadius(); + } + geometry.setRadius(radius); this.changingFeature_ = false; } break; @@ -898,7 +915,14 @@ class Modify extends PointerInteraction { circumferenceSegmentData.segment[0] = coordinates; circumferenceSegmentData.segment[1] = coordinates; this.rBush_.update(createExtent(coordinates), centerSegmentData); - this.rBush_.update(geometry.getExtent(), circumferenceSegmentData); + let circleGeometry = geometry; + const userProjection = getUserProjection(); + if (userProjection) { + const projection = evt.map.getView().getProjection(); + circleGeometry = circleGeometry.clone().transform(userProjection, projection); + circleGeometry = fromCircle(circleGeometry).transform(projection, userProjection); + } + this.rBush_.update(circleGeometry.getExtent(), circumferenceSegmentData); } else { this.rBush_.update(boundingExtent(segmentData.segment), segmentData); } @@ -1249,11 +1273,15 @@ function projectedDistanceToSegmentDataSquared(pointCoordinates, segmentData, pr const geometry = segmentData.geometry; if (geometry.getType() === GeometryType.CIRCLE) { - const circleGeometry = /** @type {import("../geom/Circle.js").default} */ (geometry); + let circleGeometry = /** @type {import("../geom/Circle.js").default} */ (geometry); if (segmentData.index === CIRCLE_CIRCUMFERENCE_INDEX) { + const userProjection = getUserProjection(); + if (userProjection) { + circleGeometry = /** @type {import("../geom/Circle.js").default} */ (circleGeometry.clone().transform(userProjection, projection)); + } const distanceToCenterSquared = - squaredCoordinateDistance(circleGeometry.getCenter(), pointCoordinates); + squaredCoordinateDistance(circleGeometry.getCenter(), fromUserCoordinate(pointCoordinates, projection)); const distanceToCircumference = Math.sqrt(distanceToCenterSquared) - circleGeometry.getRadius(); return distanceToCircumference * distanceToCircumference; @@ -1280,7 +1308,12 @@ function closestOnSegmentData(pointCoordinates, segmentData, projection) { const geometry = segmentData.geometry; if (geometry.getType() === GeometryType.CIRCLE && segmentData.index === CIRCLE_CIRCUMFERENCE_INDEX) { - return geometry.getClosestPoint(pointCoordinates); + let circleGeometry = /** @type {import("../geom/Circle.js").default} */ (geometry); + const userProjection = getUserProjection(); + if (userProjection) { + circleGeometry = /** @type {import("../geom/Circle.js").default} */ (circleGeometry.clone().transform(userProjection, projection)); + } + return toUserCoordinate(circleGeometry.getClosestPoint(fromUserCoordinate(pointCoordinates, projection)), projection); } const coordinate = fromUserCoordinate(pointCoordinates, projection); tempSegment[0] = fromUserCoordinate(segmentData.segment[0], projection); diff --git a/src/ol/interaction/Snap.js b/src/ol/interaction/Snap.js index 8cf720768a..61a5a1c6de 100644 --- a/src/ol/interaction/Snap.js +++ b/src/ol/interaction/Snap.js @@ -14,7 +14,7 @@ import PointerInteraction from './Pointer.js'; import {getValues} from '../obj.js'; import VectorEventType from '../source/VectorEventType.js'; import RBush from '../structs/RBush.js'; -import {fromUserCoordinate, toUserCoordinate} from '../proj.js'; +import {getUserProjection, fromUserCoordinate, toUserCoordinate} from '../proj.js'; /** @@ -431,8 +431,13 @@ class Snap extends PointerInteraction { } else if (this.edge_) { const isCircle = closestSegmentData.feature.getGeometry().getType() === GeometryType.CIRCLE; if (isCircle) { - vertex = closestOnCircle(pixelCoordinate, - /** @type {import("../geom/Circle.js").default} */ (closestSegmentData.feature.getGeometry())); + let circleGeometry = closestSegmentData.feature.getGeometry(); + const userProjection = getUserProjection(); + if (userProjection) { + circleGeometry = circleGeometry.clone().transform(userProjection, projection); + } + vertex = toUserCoordinate(closestOnCircle(projectedCoordinate, + /** @type {import("../geom/Circle.js").default} */ (circleGeometry)), projection); } else { tempSegment[0] = fromUserCoordinate(closestSegment[0], projection); tempSegment[1] = fromUserCoordinate(closestSegment[1], projection); @@ -482,7 +487,16 @@ class Snap extends PointerInteraction { * @private */ writeCircleGeometry_(feature, geometry) { - const polygon = fromCircle(geometry); + const projection = this.getMap().getView().getProjection(); + let circleGeometry = geometry; + const userProjection = getUserProjection(); + if (userProjection) { + circleGeometry = /** @type {import("../geom/Circle.js").default} */ (circleGeometry.clone().transform(userProjection, projection)); + } + const polygon = fromCircle(circleGeometry); + if (userProjection) { + polygon.transform(projection, userProjection); + } const coordinates = polygon.getCoordinates()[0]; for (let i = 0, ii = coordinates.length - 1; i < ii; ++i) { const segment = coordinates.slice(i, i + 2); diff --git a/test/spec/ol/interaction/modify.test.js b/test/spec/ol/interaction/modify.test.js index 7deae8d5d7..d8b8cccfb0 100644 --- a/test/spec/ol/interaction/modify.test.js +++ b/test/spec/ol/interaction/modify.test.js @@ -14,6 +14,7 @@ import VectorLayer from '../../../../src/ol/layer/Vector.js'; import VectorSource from '../../../../src/ol/source/Vector.js'; import Event from '../../../../src/ol/events/Event.js'; import {getValues} from '../../../../src/ol/obj.js'; +import {clearUserProjection, setUserProjection} from '../../../../src/ol/proj.js'; describe('ol.interaction.Modify', function() { @@ -66,6 +67,7 @@ describe('ol.interaction.Modify', function() { afterEach(function() { map.dispose(); document.body.removeChild(target); + clearUserProjection(); }); /** @@ -401,7 +403,7 @@ describe('ol.interaction.Modify', function() { expect(circleFeature.getGeometry().getRadius()).to.equal(20); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); - // Increase radius + // Increase radius along x axis simulateEvent('pointermove', 25, -4, null, 0); simulateEvent('pointerdown', 25, -4, null, 0); simulateEvent('pointermove', 30, -5, null, 0); @@ -410,6 +412,64 @@ describe('ol.interaction.Modify', function() { expect(circleFeature.getGeometry().getRadius()).to.equal(25); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); + + // Increase radius along y axis + simulateEvent('pointermove', 4, -30, null, 0); + simulateEvent('pointerdown', 4, -30, null, 0); + simulateEvent('pointermove', 5, -35, null, 0); + simulateEvent('pointerdrag', 5, -35, null, 0); + simulateEvent('pointerup', 5, -35, null, 0); + + expect(circleFeature.getGeometry().getRadius()).to.equal(30); + expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); + }); + + it('changes the circle radius and center in a user projection', function() { + const userProjection = 'EPSG:3857'; + setUserProjection(userProjection); + const viewProjection = map.getView().getProjection(); + + const circleFeature = new Feature(new Circle([10, 10], 20).transform(viewProjection, userProjection)); + features.length = 0; + features.push(circleFeature); + + const modify = new Modify({ + features: new Collection(features) + }); + map.addInteraction(modify); + + // Change center + simulateEvent('pointermove', 10, -10, null, 0); + simulateEvent('pointerdown', 10, -10, null, 0); + simulateEvent('pointermove', 5, -5, null, 0); + simulateEvent('pointerdrag', 5, -5, null, 0); + simulateEvent('pointerup', 5, -5, null, 0); + + const geometry1 = circleFeature.getGeometry().clone().transform(userProjection, viewProjection); + expect(geometry1.getRadius()).to.roughlyEqual(20, 1e-9); + expect(geometry1.getCenter()).to.eql([5, 5]); + + // Increase radius along x axis + simulateEvent('pointermove', 25, -4, null, 0); + simulateEvent('pointerdown', 25, -4, null, 0); + simulateEvent('pointermove', 30, -5, null, 0); + simulateEvent('pointerdrag', 30, -5, null, 0); + simulateEvent('pointerup', 30, -5, null, 0); + + const geometry2 = circleFeature.getGeometry().clone().transform(userProjection, viewProjection); + expect(geometry2.getRadius()).to.roughlyEqual(25, 1e-9); + expect(geometry2.getCenter()).to.eql([5, 5]); + + // Increase radius along y axis + simulateEvent('pointermove', 4, -30, null, 0); + simulateEvent('pointerdown', 4, -30, null, 0); + simulateEvent('pointermove', 5, -35, null, 0); + simulateEvent('pointerdrag', 5, -35, null, 0); + simulateEvent('pointerup', 5, -35, null, 0); + + const geometry3 = circleFeature.getGeometry().clone().transform(userProjection, viewProjection); + expect(geometry3.getRadius()).to.roughlyEqual(30, 1e-9); + expect(geometry3.getCenter()).to.eql([5, 5]); }); }); @@ -765,7 +825,7 @@ describe('ol.interaction.Modify', function() { expect(circleFeature.getGeometry().getRadius()).to.equal(20); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); - // Increase radius + // Increase radius along x axis simulateEvent('pointermove', 25, -4, null, 0); simulateEvent('pointerdown', 25, -4, null, 0); simulateEvent('pointermove', 30, -5, null, 0); @@ -774,6 +834,70 @@ describe('ol.interaction.Modify', function() { expect(circleFeature.getGeometry().getRadius()).to.equal(25); expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); + + // Increase radius along y axis + simulateEvent('pointermove', 4, -30, null, 0); + simulateEvent('pointerdown', 4, -30, null, 0); + simulateEvent('pointermove', 5, -35, null, 0); + simulateEvent('pointerdrag', 5, -35, null, 0); + simulateEvent('pointerup', 5, -35, null, 0); + + expect(circleFeature.getGeometry().getRadius()).to.equal(30); + expect(circleFeature.getGeometry().getCenter()).to.eql([5, 5]); + }); + + it('changes the circle radius and center in a user projection', function() { + const userProjection = 'EPSG:3857'; + setUserProjection(userProjection); + const viewProjection = map.getView().getProjection(); + + const circleFeature = new Feature(new Circle([10, 10], 20).transform(viewProjection, userProjection)); + features.length = 0; + features.push(circleFeature); + + const modify = new Modify({ + features: new Collection(features) + }); + map.addInteraction(modify); + + const snap = new Snap({ + features: new Collection(features), + pixelTolerance: 1 + }); + map.addInteraction(snap); + + // Change center + simulateEvent('pointermove', 10, -10, null, 0); + simulateEvent('pointerdown', 10, -10, null, 0); + simulateEvent('pointermove', 5, -5, null, 0); + simulateEvent('pointerdrag', 5, -5, null, 0); + simulateEvent('pointerup', 5, -5, null, 0); + + const geometry1 = circleFeature.getGeometry().clone().transform(userProjection, viewProjection); + expect(geometry1.getRadius()).to.roughlyEqual(20, 1e-9); + expect(geometry1.getCenter()).to.eql([5, 5]); + + // Increase radius along x axis + simulateEvent('pointermove', 25, -4, null, 0); + simulateEvent('pointerdown', 25, -4, null, 0); + simulateEvent('pointermove', 30, -5, null, 0); + simulateEvent('pointerdrag', 30, -5, null, 0); + simulateEvent('pointerup', 30, -5, null, 0); + + const geometry2 = circleFeature.getGeometry().clone().transform(userProjection, viewProjection); + expect(geometry2.getRadius()).to.roughlyEqual(25, 1e-9); + expect(geometry2.getCenter()).to.eql([5, 5]); + + // Increase radius along y axis + simulateEvent('pointermove', 4, -30, null, 0); + simulateEvent('pointerdown', 4, -30, null, 0); + simulateEvent('pointermove', 5, -35, null, 0); + simulateEvent('pointerdrag', 5, -35, null, 0); + simulateEvent('pointerup', 5, -35, null, 0); + + const geometry3 = circleFeature.getGeometry().clone().transform(userProjection, viewProjection); + expect(geometry3.getRadius()).to.roughlyEqual(30, 1e-9); + expect(geometry3.getCenter()).to.eql([5, 5]); }); }); diff --git a/test/spec/ol/interaction/snap.test.js b/test/spec/ol/interaction/snap.test.js index 329cd08172..7db59f177b 100644 --- a/test/spec/ol/interaction/snap.test.js +++ b/test/spec/ol/interaction/snap.test.js @@ -6,7 +6,7 @@ import Circle from '../../../../src/ol/geom/Circle.js'; import Point from '../../../../src/ol/geom/Point.js'; import LineString from '../../../../src/ol/geom/LineString.js'; import Snap from '../../../../src/ol/interaction/Snap.js'; -import {useGeographic, clearUserProjection} from '../../../../src/ol/proj.js'; +import {useGeographic, clearUserProjection, setUserProjection, transform} from '../../../../src/ol/proj.js'; import {overrideRAF} from '../../util.js'; @@ -55,6 +55,7 @@ describe('ol.interaction.Snap', function() { afterEach(function() { map.dispose(); document.body.removeChild(target); + clearUserProjection(); }); it('can handle XYZ coordinates', function() { @@ -129,6 +130,30 @@ describe('ol.interaction.Snap', function() { expect(event.coordinate[1]).to.roughlyEqual(Math.sin(Math.PI / 4) * 10, 1e-10); }); + it('snaps to circle in a user projection', function() { + const userProjection = 'EPSG:3857'; + setUserProjection(userProjection); + const viewProjection = map.getView().getProjection(); + + const circle = new Feature(new Circle([0, 0], 10).transform(viewProjection, userProjection)); + const snapInteraction = new Snap({ + features: new Collection([circle]), + pixelTolerance: 5 + }); + snapInteraction.setMap(map); + + const event = { + pixel: [5 + width / 2, height / 2 - 5], + coordinate: transform([5, 5], viewProjection, userProjection), + map: map + }; + snapInteraction.handleEvent(event); + + const coordinate = transform([Math.sin(Math.PI / 4) * 10, Math.sin(Math.PI / 4) * 10], viewProjection, userProjection); + expect(event.coordinate[0]).to.roughlyEqual(coordinate[0], 1e-10); + expect(event.coordinate[1]).to.roughlyEqual(coordinate[1], 1e-10); + }); + it('handle feature without geometry', function() { const feature = new Feature(); const snapInteraction = new Snap({ From 560931e9763cf990cae16083e6707b1ef8b35aec Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Sun, 15 Dec 2019 23:24:07 +0000 Subject: [PATCH 002/636] Set reprojection canvas context options Add example of disabling image smoothing Add test for reprojection context options --- examples/disable-image-smoothing.css | 15 +++ examples/disable-image-smoothing.html | 47 +++++++ examples/disable-image-smoothing.js | 115 ++++++++++++++++++ .../expected.png | Bin 0 -> 2404 bytes .../reproj-tile-disable-smoothing/main.js | 39 ++++++ src/ol/reproj.js | 6 +- src/ol/reproj/Tile.js | 14 ++- src/ol/source/BingMaps.js | 3 + src/ol/source/IIIF.js | 3 + src/ol/source/OSM.js | 5 +- src/ol/source/Stamen.js | 3 + src/ol/source/TileArcGISRest.js | 3 + src/ol/source/TileImage.js | 10 +- src/ol/source/TileJSON.js | 3 + src/ol/source/TileWMS.js | 3 + src/ol/source/WMTS.js | 3 + src/ol/source/XYZ.js | 3 + src/ol/source/Zoomify.js | 3 + 18 files changed, 272 insertions(+), 6 deletions(-) create mode 100644 examples/disable-image-smoothing.css create mode 100644 examples/disable-image-smoothing.html create mode 100644 examples/disable-image-smoothing.js create mode 100644 rendering/cases/reproj-tile-disable-smoothing/expected.png create mode 100644 rendering/cases/reproj-tile-disable-smoothing/main.js diff --git a/examples/disable-image-smoothing.css b/examples/disable-image-smoothing.css new file mode 100644 index 0000000000..db58102d58 --- /dev/null +++ b/examples/disable-image-smoothing.css @@ -0,0 +1,15 @@ +@media (min-width: 800px) { + .wrapper { + display: flex; + } + .half { + padding: 0 10px; + width: 50%; + float: left; + } +} +#opacity { + display: inline-block; + width: 150px; + vertical-align: text-bottom; +} diff --git a/examples/disable-image-smoothing.html b/examples/disable-image-smoothing.html new file mode 100644 index 0000000000..3d67e435fb --- /dev/null +++ b/examples/disable-image-smoothing.html @@ -0,0 +1,47 @@ +--- +layout: example.html +title: Disable Image Smoothing +shortdesc: Example of disabling image smoothing +docs: > + Example of disabling image smoothing when using raster DEM (digital elevation model) data. + The imageSmoothingEnabled (or for Internet Explorer msImageSmoothingEnabled) canvas + context property is set to false at the layer's prerender event. Additionally for a + reprojected source those properties must also be also be specified for the canvas contexts used in + the reprojection via the source's reprojectionContextOptions option. Elevation data is + calculated from the pixel value returned by forEachLayerAtPixel. For comparison a second map + with smoothing enabled returns inaccuate elevations which are very noticeable close to 3107 meters + due to how elevation is calculated from the pixel value. +tags: "disable image smoothing, xyz, maptiler, reprojection" +cloak: + - key: get_your_own_D6rA4zTHduk6KOKTXzGB + value: Get your own API key at https://www.maptiler.com/cloud/ +--- + + + Smoothing Disabled + + + + Elevation + 0.0 meters + + + + + Imagery opacity + + % + + + + + Uncorrected Comparison + + + + Elevation + 0.0 meters + + + + diff --git a/examples/disable-image-smoothing.js b/examples/disable-image-smoothing.js new file mode 100644 index 0000000000..52ab34265b --- /dev/null +++ b/examples/disable-image-smoothing.js @@ -0,0 +1,115 @@ +import Map from '../src/ol/Map.js'; +import View from '../src/ol/View.js'; +import TileLayer from '../src/ol/layer/Tile.js'; +import XYZ from '../src/ol/source/XYZ.js'; + +const key = 'get_your_own_D6rA4zTHduk6KOKTXzGB'; +const attributions = '© MapTiler ' + + '© OpenStreetMap contributors'; + +const disabledLayer = new TileLayer({ + // specify className so forEachLayerAtPixel can distinguish layers + className: 'ol-layer-dem', + source: new XYZ({ + attributions: attributions, + url: 'https://api.maptiler.com/tiles/terrain-rgb/{z}/{x}/{y}.png?key=' + key, + maxZoom: 10, + crossOrigin: '', + reprojectionContextOptions: {imageSmoothingEnabled: false, msImageSmoothingEnabled: false} + }) +}); + +const imagery = new TileLayer({ + className: 'ol-layer-imagery', + source: new XYZ({ + attributions: attributions, + url: 'https://api.maptiler.com/tiles/satellite/{z}/{x}/{y}.jpg?key=' + key, + maxZoom: 20, + crossOrigin: '' + }) +}); + +const enabledLayer = new TileLayer({ + source: new XYZ({ + attributions: attributions, + url: 'https://api.maptiler.com/tiles/terrain-rgb/{z}/{x}/{y}.png?key=' + key, + maxZoom: 10, + crossOrigin: '' + }) +}); + +disabledLayer.on('prerender', function(evt) { + evt.context.imageSmoothingEnabled = false; + evt.context.msImageSmoothingEnabled = false; +}); + +imagery.on('prerender', function(evt) { + // use opaque background to conceal DEM while fully opaque imagery renders + if (imagery.getOpacity() === 1) { + evt.context.fillStyle = 'white'; + evt.context.fillRect(0, 0, evt.context.canvas.width, evt.context.canvas.height); + } +}); + +const control = document.getElementById('opacity'); +const output = document.getElementById('output'); +control.addEventListener('input', function() { + output.innerText = control.value; + imagery.setOpacity(control.value / 100); +}); +output.innerText = control.value; +imagery.setOpacity(control.value / 100); + +const view = new View({ + center: [6.893, 45.8295], + zoom: 16, + projection: 'EPSG:4326' +}); + +const map1 = new Map({ + target: 'map1', + layers: [disabledLayer, imagery], + view: view +}); + +const map2 = new Map({ + target: 'map2', + layers: [enabledLayer], + view: view +}); + +const info1 = document.getElementById('info1'); +const info2 = document.getElementById('info2'); + +const showElevations = function(evt) { + if (evt.dragging) { + return; + } + map1.forEachLayerAtPixel( + evt.pixel, + function(layer, pixel) { + const height = -10000 + (pixel[0] * 256 * 256 + pixel[1] * 256 + pixel[2]) * 0.1; + info1.innerText = height.toFixed(1); + }, + { + layerFilter: function(layer) { + return layer === disabledLayer; + } + } + ); + map2.forEachLayerAtPixel( + evt.pixel, + function(layer, pixel) { + const height = -10000 + (pixel[0] * 256 * 256 + pixel[1] * 256 + pixel[2]) * 0.1; + info2.innerText = height.toFixed(1); + }, + { + layerFilter: function(layer) { + return layer === enabledLayer; + } + } + ); +}; + +map1.on('pointermove', showElevations); +map2.on('pointermove', showElevations); diff --git a/rendering/cases/reproj-tile-disable-smoothing/expected.png b/rendering/cases/reproj-tile-disable-smoothing/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..6c51884467687ebd9265be2ea10c1571fb0504ad GIT binary patch literal 2404 zcmdT_ZAepL6h7CvO=nI)L21s3e`ZB4BGB^d7M2ccN(6?OTSTEzIYOL?ZC5jAAs8lU zXgWe%Kg4NTh?#d}VkNbuV5Im-Ii}8S4rfm9y}kR<|9%9`Kksv1-t&Gu?>Xl=4{~G~ z!My!E00@?5rk@AEkcmM62SNVr7uNs+4@uKcUMMTD&GuezQbmm}CS26URxPo^OOzjC z!>joPZ(C$SVKwhyZ)0_19$nOOX6T_w**0f%rW_}=#e+ZAJR3{vk+glpdd0Ju%M|Fh zCDCpA?RPVrF91Yo)xAzXY2anZHLz$f=5&<5#*F)&S2yoFq%q!Kb$w;0F=8(?O)-A{>i1o`86WRQ_< z=7+*fCLSX2K^!_Z-}thVrj;h847=E%Ip>n6sGlwyw5*r{#5zY73Ric=7XJ`SgI(TU zBKB7iY1flfA2p6=YjzLV7CurLMQS0R@Q6b#EC7|0GpY?nUjYFkh0ts0)b}r^A|xEx z)K~AqN^ayb^7MTNMx_#TipF+GO_r-Yt?bG2w?+DB4i#4GLW&WA?*@#QdHMVds_XMg z$ou-9IxfPTVPm2Ip|3kQ*HrR(eekc5n5GEG6c3;vx-E7I>lJ0vvvkh@Rjp+bGs^(g zrOOJ1Vjx~K+a=H+2KWq9aRR0TL1)vX3c=w|Zq%L`Ka<} [resolutions] Supported resolutions as given in IIIF 'scaleFactors' * @property {import("../size.js").Size} size Size of the image [width, height]. * @property {Array} [sizes] Supported scaled image sizes. @@ -274,6 +276,7 @@ class IIIF extends TileImage { crossOrigin: options.crossOrigin, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, state: options.state, tileClass: IiifTileClass, tileGrid: tileGrid, diff --git a/src/ol/source/OSM.js b/src/ol/source/OSM.js index f95f4589a6..d2f52a6d07 100644 --- a/src/ol/source/OSM.js +++ b/src/ol/source/OSM.js @@ -26,8 +26,10 @@ export const ATTRIBUTION = '© ' + * See https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image for more detail. * @property {number} [maxZoom=19] Max zoom. * @property {boolean} [opaque=true] Whether the layer is opaque. - * @property {number} [reprojectionErrorThreshold=1.5] Maximum allowed reprojection error (in pixels). + * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. The default is * ```js * function(imageTile, src) { @@ -73,6 +75,7 @@ class OSM extends XYZ { opaque: options.opaque !== undefined ? options.opaque : true, maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, tileLoadFunction: options.tileLoadFunction, url: url, wrapX: options.wrapX, diff --git a/src/ol/source/Stamen.js b/src/ol/source/Stamen.js index d3f768a426..cc8c2ce594 100644 --- a/src/ol/source/Stamen.js +++ b/src/ol/source/Stamen.js @@ -96,6 +96,8 @@ const ProviderConfig = { * @property {number} [maxZoom] Maximum zoom. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] * Optional function to load a tile given a URL. The default is * ```js @@ -138,6 +140,7 @@ class Stamen extends XYZ { minZoom: options.minZoom != undefined ? options.minZoom : providerConfig.minZoom, opaque: layerConfig.opaque, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, tileLoadFunction: options.tileLoadFunction, transition: options.transition, url: url, diff --git a/src/ol/source/TileArcGISRest.js b/src/ol/source/TileArcGISRest.js index 5e81b927d6..8576b7d2d0 100644 --- a/src/ol/source/TileArcGISRest.js +++ b/src/ol/source/TileArcGISRest.js @@ -34,6 +34,8 @@ import {appendParams} from '../uri.js'; * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. * The default is * ```js @@ -74,6 +76,7 @@ class TileArcGISRest extends TileImage { crossOrigin: options.crossOrigin, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, tileUrlFunction: tileUrlFunction, diff --git a/src/ol/source/TileImage.js b/src/ol/source/TileImage.js index ac4ccf709e..887a23fc3b 100644 --- a/src/ol/source/TileImage.js +++ b/src/ol/source/TileImage.js @@ -25,6 +25,8 @@ import {getForProjection as getTileGridForProjection} from '../tilegrid.js'; * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {import("./State.js").default} [state] Source state. * @property {typeof import("../ImageTile.js").default} [tileClass] Class used to instantiate image tiles. * Default is {@link module:ol/ImageTile~ImageTile}. @@ -123,6 +125,12 @@ class TileImage extends UrlTile { */ this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold; + /** + * @private + * @type {object|undefined} + */ + this.reprojectionContextOptions_ = options.reprojectionContextOptions; + /** * @private * @type {boolean} @@ -296,7 +304,7 @@ class TileImage extends UrlTile { function(z, x, y, pixelRatio) { return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); }.bind(this), this.reprojectionErrorThreshold_, - this.renderReprojectionEdges_); + this.renderReprojectionEdges_, this.reprojectionContextOptions_); newTile.key = key; if (tile) { diff --git a/src/ol/source/TileJSON.js b/src/ol/source/TileJSON.js index 6fe4877095..5233766869 100644 --- a/src/ol/source/TileJSON.js +++ b/src/ol/source/TileJSON.js @@ -47,6 +47,8 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * Useful when the server does not support CORS.. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {Config} [tileJSON] TileJSON configuration for this source. * If not provided, `url` must be configured. * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. The default is @@ -80,6 +82,7 @@ class TileJSON extends TileImage { crossOrigin: options.crossOrigin, projection: getProjection('EPSG:3857'), reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, state: SourceState.LOADING, tileLoadFunction: options.tileLoadFunction, wrapX: options.wrapX !== undefined ? options.wrapX : true, diff --git a/src/ol/source/TileWMS.js b/src/ol/source/TileWMS.js index 513e7ae132..f7b1ec5467 100644 --- a/src/ol/source/TileWMS.js +++ b/src/ol/source/TileWMS.js @@ -42,6 +42,8 @@ import {appendParams} from '../uri.js'; * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {typeof import("../ImageTile.js").default} [tileClass] Class used to instantiate image tiles. * Default is {@link module:ol/ImageTile~ImageTile}. * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid. Base this on the resolutions, @@ -94,6 +96,7 @@ class TileWMS extends TileImage { opaque: !transparent, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, tileClass: options.tileClass, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, diff --git a/src/ol/source/WMTS.js b/src/ol/source/WMTS.js index 603bc78e80..9de0f9a2e2 100644 --- a/src/ol/source/WMTS.js +++ b/src/ol/source/WMTS.js @@ -23,6 +23,8 @@ import {appendParams} from '../uri.js'; * @property {import("../proj.js").ProjectionLike} [projection] Projection. Default is the view projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {import("./WMTSRequestEncoding.js").default|string} [requestEncoding='KVP'] Request encoding. * @property {string} layer Layer name as advertised in the WMTS capabilities. * @property {string} style Style name as advertised in the WMTS capabilities. @@ -87,6 +89,7 @@ class WMTS extends TileImage { crossOrigin: options.crossOrigin, projection: options.projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, tileClass: options.tileClass, tileGrid: tileGrid, tileLoadFunction: options.tileLoadFunction, diff --git a/src/ol/source/XYZ.js b/src/ol/source/XYZ.js index 6c550057a5..1195286c37 100644 --- a/src/ol/source/XYZ.js +++ b/src/ol/source/XYZ.js @@ -17,6 +17,8 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {number} [maxZoom=18] Optional max zoom level. * @property {number} [minZoom=0] Optional min zoom level. * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid. @@ -90,6 +92,7 @@ class XYZ extends TileImage { opaque: options.opaque, projection: projection, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, tileGrid: tileGrid, tileLoadFunction: options.tileLoadFunction, tilePixelRatio: options.tilePixelRatio, diff --git a/src/ol/source/Zoomify.js b/src/ol/source/Zoomify.js index 5a5bd2efa1..401c788129 100644 --- a/src/ol/source/Zoomify.js +++ b/src/ol/source/Zoomify.js @@ -93,6 +93,8 @@ export class CustomTile extends ImageTile { * @property {number} [tilePixelRatio] The pixel ratio used by the tile service. For example, if the tile service advertizes 256px by 256px tiles but actually sends 512px by 512px images (for retina/hidpi devices) then `tilePixelRatio` should be set to `2` * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. + * @property {object} [reprojectionContextOptions] Optional properties to set on the canvas context used + * for reprojection. For example specify `{imageSmoothingEnabled: false}` to disable image smoothing. * @property {string} [url] URL template or base URL of the Zoomify service. * A base URL is the fixed part * of the URL, excluding the tile group, z, x, and y folder structure, e.g. @@ -255,6 +257,7 @@ class Zoomify extends TileImage { projection: options.projection, tilePixelRatio: tilePixelRatio, reprojectionErrorThreshold: options.reprojectionErrorThreshold, + reprojectionContextOptions: options.reprojectionContextOptions, tileClass: ZoomifyTileClass, tileGrid: tileGrid, tileUrlFunction: tileUrlFunction, From b71b87d7bbf4ab1c397af77a1a34f09be5c54179 Mon Sep 17 00:00:00 2001 From: philip Date: Sat, 28 Dec 2019 03:40:10 +0000 Subject: [PATCH 003/636] Fix issue with reprojection and double drawing pixels. --- src/ol/reproj.js | 31 +++++-------------------------- src/ol/reproj/Triangulation.js | 2 +- 2 files changed, 6 insertions(+), 27 deletions(-) diff --git a/src/ol/reproj.js b/src/ol/reproj.js index 7593c6bc3b..bb54d07f01 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -53,24 +53,6 @@ export function calculateSourceResolution(sourceProj, targetProj, } -/** - * Enlarge the clipping triangle point by 1 pixel to ensure the edges overlap - * in order to mask gaps caused by antialiasing. - * - * @param {number} centroidX Centroid of the triangle (x coordinate in pixels). - * @param {number} centroidY Centroid of the triangle (y coordinate in pixels). - * @param {number} x X coordinate of the point (in pixels). - * @param {number} y Y coordinate of the point (in pixels). - * @return {import("./coordinate.js").Coordinate} New point 1 px farther from the centroid. - */ -function enlargeClipPoint(centroidX, centroidY, x, y) { - const dX = x - centroidX; - const dY = y - centroidY; - const distance = Math.sqrt(dX * dX + dY * dY); - return [Math.round(x + dX / distance), Math.round(y + dY / distance)]; -} - - /** * Renders the source data into new canvas based on the triangulation. * @@ -103,6 +85,8 @@ export function render(width, height, pixelRatio, context.scale(pixelRatio, pixelRatio); + context.globalCompositeOperation = 'lighter'; + const sourceDataExtent = createEmpty(); sources.forEach(function(src, i, arr) { extend(sourceDataExtent, src.extent); @@ -190,15 +174,10 @@ export function render(width, height, pixelRatio, context.save(); context.beginPath(); - const centroidX = (u0 + u1 + u2) / 3; - const centroidY = (v0 + v1 + v2) / 3; - const p0 = enlargeClipPoint(centroidX, centroidY, u0, v0); - const p1 = enlargeClipPoint(centroidX, centroidY, u1, v1); - const p2 = enlargeClipPoint(centroidX, centroidY, u2, v2); - context.moveTo(p1[0], p1[1]); - context.lineTo(p0[0], p0[1]); - context.lineTo(p2[0], p2[1]); + context.moveTo(u1, v1); + context.lineTo(u0, v0); + context.lineTo(u2, v2); context.clip(); context.transform( diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index ebdab668da..9fe7270abd 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -321,7 +321,7 @@ class Triangulation { } this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); - this.addTriangle_(a, b, c, aSrc, bSrc, cSrc); + this.addTriangle_(a, c, b, aSrc, cSrc, bSrc); } /** From a6b1df3574bb8f5ea520200a942fbdd7d0caba50 Mon Sep 17 00:00:00 2001 From: philip Date: Sat, 28 Dec 2019 17:47:53 +0000 Subject: [PATCH 004/636] Fix missing corners of the world --- src/ol/reproj.js | 31 ++++++++++++++++++++++++++++++- src/ol/reproj/Tile.js | 9 ++++----- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/src/ol/reproj.js b/src/ol/reproj.js index bb54d07f01..e669a4ec82 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -2,7 +2,7 @@ * @module ol/reproj */ import {createCanvasContext2D} from './dom.js'; -import {containsCoordinate, createEmpty, extend, getHeight, getTopLeft, getWidth} from './extent.js'; +import {containsCoordinate, createEmpty, extend, forEachCorner, getCenter, getHeight, getTopLeft, getWidth} from './extent.js'; import {solveLinearSystem} from './math.js'; import {getPointResolution, transform} from './proj.js'; @@ -53,6 +53,35 @@ export function calculateSourceResolution(sourceProj, targetProj, } +/** + * Calculates ideal resolution to use from the source in order to achieve + * pixel mapping as close as possible to 1:1 during reprojection. + * The resolution is calculated regardless of what resolutions + * are actually available in the dataset (TileGrid, Image, ...). + * + * @param {import("./proj/Projection.js").default} sourceProj Source projection. + * @param {import("./proj/Projection.js").default} targetProj Target projection. + * @param {import("./extent.js").Extent} targetExtent Target extent + * @param {number} targetResolution Target resolution. + * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0. + */ +export function calculateSourceExtentResolution(sourceProj, targetProj, + targetExtent, targetResolution) { + + const targetCenter = getCenter(targetExtent); + let sourceResolution = calculateSourceResolution(sourceProj, targetProj, targetCenter, targetResolution); + + if (!isFinite(sourceResolution) || sourceResolution <= 0) { + forEachCorner(targetExtent, function(corner) { + sourceResolution = calculateSourceResolution(sourceProj, targetProj, corner, targetResolution); + return isFinite(sourceResolution) && sourceResolution > 0; + }); + } + + return sourceResolution; +} + + /** * Renders the source data into new canvas based on the triangulation. * diff --git a/src/ol/reproj/Tile.js b/src/ol/reproj/Tile.js index 909b125e7e..648431ac7f 100644 --- a/src/ol/reproj/Tile.js +++ b/src/ol/reproj/Tile.js @@ -7,9 +7,9 @@ import Tile from '../Tile.js'; import TileState from '../TileState.js'; import {listen, unlistenByKey} from '../events.js'; import EventType from '../events/EventType.js'; -import {getArea, getCenter, getIntersection} from '../extent.js'; +import {getArea, getIntersection} from '../extent.js'; import {clamp} from '../math.js'; -import {calculateSourceResolution, render as renderReprojected} from '../reproj.js'; +import {calculateSourceExtentResolution, render as renderReprojected} from '../reproj.js'; import Triangulation from './Triangulation.js'; @@ -140,9 +140,8 @@ class ReprojTile extends Tile { const targetResolution = targetTileGrid.getResolution( this.wrappedTileCoord_[0]); - const targetCenter = getCenter(limitedTargetExtent); - const sourceResolution = calculateSourceResolution( - sourceProj, targetProj, targetCenter, targetResolution); + const sourceResolution = calculateSourceExtentResolution( + sourceProj, targetProj, limitedTargetExtent, targetResolution); if (!isFinite(sourceResolution) || sourceResolution <= 0) { // invalid sourceResolution -> EMPTY From 4040d03ae6ef8054671fda169d239e4dba810b12 Mon Sep 17 00:00:00 2001 From: philip Date: Sat, 28 Dec 2019 18:09:07 +0000 Subject: [PATCH 005/636] Fix problem with zero size canvas drawing --- src/ol/reproj.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/ol/reproj.js b/src/ol/reproj.js index e669a4ec82..32ded4f1a5 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -135,12 +135,15 @@ export function render(width, height, pixelRatio, const srcWidth = getWidth(src.extent); const srcHeight = getHeight(src.extent); - stitchContext.drawImage( - src.image, - gutter, gutter, - src.image.width - 2 * gutter, src.image.height - 2 * gutter, - xPos * stitchScale, yPos * stitchScale, - srcWidth * stitchScale, srcHeight * stitchScale); + // This test should never fail -- but it does. Need to find a fix the upstream condition + if (src.image.width > 0 && src.image.height > 0) { + stitchContext.drawImage( + src.image, + gutter, gutter, + src.image.width - 2 * gutter, src.image.height - 2 * gutter, + xPos * stitchScale, yPos * stitchScale, + srcWidth * stitchScale, srcHeight * stitchScale); + } }); const targetTopLeft = getTopLeft(targetExtent); From f457093baf8650edf638c577c6e0cbf033d095b5 Mon Sep 17 00:00:00 2001 From: philip Date: Sat, 28 Dec 2019 22:31:30 +0000 Subject: [PATCH 006/636] Handle the zoomed out case where the source extent is infinite. This does raise the question of whether an Infinite extent intersects a finite extent. It appears not to, but maybe it should. --- src/ol/reproj/Triangulation.js | 42 +++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index 9fe7270abd..d5c7580584 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -247,12 +247,19 @@ class Triangulation { } if (!needsSubdivision && this.maxSourceExtent_) { - if (!intersects(sourceQuadExtent, this.maxSourceExtent_)) { - // whole quad outside source projection extent -> ignore - return; + if (isFinite(sourceQuadExtent[0]) && + isFinite(sourceQuadExtent[1]) && + isFinite(sourceQuadExtent[2]) && + isFinite(sourceQuadExtent[3])) { + if (!intersects(sourceQuadExtent, this.maxSourceExtent_)) { + // whole quad outside source projection extent -> ignore + return; + } } } + let isNotFinite = 0; + if (!needsSubdivision) { if (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) || !isFinite(bSrc[0]) || !isFinite(bSrc[1]) || @@ -261,7 +268,16 @@ class Triangulation { if (maxSubdivision > 0) { needsSubdivision = true; } else { - return; + // It might be the case that only 1 of the points is infinite. In this case + // we can draw a single triangle with the other three points + isNotFinite = + (((!isFinite(aSrc[0]) || !isFinite(aSrc[1])) | 0) << 3) + + (((!isFinite(bSrc[0]) || !isFinite(bSrc[1])) | 0) << 2) + + (((!isFinite(cSrc[0]) || !isFinite(cSrc[1])) | 0) << 1) + + ((!isFinite(dSrc[0]) || !isFinite(dSrc[1])) | 0); + if (isNotFinite != 1 && isNotFinite != 2 && isNotFinite != 4 && isNotFinite != 8) { + return; + } } } } @@ -320,8 +336,22 @@ class Triangulation { this.wrapsXInSource_ = true; } - this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); - this.addTriangle_(a, c, b, aSrc, cSrc, bSrc); + // Exactly zero or one of *Src is not finite + if ((isNotFinite & 0xb) == 0) { + this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); + } + if ((isNotFinite & 0xe) == 0) { + this.addTriangle_(a, c, b, aSrc, cSrc, bSrc); + } + if (isNotFinite) { + // Try the other two triangles + if ((isNotFinite & 0xd) == 0) { + this.addTriangle_(b, d, a, bSrc, dSrc, aSrc); + } + if ((isNotFinite & 0x7) == 0) { + this.addTriangle_(b, d, c, bSrc, dSrc, cSrc); + } + } } /** From 89ed757273b596eff023552dd9e8723df09e9be0 Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 30 Dec 2019 16:22:44 +0000 Subject: [PATCH 007/636] Fix indentation --- src/ol/reproj/Triangulation.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index d5c7580584..bc3f24e522 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -251,10 +251,10 @@ class Triangulation { isFinite(sourceQuadExtent[1]) && isFinite(sourceQuadExtent[2]) && isFinite(sourceQuadExtent[3])) { - if (!intersects(sourceQuadExtent, this.maxSourceExtent_)) { - // whole quad outside source projection extent -> ignore - return; - } + if (!intersects(sourceQuadExtent, this.maxSourceExtent_)) { + // whole quad outside source projection extent -> ignore + return; + } } } From e35795c5a38d4d216128e633dbb2db5b84beef1d Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 30 Dec 2019 16:46:20 +0000 Subject: [PATCH 008/636] Rework code to pass eslint --- src/ol/reproj/Triangulation.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index bc3f24e522..1f56a7a82c 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -271,10 +271,10 @@ class Triangulation { // It might be the case that only 1 of the points is infinite. In this case // we can draw a single triangle with the other three points isNotFinite = - (((!isFinite(aSrc[0]) || !isFinite(aSrc[1])) | 0) << 3) + - (((!isFinite(bSrc[0]) || !isFinite(bSrc[1])) | 0) << 2) + - (((!isFinite(cSrc[0]) || !isFinite(cSrc[1])) | 0) << 1) + - ((!isFinite(dSrc[0]) || !isFinite(dSrc[1])) | 0); + ((!isFinite(aSrc[0]) || !isFinite(aSrc[1])) ? 8 : 0) + + ((!isFinite(bSrc[0]) || !isFinite(bSrc[1])) ? 4 : 0) + + ((!isFinite(cSrc[0]) || !isFinite(cSrc[1])) ? 2 : 0) + + ((!isFinite(dSrc[0]) || !isFinite(dSrc[1])) ? 1 : 0); if (isNotFinite != 1 && isNotFinite != 2 && isNotFinite != 4 && isNotFinite != 8) { return; } From 5b1df4438d89c18d162d775f0fdca91f6c782043 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Wed, 1 Jan 2020 22:04:10 +0000 Subject: [PATCH 009/636] Fix for undefined source in Image layer Prevent error if layer does not have a source. Also clear any existing image if source is set to null or undefined by setSource. --- src/ol/renderer/canvas/ImageLayer.js | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/ol/renderer/canvas/ImageLayer.js b/src/ol/renderer/canvas/ImageLayer.js index 5dc9895cd0..7403890219 100644 --- a/src/ol/renderer/canvas/ImageLayer.js +++ b/src/ol/renderer/canvas/ImageLayer.js @@ -55,16 +55,20 @@ class CanvasImageLayerRenderer extends CanvasLayerRenderer { } if (!hints[ViewHint.ANIMATING] && !hints[ViewHint.INTERACTING] && !isEmpty(renderedExtent)) { - let projection = viewState.projection; - if (!ENABLE_RASTER_REPROJECTION) { - const sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - projection = sourceProjection; + if (imageSource) { + let projection = viewState.projection; + if (!ENABLE_RASTER_REPROJECTION) { + const sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + projection = sourceProjection; + } } - } - const image = imageSource.getImage(renderedExtent, viewResolution, pixelRatio, projection); - if (image && this.loadImage(image)) { - this.image_ = image; + const image = imageSource.getImage(renderedExtent, viewResolution, pixelRatio, projection); + if (image && this.loadImage(image)) { + this.image_ = image; + } + } else { + this.image_ = null; } } From 902ed53999bb398e101c824029bee601bffdf7bb Mon Sep 17 00:00:00 2001 From: philip Date: Mon, 6 Jan 2020 02:23:58 +0000 Subject: [PATCH 010/636] Add detection of browsers which cannot render correctly with diagonal clipping regions. In this case, modify the shape of the clip region so that it has (a number) of horizontal and vertical edges. --- src/ol/TileCache.js | 3 +- src/ol/reproj.js | 95 ++++++++++++++++++++++++++++++++-- src/ol/reproj/Triangulation.js | 3 ++ 3 files changed, 97 insertions(+), 4 deletions(-) diff --git a/src/ol/TileCache.js b/src/ol/TileCache.js index e71cfd4bc3..5d02d2a2b0 100644 --- a/src/ol/TileCache.js +++ b/src/ol/TileCache.js @@ -24,7 +24,8 @@ class TileCache extends LRUCache { if (tile.getKey() in usedTiles) { break; } else { - this.pop().dispose(); + // This lets the GC clean up the object + this.pop(); } } } diff --git a/src/ol/reproj.js b/src/ol/reproj.js index 32ded4f1a5..2e3fd7f1ec 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -6,6 +6,68 @@ import {containsCoordinate, createEmpty, extend, forEachCorner, getCenter, getHe import {solveLinearSystem} from './math.js'; import {getPointResolution, transform} from './proj.js'; +let brokenDiagonalRendering_; + +/** + * This draws a small triangle into a canvas by setting the triangle as the clip region + * and then drawing a (too large) rectangle + * + * @param {CanvasRenderingContext2D} ctx The context in which to draw the triangle + * @param {number} u1 The x-coordinate of the second point. The first point is 0,0. + * @param {number} v1 The y-coordinate of the second point. + * @param {number} u2 The x-coordinate of the third point. + * @param {number} v2 The y-coordinate of the third point. + */ +function drawTestTriangle(ctx, u1, v1, u2, v2) { + ctx.beginPath(); + ctx.moveTo(0, 0); + ctx.lineTo(u1, v1); + ctx.lineTo(u2, v2); + ctx.closePath(); + ctx.save(); + ctx.clip(); + ctx.fillRect(0, 0, Math.max(u1, u2) + 1, Math.max(v1, v2)); + ctx.restore(); +} + +/** + * Given the data from getImageData, see if the right values appear at the provided offset. + * Returns true if either the color or transparency is off + * + * @param {Uint8ClampedArray} data The data returned from getImageData + * @param {number} offset The pixel offset from the start of data. + * @return {boolean} true if the diagonal rendering is broken + */ +function verifyBrokenDiagonalRendering(data, offset) { + // the values ought to be close to the rgba(210, 0, 0, 0.75) + return Math.abs(data[offset * 4] - 210) > 2 || Math.abs(data[offset * 4 + 3] - 0.75 * 255) > 2; +} + +/** + * Determines if the current browser configuration can render triangular clip regions correctly. + * This value is cached so the function is only expensive the first time called. + * Firefox on Windows (as of now) does not if HWA is enabled. See https://bugzilla.mozilla.org/show_bug.cgi?id=1606976 + * IE also doesn't. Chrome works, and everything seems to work on OSX and Android. This function caches the + * result. I suppose that it is conceivably possible that a browser might flip modes while the app is + * running, but lets hope not. + * + * @return {boolean} true if the Diagonal Rendering is broken. + */ +function isBrokenDiagonalRendering() { + if (brokenDiagonalRendering_ === undefined) { + const ctx = document.createElement('canvas').getContext('2d'); + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = 'rgba(210, 0, 0, 0.75)'; + drawTestTriangle(ctx, 4, 5, 4, 0); + drawTestTriangle(ctx, 4, 5, 0, 5); + const data = ctx.getImageData(0, 0, 3, 3).data; + brokenDiagonalRendering_ = verifyBrokenDiagonalRendering(data, 0) || + verifyBrokenDiagonalRendering(data, 4) || + verifyBrokenDiagonalRendering(data, 8); + } + + return brokenDiagonalRendering_; +} /** * Calculates ideal resolution to use from the source in order to achieve @@ -207,9 +269,36 @@ export function render(width, height, pixelRatio, context.save(); context.beginPath(); - context.moveTo(u1, v1); - context.lineTo(u0, v0); - context.lineTo(u2, v2); + if (isBrokenDiagonalRendering()) { + // Make sure that everything is on pixel boundaries + const u0r = Math.round(u0); + const v0r = Math.round(v0); + const u1r = Math.round(u1); + const v1r = Math.round(v1); + const u2r = Math.round(u2); + const v2r = Math.round(v2); + // Make sure that all lines are horizontal or vertical + context.moveTo(u1r, v1r); + // This is the diagonal line. Do it in 4 steps + const steps = 4; + const ud = u0r - u1r; + const vd = v0r - v1r; + for (let step = 0; step < steps; step++) { + // Go horizontally + context.lineTo(u1r + Math.round((step + 1) * ud / steps), v1r + Math.round(step * vd / (steps - 1))); + // Go vertically + if (step != (steps - 1)) { + context.lineTo(u1r + Math.round((step + 1) * ud / steps), v1r + Math.round((step + 1) * vd / (steps - 1))); + } + } + // We are almost at u0r, v0r + context.lineTo(u2r, v2r); + } else { + context.moveTo(u1, v1); + context.lineTo(u0, v0); + context.lineTo(u2, v2); + } + context.clip(); context.transform( diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index 1f56a7a82c..5375947ead 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -337,6 +337,9 @@ class Triangulation { } // Exactly zero or one of *Src is not finite + // The triangles must have the diagonal line as the first side + // This is to allow easy code in reproj.s to make it straight for broken + // browsers that can't handle diagonal clipping if ((isNotFinite & 0xb) == 0) { this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); } From 852f6552c7dae58996893ba271c672d082f71fca Mon Sep 17 00:00:00 2001 From: philip Date: Sat, 11 Jan 2020 17:26:52 +0000 Subject: [PATCH 011/636] Fix pixel rounding --- src/ol/reproj.js | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/ol/reproj.js b/src/ol/reproj.js index 2e3fd7f1ec..518e815572 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -176,6 +176,10 @@ export function render(width, height, pixelRatio, context.scale(pixelRatio, pixelRatio); + function pixelRound(value) { + return Math.round(value * pixelRatio) / pixelRatio; + } + context.globalCompositeOperation = 'lighter'; const sourceDataExtent = createEmpty(); @@ -271,12 +275,12 @@ export function render(width, height, pixelRatio, if (isBrokenDiagonalRendering()) { // Make sure that everything is on pixel boundaries - const u0r = Math.round(u0); - const v0r = Math.round(v0); - const u1r = Math.round(u1); - const v1r = Math.round(v1); - const u2r = Math.round(u2); - const v2r = Math.round(v2); + const u0r = pixelRound(u0); + const v0r = pixelRound(v0); + const u1r = pixelRound(u1); + const v1r = pixelRound(v1); + const u2r = pixelRound(u2); + const v2r = pixelRound(v2); // Make sure that all lines are horizontal or vertical context.moveTo(u1r, v1r); // This is the diagonal line. Do it in 4 steps @@ -285,10 +289,10 @@ export function render(width, height, pixelRatio, const vd = v0r - v1r; for (let step = 0; step < steps; step++) { // Go horizontally - context.lineTo(u1r + Math.round((step + 1) * ud / steps), v1r + Math.round(step * vd / (steps - 1))); + context.lineTo(u1r + pixelRound((step + 1) * ud / steps), v1r + pixelRound(step * vd / (steps - 1))); // Go vertically if (step != (steps - 1)) { - context.lineTo(u1r + Math.round((step + 1) * ud / steps), v1r + Math.round((step + 1) * vd / (steps - 1))); + context.lineTo(u1r + pixelRound((step + 1) * ud / steps), v1r + pixelRound((step + 1) * vd / (steps - 1))); } } // We are almost at u0r, v0r From 1772c8198b52f8d7576527ef267c7a7e234be542 Mon Sep 17 00:00:00 2001 From: philip Date: Sat, 11 Jan 2020 18:07:09 +0000 Subject: [PATCH 012/636] Added comment pointing to an area of improvement --- src/ol/reproj.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/reproj.js b/src/ol/reproj.js index 518e815572..cca5d98ffa 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -314,6 +314,10 @@ export function render(width, height, pixelRatio, context.scale(sourceResolution / pixelRatio, -sourceResolution / pixelRatio); + // This is rather inefficient as we draw the *entire* source canvas into the destination and + // rely on the clipping to eliminate nearly all the pixels. It seems smarter to only + // draw the relevant region of the source canvas. However, I'm having difficulty figuring + // out what the parameters ought to be. context.drawImage(stitchContext.canvas, 0, 0); context.restore(); }); From d2b05991772342cf1c9afbc8c6584f6b853a5223 Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 14 Jan 2020 13:39:52 +0000 Subject: [PATCH 013/636] Dynamically chose the number of subdivisions based on the size of the Image. --- src/ol/reproj/Image.js | 2 +- src/ol/reproj/Tile.js | 2 +- src/ol/reproj/Triangulation.js | 11 ++++++++--- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ol/reproj/Image.js b/src/ol/reproj/Image.js index e08778002c..7a5cfd7240 100644 --- a/src/ol/reproj/Image.js +++ b/src/ol/reproj/Image.js @@ -47,7 +47,7 @@ class ReprojImage extends ImageBase { const triangulation = new Triangulation( sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, - sourceResolution * errorThresholdInPixels); + sourceResolution * errorThresholdInPixels, targetResolution); const sourceExtent = triangulation.calculateSourceExtent(); const sourceImage = getImageFunction(sourceExtent, sourceResolution, pixelRatio); diff --git a/src/ol/reproj/Tile.js b/src/ol/reproj/Tile.js index 73d508d203..7defc9e48b 100644 --- a/src/ol/reproj/Tile.js +++ b/src/ol/reproj/Tile.js @@ -160,7 +160,7 @@ class ReprojTile extends Tile { */ this.triangulation_ = new Triangulation( sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, - sourceResolution * errorThresholdInPixels); + sourceResolution * errorThresholdInPixels, targetResolution); if (this.triangulation_.getTriangles().length === 0) { // no valid triangles -> EMPTY diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index ebdab668da..fc75ce7a9c 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -1,7 +1,7 @@ /** * @module ol/reproj/Triangulation */ -import {boundingExtent, createEmpty, extendCoordinate, getBottomLeft, getBottomRight, +import {boundingExtent, createEmpty, extendCoordinate, getArea, getBottomLeft, getBottomRight, getTopLeft, getTopRight, getWidth, intersects} from '../extent.js'; import {modulo} from '../math.js'; import {getTransform} from '../proj.js'; @@ -49,8 +49,9 @@ class Triangulation { * @param {import("../extent.js").Extent} targetExtent Target extent to triangulate. * @param {import("../extent.js").Extent} maxSourceExtent Maximal source extent that can be used. * @param {number} errorThreshold Acceptable error (in source units). + * @param {number} destinationResolution The (optional) resolution of the destination. */ - constructor(sourceProj, targetProj, targetExtent, maxSourceExtent, errorThreshold) { + constructor(sourceProj, targetProj, targetExtent, maxSourceExtent, errorThreshold, destinationResolution) { /** * @type {import("../proj/Projection.js").default} @@ -138,11 +139,15 @@ class Triangulation { const sourceBottomRight = this.transformInv_(destinationBottomRight); const sourceBottomLeft = this.transformInv_(destinationBottomLeft); + const maxSubdivision = MAX_SUBDIVISION + (destinationResolution ? + Math.max(0, Math.ceil(Math.log2(getArea(targetExtent) / (destinationResolution * destinationResolution * 256 * 256)))) + : 0); + this.addQuad_( destinationTopLeft, destinationTopRight, destinationBottomRight, destinationBottomLeft, sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft, - MAX_SUBDIVISION); + maxSubdivision); if (this.wrapsXInSource_) { let leftBound = Infinity; From ccf3532eb2d4a42e818930dc44e63091dfd369e4 Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 14 Jan 2020 13:48:23 +0000 Subject: [PATCH 014/636] Fix the parameter name to have the opt_ prefix. --- src/ol/reproj/Triangulation.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ol/reproj/Triangulation.js b/src/ol/reproj/Triangulation.js index fc75ce7a9c..b6091d149f 100644 --- a/src/ol/reproj/Triangulation.js +++ b/src/ol/reproj/Triangulation.js @@ -49,9 +49,9 @@ class Triangulation { * @param {import("../extent.js").Extent} targetExtent Target extent to triangulate. * @param {import("../extent.js").Extent} maxSourceExtent Maximal source extent that can be used. * @param {number} errorThreshold Acceptable error (in source units). - * @param {number} destinationResolution The (optional) resolution of the destination. + * @param {?number} opt_destinationResolution The (optional) resolution of the destination. */ - constructor(sourceProj, targetProj, targetExtent, maxSourceExtent, errorThreshold, destinationResolution) { + constructor(sourceProj, targetProj, targetExtent, maxSourceExtent, errorThreshold, opt_destinationResolution) { /** * @type {import("../proj/Projection.js").default} @@ -139,8 +139,9 @@ class Triangulation { const sourceBottomRight = this.transformInv_(destinationBottomRight); const sourceBottomLeft = this.transformInv_(destinationBottomLeft); - const maxSubdivision = MAX_SUBDIVISION + (destinationResolution ? - Math.max(0, Math.ceil(Math.log2(getArea(targetExtent) / (destinationResolution * destinationResolution * 256 * 256)))) + const maxSubdivision = MAX_SUBDIVISION + (opt_destinationResolution ? + Math.max(0, Math.ceil(Math.log2(getArea(targetExtent) / + (opt_destinationResolution * opt_destinationResolution * 256 * 256)))) : 0); this.addQuad_( From 76b926420c2bc39ffe383b17fe583452963aedd2 Mon Sep 17 00:00:00 2001 From: philip Date: Tue, 14 Jan 2020 23:52:54 +0000 Subject: [PATCH 015/636] Only draw the piece of the source canvas that is required. I.e. take the clip region into account. --- src/ol/reproj.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/ol/reproj.js b/src/ol/reproj.js index cca5d98ffa..e0420e7a8c 100644 --- a/src/ol/reproj.js +++ b/src/ol/reproj.js @@ -2,7 +2,7 @@ * @module ol/reproj */ import {createCanvasContext2D} from './dom.js'; -import {containsCoordinate, createEmpty, extend, forEachCorner, getCenter, getHeight, getTopLeft, getWidth} from './extent.js'; +import {boundingExtent, containsCoordinate, createEmpty, extend, forEachCorner, getCenter, getHeight, getTopLeft, getWidth} from './extent.js'; import {solveLinearSystem} from './math.js'; import {getPointResolution, transform} from './proj.js'; @@ -314,11 +314,13 @@ export function render(width, height, pixelRatio, context.scale(sourceResolution / pixelRatio, -sourceResolution / pixelRatio); - // This is rather inefficient as we draw the *entire* source canvas into the destination and - // rely on the clipping to eliminate nearly all the pixels. It seems smarter to only - // draw the relevant region of the source canvas. However, I'm having difficulty figuring - // out what the parameters ought to be. - context.drawImage(stitchContext.canvas, 0, 0); + const sourceTriangleExtent = boundingExtent(source); + const topLeftX = Math.floor((sourceTriangleExtent[0] - sourceDataExtent[0]) * stitchScale); + const topLeftY = Math.floor((sourceDataExtent[3] - sourceTriangleExtent[3]) * stitchScale); + const width = Math.ceil((sourceTriangleExtent[2] - sourceTriangleExtent[0]) * stitchScale) + 2; + const height = Math.ceil((sourceTriangleExtent[3] - sourceTriangleExtent[1]) * stitchScale) + 2; + + context.drawImage(stitchContext.canvas, topLeftX, topLeftY, width, height, topLeftX, topLeftY, width, height); context.restore(); }); From 0057144b52e285c8dbaae5a28896cb6298c74e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Fri, 7 Feb 2020 00:24:56 +0100 Subject: [PATCH 016/636] Add apidoc-debug task to debug the apidoc generation process --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 5c5dad91a3..a99e2855af 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "copy-css": "shx cp src/ol/ol.css build/ol/ol.css", "transpile": "shx rm -rf build/ol && shx mkdir -p build/ol && shx cp -rf src/ol build/ol/src && node tasks/serialize-workers && tsc --project config/tsconfig-build.json", "typecheck": "tsc --pretty", + "apidoc-debug": "shx rm -rf build/apidoc && node --inspect-brk=9229 ./node_modules/jsdoc/jsdoc.js -R config/jsdoc/api/index.md -c config/jsdoc/api/conf.json -P package.json -d build/apidoc", "apidoc": "shx rm -rf build/apidoc && jsdoc -R config/jsdoc/api/index.md -c config/jsdoc/api/conf.json -P package.json -d build/apidoc" }, "main": "index.js", From 7a77793d697fa5c8c07497cd1b9c78f20655bb45 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Tue, 11 Feb 2020 14:58:57 +0000 Subject: [PATCH 017/636] Write fill and outline in PolyStyle Write styles based on style objects appropriate for geometry. Write fill and outline in PolyStyle if false (i.e. non-default) Handle MultiLineString, MultiPoint and MultiPolygon within heterogenous MultiGeometry when writing features Add getGeometriesArrayRecursive method to ol/geom/GeometryCollection to allow for nested MultiGeometry Enhanced write GeometryCollection geometries test A more rigorous write GeometryCollection geometries test including nested collections (the output is simplified to a single MultiGeomtry) Add writeFeatures to outline and fill tests, setting geometry for geometry specific tests Add 0 and 0 to some existing tests --- src/ol/format/KML.js | 173 ++++++++++++++++++------- src/ol/geom/GeometryCollection.js | 17 +++ test/spec/ol/format/kml.test.js | 205 +++++++++++++++++++++++++++++- 3 files changed, 344 insertions(+), 51 deletions(-) diff --git a/src/ol/format/KML.js b/src/ol/format/KML.js index d94a4c473b..68506c466e 100644 --- a/src/ol/format/KML.js +++ b/src/ol/format/KML.js @@ -918,11 +918,7 @@ function createNameStyleFunction(foundStyle, name) { const nameStyle = new Style({ image: imageStyle, - text: textStyle, - // although nameStyle will be used only for Point geometries - // fill and stroke are included to assist writing of MultiGeometry styles - fill: foundStyle.getFill(), - stroke: foundStyle.getStroke() + text: textStyle }); return nameStyle; } @@ -953,7 +949,7 @@ function createFeatureStyleFunction(style, styleUrl, defaultStyle, sharedStyles, if (geometry) { const type = geometry.getType(); if (type === GeometryType.GEOMETRY_COLLECTION) { - multiGeometryPoints = geometry.getGeometriesArray().filter(function(geometry) { + multiGeometryPoints = geometry.getGeometriesArrayRecursive().filter(function(geometry) { const type = geometry.getType(); return type === GeometryType.POINT || type === GeometryType.MULTI_POINT; }); @@ -1806,7 +1802,7 @@ function readStyle(node, objectStack) { const type = geometry.getType(); if (type === GeometryType.GEOMETRY_COLLECTION) { return new GeometryCollection( - geometry.getGeometriesArray().filter(function(geometry) { + geometry.getGeometriesArrayRecursive().filter(function(geometry) { const type = geometry.getType(); return type !== GeometryType.POLYGON && type !== GeometryType.MULTI_POLYGON; }) @@ -1827,7 +1823,7 @@ function readStyle(node, objectStack) { const type = geometry.getType(); if (type === GeometryType.GEOMETRY_COLLECTION) { return new GeometryCollection( - geometry.getGeometriesArray().filter(function(geometry) { + geometry.getGeometriesArrayRecursive().filter(function(geometry) { const type = geometry.getType(); return type === GeometryType.POLYGON || type === GeometryType.MULTI_POLYGON; }) @@ -2703,20 +2699,35 @@ function writeMultiGeometry(node, geometry, objectStack) { const context = {node: node}; const type = geometry.getType(); /** @type {Array} */ - let geometries; + let geometries = []; /** @type {function(*, Array<*>, string=): (Node|undefined)} */ let factory; - if (type == GeometryType.GEOMETRY_COLLECTION) { - geometries = /** @type {GeometryCollection} */ (geometry).getGeometries(); + if (type === GeometryType.GEOMETRY_COLLECTION) { + /** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().forEach(function(geometry) { + const type = geometry.getType(); + if (type === GeometryType.MULTI_POINT) { + geometries = geometries.concat(/** @type {MultiPoint} */ (geometry).getPoints()); + } else if (type === GeometryType.MULTI_LINE_STRING) { + geometries = geometries.concat(/** @type {MultiLineString} */ (geometry).getLineStrings()); + } else if (type === GeometryType.MULTI_POLYGON) { + geometries = geometries.concat(/** @type {MultiPolygon} */ (geometry).getPolygons()); + } else if (type === GeometryType.POINT + || type === GeometryType.LINE_STRING + || type === GeometryType.POLYGON) { + geometries.push(geometry); + } else { + assert(false, 39); // Unknown geometry type + } + }); factory = GEOMETRY_NODE_FACTORY; - } else if (type == GeometryType.MULTI_POINT) { + } else if (type === GeometryType.MULTI_POINT) { geometries = /** @type {MultiPoint} */ (geometry).getPoints(); factory = POINT_NODE_FACTORY; - } else if (type == GeometryType.MULTI_LINE_STRING) { + } else if (type === GeometryType.MULTI_LINE_STRING) { geometries = (/** @type {MultiLineString} */ (geometry)).getLineStrings(); factory = LINE_STRING_NODE_FACTORY; - } else if (type == GeometryType.MULTI_POLYGON) { + } else if (type === GeometryType.MULTI_POLYGON) { geometries = (/** @type {MultiPolygon} */ (geometry)).getPolygons(); factory = POLYGON_NODE_FACTORY; @@ -2831,13 +2842,61 @@ function writePlacemark(node, feature, objectStack) { // resolution-independent here const styles = styleFunction(feature, 0); if (styles) { - const style = Array.isArray(styles) ? styles[0] : styles; - if (this.writeStyles_) { - properties['Style'] = style; + const styleArray = Array.isArray(styles) ? styles : [styles]; + let pointStyles = styleArray; + if (feature.getGeometry()) { + pointStyles = styleArray.filter(function(style) { + const geometry = style.getGeometryFunction()(feature); + if (geometry) { + const type = geometry.getType(); + if (type === GeometryType.GEOMETRY_COLLECTION) { + return /** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().filter(function(geometry) { + const type = geometry.getType(); + return type === GeometryType.POINT || type === GeometryType.MULTI_POINT; + }).length; + } + return type === GeometryType.POINT || type === GeometryType.MULTI_POINT; + } + }); } - const textStyle = style.getText(); - if (textStyle) { - properties['name'] = textStyle.getText(); + if (this.writeStyles_) { + let lineStyles = styleArray; + let polyStyles = styleArray; + if (feature.getGeometry()) { + lineStyles = styleArray.filter(function(style) { + const geometry = style.getGeometryFunction()(feature); + if (geometry) { + const type = geometry.getType(); + if (type === GeometryType.GEOMETRY_COLLECTION) { + return /** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().filter(function(geometry) { + const type = geometry.getType(); + return type === GeometryType.LINE_STRING || type === GeometryType.MULTI_LINE_STRING; + }).length; + } + return type === GeometryType.LINE_STRING || type === GeometryType.MULTI_LINE_STRING; + } + }); + polyStyles = styleArray.filter(function(style) { + const geometry = style.getGeometryFunction()(feature); + if (geometry) { + const type = geometry.getType(); + if (type === GeometryType.GEOMETRY_COLLECTION) { + return /** @type {GeometryCollection} */ (geometry).getGeometriesArrayRecursive().filter(function(geometry) { + const type = geometry.getType(); + return type === GeometryType.POLYGON || type === GeometryType.MULTI_POLYGON; + }).length; + } + return type === GeometryType.POLYGON || type === GeometryType.MULTI_POLYGON; + } + }); + } + properties['Style'] = {pointStyles: pointStyles, lineStyles: lineStyles, polyStyles: polyStyles}; + } + if (pointStyles.length && properties['name'] === undefined) { + const textStyle = pointStyles[0].getText(); + if (textStyle) { + properties['name'] = textStyle.getText(); + } } } } @@ -2913,6 +2972,17 @@ function writePrimitiveGeometry(node, geometry, objectStack) { } +/** + * @const + * @type {Object>} + */ +// @ts-ignore +const POLY_STYLE_SEQUENCE = makeStructureNS( + NAMESPACE_URIS, [ + 'color', 'fill', 'outline' + ]); + + /** * @const * @type {Object>} @@ -2972,27 +3042,31 @@ function writePolygon(node, polygon, objectStack) { // @ts-ignore const POLY_STYLE_SERIALIZERS = makeStructureNS( NAMESPACE_URIS, { - 'color': makeChildAppender(writeColorTextNode) + 'color': makeChildAppender(writeColorTextNode), + 'fill': makeChildAppender(writeBooleanTextNode), + 'outline': makeChildAppender(writeBooleanTextNode) }); -/** - * A factory for creating coordinates nodes. - * @const - * @type {function(*, Array<*>, string=): (Node|undefined)} - */ -const COLOR_NODE_FACTORY = makeSimpleNodeFactory('color'); - - /** * @param {Node} node Node. - * @param {Fill} style Style. + * @param {Style} style Style. * @param {Array<*>} objectStack Object stack. */ function writePolyStyle(node, style, objectStack) { const /** @type {import("../xml.js").NodeStackItem} */ context = {node: node}; + const fill = style.getFill(); + const stroke = style.getStroke(); + const properties = { + 'color': fill ? fill.getColor() : undefined, + 'fill': fill ? undefined : false, + 'outline': stroke ? undefined : false + }; + const parentNode = objectStack[objectStack.length - 1].node; + const orderedKeys = POLY_STYLE_SEQUENCE[parentNode.namespaceURI]; + const values = makeSequence(properties, orderedKeys); pushSerializeAndPop(context, POLY_STYLE_SERIALIZERS, - COLOR_NODE_FACTORY, [style.getColor()], objectStack); + OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys); } @@ -3034,27 +3108,34 @@ const STYLE_SERIALIZERS = makeStructureNS( /** * @param {Node} node Node. - * @param {Style} style Style. + * @param {Object>} styles Styles. * @param {Array<*>} objectStack Object stack. */ -function writeStyle(node, style, objectStack) { +function writeStyle(node, styles, objectStack) { const /** @type {import("../xml.js").NodeStackItem} */ context = {node: node}; const properties = {}; - const fillStyle = style.getFill(); - const strokeStyle = style.getStroke(); - const imageStyle = style.getImage(); - const textStyle = style.getText(); - if (imageStyle && typeof /** @type {?} */ (imageStyle).getSrc === 'function') { - properties['IconStyle'] = imageStyle; + if (styles.pointStyles.length) { + const textStyle = styles.pointStyles[0].getText(); + if (textStyle) { + properties['LabelStyle'] = textStyle; + } + const imageStyle = styles.pointStyles[0].getImage(); + if (imageStyle && typeof /** @type {?} */ (imageStyle).getSrc === 'function') { + properties['IconStyle'] = imageStyle; + } } - if (textStyle) { - properties['LabelStyle'] = textStyle; + if (styles.lineStyles.length) { + const strokeStyle = styles.lineStyles[0].getStroke(); + if (strokeStyle) { + properties['LineStyle'] = strokeStyle; + } } - if (strokeStyle) { - properties['LineStyle'] = strokeStyle; - } - if (fillStyle) { - properties['PolyStyle'] = fillStyle; + if (styles.polyStyles.length) { + const strokeStyle = styles.polyStyles[0].getStroke(); + if (strokeStyle && !properties['LineStyle']) { + properties['LineStyle'] = strokeStyle; + } + properties['PolyStyle'] = styles.polyStyles[0]; } const parentNode = objectStack[objectStack.length - 1].node; const orderedKeys = STYLE_SEQUENCE[parentNode.namespaceURI]; diff --git a/src/ol/geom/GeometryCollection.js b/src/ol/geom/GeometryCollection.js index e1b63d76dd..1d407f2fdc 100644 --- a/src/ol/geom/GeometryCollection.js +++ b/src/ol/geom/GeometryCollection.js @@ -126,6 +126,23 @@ class GeometryCollection extends Geometry { return this.geometries_; } + /** + * @return {Array} Geometries. + */ + getGeometriesArrayRecursive() { + /** @type {Array} */ + let geometriesArray = []; + const geometries = this.geometries_; + for (let i = 0, ii = geometries.length; i < ii; ++i) { + if (geometries[i].getType() === this.getType()) { + geometriesArray = geometriesArray.concat(/** @type {GeometryCollection} */ (geometries[i]).getGeometriesArrayRecursive()); + } else { + geometriesArray.push(geometries[i]); + } + } + return geometriesArray; + } + /** * @inheritDoc */ diff --git a/test/spec/ol/format/kml.test.js b/test/spec/ol/format/kml.test.js index e1a248bb75..60d235387f 100644 --- a/test/spec/ol/format/kml.test.js +++ b/test/spec/ol/format/kml.test.js @@ -1207,9 +1207,16 @@ describe('ol.format.KML', function() { it('can write GeometryCollection geometries', function() { const collection = new GeometryCollection([ - new Point([1, 2]), - new LineString([[1, 2], [3, 4]]), - new Polygon([[[1, 2], [3, 4], [3, 2], [1, 2]]]) + new GeometryCollection([ + new Point([1, 2]), + new LineString([[1, 2], [3, 4]]), + new Polygon([[[1, 2], [3, 4], [3, 2], [1, 2]]]) + ]), + new GeometryCollection([ + new MultiPoint([[5, 6], [9, 10]]), + new MultiLineString([[[5, 6], [7, 8]], [[9, 10], [11, 12]]]), + new MultiPolygon([[[[5, 6], [7, 8], [7, 6], [5, 6]]], [[[9, 10], [11, 12], [11, 10], [9, 10]]]]) + ]) ]); const features = [new Feature(collection)]; const node = format.writeFeaturesNode(features); @@ -1234,6 +1241,32 @@ describe('ol.format.KML', function() { ' ' + ' ' + ' ' + + ' ' + + ' 5,6' + + ' ' + + ' ' + + ' 9,10' + + ' ' + + ' ' + + ' 5,6 7,8' + + ' ' + + ' ' + + ' 9,10 11,12' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 5,6 7,8 7,6 5,6' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 9,10 11,12 11,10 9,10' + + ' ' + + ' ' + + ' ' + ' ' + ' ' + ''; @@ -1621,6 +1654,9 @@ describe('ol.format.KML', function() { ' ff332211' + ' 2' + ' ' + + ' ' + + ' 0' + + ' ' + ' ' + ' ' + ' ' + @@ -2164,6 +2200,43 @@ describe('ol.format.KML', function() { expect(strokeStyle.getWidth()).to.be(9); expect(style.getText()).to.be(getDefaultTextStyle()); expect(style.getZIndex()).to.be(undefined); + + const lineString = new LineString([[1, 2], [3, 4]]); + const polygon = new Polygon([[[0, 0], [0, 2], [2, 2], [2, 0], [0, 0]]]); + const collection = new GeometryCollection([lineString, polygon]); + f.setGeometry(collection); + const node = format.writeFeaturesNode(fs); + const text1 = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 1,2 3,4' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 0,0 0,2 2,2 2,0 0,0' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + expect(node).to.xmleql(parse(text1)); }); it('disables the stroke when outline is \'0\'', function() { @@ -2233,6 +2306,41 @@ describe('ol.format.KML', function() { expect(style1.getFill()).to.be(fillStyle); expect(style1.getStroke()).to.be(null); expect(style1.getZIndex()).to.be(undefined); + + f.setGeometry(collectionFeature.getGeometry()); + const node = format.writeFeaturesNode(fs); + const text1 = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 1,2 3,4' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 0,0 0,2 2,2 2,0 0,0' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + expect(node).to.xmleql(parse(text1)); }); it('disables both fill and stroke when fill and outline are \'0\'', @@ -2302,6 +2410,41 @@ describe('ol.format.KML', function() { expect(style1.getFill()).to.be(null); expect(style1.getStroke()).to.be(null); expect(style1.getZIndex()).to.be(undefined); + + f.setGeometry(collectionFeature.getGeometry()); + const node = format.writeFeaturesNode(fs); + const text1 = + '' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 1,2 3,4' + + ' ' + + ' ' + + ' ' + + ' ' + + ' 0,0 0,2 2,2 2,0 0,0' + + ' ' + + ' ' + + ' ' + + ' ' + + ' ' + + ''; + expect(node).to.xmleql(parse(text1)); }); it('can create text style for named point placemarks (including html character codes)', function() { @@ -2392,6 +2535,10 @@ describe('ol.format.KML', function() { ' ' + ' ' + + ' ' + + ' 0' + + ' 0' + + ' ' + ' ' + ' ' + ''; @@ -2440,6 +2587,10 @@ describe('ol.format.KML', function() { ' https://developers.google.com/kml/schema/kml22gx.xsd">' + ' ' + ' ' + ' ' + ''; @@ -2472,13 +2623,17 @@ describe('ol.format.KML', function() { ' ffdf220c' + ' 0.5' + ' ' + + ' ' + + ' 0' + + ' 0' + + ' ' + ' ' + ' ' + ''; expect(node).to.xmleql(parse(text)); }); - it('can write an feature\'s stroke style', function() { + it('can write an feature\'s stroke style without fill', function() { const style = new Style({ stroke: new Stroke({ color: '#112233', @@ -2500,13 +2655,16 @@ describe('ol.format.KML', function() { ' ff332211' + ' 2' + ' ' + + ' ' + + ' 0' + + ' ' + ' ' + ' ' + ''; expect(node).to.xmleql(parse(text)); }); - it('can write an feature\'s fill style', function() { + it('can write an feature\'s fill style without outline', function() { const style = new Style({ fill: new Fill({ color: 'rgba(12, 34, 223, 0.7)' @@ -2525,6 +2683,41 @@ describe('ol.format.KML', function() { ' ' + + ' ' + + ''; + expect(node).to.xmleql(parse(text)); + }); + + it('can write an feature\'s fill style and outline', function() { + const style = new Style({ + fill: new Fill({ + color: 'rgba(12, 34, 223, 0.7)' + }), + stroke: new Stroke({ + color: '#112233', + width: 2 + }) + }); + const feature = new Feature(); + feature.setStyle([style]); + const node = format.writeFeaturesNode([feature]); + const text = + '' + + ' ' + + ' ' + ' ' + @@ -2554,6 +2747,7 @@ describe('ol.format.KML', function() { ' ' + ' ' + @@ -2561,6 +2755,7 @@ describe('ol.format.KML', function() { ' ' + ' ' + From ea5c91e19e55d0f7054ebe365fb8a80f8689e61f Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 14 Feb 2020 11:46:57 +0100 Subject: [PATCH 018/636] Revert "Merge pull request #9565 from dbrnz/v6.0.0-beta.7-branch" This reverts commit 35569a84277854109281946b73d95ba1b0bff5d1, reversing changes made to 29a434314b770a596baa981793b336575a406a6a. --- src/ol/interaction/MouseWheelZoom.js | 85 ++++++++++--- .../ol/interaction/mousewheelzoom.test.js | 112 ++++++++---------- 2 files changed, 122 insertions(+), 75 deletions(-) diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 37a4e4022f..1ed762bc17 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -4,7 +4,17 @@ import {always, focus} from '../events/condition.js'; import EventType from '../events/EventType.js'; import {DEVICE_PIXEL_RATIO, FIREFOX} from '../has.js'; -import Interaction from './Interaction.js'; +import Interaction, {zoomByDelta} from './Interaction.js'; +import {clamp} from '../math.js'; + + +/** + * @enum {string} + */ +export const Mode = { + TRACKPAD: 'trackpad', + WHEEL: 'wheel' +}; /** @@ -92,23 +102,35 @@ class MouseWheelZoom extends Interaction { this.startTime_ = undefined; /** - * Events separated by this delay will be considered separate + * @private + * @type {?} + */ + this.timeoutId_; + + /** + * @private + * @type {Mode|undefined} + */ + this.mode_ = undefined; + + /** + * Trackpad events separated by this delay will be considered separate * interactions. * @type {number} */ - this.eventGap_ = 400; + this.trackpadEventGap_ = 400; /** * @type {?} */ - this.timeoutId_; + this.trackpadTimeoutId_; /** * The number of delta values per zoom level * @private * @type {number} */ - this.deltaPerZoom_ = 300; + this.trackpadDeltaPerZoom_ = 300; } @@ -130,7 +152,7 @@ class MouseWheelZoom extends Interaction { * @private */ endInteraction_() { - this.timeoutId_ = undefined; + this.trackpadTimeoutId_ = undefined; const view = this.getMap().getView(); view.endInteraction(undefined, this.lastDelta_ ? (this.lastDelta_ > 0 ? 1 : -1) : 0, this.lastAnchor_); } @@ -184,18 +206,53 @@ class MouseWheelZoom extends Interaction { this.startTime_ = now; } - const view = map.getView(); - if (this.timeoutId_) { - clearTimeout(this.timeoutId_); - } else { - view.beginInteraction(); + if (!this.mode_ || now - this.startTime_ > this.trackpadEventGap_) { + this.mode_ = Math.abs(delta) < 4 ? + Mode.TRACKPAD : + Mode.WHEEL; } - this.timeoutId_ = setTimeout(this.endInteraction_.bind(this), this.eventGap_); - view.adjustZoom(-delta / this.deltaPerZoom_, this.lastAnchor_); - this.startTime_ = now; + + if (this.mode_ === Mode.TRACKPAD) { + const view = map.getView(); + if (this.trackpadTimeoutId_) { + clearTimeout(this.trackpadTimeoutId_); + } else { + view.beginInteraction(); + } + this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.trackpadEventGap_); + view.adjustZoom(-delta / this.trackpadDeltaPerZoom_, this.lastAnchor_); + this.startTime_ = now; + return false; + } + + this.totalDelta_ += delta; + + const timeLeft = Math.max(this.timeout_ - (now - this.startTime_), 0); + + clearTimeout(this.timeoutId_); + this.timeoutId_ = setTimeout(this.handleWheelZoom_.bind(this, map), timeLeft); + return false; } + /** + * @private + * @param {import("../PluggableMap.js").default} map Map. + */ + handleWheelZoom_(map) { + const view = map.getView(); + if (view.getAnimating()) { + view.cancelAnimations(); + } + const delta = clamp(this.totalDelta_, -this.maxDelta_, this.maxDelta_); + zoomByDelta(view, -delta, this.lastAnchor_, this.duration_); + this.mode_ = undefined; + this.totalDelta_ = 0; + this.lastAnchor_ = null; + this.startTime_ = undefined; + this.timeoutId_ = undefined; + } + /** * Enable or disable using the mouse's location as an anchor when zooming * @param {boolean} useAnchor true to zoom to the mouse's location, false diff --git a/test/spec/ol/interaction/mousewheelzoom.test.js b/test/spec/ol/interaction/mousewheelzoom.test.js index 8eea3ea3c3..ee1858bf5d 100644 --- a/test/spec/ol/interaction/mousewheelzoom.test.js +++ b/test/spec/ol/interaction/mousewheelzoom.test.js @@ -3,7 +3,7 @@ import MapBrowserEvent from '../../../../src/ol/MapBrowserEvent.js'; import View from '../../../../src/ol/View.js'; import Event from '../../../../src/ol/events/Event.js'; import {DEVICE_PIXEL_RATIO, FIREFOX} from '../../../../src/ol/has.js'; -import MouseWheelZoom from '../../../../src/ol/interaction/MouseWheelZoom.js'; +import MouseWheelZoom, {Mode} from '../../../../src/ol/interaction/MouseWheelZoom.js'; describe('ol.interaction.MouseWheelZoom', function() { @@ -32,13 +32,13 @@ describe('ol.interaction.MouseWheelZoom', function() { describe('timeout duration', function() { let clock; beforeEach(function() { - sinon.spy(interaction, 'endInteraction_'); + sinon.spy(interaction, 'handleWheelZoom_'); clock = sinon.useFakeTimers(); }); afterEach(function() { clock.restore(); - interaction.endInteraction_.restore(); + interaction.handleWheelZoom_.restore(); }); it('works with the default value', function(done) { @@ -49,12 +49,12 @@ describe('ol.interaction.MouseWheelZoom', function() { }); map.handleMapBrowserEvent(event); - clock.tick(100); - // default timeout is 400 ms, not called yet - expect(interaction.endInteraction_.called).to.be(false); + clock.tick(50); + // default timeout is 80 ms, not called yet + expect(interaction.handleWheelZoom_.called).to.be(false); - clock.tick(300); - expect(interaction.endInteraction_.called).to.be(true); + clock.tick(30); + expect(interaction.handleWheelZoom_.called).to.be(true); done(); }); @@ -63,15 +63,10 @@ describe('ol.interaction.MouseWheelZoom', function() { describe('handleEvent()', function() { - let view; - beforeEach(function() { - view = map.getView(); - }); - if (FIREFOX) { it('works on Firefox in DOM_DELTA_PIXEL mode (trackpad)', function(done) { map.once('postrender', function() { - expect(interaction.lastDelta_).to.be(1); + expect(interaction.mode_).to.be(Mode.TRACKPAD); done(); }); const event = new MapBrowserEvent('wheel', map, { @@ -89,7 +84,7 @@ describe('ol.interaction.MouseWheelZoom', function() { if (!FIREFOX) { it('works in DOM_DELTA_PIXEL mode (trackpad)', function(done) { map.once('postrender', function() { - expect(interaction.lastDelta_).to.be(1); + expect(interaction.mode_).to.be(Mode.TRACKPAD); done(); }); const event = new MapBrowserEvent('wheel', map, { @@ -104,61 +99,56 @@ describe('ol.interaction.MouseWheelZoom', function() { }); } - - it('works in DOM_DELTA_LINE mode (wheel)', function(done) { - map.once('postrender', function() { - expect(view.getResolution()).to.be(2); - expect(view.getCenter()).to.eql([0, 0]); - done(); + describe('spying on view.animateInternal()', function() { + let view; + beforeEach(function() { + view = map.getView(); + sinon.spy(view, 'animateInternal'); }); - const event = new MapBrowserEvent('wheel', map, { - type: 'wheel', - deltaMode: WheelEvent.DOM_DELTA_LINE, - deltaY: 7.5, - target: map.getViewport(), - preventDefault: Event.prototype.preventDefault - }); - event.coordinate = [0, 0]; - - map.handleMapBrowserEvent(event); - }); - - it('works on all browsers (wheel)', function(done) { - map.once('postrender', function() { - expect(view.getResolution()).to.be(2); - expect(view.getCenter()).to.eql([0, 0]); - done(); + afterEach(function() { + view.animateInternal.restore(); }); - const event = new MapBrowserEvent('wheel', map, { - type: 'wheel', - deltaY: 300, // trackpadDeltaPerZoom_ - target: map.getViewport(), - preventDefault: Event.prototype.preventDefault - }); - event.coordinate = [0, 0]; + it('works in DOM_DELTA_LINE mode (wheel)', function(done) { + map.once('postrender', function() { + const call = view.animateInternal.getCall(0); + expect(call.args[0].resolution).to.be(2); + expect(call.args[0].anchor).to.eql([0, 0]); + done(); + }); - map.handleMapBrowserEvent(event); - }); + const event = new MapBrowserEvent('wheel', map, { + type: 'wheel', + deltaMode: WheelEvent.DOM_DELTA_LINE, + deltaY: 3.714599609375, + target: map.getViewport(), + preventDefault: Event.prototype.preventDefault + }); + event.coordinate = [0, 0]; - it('works in DOM_DELTA_LINE mode (wheel)', function(done) { - map.once('postrender', function() { - expect(view.getResolution()).to.be(2); - expect(view.getCenter()).to.eql([0, 0]); - done(); + map.handleMapBrowserEvent(event); }); - const event = new MapBrowserEvent('wheel', map, { - type: 'wheel', - deltaMode: WheelEvent.DOM_DELTA_LINE, - deltaY: 7.5, // trackpadDeltaPerZoom_ / 40 - target: map.getViewport(), - preventDefault: Event.prototype.preventDefault - }); - event.coordinate = [0, 0]; + it('works on all browsers (wheel)', function(done) { + map.once('postrender', function() { + const call = view.animateInternal.getCall(0); + expect(call.args[0].resolution).to.be(2); + expect(call.args[0].anchor).to.eql([0, 0]); + done(); + }); + + const event = new MapBrowserEvent('wheel', map, { + type: 'wheel', + deltaY: 120, + target: map.getViewport(), + preventDefault: Event.prototype.preventDefault + }); + event.coordinate = [0, 0]; + + map.handleMapBrowserEvent(event); + }); - map.handleMapBrowserEvent(event); }); }); From 8fe71bbbff95eb96f91ab8b046ffa09a6dbdb9cb Mon Sep 17 00:00:00 2001 From: John Leonard Date: Fri, 14 Feb 2020 11:39:54 +0000 Subject: [PATCH 019/636] perf: only do expensive reload when texture changes --- src/ol/webgl/Helper.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ol/webgl/Helper.js b/src/ol/webgl/Helper.js index cf2ceeab3a..ce97b33a01 100644 --- a/src/ol/webgl/Helper.js +++ b/src/ol/webgl/Helper.js @@ -558,6 +558,7 @@ class WebGLHelper extends Disposable { if (value instanceof HTMLCanvasElement || value instanceof HTMLImageElement || value instanceof ImageData) { // create a texture & put data if (!uniform.texture) { + uniform.prevValue = undefined; uniform.texture = gl.createTexture(); } gl.activeTexture(gl[`TEXTURE${textureSlot}`]); @@ -567,7 +568,8 @@ class WebGLHelper extends Disposable { gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE); const imageReady = !(value instanceof HTMLImageElement) || /** @type {HTMLImageElement} */(value).complete; - if (imageReady) { + if (imageReady && uniform.prevValue !== value) { + uniform.prevValue = value; gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, value); } From f3ce8e23b47cc98da0472303c62b2f3568b5a568 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Fri, 14 Feb 2020 13:49:08 +0100 Subject: [PATCH 020/636] Fractional zoom changes in WHEEL mode --- src/ol/interaction/MouseWheelZoom.js | 15 +++++++++++---- test/spec/ol/interaction/mousewheelzoom.test.js | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 1ed762bc17..92c57b1ad7 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -130,7 +130,7 @@ class MouseWheelZoom extends Interaction { * @private * @type {number} */ - this.trackpadDeltaPerZoom_ = 300; + this.deltaPerZoom_ = 300; } @@ -220,7 +220,7 @@ class MouseWheelZoom extends Interaction { view.beginInteraction(); } this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.trackpadEventGap_); - view.adjustZoom(-delta / this.trackpadDeltaPerZoom_, this.lastAnchor_); + view.adjustZoom(-delta / this.deltaPerZoom_, this.lastAnchor_); this.startTime_ = now; return false; } @@ -244,8 +244,15 @@ class MouseWheelZoom extends Interaction { if (view.getAnimating()) { view.cancelAnimations(); } - const delta = clamp(this.totalDelta_, -this.maxDelta_, this.maxDelta_); - zoomByDelta(view, -delta, this.lastAnchor_, this.duration_); + let delta = -clamp(this.totalDelta_, -this.maxDelta_ * this.deltaPerZoom_, this.maxDelta_ * this.deltaPerZoom_) / this.deltaPerZoom_; + const currentZoom = view.getZoom(); + const newZoom = view.getConstrainedZoom(currentZoom + delta); + if (currentZoom === newZoom) { + // view has a zoom constraint, zoom by 1 + delta = delta ? delta > 0 ? 1 : -1 : 0; + } + zoomByDelta(view, delta, this.lastAnchor_, this.duration_); + this.mode_ = undefined; this.totalDelta_ = 0; this.lastAnchor_ = null; diff --git a/test/spec/ol/interaction/mousewheelzoom.test.js b/test/spec/ol/interaction/mousewheelzoom.test.js index ee1858bf5d..8f0343446d 100644 --- a/test/spec/ol/interaction/mousewheelzoom.test.js +++ b/test/spec/ol/interaction/mousewheelzoom.test.js @@ -121,7 +121,7 @@ describe('ol.interaction.MouseWheelZoom', function() { const event = new MapBrowserEvent('wheel', map, { type: 'wheel', deltaMode: WheelEvent.DOM_DELTA_LINE, - deltaY: 3.714599609375, + deltaY: 20, target: map.getViewport(), preventDefault: Event.prototype.preventDefault }); @@ -140,7 +140,7 @@ describe('ol.interaction.MouseWheelZoom', function() { const event = new MapBrowserEvent('wheel', map, { type: 'wheel', - deltaY: 120, + deltaY: 300, target: map.getViewport(), preventDefault: Event.prototype.preventDefault }); From bed2b6e22291dad37e4028e4b33145b25cb97eda Mon Sep 17 00:00:00 2001 From: jipexu Date: Sat, 15 Feb 2020 22:10:41 +0100 Subject: [PATCH 021/636] typo --- examples/snap.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/snap.html b/examples/snap.html index c47b279255..409f09491d 100644 --- a/examples/snap.html +++ b/examples/snap.html @@ -10,7 +10,7 @@ docs: > tags: "draw, edit, modify, vector, snap" --- - + From 5150378983867f77ed485af6df3282fe40a8933d Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 16 Feb 2020 09:08:27 +0100 Subject: [PATCH 022/636] Improve trackpad rebound behavior --- src/ol/interaction/MouseWheelZoom.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 92c57b1ad7..9ab143ee4d 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -217,9 +217,12 @@ class MouseWheelZoom extends Interaction { if (this.trackpadTimeoutId_) { clearTimeout(this.trackpadTimeoutId_); } else { + if (view.getAnimating()) { + view.cancelAnimations(); + } view.beginInteraction(); } - this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.trackpadEventGap_); + this.trackpadTimeoutId_ = setTimeout(this.endInteraction_.bind(this), this.timeoutId_); view.adjustZoom(-delta / this.deltaPerZoom_, this.lastAnchor_); this.startTime_ = now; return false; From d93073f8d077c2797d7fd08a7fcf8daf7c9077a4 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Sun, 16 Feb 2020 10:05:32 +0100 Subject: [PATCH 023/636] No trackpad mode for constrained resolutions --- src/ol/View.js | 7 +++++++ src/ol/interaction/MouseWheelZoom.js | 8 +++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ol/View.js b/src/ol/View.js index bbc373067a..27ab568daa 100644 --- a/src/ol/View.js +++ b/src/ol/View.js @@ -793,6 +793,13 @@ class View extends BaseObject { return this.constraints_; } + /** + * @return {boolean} Resolution constraint is set + */ + getConstrainResolution() { + return this.options_.constrainResolution; + } + /** * @param {Array=} opt_hints Destination array. * @return {Array} Hint. diff --git a/src/ol/interaction/MouseWheelZoom.js b/src/ol/interaction/MouseWheelZoom.js index 9ab143ee4d..f782af739d 100644 --- a/src/ol/interaction/MouseWheelZoom.js +++ b/src/ol/interaction/MouseWheelZoom.js @@ -212,8 +212,8 @@ class MouseWheelZoom extends Interaction { Mode.WHEEL; } - if (this.mode_ === Mode.TRACKPAD) { - const view = map.getView(); + const view = map.getView(); + if (this.mode_ === Mode.TRACKPAD && !view.getConstrainResolution()) { if (this.trackpadTimeoutId_) { clearTimeout(this.trackpadTimeoutId_); } else { @@ -248,9 +248,7 @@ class MouseWheelZoom extends Interaction { view.cancelAnimations(); } let delta = -clamp(this.totalDelta_, -this.maxDelta_ * this.deltaPerZoom_, this.maxDelta_ * this.deltaPerZoom_) / this.deltaPerZoom_; - const currentZoom = view.getZoom(); - const newZoom = view.getConstrainedZoom(currentZoom + delta); - if (currentZoom === newZoom) { + if (view.getConstrainResolution()) { // view has a zoom constraint, zoom by 1 delta = delta ? delta > 0 ? 1 : -1 : 0; } From 2e1e0dba546fae74ffb21e6e6888c537804911b5 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Sun, 16 Feb 2020 11:11:37 +0000 Subject: [PATCH 024/636] document that views must use same projection --- src/ol/control/OverviewMap.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ol/control/OverviewMap.js b/src/ol/control/OverviewMap.js index 5a83915198..4ee1242726 100644 --- a/src/ol/control/OverviewMap.js +++ b/src/ol/control/OverviewMap.js @@ -60,7 +60,8 @@ class ControlledMap extends PluggableMap { * to be rendered outside of the map's viewport. * @property {string} [tipLabel='Overview map'] Text label to use for the button tip. * @property {import("../View.js").default} [view] Custom view for the overview map. If not provided, - * a default view with an EPSG:3857 projection will be used. + * a default view with an EPSG:3857 projection will be used. The main map and the overviewmap must use + * the same projection, so a view must be provided when the default projection is not used. */ From 533b38a3c159e107650bce8e356397e4d597ad87 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2020 08:03:25 +0000 Subject: [PATCH 025/636] Bump rollup from 1.31.0 to 1.31.1 Bumps [rollup](https://github.com/rollup/rollup) from 1.31.0 to 1.31.1. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v1.31.0...v1.31.1) Signed-off-by: dependabot-preview[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f9a6ff567..082ff7524c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10889,9 +10889,9 @@ } }, "rollup": { - "version": "1.31.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.31.0.tgz", - "integrity": "sha512-9C6ovSyNeEwvuRuUUmsTpJcXac1AwSL1a3x+O5lpmQKZqi5mmrjauLeqIjvREC+yNRR8fPdzByojDng+af3nVw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-1.31.1.tgz", + "integrity": "sha512-2JREN1YdrS/kpPzEd33ZjtuNbOuBC3ePfuZBdKEybvqcEcszW1ckyVqzcEiEe0nE8sqHK+pbJg+PsAgRJ8+1dg==", "dev": true, "requires": { "@types/estree": "*", From 88fbdff322e81feeae61b7b1d6f4072ced137ac0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2020 08:03:59 +0000 Subject: [PATCH 026/636] Bump terser-webpack-plugin from 2.3.4 to 2.3.5 Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 2.3.4 to 2.3.5. - [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases) - [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v2.3.4...v2.3.5) Signed-off-by: dependabot-preview[bot] --- package-lock.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f9a6ff567..824ece3cbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12238,9 +12238,9 @@ } }, "terser-webpack-plugin": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.4.tgz", - "integrity": "sha512-Nv96Nws2R2nrFOpbzF6IxRDpIkkIfmhvOws+IqMvYdFLO7o6wAILWFKONFgaYy8+T4LVz77DQW0f7wOeDEAjrg==", + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz", + "integrity": "sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w==", "dev": true, "requires": { "cacache": "^13.0.1", @@ -12299,9 +12299,9 @@ } }, "chownr": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.3.tgz", - "integrity": "sha512-i70fVHhmV3DtTl6nqvZOnIjbY0Pe4kAUjwHj8z0zAdgBtYrJyYwLKCCuRBQ5ppkyL0AkN7HKRnETdmdp1zqNXw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "dev": true }, "fast-deep-equal": { @@ -12363,9 +12363,9 @@ } }, "make-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.0.tgz", - "integrity": "sha512-grNJDhb8b1Jm1qeqW5R/O63wUo4UXo2v2HMic6YT9i/HBlF93S8jkMgH7yugvY9ABDShH4VZMn8I+U8+fCNegw==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.0.2.tgz", + "integrity": "sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w==", "dev": true, "requires": { "semver": "^6.0.0" From 5b7d2d15e9818886b1ac759913ea42b1eee2ecfe Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2020 08:05:05 +0000 Subject: [PATCH 027/636] Bump webpack-cli from 3.3.10 to 3.3.11 Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 3.3.10 to 3.3.11. - [Release notes](https://github.com/webpack/webpack-cli/releases) - [Changelog](https://github.com/webpack/webpack-cli/blob/next/CHANGELOG_v3.md) - [Commits](https://github.com/webpack/webpack-cli/compare/v3.3.10...v3.3.11) Signed-off-by: dependabot-preview[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4f9a6ff567..19e498f803 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13113,9 +13113,9 @@ } }, "webpack-cli": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.10.tgz", - "integrity": "sha512-u1dgND9+MXaEt74sJR4PR7qkPxXUSQ0RXYq8x1L6Jg1MYVEmGPrH6Ah6C4arD4r0J1P5HKjRqpab36k0eIzPqg==", + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.11.tgz", + "integrity": "sha512-dXlfuml7xvAFwYUPsrtQAA9e4DOe58gnzSxhgrO/ZM/gyXTBowrsYeubyN4mqGhYdpXMFNyQ6emjJS9M7OBd4g==", "dev": true, "requires": { "chalk": "2.4.2", From 9a03ca84176f89ef40c19131280cae3ad7b25617 Mon Sep 17 00:00:00 2001 From: Frederic Junod Date: Mon, 17 Feb 2020 10:48:29 +0100 Subject: [PATCH 028/636] Dispatch enterfullscreen and leavefullscreen from the FullScreen control --- src/ol/control/FullScreen.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/ol/control/FullScreen.js b/src/ol/control/FullScreen.js index e8419a87a5..d3741fb8e6 100644 --- a/src/ol/control/FullScreen.js +++ b/src/ol/control/FullScreen.js @@ -9,6 +9,29 @@ import EventType from '../events/EventType.js'; const events = ['fullscreenchange', 'webkitfullscreenchange', 'MSFullscreenChange']; + +/** + * @enum {string} + */ +const FullScreenEventType = { + + /** + * Triggered after the map entered fullscreen. + * @event FullScreenEventType#enterfullscreen + * @api + */ + ENTERFULLSCREEN: 'enterfullscreen', + + /** + * Triggered after the map leave fullscreen. + * @event FullScreenEventType#leavefullscreen + * @api + */ + LEAVEFULLSCREEN: 'leavefullscreen' + +}; + + /** * @typedef {Object} Options * @property {string} [className='ol-full-screen'] CSS class name. @@ -38,6 +61,8 @@ const events = ['fullscreenchange', 'webkitfullscreenchange', 'MSFullscreenChang * The [Fullscreen API](http://www.w3.org/TR/fullscreen/) is used to * toggle the map in full screen mode. * + * @fires FullScreenEventType#enterfullscreen + * @fires FullScreenEventType#leavefullscreen * @api */ class FullScreen extends Control { @@ -162,9 +187,11 @@ class FullScreen extends Control { if (isFullScreen()) { this.setClassName_(this.button_, true); replaceNode(this.labelActiveNode_, this.labelNode_); + this.dispatchEvent(FullScreenEventType.ENTERFULLSCREEN); } else { this.setClassName_(this.button_, false); replaceNode(this.labelNode_, this.labelActiveNode_); + this.dispatchEvent(FullScreenEventType.LEAVEFULLSCREEN); } if (map) { map.updateSize(); From 058d1c2e51c2e45cd79184b2284999568d57f32d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Feb 2020 09:54:23 +0000 Subject: [PATCH 029/636] Bump webpack from 4.41.5 to 4.41.6 Bumps [webpack](https://github.com/webpack/webpack) from 4.41.5 to 4.41.6. - [Release notes](https://github.com/webpack/webpack/releases) - [Commits](https://github.com/webpack/webpack/compare/v4.41.5...v4.41.6) Signed-off-by: dependabot-preview[bot] --- package-lock.json | 38 ++++++++++++++++++++++---------------- package.json | 2 +- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2b7f70bb00..9e83d7d08b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5328,9 +5328,9 @@ "dev": true }, "events": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.0.0.tgz", - "integrity": "sha512-Dc381HFWJzEOhQ+d8pkNon++bk9h6cdAoAj4iE6Q4y6xgTzySWXlKn05/TVNpjnfRqi/X0EpJEJohPjNI3zpVA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.1.0.tgz", + "integrity": "sha512-Rv+u8MLHNOdMjTAFeT3nCjHn2aGlx435FP/sDHNaRhDEMwyI/aB22Kj2qIN8R0cw3z28psEQLYwxVKLsKrMgWg==", "dev": true }, "eventsource": { @@ -9915,9 +9915,9 @@ "dev": true }, "pako": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.10.tgz", - "integrity": "sha512-0DTvPVU3ed8+HNXOu5Bs+o//Mbdj9VNQMUOe9oKCwh8l0GNwpTDMKCWbRjgtD291AWnkAgkqA/LOnQS8AmS1tw==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "dev": true }, "parallel-transform": { @@ -12227,9 +12227,9 @@ "dev": true }, "terser": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.2.1.tgz", - "integrity": "sha512-cGbc5utAcX4a9+2GGVX4DsenG6v0x3glnDi5hx8816X1McEAwPlPgRtXPJzSBsbpILxZ8MQMT0KvArLuE0HP5A==", + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.3.tgz", + "integrity": "sha512-Lw+ieAXmY69d09IIc/yqeBqXpEQIpDGZqT34ui1QWXIUpR2RjbqEkT8X7Lgex19hslSqcWM5iMN2kM11eMsESQ==", "dev": true, "requires": { "commander": "^2.20.0", @@ -13033,9 +13033,9 @@ "dev": true }, "webpack": { - "version": "4.41.5", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.5.tgz", - "integrity": "sha512-wp0Co4vpyumnp3KlkmpM5LWuzvZYayDwM2n17EHFr4qxBBbRokC7DJawPJC7TfSFZ9HZ6GsdH40EBj4UV0nmpw==", + "version": "4.41.6", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.41.6.tgz", + "integrity": "sha512-yxXfV0Zv9WMGRD+QexkZzmGIh54bsvEs+9aRWxnN8erLWEOehAKUTeNBoUbA6HPEZPlRo7KDi2ZcNveoZgK9MA==", "dev": true, "requires": { "@webassemblyjs/ast": "1.8.5", @@ -13070,12 +13070,12 @@ "dev": true }, "ajv": { - "version": "6.10.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.10.2.tgz", - "integrity": "sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw==", + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.11.0.tgz", + "integrity": "sha512-nCprB/0syFYy9fVYU1ox1l2KN8S9I+tziH8D4zdZuLT3N6RMlGSGt5FSTpAiHB/Whv8Qs1cWHma1aMKZyaHRKA==", "dev": true, "requires": { - "fast-deep-equal": "^2.0.1", + "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" @@ -13087,6 +13087,12 @@ "integrity": "sha512-RO1ibKvd27e6FEShVFfPALuHI3WjSVNeK5FIsmme/LYRNxjKuNj+Dt7bucLa6NdSv3JcVTyMlm9kGR84z1XpaQ==", "dev": true }, + "fast-deep-equal": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.1.tgz", + "integrity": "sha512-8UEa58QDLauDNfpbrX55Q9jrGHThw2ZMdOky5Gl1CDtVeJDPVrG4Jxx1N8jw2gkWaff5UUuX1KJd+9zGe2B+ZA==", + "dev": true + }, "neo-async": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", diff --git a/package.json b/package.json index 9086914964..e28200078c 100644 --- a/package.json +++ b/package.json @@ -97,7 +97,7 @@ "typescript": "3.5.3", "url-polyfill": "^1.1.5", "walk": "^2.3.9", - "webpack": "4.41.5", + "webpack": "4.41.6", "webpack-cli": "^3.3.2", "webpack-dev-middleware": "^3.6.2", "webpack-dev-server": "^3.3.1", From 09689e547cf08bd0744d8ff983bdada10fdb3b24 Mon Sep 17 00:00:00 2001 From: Frederic Junod Date: Mon, 17 Feb 2020 13:26:44 +0100 Subject: [PATCH 030/636] Increase timeout in listenImage test Fixes failing test with Chrome 80 on Windows 10 --- test/spec/ol/image.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/spec/ol/image.test.js b/test/spec/ol/image.test.js index bf6e19ef06..b6f75abf40 100644 --- a/test/spec/ol/image.test.js +++ b/test/spec/ol/image.test.js @@ -40,7 +40,7 @@ describe('HTML Image loading', function() { expect(handleLoad.called).to.be(false); expect(handleError.called).to.be(true); done(); - }, 200); + }, 500); }); it('handles cancelation', function(done) { From 3dc4e546bf96a3a7fc230153c2ab05b2fbcd0676 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:47:55 +0000 Subject: [PATCH 031/636] add minZoom and maxZoom options --- src/ol/layer/BaseImage.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/BaseImage.js b/src/ol/layer/BaseImage.js index 59b971c657..676d7f3154 100644 --- a/src/ol/layer/BaseImage.js +++ b/src/ol/layer/BaseImage.js @@ -19,6 +19,10 @@ import Layer from './Layer.js'; * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {import("../PluggableMap.js").default} [map] Sets the layer as overlay on a map. The map will not manage * this layer in its layers collection, and the layer will be rendered on top. This is useful for * temporary layers. The standard way to add a layer to a map and have it managed by the map is to From fbaa4da153b9414df1ea85d66ad458234fdbfaa0 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:49:47 +0000 Subject: [PATCH 032/636] add minZoom and maxZoom options --- src/ol/layer/BaseTile.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/BaseTile.js b/src/ol/layer/BaseTile.js index 3e2f70f8d4..3ac20024fd 100644 --- a/src/ol/layer/BaseTile.js +++ b/src/ol/layer/BaseTile.js @@ -21,6 +21,10 @@ import {assign} from '../obj.js'; * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {number} [preload=0] Preload. Load low-resolution tiles up to `preload` levels. `0` * means no preloading. * @property {import("../source/Tile.js").default} [source] Source for this layer. From 265eb5421dea71a4227bca04c80dac40100766fa Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:51:08 +0000 Subject: [PATCH 033/636] add minZoom and maxZoom options --- src/ol/layer/BaseVector.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/BaseVector.js b/src/ol/layer/BaseVector.js index 7201c448b1..5f160faf57 100644 --- a/src/ol/layer/BaseVector.js +++ b/src/ol/layer/BaseVector.js @@ -21,6 +21,10 @@ import {createDefaultStyle, toFunction as toStyleFunction} from '../style/Style. * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting * features before rendering. By default features are drawn in the order that they are created. Use * `null` to avoid the sort, but get an undefined draw order. From d102f561f964c7e48cb3e87dd447383fd1dc1b0e Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:52:12 +0000 Subject: [PATCH 034/636] add minZoom and maxZoom options --- src/ol/layer/Graticule.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/Graticule.js b/src/ol/layer/Graticule.js index 0c5e34f6e7..b0e681926c 100644 --- a/src/ol/layer/Graticule.js +++ b/src/ol/layer/Graticule.js @@ -64,6 +64,10 @@ const INTERVALS = [ * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {number} [maxLines=100] The maximum number of meridians and * parallels from the center of the map. The default value of 100 means that at * most 200 meridians and 200 parallels will be displayed. The default value is From 501546bc4407dbb0e9e14c9c43d9b210e264eaaf Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:52:59 +0000 Subject: [PATCH 035/636] add minZoom and maxZoom options --- src/ol/layer/Group.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/Group.js b/src/ol/layer/Group.js index f03fe7b7dd..12dff00af2 100644 --- a/src/ol/layer/Group.js +++ b/src/ol/layer/Group.js @@ -33,6 +33,10 @@ import SourceState from '../source/State.js'; * visible. * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {Array|import("../Collection.js").default} [layers] Child layers. */ From edb94af31473f511133c3c2a2d713f1e78a2e82b Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:53:57 +0000 Subject: [PATCH 036/636] add minZoom and maxZoom options --- src/ol/layer/Heatmap.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/Heatmap.js b/src/ol/layer/Heatmap.js index eb7abdf0ad..0d015bf985 100644 --- a/src/ol/layer/Heatmap.js +++ b/src/ol/layer/Heatmap.js @@ -24,6 +24,10 @@ import WebGLPointsLayerRenderer from '../renderer/webgl/PointsLayer.js'; * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {Array} [gradient=['#00f', '#0ff', '#0f0', '#ff0', '#f00']] The color gradient * of the heatmap, specified as an array of CSS color strings. * @property {number} [radius=8] Radius size in pixels. From d0e439ee80795f7e0d4e989aa89d7cb64ac0042a Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:55:15 +0000 Subject: [PATCH 037/636] add minZoom and maxZoom options --- src/ol/layer/Layer.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/Layer.js b/src/ol/layer/Layer.js index 4d3d846845..2f6b2fb0b7 100644 --- a/src/ol/layer/Layer.js +++ b/src/ol/layer/Layer.js @@ -31,6 +31,10 @@ import {assert} from '../asserts.js'; * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {import("../source/Source.js").default} [source] Source for this layer. If not provided to the constructor, * the source can be set by calling {@link module:ol/layer/Layer#setSource layer.setSource(source)} after * construction. From c5885cc649efcce6b3ebcf378ac16b2f3f48cc8b Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:57:00 +0000 Subject: [PATCH 038/636] add minZoom and maxZoom options --- src/ol/layer/VectorImage.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/VectorImage.js b/src/ol/layer/VectorImage.js index b121cc1b83..14b7c7a067 100644 --- a/src/ol/layer/VectorImage.js +++ b/src/ol/layer/VectorImage.js @@ -20,6 +20,10 @@ import CanvasVectorImageLayerRenderer from '../renderer/canvas/VectorImageLayer. * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting * features before rendering. By default features are drawn in the order that they are created. Use * `null` to avoid the sort, but get an undefined draw order. From 4cf093ab757b1be6c1336555b72c035b543f2059 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:58:46 +0000 Subject: [PATCH 039/636] add minZoom and maxZoom options --- src/ol/layer/VectorTile.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/VectorTile.js b/src/ol/layer/VectorTile.js index a53e377215..8ea764e131 100644 --- a/src/ol/layer/VectorTile.js +++ b/src/ol/layer/VectorTile.js @@ -24,6 +24,10 @@ import {assign} from '../obj.js'; * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {import("../render.js").OrderFunction} [renderOrder] Render order. Function to be used when sorting * features before rendering. By default features are drawn in the order that they are created. Use * `null` to avoid the sort, but get an undefined draw order. From fa4fcf3886fd28fcff423370831ff7176f734924 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Mon, 17 Feb 2020 16:59:40 +0000 Subject: [PATCH 040/636] add minZoom and maxZoom options --- src/ol/layer/WebGLPoints.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ol/layer/WebGLPoints.js b/src/ol/layer/WebGLPoints.js index cddb218a54..cdcea572b4 100644 --- a/src/ol/layer/WebGLPoints.js +++ b/src/ol/layer/WebGLPoints.js @@ -23,6 +23,10 @@ import Layer from './Layer.js'; * visible. * @property {number} [maxResolution] The maximum resolution (exclusive) below which this layer will * be visible. + * @property {number} [minZoom] The minimum view zoom level (exclusive) above which this layer will be + * visible. + * @property {number} [maxZoom] The maximum view zoom level (inclusive) at which this layer will + * be visible. * @property {import("../source/Vector.js").default} [source] Source. * @property {boolean} [disableHitDetection=false] Setting this to true will provide a slight performance boost, but will * prevent all hit detection on the layer. From 1416e30127d8b545aea13136e03f289d3306cef1 Mon Sep 17 00:00:00 2001 From: mike-000 <49240900+mike-000@users.noreply.github.com> Date: Sun, 16 Feb 2020 17:14:31 +0000 Subject: [PATCH 041/636] Add maxResolution option to createXYZ() include maxResolution in options test --- examples/xyz-esri-4326-512.html | 2 +- examples/xyz-esri-4326-512.js | 18 +++++------------- src/ol/source/VectorTile.js | 8 +++++--- src/ol/source/XYZ.js | 9 ++++++--- src/ol/tilegrid.js | 15 +++++++++------ test/spec/ol/tilegrid/tilegrid.test.js | 3 ++- 6 files changed, 28 insertions(+), 27 deletions(-) diff --git a/examples/xyz-esri-4326-512.html b/examples/xyz-esri-4326-512.html index 94225e4c71..7b467116e3 100644 --- a/examples/xyz-esri-4326-512.html +++ b/examples/xyz-esri-4326-512.html @@ -3,7 +3,7 @@ layout: example.html title: ArcGIS REST with 512x512 Tiles shortdesc: Example of a XYZ source in EPSG:4326 using Esri 512x512 tiles. docs: > - ArcGIS REST tile services with custom tile sizes (here: 512x512 pixels) and projection (here: EPSG:4326) are supported by `ol/source/XYZ`. A custom tile url function is used to handle zoom level offsets. + ArcGIS REST tile services with custom tile sizes (here: 512x512 pixels) and projection (here: EPSG:4326) are supported by `ol/source/XYZ`. tags: "xyz, esri, tilesize, custom projection" --- diff --git a/examples/xyz-esri-4326-512.js b/examples/xyz-esri-4326-512.js index 2edc9d1ba0..407e2e3e6e 100644 --- a/examples/xyz-esri-4326-512.js +++ b/examples/xyz-esri-4326-512.js @@ -3,26 +3,18 @@ import View from '../src/ol/View.js'; import TileLayer from '../src/ol/layer/Tile.js'; import XYZ from '../src/ol/source/XYZ.js'; -// The tile size supported by the ArcGIS tile service. -const tileSize = 512; - -const urlTemplate = 'https://services.arcgisonline.com/arcgis/rest/services/' + - 'ESRI_Imagery_World_2D/MapServer/tile/{z}/{y}/{x}'; - const map = new Map({ target: 'map', layers: [ new TileLayer({ source: new XYZ({ attributions: 'Copyright:© 2013 ESRI, i-cubed, GeoEye', - maxZoom: 16, + url: 'https://services.arcgisonline.com/arcgis/rest/services/' + + 'ESRI_Imagery_World_2D/MapServer/tile/{z}/{y}/{x}', + maxZoom: 15, projection: 'EPSG:4326', - tileSize: tileSize, - tileUrlFunction: function(tileCoord) { - return urlTemplate.replace('{z}', (tileCoord[0] - 1).toString()) - .replace('{x}', tileCoord[1].toString()) - .replace('{y}', tileCoord[2].toString()); - }, + tileSize: 512, // the tile size supported by the ArcGIS tile service + maxResolution: 180 / 512, // Esri's tile grid fits 180 degrees on one 512 px tile wrapX: true }) }) diff --git a/src/ol/source/VectorTile.js b/src/ol/source/VectorTile.js index c4ac79a917..737883fadb 100644 --- a/src/ol/source/VectorTile.js +++ b/src/ol/source/VectorTile.js @@ -30,9 +30,10 @@ import TileCache from '../TileCache.js'; * @property {import("./State.js").default} [state] Source state. * @property {typeof import("../VectorTile.js").default} [tileClass] Class used to instantiate image tiles. * Default is {@link module:ol/VectorTile}. - * @property {number} [maxZoom=22] Optional max zoom level. - * @property {number} [minZoom] Optional min zoom level. - * @property {number|import("../size.js").Size} [tileSize=512] Optional tile size. + * @property {number} [maxZoom=22] Optional max zoom level. Not used if `tileGrid` is provided. + * @property {number} [minZoom] Optional min zoom level. Not used if `tileGrid` is provided. + * @property {number|import("../size.js").Size} [tileSize=512] Optional tile size. Not used if `tileGrid` is provided. + * @property {number} [maxResolution] Optional tile grid resolution at level zero. Not used if `tileGrid` is provided. * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid. * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] * Optional function to load a tile given a URL. Could look like this for pbf tiles: @@ -105,6 +106,7 @@ class VectorTile extends UrlTile { const tileGrid = options.tileGrid || createXYZ({ extent: extent, + maxResolution: options.maxResolution, maxZoom: options.maxZoom !== undefined ? options.maxZoom : 22, minZoom: options.minZoom, tileSize: options.tileSize || 512 diff --git a/src/ol/source/XYZ.js b/src/ol/source/XYZ.js index 6c550057a5..638bf5eb24 100644 --- a/src/ol/source/XYZ.js +++ b/src/ol/source/XYZ.js @@ -17,8 +17,9 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * @property {import("../proj.js").ProjectionLike} [projection='EPSG:3857'] Projection. * @property {number} [reprojectionErrorThreshold=0.5] Maximum allowed reprojection error (in pixels). * Higher values can increase reprojection performance, but decrease precision. - * @property {number} [maxZoom=18] Optional max zoom level. - * @property {number} [minZoom=0] Optional min zoom level. + * @property {number} [maxZoom=42] Optional max zoom level. Not used if `tileGrid` is provided. + * @property {number} [minZoom=0] Optional min zoom level. Not used if `tileGrid` is provided. + * @property {number} [maxResolution] Optional tile grid resolution at level zero. Not used if `tileGrid` is provided. * @property {import("../tilegrid/TileGrid.js").default} [tileGrid] Tile grid. * @property {import("../Tile.js").LoadFunction} [tileLoadFunction] Optional function to load a tile given a URL. The default is * ```js @@ -31,9 +32,10 @@ import {createXYZ, extentFromProjection} from '../tilegrid.js'; * by 512px images (for retina/hidpi devices) then `tilePixelRatio` * should be set to `2`. * @property {number|import("../size.js").Size} [tileSize=[256, 256]] The tile size used by the tile service. + * Not used if `tileGrid` is provided. * @property {import("../Tile.js").UrlFunction} [tileUrlFunction] Optional function to get * tile URL given a tile coordinate and the projection. - * Required if url or urls are not provided. + * Required if `url` or `urls` are not provided. * @property {string} [url] URL template. Must include `{x}`, `{y}` or `{-y}`, * and `{z}` placeholders. A `{?-?}` template pattern, for example `subdomain{a-f}.domain.com`, * may be used instead of defining each one separately in the `urls` option. @@ -78,6 +80,7 @@ class XYZ extends TileImage { const tileGrid = options.tileGrid !== undefined ? options.tileGrid : createXYZ({ extent: extentFromProjection(projection), + maxResolution: options.maxResolution, maxZoom: options.maxZoom, minZoom: options.minZoom, tileSize: options.tileSize diff --git a/src/ol/tilegrid.js b/src/ol/tilegrid.js index 94fa003229..c152636216 100644 --- a/src/ol/tilegrid.js +++ b/src/ol/tilegrid.js @@ -72,8 +72,9 @@ export function createForExtent(extent, opt_maxZoom, opt_tileSize, opt_corner) { /** * @typedef {Object} XYZOptions * @property {import("./extent.js").Extent} [extent] Extent for the tile grid. The origin for an XYZ tile grid is the - * top-left corner of the extent. The zero level of the grid is defined by the resolution at which one tile fits in the - * provided extent. If not provided, the extent of the EPSG:3857 projection is used. + * top-left corner of the extent. If `maxResolution` is not provided the zero level of the grid is defined by the resolution + * at which one tile fits in the provided extent. If not provided, the extent of the EPSG:3857 projection is used. + * @property {number} [maxResolution] Resolution at level zero. * @property {number} [maxZoom] Maximum zoom. The default is `42`. This determines the number of levels * in the grid set. For example, a `maxZoom` of 21 means there are 22 levels in the grid set. * @property {number} [minZoom=0] Minimum zoom. @@ -99,7 +100,8 @@ export function createXYZ(opt_options) { resolutions: resolutionsFromExtent( extent, xyzOptions.maxZoom, - xyzOptions.tileSize + xyzOptions.tileSize, + xyzOptions.maxResolution ) }; return new TileGrid(gridOptions); @@ -113,9 +115,10 @@ export function createXYZ(opt_options) { * DEFAULT_MAX_ZOOM). * @param {number|import("./size.js").Size=} opt_tileSize Tile size (default uses * DEFAULT_TILE_SIZE). + * @param {number=} opt_maxResolution Resolution at level zero. * @return {!Array} Resolutions array. */ -function resolutionsFromExtent(extent, opt_maxZoom, opt_tileSize) { +function resolutionsFromExtent(extent, opt_maxZoom, opt_tileSize, opt_maxResolution) { const maxZoom = opt_maxZoom !== undefined ? opt_maxZoom : DEFAULT_MAX_ZOOM; @@ -124,8 +127,8 @@ function resolutionsFromExtent(extent, opt_maxZoom, opt_tileSize) { const tileSize = toSize(opt_tileSize !== undefined ? opt_tileSize : DEFAULT_TILE_SIZE); - const maxResolution = Math.max( - width / tileSize[0], height / tileSize[1]); + const maxResolution = opt_maxResolution > 0 ? opt_maxResolution : + Math.max(width / tileSize[0], height / tileSize[1]); const length = maxZoom + 1; const resolutions = new Array(length); diff --git a/test/spec/ol/tilegrid/tilegrid.test.js b/test/spec/ol/tilegrid/tilegrid.test.js index 33cc3e99dc..ff503e5315 100644 --- a/test/spec/ol/tilegrid/tilegrid.test.js +++ b/test/spec/ol/tilegrid/tilegrid.test.js @@ -404,16 +404,17 @@ describe('ol.tilegrid.TileGrid', function() { it('respects configuration options', function() { const tileGrid = createXYZ({ extent: [10, 20, 30, 40], + maxResolution: 10 / 128, minZoom: 1, maxZoom: 2, tileSize: 128 }); expect(tileGrid.getExtent()).to.eql([10, 20, 30, 40]); + expect(tileGrid.getResolutions()).to.eql([10 / 128, 5 / 128, 2.5 / 128]); expect(tileGrid.getMinZoom()).to.equal(1); expect(tileGrid.getMaxZoom()).to.equal(2); expect(tileGrid.getTileSize()).to.equal(128); }); - }); describe('getForProjection', function() { From 07b02fe94769ad99828d86f74d616955a0c2e9ca Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Mon, 17 Feb 2020 21:10:29 +0100 Subject: [PATCH 042/636] Do not exceed color range --- src/ol/render/canvas/hitdetect.js | 5 +-- test/spec/ol/render/canvas/hitdetect.test.js | 33 ++++++++++++++++++++ 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 test/spec/ol/render/canvas/hitdetect.test.js diff --git a/src/ol/render/canvas/hitdetect.js b/src/ol/render/canvas/hitdetect.js index 256326a952..ef21f3d9e9 100644 --- a/src/ol/render/canvas/hitdetect.js +++ b/src/ol/render/canvas/hitdetect.js @@ -33,7 +33,7 @@ export function createHitDetectionImageData(size, transforms, features, styleFun const renderer = new CanvasImmediateRenderer(context, 0.5, extent, null, rotation); const featureCount = features.length; // Stretch hit detection index to use the whole available color range - const indexFactor = Math.ceil((256 * 256 * 256 - 1) / featureCount); + const indexFactor = Math.floor((256 * 256 * 256 - 1) / featureCount); const featuresByZIndex = {}; for (let i = 1; i <= featureCount; ++i) { const feature = features[i - 1]; @@ -121,6 +121,7 @@ export function createHitDetectionImageData(size, transforms, features, styleFun } } } + document.body.appendChild(context.canvas); return context.getImageData(0, 0, canvas.width, canvas.height); } @@ -141,7 +142,7 @@ export function hitDetect(pixel, features, imageData) { const g = imageData.data[index + 1]; const b = imageData.data[index + 2]; const i = b + (256 * (g + (256 * r))); - const indexFactor = Math.ceil((256 * 256 * 256 - 1) / features.length); + const indexFactor = Math.floor((256 * 256 * 256 - 1) / features.length); if (i && i % indexFactor === 0) { resultFeatures.push(features[i / indexFactor - 1]); } diff --git a/test/spec/ol/render/canvas/hitdetect.test.js b/test/spec/ol/render/canvas/hitdetect.test.js new file mode 100644 index 0000000000..93a3d92ad4 --- /dev/null +++ b/test/spec/ol/render/canvas/hitdetect.test.js @@ -0,0 +1,33 @@ +import {createHitDetectionImageData} from '../../../../../src/ol/render/canvas/hitdetect.js'; +import {create} from '../../../../../src/ol/transform.js'; +import Feature from '../../../../../src/ol/Feature.js'; +import Point from '../../../../../src/ol/geom/Point.js'; +import {Style} from '../../../../../src/ol/style.js'; +import Circle from '../../../../../src/ol/style/Circle.js'; + +describe('hitdetect', function() { + + let features, styleFunction; + + beforeEach(function() { + features = [ + new Feature(new Point([0, 75])), + new Feature(new Point([0, 50])), + new Feature(new Point([0, 25])), + new Feature(new Point([0, 0])) + ]; + styleFunction = function() { + return new Style({ + image: new Circle({ + radius: 5 + }) + }); + }; + }); + + it ('does not exceed the color range', function() { + const imageData = createHitDetectionImageData([2, 2], [create()], features, styleFunction, [0, 0, 0, 0], 1, 0); + expect(Array.prototype.slice.call(imageData.data, 0, 3)).to.eql([255, 255, 252]); + }); + +}); From ab9d7cdc7097615040f38c0be4a5256d3b6efdeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Tue, 18 Feb 2020 13:23:49 +0100 Subject: [PATCH 043/636] Draw image with configured opacity --- src/ol/render/canvas.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/ol/render/canvas.js b/src/ol/render/canvas.js index 3fd49d7443..adb892dd34 100644 --- a/src/ol/render/canvas.js +++ b/src/ol/render/canvas.js @@ -404,6 +404,9 @@ export function drawImageOrLabel(context, transform, opacity, labelOrImage, originX, originY, w, h, x, y, scale) { context.save(); + if (opacity !== 1) { + context.globalAlpha *= opacity; + } if (transform) { context.setTransform.apply(context, transform); } From 6f18350eda5d93e4f4d6ebaf94a5ecab904a5955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maximilian=20Kr=C3=B6g?= Date: Tue, 18 Feb 2020 15:19:51 +0100 Subject: [PATCH 044/636] Add regression test for image opacity --- test/spec/ol/render/canvas/index.test.js | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/spec/ol/render/canvas/index.test.js b/test/spec/ol/render/canvas/index.test.js index 58a4a5551d..01589ec3a2 100644 --- a/test/spec/ol/render/canvas/index.test.js +++ b/test/spec/ol/render/canvas/index.test.js @@ -100,4 +100,34 @@ describe('ol.render.canvas', function() { }); }); + describe('drawImageOrLabel', function() { + it('draws the image with correct parameters', function() { + const layerContext = { + save: sinon.spy(), + setTransform: sinon.spy(), + drawImage: sinon.spy(), + restore: sinon.spy(), + globalAlpha: 1 + }; + const transform = [1, 0, 0, 1, 0, 0]; + const opacity = 0.5; + const image = {}; + const x = 0; + const y = 0; + const w = 1; + const h = 1; + const scale = 1; + + render.drawImageOrLabel(layerContext, transform.slice(), opacity, image, + x, y, w, h, x, y, scale); + + expect(layerContext.save.callCount).to.be(1); + expect(layerContext.setTransform.callCount).to.be(1); + expect(layerContext.setTransform.firstCall.args).to.eql(transform); + expect(layerContext.drawImage.callCount).to.be(1); + expect(layerContext.globalAlpha).to.be(.5); + expect(layerContext.restore.callCount).to.be(1); + }); + }); + }); From b6abe036ce2ed429f0748cc612399a7cbab2b533 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 18 Feb 2020 21:32:09 +0100 Subject: [PATCH 045/636] Make events work in shadow dom --- src/ol/PluggableMap.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ol/PluggableMap.js b/src/ol/PluggableMap.js index 75c1f4a7be..eb6c7646e4 100644 --- a/src/ol/PluggableMap.js +++ b/src/ol/PluggableMap.js @@ -934,7 +934,7 @@ class PluggableMap extends BaseObject { } const target = /** @type {Node} */ (mapBrowserEvent.originalEvent.target); if (!mapBrowserEvent.dragging) { - if (this.overlayContainerStopEvent_.contains(target) || !document.body.contains(target)) { + if (this.overlayContainerStopEvent_.contains(target) || !(document.body.contains(target) || this.viewport_.getRootNode && this.viewport_.getRootNode().contains(target))) { // Abort if the event target is a child of the container that doesn't allow // event propagation or is no longer in the page. It's possible for the target to no longer // be in the page if it has been removed in an event listener, this might happen in a Control From 7fee85734a7d41b8335818ce93adf9be19e3aea2 Mon Sep 17 00:00:00 2001 From: Andreas Hocevar Date: Tue, 18 Feb 2020 21:32:31 +0100 Subject: [PATCH 046/636] Add custom element example --- examples/es2015-custom-element.html | 9 ++++++ examples/es2015-custom-element.js | 43 +++++++++++++++++++++++++++++ examples/templates/readme.md | 16 +---------- examples/webpack/config.js | 2 +- 4 files changed, 54 insertions(+), 16 deletions(-) create mode 100644 examples/es2015-custom-element.html create mode 100644 examples/es2015-custom-element.js diff --git a/examples/es2015-custom-element.html b/examples/es2015-custom-element.html new file mode 100644 index 0000000000..a9d76bc683 --- /dev/null +++ b/examples/es2015-custom-element.html @@ -0,0 +1,9 @@ +--- +layout: example.html +title: Custom map element +shortdesc: Example of a custom element with a map. +docs: > + The example creates and registers a custom element, `ol-map`, which contains a simple map. **Note:** Only works in browsers that supports `ShadowRoot`. +tags: "es2015, web-component, custom-element, shadow-dom" +--- + diff --git a/examples/es2015-custom-element.js b/examples/es2015-custom-element.js new file mode 100644 index 0000000000..ff632b2b2e --- /dev/null +++ b/examples/es2015-custom-element.js @@ -0,0 +1,43 @@ +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'; + +class OLComponent extends HTMLElement { + + constructor() { + super(); + this.shadow = this.attachShadow({mode: 'open'}); + const link = document.createElement('link'); + link.setAttribute('rel', 'stylesheet'); + link.setAttribute('href', 'css/ol.css'); + this.shadow.appendChild(link); + const style = document.createElement('style'); + style.innerText = ` + :host { + display: block; + } + `; + this.shadow.appendChild(style); + const div = document.createElement('div'); + div.style.width = '100%'; + div.style.height = '100%'; + this.shadow.appendChild(div); + + this.map = new Map({ + target: div, + layers: [ + new TileLayer({ + source: new OSM() + }) + ], + view: new View({ + center: [0, 0], + zoom: 2 + }) + }); + + } +} + +customElements.define('ol-map', OLComponent); diff --git a/examples/templates/readme.md b/examples/templates/readme.md index 8159182b08..fec59881a8 100644 --- a/examples/templates/readme.md +++ b/examples/templates/readme.md @@ -1,19 +1,5 @@ This folder contains example templates. These templates are used to build the examples in the `examples/` folder. The resulting examples are written to the `build/examples` folder. -Although the main purpose of these examples is to demonstrate how to use the API, they also serve other purposes in the development cycle, and so are not exactly as they would be in normal application code: - - * every time the library changes, they are compiled together with the library as a basic check that they remain in sync with the library - - * they use a special loader script to enable defining at run time which build mode (raw/debug/advanced) to use - -To enable this, examples have the following, not needed in application code: - - * each html file loads `loader.js`; application code would not need this, but would instead load the appropriate library build file, either a hosted version or a custom build - - * each js file starts with `goog.require` functions, used by the compiler; application code would only have these if the code is to be compiled together with the library and/or Closure library - - * some js files use type definitions (comments with @type tags); these are also used by the compiler, and are only needed if the code is to be compiled together with the library - - * html files load `resources/common.js` and some scripts use `common.getRendererFromQueryString()` to set the map renderer; application code would not need these +The examples are transpiled to es5. If the name of an example starts with `es2015-`, transpilation will be bypassed. This allows for showing es2015+ features in examples. At the bottom of each example generated in the `build/examples` folder, a modified version of its source code is shown. That modified version can be run standalone and is usually used as starting point for users to extend examples into their own application. diff --git a/examples/webpack/config.js b/examples/webpack/config.js index 6121b4b66f..5ff92a550d 100644 --- a/examples/webpack/config.js +++ b/examples/webpack/config.js @@ -22,7 +22,7 @@ module.exports = { stats: 'minimal', module: { rules: [{ - test: /\.js$/, + test: /^((?!es2015-)[\s\S])*\.js$/, use: { loader: 'buble-loader' }, From 341e482f309bd55bf37a13c959046bf058d0f8ce Mon Sep 17 00:00:00 2001 From: Tim Schaub Date: Tue, 18 Feb 2020 18:31:57 -0700 Subject: [PATCH 047/636] Add rendering test for image style opacity --- rendering/cases/icon-opacity/expected.png | Bin 0 -> 121631 bytes rendering/cases/icon-opacity/main.js | 47 ++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 rendering/cases/icon-opacity/expected.png create mode 100644 rendering/cases/icon-opacity/main.js diff --git a/rendering/cases/icon-opacity/expected.png b/rendering/cases/icon-opacity/expected.png new file mode 100644 index 0000000000000000000000000000000000000000..a7f59cdf296910ac26db99a00091ecfff7b78214 GIT binary patch literal 121631 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
y%*@ZrNJe){yl&v%cEV*tBK7)AcXpQklB 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
Q?k2gC@M=(f!o_XU3aEyj}&z+)6CB` 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