diff --git a/examples/reprojection-by-code.html b/examples/reprojection-by-code.html new file mode 100644 index 0000000000..ab784c4607 --- /dev/null +++ b/examples/reprojection-by-code.html @@ -0,0 +1,26 @@ +--- +template: example.html +title: Reprojection with EPSG.io database search +shortdesc: Demonstrates client-side raster reprojection of MapQuest OSM to arbitrary projection +docs: > + This example shows client-side raster reprojection capabilities from + MapQuest OSM (EPSG:3857) to arbitrary projection by searching + in EPSG.io database. +tags: "reprojection, projection, proj4js, mapquest, epsg.io" +resources: + - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js +--- +
+
+
+
+
+ + + + +
+ +
+
+
diff --git a/examples/reprojection-by-code.js b/examples/reprojection-by-code.js new file mode 100644 index 0000000000..96203a4177 --- /dev/null +++ b/examples/reprojection-by-code.js @@ -0,0 +1,116 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.extent'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.MapQuest'); +goog.require('ol.source.TileImage'); + + + +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'osm'}) + }) + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + projection: 'EPSG:3857', + center: [0, 0], + zoom: 1 + }) +}); + + +var queryInput = document.getElementById('epsg-query'); +var searchButton = document.getElementById('epsg-search'); +var resultSpan = document.getElementById('epsg-result'); +var renderEdgesCheckbox = document.getElementById('render-edges'); + +function setProjection(code, name, proj4def, bbox) { + if (code === null || name === null || proj4def === null || bbox === null) { + resultSpan.innerHTML = 'Nothing usable found, using EPSG:3857...'; + map.setView(new ol.View({ + projection: 'EPSG:3857', + center: [0, 0], + zoom: 1 + })); + return; + } + + resultSpan.innerHTML = '(' + code + ') ' + name; + + var newProjCode = 'EPSG:' + code; + proj4.defs(newProjCode, proj4def); + var newProj = ol.proj.get(newProjCode); + var fromLonLat = ol.proj.getTransform('EPSG:4326', newProj); + + // very approximate calculation of projection extent + var extent = ol.extent.applyTransform( + [bbox[1], bbox[2], bbox[3], bbox[0]], fromLonLat); + newProj.setExtent(extent); + var newView = new ol.View({ + projection: newProj + }); + map.setView(newView); + + var size = map.getSize(); + if (size) { + newView.fit(extent, size); + } +} + + +function search(query) { + resultSpan.innerHTML = 'Searching...'; + $.ajax({ + url: 'http://epsg.io/?format=json&q=' + query, + dataType: 'jsonp', + success: function(response) { + if (response) { + var results = response['results']; + if (results && results.length > 0) { + for (var i = 0, ii = results.length; i < ii; i++) { + var result = results[i]; + if (result) { + var code = result['code'], name = result['name'], + proj4def = result['proj4'], bbox = result['bbox']; + if (code && code.length > 0 && proj4def && proj4def.length > 0 && + bbox && bbox.length == 4) { + setProjection(code, name, proj4def, bbox); + return; + } + } + } + } + } + setProjection(null, null, null, null); + } + }); +} + + +/** + * @param {Event} e Change event. + */ +searchButton.onclick = function(e) { + search(queryInput.value); + e.preventDefault(); +}; + + +/** + * @param {Event} e Change event. + */ +renderEdgesCheckbox.onchange = function(e) { + map.getLayers().forEach(function(layer) { + if (layer instanceof ol.layer.Tile) { + var source = layer.getSource(); + if (source instanceof ol.source.TileImage) { + source.setRenderReprojectionEdges(renderEdgesCheckbox.checked); + } + } + }); +}; diff --git a/examples/reprojection-image.html b/examples/reprojection-image.html new file mode 100644 index 0000000000..4b505605ad --- /dev/null +++ b/examples/reprojection-image.html @@ -0,0 +1,15 @@ +--- +template: example.html +title: Image reprojection example +shortdesc: Demonstrates client-side reprojection of single image source. +docs: > + This example shows client-side reprojection of single image source. +tags: "reprojection, projection, proj4js, mapquest, image, imagestatic" +resources: + - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js +--- +
+
+
+
+
diff --git a/examples/reprojection-image.js b/examples/reprojection-image.js new file mode 100644 index 0000000000..3688097980 --- /dev/null +++ b/examples/reprojection-image.js @@ -0,0 +1,39 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.extent'); +goog.require('ol.layer.Image'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.ImageStatic'); +goog.require('ol.source.MapQuest'); + + +proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + + '+x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); +var imageExtent = [0, 0, 700000, 1300000]; + +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'osm'}) + }), + new ol.layer.Image({ + source: new ol.source.ImageStatic({ + url: 'http://upload.wikimedia.org/wikipedia/commons/thumb/1/18/' + + 'British_National_Grid.svg/2000px-British_National_Grid.svg.png', + crossOrigin: '', + projection: 'EPSG:27700', + imageExtent: imageExtent + }) + }) + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + center: ol.proj.transform(ol.extent.getCenter(imageExtent), + 'EPSG:27700', 'EPSG:3857'), + zoom: 4 + }) +}); diff --git a/examples/reprojection-wgs84.html b/examples/reprojection-wgs84.html new file mode 100644 index 0000000000..f6f1ec3754 --- /dev/null +++ b/examples/reprojection-wgs84.html @@ -0,0 +1,13 @@ +--- +template: example.html +title: OpenStreetMap reprojection example +shortdesc: Demonstrates client-side reprojection of OpenStreetMap in WGS84. +docs: > + This example shows client-side reprojection of OpenStreetMap in WGS84. +tags: "reprojection, projection, openstreetmap, wgs84, tile" +--- +
+
+
+
+
diff --git a/examples/reprojection-wgs84.js b/examples/reprojection-wgs84.js new file mode 100644 index 0000000000..6f85d7d219 --- /dev/null +++ b/examples/reprojection-wgs84.js @@ -0,0 +1,19 @@ +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.layer.Tile'); +goog.require('ol.source.OSM'); + +var map = new ol.Map({ + layers: [ + new ol.layer.Tile({ + source: new ol.source.OSM() + }) + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + projection: 'EPSG:4326', + center: [0, 0], + zoom: 1 + }) +}); diff --git a/examples/reprojection.html b/examples/reprojection.html new file mode 100644 index 0000000000..365c612945 --- /dev/null +++ b/examples/reprojection.html @@ -0,0 +1,48 @@ +--- +template: example.html +title: Raster reprojection example +shortdesc: Demonstrates client-side raster reprojection between various projections. +docs: > + This example shows client-side raster reprojection between various projections. +tags: "reprojection, projection, proj4js, mapquest, wms, wmts, hidpi" +resources: + - http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js +--- +
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+ (only displayed on reprojected data) +
+
diff --git a/examples/reprojection.js b/examples/reprojection.js new file mode 100644 index 0000000000..dec799b8eb --- /dev/null +++ b/examples/reprojection.js @@ -0,0 +1,244 @@ +goog.require('ol.Attribution'); +goog.require('ol.Map'); +goog.require('ol.View'); +goog.require('ol.extent'); +goog.require('ol.format.WMTSCapabilities'); +goog.require('ol.layer.Tile'); +goog.require('ol.proj'); +goog.require('ol.source.MapQuest'); +goog.require('ol.source.TileImage'); +goog.require('ol.source.TileWMS'); +goog.require('ol.source.WMTS'); +goog.require('ol.source.XYZ'); +goog.require('ol.tilegrid.TileGrid'); + + +proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 ' + + '+x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); +var proj27700 = ol.proj.get('EPSG:27700'); +proj27700.setExtent([0, 0, 700000, 1300000]); + +proj4.defs('EPSG:23032', '+proj=utm +zone=32 +ellps=intl ' + + '+towgs84=-87,-98,-121,0,0,0,0 +units=m +no_defs'); +var proj23032 = ol.proj.get('EPSG:23032'); +proj23032.setExtent([-1206118.71, 4021309.92, 1295389.00, 8051813.28]); + +proj4.defs('EPSG:5479', '+proj=lcc +lat_1=-76.66666666666667 +lat_2=' + + '-79.33333333333333 +lat_0=-78 +lon_0=163 +x_0=7000000 +y_0=5000000 ' + + '+ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); +var proj5479 = ol.proj.get('EPSG:5479'); +proj5479.setExtent([6825737.53, 4189159.80, 9633741.96, 5782472.71]); + +proj4.defs('EPSG:21781', '+proj=somerc +lat_0=46.95240555555556 ' + + '+lon_0=7.439583333333333 +k_0=1 +x_0=600000 +y_0=200000 +ellps=bessel ' + + '+towgs84=674.4,15.1,405.3,0,0,0,0 +units=m +no_defs'); +var proj21781 = ol.proj.get('EPSG:21781'); +proj21781.setExtent([485071.54, 75346.36, 828515.78, 299941.84]); + +proj4.defs('EPSG:3413', '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 +k=1 ' + + '+x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); +var proj3413 = ol.proj.get('EPSG:3413'); +proj3413.setExtent([-4194304, -4194304, 4194304, 4194304]); + +proj4.defs('EPSG:2163', '+proj=laea +lat_0=45 +lon_0=-100 +x_0=0 +y_0=0 ' + + '+a=6370997 +b=6370997 +units=m +no_defs'); +var proj2163 = ol.proj.get('EPSG:2163'); +proj2163.setExtent([-8040784.5135, -2577524.9210, 3668901.4484, 4785105.1096]); + +proj4.defs('ESRI:54009', '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 ' + + '+units=m +no_defs'); +var proj54009 = ol.proj.get('ESRI:54009'); +proj54009.setExtent([-18e6, -9e6, 18e6, 9e6]); + + +var layers = []; + +layers['bng'] = new ol.layer.Tile({ + source: new ol.source.XYZ({ + projection: 'EPSG:27700', + url: 'http://tileserver.maptiler.com/miniscale/{z}/{x}/{y}.png', + crossOrigin: '', + maxZoom: 6 + }) +}); + +layers['mapquest'] = new ol.layer.Tile({ + source: new ol.source.MapQuest({layer: 'osm'}) +}); + +layers['wms4326'] = new ol.layer.Tile({ + source: new ol.source.TileWMS({ + url: 'http://demo.boundlessgeo.com/geoserver/wms', + crossOrigin: '', + params: { + 'LAYERS': 'ne:NE1_HR_LC_SR_W_DR' + }, + projection: 'EPSG:4326' + }) +}); + +layers['wms21781'] = new ol.layer.Tile({ + source: new ol.source.TileWMS({ + attributions: [new ol.Attribution({ + html: '© ' + + '' + + 'Pixelmap 1:1000000 / geo.admin.ch' + })], + crossOrigin: 'anonymous', + params: { + 'LAYERS': 'ch.swisstopo.pixelkarte-farbe-pk1000.noscale', + 'FORMAT': 'image/jpeg' + }, + url: 'http://wms.geo.admin.ch/', + projection: 'EPSG:21781' + }) +}); + +var parser = new ol.format.WMTSCapabilities(); +$.ajax('http://map1.vis.earthdata.nasa.gov/wmts-arctic/' + + 'wmts.cgi?SERVICE=WMTS&request=GetCapabilities').then(function(response) { + var result = parser.read(response); + var options = ol.source.WMTS.optionsFromCapabilities(result, + {layer: 'OSM_Land_Mask', matrixSet: 'EPSG3413_250m'}); + options.crossOrigin = ''; + options.projection = 'EPSG:3413'; + options.wrapX = false; + layers['wmts3413'] = new ol.layer.Tile({ + source: new ol.source.WMTS(options) + }); +}); + +layers['grandcanyon'] = new ol.layer.Tile({ + source: new ol.source.XYZ({ + url: 'http://tileserver.maptiler.com/grandcanyon@2x/{z}/{x}/{y}.png', + crossOrigin: '', + tilePixelRatio: 2, + maxZoom: 15, + attributions: [new ol.Attribution({ + html: 'Tiles © USGS, rendered with ' + + 'MapTiler' + })] + }) +}); + +var startResolution = + ol.extent.getWidth(ol.proj.get('EPSG:3857').getExtent()) / 256; +var resolutions = new Array(22); +for (var i = 0, ii = resolutions.length; i < ii; ++i) { + resolutions[i] = startResolution / Math.pow(2, i); +} + +layers['states'] = new ol.layer.Tile({ + source: new ol.source.TileWMS({ + url: 'http://demo.boundlessgeo.com/geoserver/wms', + crossOrigin: '', + params: {'LAYERS': 'topp:states', 'TILED': true}, + serverType: 'geoserver', + tileGrid: new ol.tilegrid.TileGrid({ + extent: [-13884991, 2870341, -7455066, 6338219], + resolutions: resolutions, + tileSize: [512, 256] + }), + projection: 'EPSG:3857' + }) +}); + + +var map = new ol.Map({ + layers: [ + layers['mapquest'], + layers['bng'] + ], + renderer: common.getRendererFromQueryString(), + target: 'map', + view: new ol.View({ + projection: 'EPSG:3857', + center: [0, 0], + zoom: 2 + }) +}); + + +var baseLayerSelect = document.getElementById('base-layer'); +var overlayLayerSelect = document.getElementById('overlay-layer'); +var viewProjSelect = document.getElementById('view-projection'); +var renderEdgesCheckbox = document.getElementById('render-edges'); +var renderEdges = false; + +function updateViewProjection() { + var newProj = ol.proj.get(viewProjSelect.value); + var newProjExtent = newProj.getExtent(); + var newView = new ol.View({ + projection: newProj, + center: ol.extent.getCenter(newProjExtent || [0, 0, 0, 0]), + zoom: 0, + extent: newProjExtent || undefined + }); + map.setView(newView); + + // Example how to prevent double occurence of map by limiting layer extent + if (newProj == ol.proj.get('EPSG:3857')) { + layers['bng'].setExtent([-1057216, 6405988, 404315, 8759696]); + } else { + layers['bng'].setExtent(undefined); + } +} + + +/** + * @param {Event} e Change event. + */ +viewProjSelect.onchange = function(e) { + updateViewProjection(); +}; + +updateViewProjection(); + +var updateRenderEdgesOnLayer = function(layer) { + if (layer instanceof ol.layer.Tile) { + var source = layer.getSource(); + if (source instanceof ol.source.TileImage) { + source.setRenderReprojectionEdges(renderEdges); + } + } +}; + + +/** + * @param {Event} e Change event. + */ +baseLayerSelect.onchange = function(e) { + var layer = layers[baseLayerSelect.value]; + if (layer) { + layer.setOpacity(1); + updateRenderEdgesOnLayer(layer); + map.getLayers().setAt(0, layer); + } +}; + + +/** + * @param {Event} e Change event. + */ +overlayLayerSelect.onchange = function(e) { + var layer = layers[overlayLayerSelect.value]; + if (layer) { + layer.setOpacity(0.7); + updateRenderEdgesOnLayer(layer); + map.getLayers().setAt(1, layer); + } +}; + + +/** + * @param {Event} e Change event. + */ +renderEdgesCheckbox.onchange = function(e) { + renderEdges = renderEdgesCheckbox.checked; + map.getLayers().forEach(function(layer) { + updateRenderEdgesOnLayer(layer); + }); +}; diff --git a/externs/olx.js b/externs/olx.js index b7abb24f37..60da9abb15 100644 --- a/externs/olx.js +++ b/externs/olx.js @@ -3602,6 +3602,7 @@ olx.source; * key: string, * imagerySet: string, * maxZoom: (number|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * wrapX: (boolean|undefined)}} * @api @@ -3642,6 +3643,15 @@ olx.source.BingMapsOptions.prototype.imagerySet; olx.source.BingMapsOptions.prototype.maxZoom; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.BingMapsOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -3761,6 +3771,7 @@ olx.source.TileUTFGridOptions.prototype.url; * logo: (string|olx.LogoOptions|undefined), * opaque: (boolean|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * state: (ol.source.State|string|undefined), * tileClass: (function(new: ol.ImageTile, ol.TileCoord, * ol.TileState, string, ?string, @@ -3819,6 +3830,15 @@ olx.source.TileImageOptions.prototype.opaque; olx.source.TileImageOptions.prototype.projection; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.TileImageOptions.prototype.reprojectionErrorThreshold; + + /** * Source state. * @type {ol.source.State|string|undefined} @@ -4076,6 +4096,7 @@ olx.source.ImageMapGuideOptions.prototype.params; /** * @typedef {{layer: string, + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined)}} * @api @@ -4091,6 +4112,15 @@ olx.source.MapQuestOptions; olx.source.MapQuestOptions.prototype.layer; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.MapQuestOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4144,6 +4174,7 @@ olx.source.TileDebugOptions.prototype.wrapX; * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), * maxZoom: (number|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), * wrapX: (boolean|undefined)}} @@ -4182,6 +4213,15 @@ olx.source.OSMOptions.prototype.crossOrigin; olx.source.OSMOptions.prototype.maxZoom; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.OSMOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4538,6 +4578,7 @@ olx.source.ImageWMSOptions.prototype.url; * minZoom: (number|undefined), * maxZoom: (number|undefined), * opaque: (boolean|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined)}} * @api @@ -4577,6 +4618,15 @@ olx.source.StamenOptions.prototype.maxZoom; olx.source.StamenOptions.prototype.opaque; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.StamenOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4683,6 +4733,7 @@ olx.source.ImageStaticOptions.prototype.url; * logo: (string|olx.LogoOptions|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), * urls: (Array.|undefined), @@ -4754,6 +4805,15 @@ olx.source.TileArcGISRestOptions.prototype.tileGrid; olx.source.TileArcGISRestOptions.prototype.projection; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.TileArcGISRestOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4791,6 +4851,7 @@ olx.source.TileArcGISRestOptions.prototype.urls; /** * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), + * reprojectionErrorThreshold: (number|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: string, * wrapX: (boolean|undefined)}} @@ -4821,6 +4882,15 @@ olx.source.TileJSONOptions.prototype.attributions; olx.source.TileJSONOptions.prototype.crossOrigin; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.TileJSONOptions.prototype.reprojectionErrorThreshold; + + /** * Optional function to load a tile given a URL. * @type {ol.TileLoadFunctionType|undefined} @@ -4855,6 +4925,7 @@ olx.source.TileJSONOptions.prototype.wrapX; * tileGrid: (ol.tilegrid.TileGrid|undefined), * maxZoom: (number|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * serverType: (ol.source.wms.ServerType|string|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined), * url: (string|undefined), @@ -4955,6 +5026,15 @@ olx.source.TileWMSOptions.prototype.maxZoom; olx.source.TileWMSOptions.prototype.projection; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.TileWMSOptions.prototype.reprojectionErrorThreshold; + + /** * The type of the remote WMS server. Currently only used when `hidpi` is * `true`. Default is `undefined`. @@ -5120,6 +5200,7 @@ olx.source.VectorOptions.prototype.wrapX; * logo: (string|olx.LogoOptions|undefined), * tileGrid: ol.tilegrid.WMTS, * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * requestEncoding: (ol.source.WMTSRequestEncoding|string|undefined), * layer: string, * style: string, @@ -5185,6 +5266,15 @@ olx.source.WMTSOptions.prototype.tileGrid; olx.source.WMTSOptions.prototype.projection; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.WMTSOptions.prototype.reprojectionErrorThreshold; + + /** * Request encoding. Default is `KVP`. * @type {ol.source.WMTSRequestEncoding|string|undefined} @@ -5311,6 +5401,7 @@ olx.source.WMTSOptions.prototype.wrapX; * crossOrigin: (null|string|undefined), * logo: (string|olx.LogoOptions|undefined), * projection: ol.proj.ProjectionLike, + * reprojectionErrorThreshold: (number|undefined), * maxZoom: (number|undefined), * minZoom: (number|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined), @@ -5362,6 +5453,15 @@ olx.source.XYZOptions.prototype.logo; olx.source.XYZOptions.prototype.projection; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.XYZOptions.prototype.reprojectionErrorThreshold; + + /** * Optional max zoom level. Default is `18`. * @type {number|undefined} @@ -5452,6 +5552,7 @@ olx.source.XYZOptions.prototype.wrapX; * @typedef {{attributions: (Array.|undefined), * crossOrigin: (null|string|undefined), * logo: (string|olx.LogoOptions|undefined), + * reprojectionErrorThreshold: (number|undefined), * url: !string, * tierSizeCalculation: (string|undefined), * size: ol.Size}} @@ -5488,6 +5589,15 @@ olx.source.ZoomifyOptions.prototype.crossOrigin; olx.source.ZoomifyOptions.prototype.logo; +/** + * Maximum allowed reprojection error (in pixels). Default is `0.5`. + * Higher values can increase reprojection performance, but decrease precision. + * @type {number|undefined} + * @api + */ +olx.source.ZoomifyOptions.prototype.reprojectionErrorThreshold; + + /** * Prefix of URL template. * @type {!string} diff --git a/src/ol/math.js b/src/ol/math.js index c0244bda01..6884b965eb 100644 --- a/src/ol/math.js +++ b/src/ol/math.js @@ -78,6 +78,69 @@ ol.math.squaredDistance = function(x1, y1, x2, y2) { }; +/** + * Solves system of linear equations using Gaussian elimination method. + * + * @param {Array.>} mat Augmented matrix (n x n + 1 column) + * in row-major order. + * @return {Array.} The resulting vector. + */ +ol.math.solveLinearSystem = function(mat) { + var n = mat.length; + + if (goog.asserts.ENABLE_ASSERTS) { + for (var row = 0; row < n; row++) { + goog.asserts.assert(mat[row].length == n + 1, + 'every row should have correct number of columns'); + } + } + + for (var i = 0; i < n; i++) { + // Find max in the i-th column (ignoring i - 1 first rows) + var maxRow = i; + var maxEl = Math.abs(mat[i][i]); + for (var r = i + 1; r < n; r++) { + var absValue = Math.abs(mat[r][i]); + if (absValue > maxEl) { + maxEl = absValue; + maxRow = r; + } + } + + if (maxEl === 0) { + return null; // matrix is singular + } + + // Swap max row with i-th (current) row + var tmp = mat[maxRow]; + mat[maxRow] = mat[i]; + mat[i] = tmp; + + // Subtract the i-th row to make all the remaining rows 0 in the i-th column + for (var j = i + 1; j < n; j++) { + var coef = -mat[j][i] / mat[i][i]; + for (var k = i; k < n + 1; k++) { + if (i == k) { + mat[j][k] = 0; + } else { + mat[j][k] += coef * mat[i][k]; + } + } + } + } + + // Solve Ax=b for upper triangular matrix A (mat) + var x = new Array(n); + for (var l = n - 1; l >= 0; l--) { + x[l] = mat[l][n] / mat[l][l]; + for (var m = l - 1; m >= 0; m--) { + mat[m][n] -= mat[m][l] * x[l]; + } + } + return x; +}; + + /** * Converts radians to to degrees. * diff --git a/src/ol/ol.js b/src/ol/ol.js index cffdecbc29..23c754f49a 100644 --- a/src/ol/ol.js +++ b/src/ol/ol.js @@ -28,6 +28,13 @@ ol.DEFAULT_MAX_ZOOM = 42; ol.DEFAULT_MIN_ZOOM = 0; +/** + * @define {number} Default maximum allowed threshold (in pixels) for + * reprojection triangulation. Default is `0.5`. + */ +ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD = 0.5; + + /** * @define {number} Default high water mark. */ @@ -94,6 +101,13 @@ ol.ENABLE_NAMED_COLORS = false; ol.ENABLE_PROJ4JS = true; +/** + * @define {boolean} Enable automatic reprojection of raster sources. Default is + * `true`. + */ +ol.ENABLE_RASTER_REPROJECTION = true; + + /** * @define {boolean} Enable rendering of ol.layer.Tile based layers. Default is * `true`. Setting this to false at compile time in advanced mode removes @@ -159,6 +173,41 @@ ol.OVERVIEWMAP_MAX_RATIO = 0.75; ol.OVERVIEWMAP_MIN_RATIO = 0.1; +/** + * @define {number} Maximum number of source tiles for raster reprojection of + * a single tile. + * If too many source tiles are determined to be loaded to create a single + * reprojected tile the browser can become unresponsive or even crash. + * This can happen if the developer defines projections improperly and/or + * with unlimited extents. + * If too many tiles are required, no tiles are loaded and + * `ol.TileState.ERROR` state is set. Default is `100`. + */ +ol.RASTER_REPROJECTION_MAX_SOURCE_TILES = 100; + + +/** + * @define {number} Maximum number of subdivision steps during raster + * reprojection triangulation. Prevents high memory usage and large + * number of proj4 calls (for certain transformations and areas). + * At most `2*(2^this)` triangles are created for each triangulated + * extent (tile/image). Default is `10`. + */ +ol.RASTER_REPROJECTION_MAX_SUBDIVISION = 10; + + +/** + * @define {number} Maximum allowed size of triangle relative to world width. + * When transforming corners of world extent between certain projections, + * the resulting triangulation seems to have zero error and no subdivision + * is performed. + * If the triangle width is more than this (relative to world width; 0-1), + * subdivison is forced (up to `ol.RASTER_REPROJECTION_MAX_SUBDIVISION`). + * Default is `0.25`. + */ +ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH = 0.25; + + /** * @define {number} Tolerance for geometry simplification in device pixels. */ diff --git a/src/ol/renderer/canvas/canvasimagelayerrenderer.js b/src/ol/renderer/canvas/canvasimagelayerrenderer.js index c752dd4630..22fae30287 100644 --- a/src/ol/renderer/canvas/canvasimagelayerrenderer.js +++ b/src/ol/renderer/canvas/canvasimagelayerrenderer.js @@ -170,11 +170,13 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame = if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && !ol.extent.isEmpty(renderedExtent)) { var projection = viewState.projection; - var sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), - 'projection and sourceProjection are equivalent'); - projection = sourceProjection; + if (!ol.ENABLE_RASTER_REPROJECTION) { + var sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), + 'projection and sourceProjection are equivalent'); + projection = sourceProjection; + } } image = imageSource.getImage( renderedExtent, viewResolution, pixelRatio, projection); diff --git a/src/ol/renderer/canvas/canvastilelayerrenderer.js b/src/ol/renderer/canvas/canvastilelayerrenderer.js index e6ce046ab9..14ada48772 100644 --- a/src/ol/renderer/canvas/canvastilelayerrenderer.js +++ b/src/ol/renderer/canvas/canvastilelayerrenderer.js @@ -309,7 +309,8 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame = /** @type {Array.} */ var tilesToClear = []; - var findLoadedTiles = this.createLoadedTileFinder(tileSource, tilesToDrawByZ); + var findLoadedTiles = this.createLoadedTileFinder( + tileSource, projection, tilesToDrawByZ); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); diff --git a/src/ol/renderer/dom/domimagelayerrenderer.js b/src/ol/renderer/dom/domimagelayerrenderer.js index 29308d3ee2..033de8614b 100644 --- a/src/ol/renderer/dom/domimagelayerrenderer.js +++ b/src/ol/renderer/dom/domimagelayerrenderer.js @@ -101,11 +101,13 @@ ol.renderer.dom.ImageLayer.prototype.prepareFrame = if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && !ol.extent.isEmpty(renderedExtent)) { var projection = viewState.projection; - var sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), - 'projection and sourceProjection are equivalent'); - projection = sourceProjection; + if (!ol.ENABLE_RASTER_REPROJECTION) { + var sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), + 'projection and sourceProjection are equivalent'); + projection = sourceProjection; + } } var image_ = imageSource.getImage(renderedExtent, viewResolution, frameState.pixelRatio, projection); diff --git a/src/ol/renderer/dom/domtilelayerrenderer.js b/src/ol/renderer/dom/domtilelayerrenderer.js index eb93e39538..d9a66b5f7e 100644 --- a/src/ol/renderer/dom/domtilelayerrenderer.js +++ b/src/ol/renderer/dom/domtilelayerrenderer.js @@ -121,7 +121,8 @@ ol.renderer.dom.TileLayer.prototype.prepareFrame = var tilesToDrawByZ = {}; tilesToDrawByZ[z] = {}; - var findLoadedTiles = this.createLoadedTileFinder(tileSource, tilesToDrawByZ); + var findLoadedTiles = this.createLoadedTileFinder( + tileSource, projection, tilesToDrawByZ); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index f6dd738106..cd0dba0093 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -87,6 +87,7 @@ ol.renderer.Layer.prototype.hasFeatureAtCoordinate = goog.functions.FALSE; /** * Create a function that adds loaded tiles to the tile lookup. * @param {ol.source.Tile} source Tile source. + * @param {ol.proj.Projection} projection Projection of the tiles. * @param {Object.>} tiles Lookup of loaded * tiles by zoom level. * @return {function(number, ol.TileRange):boolean} A function that can be @@ -94,7 +95,8 @@ ol.renderer.Layer.prototype.hasFeatureAtCoordinate = goog.functions.FALSE; * lookup. * @protected */ -ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) { +ol.renderer.Layer.prototype.createLoadedTileFinder = + function(source, projection, tiles) { return ( /** * @param {number} zoom Zoom level. @@ -102,12 +104,13 @@ ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) { * @return {boolean} The tile range is fully loaded. */ function(zoom, tileRange) { - return source.forEachLoadedTile(zoom, tileRange, function(tile) { - if (!tiles[zoom]) { - tiles[zoom] = {}; - } - tiles[zoom][tile.tileCoord.toString()] = tile; - }); + return source.forEachLoadedTile(projection, zoom, + tileRange, function(tile) { + if (!tiles[zoom]) { + tiles[zoom] = {}; + } + tiles[zoom][tile.tileCoord.toString()] = tile; + }); }); }; @@ -156,8 +159,9 @@ ol.renderer.Layer.prototype.loadImage = function(image) { if (imageState == ol.ImageState.IDLE) { image.load(); imageState = image.getState(); - goog.asserts.assert(imageState == ol.ImageState.LOADING, - 'imageState is "loading"'); + goog.asserts.assert(imageState == ol.ImageState.LOADING || + imageState == ol.ImageState.LOADED, + 'imageState is "loading" or "loaded"'); } return imageState == ol.ImageState.LOADED; }; @@ -191,7 +195,8 @@ ol.renderer.Layer.prototype.scheduleExpireCache = */ function(tileSource, map, frameState) { var tileSourceKey = goog.getUid(tileSource).toString(); - tileSource.expireCache(frameState.usedTiles[tileSourceKey]); + tileSource.expireCache(frameState.viewState.projection, + frameState.usedTiles[tileSourceKey]); }, tileSource)); } }; @@ -324,7 +329,7 @@ ol.renderer.Layer.prototype.manageTilePyramid = function( opt_tileCallback.call(opt_this, tile); } } else { - tileSource.useTile(z, x, y); + tileSource.useTile(z, x, y, projection); } } } diff --git a/src/ol/renderer/webgl/webglimagelayerrenderer.js b/src/ol/renderer/webgl/webglimagelayerrenderer.js index 317544fc66..8aad156335 100644 --- a/src/ol/renderer/webgl/webglimagelayerrenderer.js +++ b/src/ol/renderer/webgl/webglimagelayerrenderer.js @@ -125,11 +125,13 @@ ol.renderer.webgl.ImageLayer.prototype.prepareFrame = if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && !ol.extent.isEmpty(renderedExtent)) { var projection = viewState.projection; - var sourceProjection = imageSource.getProjection(); - if (sourceProjection) { - goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), - 'projection and sourceProjection are equivalent'); - projection = sourceProjection; + if (!ol.ENABLE_RASTER_REPROJECTION) { + var sourceProjection = imageSource.getProjection(); + if (sourceProjection) { + goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), + 'projection and sourceProjection are equivalent'); + projection = sourceProjection; + } } var image_ = imageSource.getImage(renderedExtent, viewResolution, pixelRatio, projection); diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index 6752062548..675aac21ae 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -105,6 +105,7 @@ ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() { /** * Create a function that adds loaded tiles to the tile lookup. * @param {ol.source.Tile} source Tile source. + * @param {ol.proj.Projection} projection Projection of the tiles. * @param {Object.>} tiles Lookup of loaded * tiles by zoom level. * @return {function(number, ol.TileRange):boolean} A function that can be @@ -113,7 +114,7 @@ ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() { * @protected */ ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = - function(source, tiles) { + function(source, projection, tiles) { var mapRenderer = this.mapRenderer; return ( @@ -123,16 +124,17 @@ ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = * @return {boolean} The tile range is fully loaded. */ function(zoom, tileRange) { - return source.forEachLoadedTile(zoom, tileRange, function(tile) { - var loaded = mapRenderer.isTileTextureLoaded(tile); - if (loaded) { - if (!tiles[zoom]) { - tiles[zoom] = {}; - } - tiles[zoom][tile.tileCoord.toString()] = tile; - } - return loaded; - }); + return source.forEachLoadedTile(projection, zoom, + tileRange, function(tile) { + var loaded = mapRenderer.isTileTextureLoaded(tile); + if (loaded) { + if (!tiles[zoom]) { + tiles[zoom] = {}; + } + tiles[zoom][tile.tileCoord.toString()] = tile; + } + return loaded; + }); }); }; @@ -239,7 +241,7 @@ ol.renderer.webgl.TileLayer.prototype.prepareFrame = tilesToDrawByZ[z] = {}; var findLoadedTiles = this.createLoadedTileFinder( - tileSource, tilesToDrawByZ); + tileSource, projection, tilesToDrawByZ); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); var allTilesLoaded = true; diff --git a/src/ol/reproj/image.js b/src/ol/reproj/image.js new file mode 100644 index 0000000000..0292d7168e --- /dev/null +++ b/src/ol/reproj/image.js @@ -0,0 +1,210 @@ +goog.provide('ol.reproj.Image'); +goog.provide('ol.reproj.ImageFunctionType'); + +goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('ol.ImageBase'); +goog.require('ol.ImageState'); +goog.require('ol.extent'); +goog.require('ol.proj'); +goog.require('ol.reproj'); +goog.require('ol.reproj.Triangulation'); + + +/** + * @typedef {function(ol.Extent, number, number) : ol.ImageBase} + */ +ol.reproj.ImageFunctionType; + + + +/** + * @classdesc + * Class encapsulating single reprojected image. + * See {@link ol.source.Image}. + * + * @constructor + * @extends {ol.ImageBase} + * @param {ol.proj.Projection} sourceProj Source projection (of the data). + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.Extent} targetExtent Target extent. + * @param {number} targetResolution Target resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {ol.reproj.ImageFunctionType} getImageFunction + * Function returning source images (extent, resolution, pixelRatio). + */ +ol.reproj.Image = function(sourceProj, targetProj, + targetExtent, targetResolution, pixelRatio, getImageFunction) { + + /** + * @private + * @type {ol.proj.Projection} + */ + this.targetProj_ = targetProj; + + /** + * @private + * @type {ol.Extent} + */ + this.maxSourceExtent_ = sourceProj.getExtent(); + var maxTargetExtent = targetProj.getExtent(); + + var limitedTargetExtent = maxTargetExtent ? + ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent; + + var targetCenter = ol.extent.getCenter(limitedTargetExtent); + var sourceResolution = ol.reproj.calculateSourceResolution( + sourceProj, targetProj, targetCenter, targetResolution); + + var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD; + + /** + * @private + * @type {!ol.reproj.Triangulation} + */ + this.triangulation_ = new ol.reproj.Triangulation( + sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_, + sourceResolution * errorThresholdInPixels); + + /** + * @private + * @type {number} + */ + this.targetResolution_ = targetResolution; + + /** + * @private + * @type {ol.Extent} + */ + this.targetExtent_ = targetExtent; + + var sourceExtent = this.triangulation_.calculateSourceExtent(); + + /** + * @private + * @type {ol.ImageBase} + */ + this.sourceImage_ = + getImageFunction(sourceExtent, sourceResolution, pixelRatio); + + /** + * @private + * @type {number} + */ + this.sourcePixelRatio_ = + this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1; + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = null; + + /** + * @private + * @type {goog.events.Key} + */ + this.sourceListenerKey_ = null; + + + var state = ol.ImageState.LOADED; + var attributions = []; + + if (this.sourceImage_) { + state = ol.ImageState.IDLE; + attributions = this.sourceImage_.getAttributions(); + } + + goog.base(this, targetExtent, targetResolution, this.sourcePixelRatio_, + state, attributions); +}; +goog.inherits(ol.reproj.Image, ol.ImageBase); + + +/** + * @inheritDoc + */ +ol.reproj.Image.prototype.disposeInternal = function() { + if (this.state == ol.ImageState.LOADING) { + this.unlistenSource_(); + } + goog.base(this, 'disposeInternal'); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Image.prototype.getImage = function(opt_context) { + return this.canvas_; +}; + + +/** + * @return {ol.proj.Projection} Projection. + */ +ol.reproj.Image.prototype.getProjection = function() { + return this.targetProj_; +}; + + +/** + * @private + */ +ol.reproj.Image.prototype.reproject_ = function() { + var sourceState = this.sourceImage_.getState(); + if (sourceState == ol.ImageState.LOADED) { + var width = ol.extent.getWidth(this.targetExtent_) / this.targetResolution_; + var height = + ol.extent.getHeight(this.targetExtent_) / this.targetResolution_; + + this.canvas_ = ol.reproj.render(width, height, this.sourcePixelRatio_, + this.sourceImage_.getResolution(), this.maxSourceExtent_, + this.targetResolution_, this.targetExtent_, this.triangulation_, [{ + extent: this.sourceImage_.getExtent(), + image: this.sourceImage_.getImage() + }]); + } + this.state = sourceState; + this.changed(); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Image.prototype.load = function() { + if (this.state == ol.ImageState.IDLE) { + this.state = ol.ImageState.LOADING; + this.changed(); + + var sourceState = this.sourceImage_.getState(); + if (sourceState == ol.ImageState.LOADED || + sourceState == ol.ImageState.ERROR) { + this.reproject_(); + } else { + this.sourceListenerKey_ = this.sourceImage_.listen( + goog.events.EventType.CHANGE, function(e) { + var sourceState = this.sourceImage_.getState(); + if (sourceState == ol.ImageState.LOADED || + sourceState == ol.ImageState.ERROR) { + this.unlistenSource_(); + this.reproject_(); + } + }, false, this); + this.sourceImage_.load(); + } + } +}; + + +/** + * @private + */ +ol.reproj.Image.prototype.unlistenSource_ = function() { + goog.asserts.assert(this.sourceListenerKey_, + 'this.sourceListenerKey_ should not be null'); + goog.events.unlistenByKey(this.sourceListenerKey_); + this.sourceListenerKey_ = null; +}; diff --git a/src/ol/reproj/reproj.js b/src/ol/reproj/reproj.js new file mode 100644 index 0000000000..fbca15fa90 --- /dev/null +++ b/src/ol/reproj/reproj.js @@ -0,0 +1,258 @@ +goog.provide('ol.reproj'); + +goog.require('goog.labs.userAgent.browser'); +goog.require('goog.labs.userAgent.platform'); +goog.require('goog.math'); +goog.require('ol.dom'); +goog.require('ol.extent'); +goog.require('ol.math'); +goog.require('ol.proj'); + + +/** + * We need to employ more sophisticated solution + * if the web browser antialiases clipping edges on canvas. + * + * Currently only Chrome does not antialias the edges, but this is probably + * going to be "fixed" in the future: http://crbug.com/424291 + * + * @type {boolean} + * @private + */ +ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() || + goog.labs.userAgent.platform.isIos(); + + +/** + * 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 {ol.proj.Projection} sourceProj Source projection. + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.Coordinate} targetCenter Target center. + * @param {number} targetResolution Target resolution. + * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0. + */ +ol.reproj.calculateSourceResolution = function(sourceProj, targetProj, + targetCenter, targetResolution) { + + var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj); + + // calculate the ideal resolution of the source data + var sourceResolution = + targetProj.getPointResolution(targetResolution, targetCenter); + + var targetMetersPerUnit = targetProj.getMetersPerUnit(); + if (targetMetersPerUnit !== undefined) { + sourceResolution *= targetMetersPerUnit; + } + var sourceMetersPerUnit = sourceProj.getMetersPerUnit(); + if (sourceMetersPerUnit !== undefined) { + sourceResolution /= sourceMetersPerUnit; + } + + // Based on the projection properties, the point resolution at the specified + // coordinates may be slightly different. We need to reverse-compensate this + // in order to achieve optimal results. + + var compensationFactor = + sourceProj.getPointResolution(sourceResolution, sourceCenter) / + sourceResolution; + + if (goog.math.isFiniteNumber(compensationFactor) && compensationFactor > 0) { + sourceResolution /= compensationFactor; + } + + return sourceResolution; +}; + + +/** + * 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 {ol.Coordinate} New point 1 px farther from the centroid. + * @private + */ +ol.reproj.enlargeClipPoint_ = function(centroidX, centroidY, x, y) { + var dX = x - centroidX, dY = y - centroidY; + var 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. + * + * @param {number} width Width of the canvas. + * @param {number} height Height of the canvas. + * @param {number} pixelRatio Pixel ratio. + * @param {number} sourceResolution Source resolution. + * @param {ol.Extent} sourceExtent Extent of the data source. + * @param {number} targetResolution Target resolution. + * @param {ol.Extent} targetExtent Target extent. + * @param {ol.reproj.Triangulation} triangulation Calculated triangulation. + * @param {Array.<{extent: ol.Extent, + * image: (HTMLCanvasElement|Image|HTMLVideoElement)}>} sources + * Array of sources. + * @param {boolean=} opt_renderEdges Render reprojection edges. + * @return {HTMLCanvasElement} Canvas with reprojected data. + */ +ol.reproj.render = function(width, height, pixelRatio, + sourceResolution, sourceExtent, targetResolution, targetExtent, + triangulation, sources, opt_renderEdges) { + + var context = ol.dom.createCanvasContext2D(Math.round(pixelRatio * width), + Math.round(pixelRatio * height)); + + if (sources.length === 0) { + return context.canvas; + } + + context.scale(pixelRatio, pixelRatio); + + var sourceDataExtent = ol.extent.createEmpty(); + sources.forEach(function(src, i, arr) { + ol.extent.extend(sourceDataExtent, src.extent); + }); + + var canvasWidthInUnits = ol.extent.getWidth(sourceDataExtent); + var canvasHeightInUnits = ol.extent.getHeight(sourceDataExtent); + var stitchContext = ol.dom.createCanvasContext2D( + Math.round(pixelRatio * canvasWidthInUnits / sourceResolution), + Math.round(pixelRatio * canvasHeightInUnits / sourceResolution)); + + stitchContext.scale(pixelRatio / sourceResolution, + pixelRatio / sourceResolution); + stitchContext.translate(-sourceDataExtent[0], sourceDataExtent[3]); + + sources.forEach(function(src, i, arr) { + var xPos = src.extent[0]; + var yPos = -src.extent[3]; + var srcWidth = ol.extent.getWidth(src.extent); + var srcHeight = ol.extent.getHeight(src.extent); + + stitchContext.drawImage(src.image, xPos, yPos, srcWidth, srcHeight); + }); + + var targetTopLeft = ol.extent.getTopLeft(targetExtent); + + triangulation.getTriangles().forEach(function(triangle, i, arr) { + /* Calculate affine transform (src -> dst) + * Resulting matrix can be used to transform coordinate + * from `sourceProjection` to destination pixels. + * + * To optimize number of context calls and increase numerical stability, + * we also do the following operations: + * trans(-topLeftExtentCorner), scale(1 / targetResolution), scale(1, -1) + * here before solving the linear system so [ui, vi] are pixel coordinates. + * + * Src points: xi, yi + * Dst points: ui, vi + * Affine coefficients: aij + * + * | x0 y0 1 0 0 0 | |a00| |u0| + * | x1 y1 1 0 0 0 | |a01| |u1| + * | x2 y2 1 0 0 0 | x |a02| = |u2| + * | 0 0 0 x0 y0 1 | |a10| |v0| + * | 0 0 0 x1 y1 1 | |a11| |v1| + * | 0 0 0 x2 y2 1 | |a12| |v2| + */ + var source = triangle.source, target = triangle.target; + var x0 = source[0][0], y0 = source[0][1], + x1 = source[1][0], y1 = source[1][1], + x2 = source[2][0], y2 = source[2][1]; + var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution, + v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution; + var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution, + v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution; + var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution, + v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution; + + // Shift all the source points to improve numerical stability + // of all the subsequent calculations. The [x0, y0] is used here. + // This is also used to simplify the linear system. + var sourceNumericalShiftX = x0, sourceNumericalShiftY = y0; + x0 = 0; + y0 = 0; + x1 -= sourceNumericalShiftX; + y1 -= sourceNumericalShiftY; + x2 -= sourceNumericalShiftX; + y2 -= sourceNumericalShiftY; + + var augmentedMatrix = [ + [x1, y1, 0, 0, u1 - u0], + [x2, y2, 0, 0, u2 - u0], + [0, 0, x1, y1, v1 - v0], + [0, 0, x2, y2, v2 - v0] + ]; + var affineCoefs = ol.math.solveLinearSystem(augmentedMatrix); + if (!affineCoefs) { + return; + } + + context.save(); + context.beginPath(); + if (ol.reproj.browserAntialiasesClip_) { + var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3; + var p0 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u0, v0); + var p1 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u1, v1); + var p2 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u2, v2); + + context.moveTo(p0[0], p0[1]); + context.lineTo(p1[0], p1[1]); + context.lineTo(p2[0], p2[1]); + } else { + context.moveTo(u0, v0); + context.lineTo(u1, v1); + context.lineTo(u2, v2); + } + context.closePath(); + context.clip(); + + context.transform( + affineCoefs[0], affineCoefs[2], affineCoefs[1], affineCoefs[3], u0, v0); + + context.translate(sourceDataExtent[0] - sourceNumericalShiftX, + sourceDataExtent[3] - sourceNumericalShiftY); + + context.scale(sourceResolution / pixelRatio, + -sourceResolution / pixelRatio); + + context.drawImage(stitchContext.canvas, 0, 0); + context.restore(); + }); + + if (opt_renderEdges) { + context.save(); + + context.strokeStyle = 'black'; + context.lineWidth = 1; + + triangulation.getTriangles().forEach(function(triangle, i, arr) { + var target = triangle.target; + var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution, + v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution; + var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution, + v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution; + var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution, + v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution; + + context.beginPath(); + context.moveTo(u0, v0); + context.lineTo(u1, v1); + context.lineTo(u2, v2); + context.closePath(); + context.stroke(); + }); + + context.restore(); + } + return context.canvas; +}; diff --git a/src/ol/reproj/tile.js b/src/ol/reproj/tile.js new file mode 100644 index 0000000000..a1c819d620 --- /dev/null +++ b/src/ol/reproj/tile.js @@ -0,0 +1,332 @@ +goog.provide('ol.reproj.Tile'); +goog.provide('ol.reproj.TileFunctionType'); + +goog.require('goog.asserts'); +goog.require('goog.events'); +goog.require('goog.events.EventType'); +goog.require('goog.math'); +goog.require('goog.object'); +goog.require('ol.Tile'); +goog.require('ol.TileState'); +goog.require('ol.extent'); +goog.require('ol.math'); +goog.require('ol.proj'); +goog.require('ol.reproj'); +goog.require('ol.reproj.Triangulation'); + + +/** + * @typedef {function(number, number, number, number) : ol.Tile} + */ +ol.reproj.TileFunctionType; + + + +/** + * @classdesc + * Class encapsulating single reprojected tile. + * See {@link ol.source.TileImage}. + * + * @constructor + * @extends {ol.Tile} + * @param {ol.proj.Projection} sourceProj Source projection. + * @param {ol.tilegrid.TileGrid} sourceTileGrid Source tile grid. + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.tilegrid.TileGrid} targetTileGrid Target tile grid. + * @param {number} z Zoom level. + * @param {number} x X. + * @param {number} y Y. + * @param {number} pixelRatio Pixel ratio. + * @param {ol.reproj.TileFunctionType} getTileFunction + * Function returning source tiles (z, x, y, pixelRatio). + * @param {number=} opt_errorThreshold Acceptable reprojection error (in px). + * @param {boolean=} opt_renderEdges Render reprojection edges. + */ +ol.reproj.Tile = function(sourceProj, sourceTileGrid, + targetProj, targetTileGrid, z, x, y, pixelRatio, getTileFunction, + opt_errorThreshold, + opt_renderEdges) { + goog.base(this, [z, x, y], ol.TileState.IDLE); + + /** + * @private + * @type {boolean} + */ + this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false; + + /** + * @private + * @type {number} + */ + this.pixelRatio_ = pixelRatio; + + /** + * @private + * @type {HTMLCanvasElement} + */ + this.canvas_ = null; + + /** + * @private + * @type {Object.} + */ + this.canvasByContext_ = {}; + + /** + * @private + * @type {ol.tilegrid.TileGrid} + */ + this.sourceTileGrid_ = sourceTileGrid; + + /** + * @private + * @type {ol.tilegrid.TileGrid} + */ + this.targetTileGrid_ = targetTileGrid; + + /** + * @private + * @type {!Array.} + */ + this.sourceTiles_ = []; + + /** + * @private + * @type {Array.} + */ + this.sourcesListenerKeys_ = null; + + /** + * @private + * @type {number} + */ + this.sourceZ_ = 0; + + var targetExtent = targetTileGrid.getTileCoordExtent(this.getTileCoord()); + var maxTargetExtent = this.targetTileGrid_.getExtent(); + var maxSourceExtent = this.sourceTileGrid_.getExtent(); + + var limitedTargetExtent = maxTargetExtent ? + ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent; + + if (ol.extent.getArea(limitedTargetExtent) === 0) { + // Tile is completely outside range -> EMPTY + // TODO: is it actually correct that the source even creates the tile ? + this.state = ol.TileState.EMPTY; + return; + } + + var sourceProjExtent = sourceProj.getExtent(); + if (sourceProjExtent) { + if (!maxSourceExtent) { + maxSourceExtent = sourceProjExtent; + } else { + maxSourceExtent = ol.extent.getIntersection( + maxSourceExtent, sourceProjExtent); + } + } + + var targetResolution = targetTileGrid.getResolution(z); + + var targetCenter = ol.extent.getCenter(limitedTargetExtent); + var sourceResolution = ol.reproj.calculateSourceResolution( + sourceProj, targetProj, targetCenter, targetResolution); + + if (!goog.math.isFiniteNumber(sourceResolution) || sourceResolution <= 0) { + // invalid sourceResolution -> EMPTY + // probably edges of the projections when no extent is defined + this.state = ol.TileState.EMPTY; + return; + } + + var errorThresholdInPixels = opt_errorThreshold !== undefined ? + opt_errorThreshold : ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD; + + /** + * @private + * @type {!ol.reproj.Triangulation} + */ + this.triangulation_ = new ol.reproj.Triangulation( + sourceProj, targetProj, limitedTargetExtent, maxSourceExtent, + sourceResolution * errorThresholdInPixels); + + if (this.triangulation_.getTriangles().length === 0) { + // no valid triangles -> EMPTY + this.state = ol.TileState.EMPTY; + return; + } + + this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution); + var sourceExtent = this.triangulation_.calculateSourceExtent(); + + if (maxSourceExtent) { + if (sourceProj.canWrapX()) { + sourceExtent[1] = ol.math.clamp( + sourceExtent[1], maxSourceExtent[1], maxSourceExtent[3]); + sourceExtent[3] = ol.math.clamp( + sourceExtent[3], maxSourceExtent[1], maxSourceExtent[3]); + } else { + sourceExtent = ol.extent.getIntersection(sourceExtent, maxSourceExtent); + } + } + + if (!ol.extent.getArea(sourceExtent)) { + this.state = ol.TileState.EMPTY; + } else { + var sourceRange = sourceTileGrid.getTileRangeForExtentAndZ( + sourceExtent, this.sourceZ_); + + var tilesRequired = sourceRange.getWidth() * sourceRange.getHeight(); + if (!goog.asserts.assert( + tilesRequired < ol.RASTER_REPROJECTION_MAX_SOURCE_TILES, + 'reasonable number of tiles is required')) { + this.state = ol.TileState.ERROR; + return; + } + for (var srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) { + for (var srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) { + var tile = getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio); + if (tile) { + this.sourceTiles_.push(tile); + } + } + } + + if (this.sourceTiles_.length === 0) { + this.state = ol.TileState.EMPTY; + } + } +}; +goog.inherits(ol.reproj.Tile, ol.Tile); + + +/** + * @inheritDoc + */ +ol.reproj.Tile.prototype.disposeInternal = function() { + if (this.state == ol.TileState.LOADING) { + this.unlistenSources_(); + } + goog.base(this, 'disposeInternal'); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Tile.prototype.getImage = function(opt_context) { + if (opt_context !== undefined) { + var image; + var key = goog.getUid(opt_context); + if (key in this.canvasByContext_) { + return this.canvasByContext_[key]; + } else if (goog.object.isEmpty(this.canvasByContext_)) { + image = this.canvas_; + } else { + image = /** @type {HTMLCanvasElement} */ (this.canvas_.cloneNode(false)); + } + this.canvasByContext_[key] = image; + return image; + } else { + return this.canvas_; + } +}; + + +/** + * @private + */ +ol.reproj.Tile.prototype.reproject_ = function() { + var sources = []; + this.sourceTiles_.forEach(function(tile, i, arr) { + if (tile && tile.getState() == ol.TileState.LOADED) { + sources.push({ + extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord), + image: tile.getImage() + }); + } + }, this); + this.sourceTiles_.length = 0; + + var tileCoord = this.getTileCoord(); + var z = tileCoord[0]; + var size = this.targetTileGrid_.getTileSize(z); + var width = goog.isNumber(size) ? size : size[0]; + var height = goog.isNumber(size) ? size : size[1]; + var targetResolution = this.targetTileGrid_.getResolution(z); + var sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_); + + var targetExtent = this.targetTileGrid_.getTileCoordExtent(tileCoord); + this.canvas_ = ol.reproj.render(width, height, this.pixelRatio_, + sourceResolution, this.sourceTileGrid_.getExtent(), + targetResolution, targetExtent, this.triangulation_, sources, + this.renderEdges_); + + this.state = ol.TileState.LOADED; + this.changed(); +}; + + +/** + * @inheritDoc + */ +ol.reproj.Tile.prototype.load = function() { + if (this.state == ol.TileState.IDLE) { + this.state = ol.TileState.LOADING; + this.changed(); + + var leftToLoad = 0; + + goog.asserts.assert(!this.sourcesListenerKeys_, + 'this.sourcesListenerKeys_ should be null'); + + this.sourcesListenerKeys_ = []; + this.sourceTiles_.forEach(function(tile, i, arr) { + var state = tile.getState(); + if (state == ol.TileState.IDLE || state == ol.TileState.LOADING) { + leftToLoad++; + + var sourceListenKey; + sourceListenKey = tile.listen(goog.events.EventType.CHANGE, + function(e) { + var state = tile.getState(); + if (state == ol.TileState.LOADED || + state == ol.TileState.ERROR || + state == ol.TileState.EMPTY) { + goog.events.unlistenByKey(sourceListenKey); + leftToLoad--; + goog.asserts.assert(leftToLoad >= 0, + 'leftToLoad should not be negative'); + if (leftToLoad === 0) { + this.unlistenSources_(); + this.reproject_(); + } + } + }, false, this); + this.sourcesListenerKeys_.push(sourceListenKey); + } + }, this); + + this.sourceTiles_.forEach(function(tile, i, arr) { + var state = tile.getState(); + if (state == ol.TileState.IDLE) { + tile.load(); + } + }); + + if (leftToLoad === 0) { + this.reproject_(); + } + } +}; + + +/** + * @private + */ +ol.reproj.Tile.prototype.unlistenSources_ = function() { + goog.asserts.assert(this.sourcesListenerKeys_, + 'this.sourcesListenerKeys_ should not be null'); + this.sourcesListenerKeys_.forEach(goog.events.unlistenByKey); + this.sourcesListenerKeys_ = null; +}; diff --git a/src/ol/reproj/triangulation.js b/src/ol/reproj/triangulation.js new file mode 100644 index 0000000000..571c676c6a --- /dev/null +++ b/src/ol/reproj/triangulation.js @@ -0,0 +1,341 @@ +goog.provide('ol.reproj.Triangulation'); + +goog.require('goog.asserts'); +goog.require('goog.math'); +goog.require('ol.extent'); +goog.require('ol.proj'); + + +/** + * Single triangle; consists of 3 source points and 3 target points. + * + * @typedef {{source: Array., + * target: Array.}} + */ +ol.reproj.Triangle; + + + +/** + * @classdesc + * Class containing triangulation of the given target extent. + * Used for determining source data and the reprojection itself. + * + * @param {ol.proj.Projection} sourceProj Source projection. + * @param {ol.proj.Projection} targetProj Target projection. + * @param {ol.Extent} targetExtent Target extent to triangulate. + * @param {ol.Extent} maxSourceExtent Maximal source extent that can be used. + * @param {number} errorThreshold Acceptable error (in source units). + * @constructor + */ +ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent, + maxSourceExtent, errorThreshold) { + + /** + * @type {ol.proj.Projection} + * @private + */ + this.sourceProj_ = sourceProj; + + /** + * @type {ol.proj.Projection} + * @private + */ + this.targetProj_ = targetProj; + + /** @type {!Object.} */ + var transformInvCache = {}; + var transformInv = ol.proj.getTransform(this.targetProj_, this.sourceProj_); + + /** + * @param {ol.Coordinate} c + * @return {ol.Coordinate} + * @private + */ + this.transformInv_ = function(c) { + var key = c[0] + '/' + c[1]; + if (!transformInvCache[key]) { + transformInvCache[key] = transformInv(c); + } + return transformInvCache[key]; + }; + + /** + * @type {ol.Extent} + * @private + */ + this.maxSourceExtent_ = maxSourceExtent; + + /** + * @type {number} + * @private + */ + this.errorThresholdSquared_ = errorThreshold * errorThreshold; + + /** + * @type {Array.} + * @private + */ + this.triangles_ = []; + + /** + * Indicates that the triangulation crosses edge of the source projection. + * @type {boolean} + * @private + */ + this.wrapsXInSource_ = false; + + /** + * @type {boolean} + * @private + */ + this.canWrapXInSource_ = this.sourceProj_.canWrapX() && + !!maxSourceExtent && + !!this.sourceProj_.getExtent() && + (ol.extent.getWidth(maxSourceExtent) == + ol.extent.getWidth(this.sourceProj_.getExtent())); + + /** + * @type {?number} + * @private + */ + this.sourceWorldWidth_ = this.sourceProj_.getExtent() ? + ol.extent.getWidth(this.sourceProj_.getExtent()) : null; + + /** + * @type {?number} + * @private + */ + this.targetWorldWidth_ = this.targetProj_.getExtent() ? + ol.extent.getWidth(this.targetProj_.getExtent()) : null; + + var destinationTopLeft = ol.extent.getTopLeft(targetExtent); + var destinationTopRight = ol.extent.getTopRight(targetExtent); + var destinationBottomRight = ol.extent.getBottomRight(targetExtent); + var destinationBottomLeft = ol.extent.getBottomLeft(targetExtent); + var sourceTopLeft = this.transformInv_(destinationTopLeft); + var sourceTopRight = this.transformInv_(destinationTopRight); + var sourceBottomRight = this.transformInv_(destinationBottomRight); + var sourceBottomLeft = this.transformInv_(destinationBottomLeft); + + this.addQuad_( + destinationTopLeft, destinationTopRight, + destinationBottomRight, destinationBottomLeft, + sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft, + ol.RASTER_REPROJECTION_MAX_SUBDIVISION); + + if (this.wrapsXInSource_) { + // Fix coordinates (ol.proj returns wrapped coordinates, "unwrap" here). + // This significantly simplifies the rest of the reprojection process. + + goog.asserts.assert(this.sourceWorldWidth_ !== null); + var leftBound = Infinity; + this.triangles_.forEach(function(triangle, i, arr) { + leftBound = Math.min(leftBound, + triangle.source[0][0], triangle.source[1][0], triangle.source[2][0]); + }); + + // Shift triangles to be as close to `leftBound` as possible + // (if the distance is more than `worldWidth / 2` it can be closer. + this.triangles_.forEach(function(triangle) { + if (Math.max(triangle.source[0][0], triangle.source[1][0], + triangle.source[2][0]) - leftBound > this.sourceWorldWidth_ / 2) { + var newTriangle = [[triangle.source[0][0], triangle.source[0][1]], + [triangle.source[1][0], triangle.source[1][1]], + [triangle.source[2][0], triangle.source[2][1]]]; + if ((newTriangle[0][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[0][0] -= this.sourceWorldWidth_; + } + if ((newTriangle[1][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[1][0] -= this.sourceWorldWidth_; + } + if ((newTriangle[2][0] - leftBound) > this.sourceWorldWidth_ / 2) { + newTriangle[2][0] -= this.sourceWorldWidth_; + } + + // Rarely (if the extent contains both the dateline and prime meridian) + // the shift can in turn break some triangles. + // Detect this here and don't shift in such cases. + var minX = Math.min( + newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); + var maxX = Math.max( + newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]); + if ((maxX - minX) < this.sourceWorldWidth_ / 2) { + triangle.source = newTriangle; + } + } + }, this); + } + + transformInvCache = {}; +}; + + +/** + * Adds triangle to the triangulation. + * @param {ol.Coordinate} a + * @param {ol.Coordinate} b + * @param {ol.Coordinate} c + * @param {ol.Coordinate} aSrc + * @param {ol.Coordinate} bSrc + * @param {ol.Coordinate} cSrc + * @private + */ +ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c, + aSrc, bSrc, cSrc) { + this.triangles_.push({ + source: [aSrc, bSrc, cSrc], + target: [a, b, c] + }); +}; + + +/** + * Adds quad (points in clock-wise order) to the triangulation + * (and reprojects the vertices) if valid. + * Performs quad subdivision if needed to increase precision. + * + * @param {ol.Coordinate} a + * @param {ol.Coordinate} b + * @param {ol.Coordinate} c + * @param {ol.Coordinate} d + * @param {ol.Coordinate} aSrc + * @param {ol.Coordinate} bSrc + * @param {ol.Coordinate} cSrc + * @param {ol.Coordinate} dSrc + * @param {number} maxSubdivision Maximal allowed subdivision of the quad. + * @private + */ +ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d, + aSrc, bSrc, cSrc, dSrc, maxSubdivision) { + + var sourceQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]); + var sourceCoverageX = this.sourceWorldWidth_ ? + ol.extent.getWidth(sourceQuadExtent) / this.sourceWorldWidth_ : null; + + // when the quad is wrapped in the source projection + // it covers most of the projection extent, but not fully + var wrapsX = this.sourceProj_.canWrapX() && + sourceCoverageX > 0.5 && sourceCoverageX < 1; + + var needsSubdivision = false; + + if (maxSubdivision > 0) { + if (this.targetProj_.isGlobal() && this.targetWorldWidth_) { + var targetQuadExtent = ol.extent.boundingExtent([a, b, c, d]); + var targetCoverageX = + ol.extent.getWidth(targetQuadExtent) / this.targetWorldWidth_; + needsSubdivision |= + targetCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; + } + if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) { + needsSubdivision |= + sourceCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH; + } + } + + if (!needsSubdivision && this.maxSourceExtent_) { + if (!ol.extent.intersects(sourceQuadExtent, this.maxSourceExtent_)) { + // whole quad outside source projection extent -> ignore + return; + } + } + + if (!needsSubdivision) { + if (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) || + !isFinite(bSrc[0]) || !isFinite(bSrc[1]) || + !isFinite(cSrc[0]) || !isFinite(cSrc[1]) || + !isFinite(dSrc[0]) || !isFinite(dSrc[1])) { + if (maxSubdivision > 0) { + needsSubdivision = true; + } else { + return; + } + } + } + + if (maxSubdivision > 0) { + if (!needsSubdivision) { + var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2]; + var centerSrc = this.transformInv_(center); + + var dx; + if (wrapsX) { + goog.asserts.assert(this.sourceWorldWidth_); + var centerSrcEstimX = + (goog.math.modulo(aSrc[0], this.sourceWorldWidth_) + + goog.math.modulo(cSrc[0], this.sourceWorldWidth_)) / 2; + dx = centerSrcEstimX - + goog.math.modulo(centerSrc[0], this.sourceWorldWidth_); + } else { + dx = (aSrc[0] + cSrc[0]) / 2 - centerSrc[0]; + } + var dy = (aSrc[1] + cSrc[1]) / 2 - centerSrc[1]; + var centerSrcErrorSquared = dx * dx + dy * dy; + needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_; + } + if (needsSubdivision) { + if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) { + // split horizontally (top & bottom) + var bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2]; + var bcSrc = this.transformInv_(bc); + var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2]; + var daSrc = this.transformInv_(da); + + this.addQuad_( + a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdivision - 1); + this.addQuad_( + da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdivision - 1); + } else { + // split vertically (left & right) + var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2]; + var abSrc = this.transformInv_(ab); + var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2]; + var cdSrc = this.transformInv_(cd); + + this.addQuad_( + a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdivision - 1); + this.addQuad_( + ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdivision - 1); + } + return; + } + } + + if (wrapsX) { + if (!this.canWrapXInSource_) { + return; + } + this.wrapsXInSource_ = true; + } + + this.addTriangle_(a, c, d, aSrc, cSrc, dSrc); + this.addTriangle_(a, b, c, aSrc, bSrc, cSrc); +}; + + +/** + * Calculates extent of the 'source' coordinates from all the triangles. + * + * @return {ol.Extent} Calculated extent. + */ +ol.reproj.Triangulation.prototype.calculateSourceExtent = function() { + var extent = ol.extent.createEmpty(); + + this.triangles_.forEach(function(triangle, i, arr) { + var src = triangle.source; + ol.extent.extendCoordinate(extent, src[0]); + ol.extent.extendCoordinate(extent, src[1]); + ol.extent.extendCoordinate(extent, src[2]); + }); + + return extent; +}; + + +/** + * @return {Array.} Array of the calculated triangles. + */ +ol.reproj.Triangulation.prototype.getTriangles = function() { + return this.triangles_; +}; diff --git a/src/ol/source/bingmapssource.js b/src/ol/source/bingmapssource.js index 998d439442..8c42d1b152 100644 --- a/src/ol/source/bingmapssource.js +++ b/src/ol/source/bingmapssource.js @@ -29,6 +29,7 @@ ol.source.BingMaps = function(options) { crossOrigin: 'anonymous', opaque: true, projection: ol.proj.get('EPSG:3857'), + reprojectionErrorThreshold: options.reprojectionErrorThreshold, state: ol.source.State.LOADING, tileLoadFunction: options.tileLoadFunction, wrapX: options.wrapX !== undefined ? options.wrapX : true diff --git a/src/ol/source/imagecanvassource.js b/src/ol/source/imagecanvassource.js index 8532e8b313..57f3d8b5c3 100644 --- a/src/ol/source/imagecanvassource.js +++ b/src/ol/source/imagecanvassource.js @@ -59,7 +59,7 @@ goog.inherits(ol.source.ImageCanvas, ol.source.Image); /** * @inheritDoc */ -ol.source.ImageCanvas.prototype.getImage = +ol.source.ImageCanvas.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { resolution = this.findNearestResolution(resolution); diff --git a/src/ol/source/imagemapguidesource.js b/src/ol/source/imagemapguidesource.js index 031becffb9..2ff6a92701 100644 --- a/src/ol/source/imagemapguidesource.js +++ b/src/ol/source/imagemapguidesource.js @@ -126,7 +126,7 @@ ol.source.ImageMapGuide.prototype.getParams = function() { /** * @inheritDoc */ -ol.source.ImageMapGuide.prototype.getImage = +ol.source.ImageMapGuide.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { resolution = this.findNearestResolution(resolution); pixelRatio = this.hidpi_ ? pixelRatio : 1; diff --git a/src/ol/source/imagesource.js b/src/ol/source/imagesource.js index 3bd9afe517..a0a4b4cb09 100644 --- a/src/ol/source/imagesource.js +++ b/src/ol/source/imagesource.js @@ -5,9 +5,11 @@ goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.events.Event'); goog.require('ol.Attribution'); -goog.require('ol.Extent'); goog.require('ol.ImageState'); goog.require('ol.array'); +goog.require('ol.extent'); +goog.require('ol.proj'); +goog.require('ol.reproj.Image'); goog.require('ol.source.Source'); @@ -56,6 +58,20 @@ ol.source.Image = function(options) { return b - a; }, true), 'resolutions must be null or sorted in descending order'); + + /** + * @private + * @type {ol.reproj.Image} + */ + this.reprojectedImage_ = null; + + + /** + * @private + * @type {number} + */ + this.reprojectedRevision_ = 0; + }; goog.inherits(ol.source.Image, ol.source.Source); @@ -90,7 +106,53 @@ ol.source.Image.prototype.findNearestResolution = * @param {ol.proj.Projection} projection Projection. * @return {ol.ImageBase} Single image. */ -ol.source.Image.prototype.getImage = goog.abstractMethod; +ol.source.Image.prototype.getImage = + function(extent, resolution, pixelRatio, projection) { + var sourceProjection = this.getProjection(); + if (!ol.ENABLE_RASTER_REPROJECTION || + !sourceProjection || + !projection || + ol.proj.equivalent(sourceProjection, projection)) { + if (sourceProjection) { + projection = sourceProjection; + } + return this.getImageInternal(extent, resolution, pixelRatio, projection); + } else { + if (this.reprojectedImage_) { + if (this.reprojectedRevision_ == this.getRevision() && + ol.proj.equivalent( + this.reprojectedImage_.getProjection(), projection) && + this.reprojectedImage_.getResolution() == resolution && + this.reprojectedImage_.getPixelRatio() == pixelRatio && + ol.extent.equals(this.reprojectedImage_.getExtent(), extent)) { + return this.reprojectedImage_; + } + this.reprojectedImage_.dispose(); + this.reprojectedImage_ = null; + } + + this.reprojectedImage_ = new ol.reproj.Image( + sourceProjection, projection, extent, resolution, pixelRatio, + goog.bind(function(extent, resolution, pixelRatio) { + return this.getImageInternal(extent, resolution, + pixelRatio, sourceProjection); + }, this)); + this.reprojectedRevision_ = this.getRevision(); + + return this.reprojectedImage_; + } +}; + + +/** + * @param {ol.Extent} extent Extent. + * @param {number} resolution Resolution. + * @param {number} pixelRatio Pixel ratio. + * @param {ol.proj.Projection} projection Projection. + * @return {ol.ImageBase} Single image. + * @protected + */ +ol.source.Image.prototype.getImageInternal = goog.abstractMethod; /** diff --git a/src/ol/source/imagestaticsource.js b/src/ol/source/imagestaticsource.js index 8bffee68fe..f6e57ce82d 100644 --- a/src/ol/source/imagestaticsource.js +++ b/src/ol/source/imagestaticsource.js @@ -62,7 +62,7 @@ goog.inherits(ol.source.ImageStatic, ol.source.Image); /** * @inheritDoc */ -ol.source.ImageStatic.prototype.getImage = +ol.source.ImageStatic.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { if (ol.extent.intersects(extent, this.image_.getExtent())) { return this.image_; diff --git a/src/ol/source/imagewmssource.js b/src/ol/source/imagewmssource.js index e9b49b95fd..72eabf98fb 100644 --- a/src/ol/source/imagewmssource.js +++ b/src/ol/source/imagewmssource.js @@ -185,7 +185,7 @@ ol.source.ImageWMS.prototype.getParams = function() { /** * @inheritDoc */ -ol.source.ImageWMS.prototype.getImage = +ol.source.ImageWMS.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) { if (this.url_ === undefined) { diff --git a/src/ol/source/mapquestsource.js b/src/ol/source/mapquestsource.js index 69ecc336b7..49e5241b24 100644 --- a/src/ol/source/mapquestsource.js +++ b/src/ol/source/mapquestsource.js @@ -40,6 +40,7 @@ ol.source.MapQuest = function(opt_options) { crossOrigin: 'anonymous', logo: 'https://developer.mapquest.com/content/osm/mq_logo.png', maxZoom: layerConfig.maxZoom, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, opaque: true, tileLoadFunction: options.tileLoadFunction, url: url diff --git a/src/ol/source/osmsource.js b/src/ol/source/osmsource.js index af17595c5e..f00859485e 100644 --- a/src/ol/source/osmsource.js +++ b/src/ol/source/osmsource.js @@ -36,6 +36,7 @@ ol.source.OSM = function(opt_options) { crossOrigin: crossOrigin, opaque: true, maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileLoadFunction: options.tileLoadFunction, url: url, wrapX: options.wrapX diff --git a/src/ol/source/stamensource.js b/src/ol/source/stamensource.js index f49f2d8b98..733542835e 100644 --- a/src/ol/source/stamensource.js +++ b/src/ol/source/stamensource.js @@ -109,6 +109,7 @@ ol.source.Stamen = function(options) { // FIXME uncomment the following when tilegrid supports minZoom //minZoom: providerConfig.minZoom, opaque: layerConfig.opaque, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileLoadFunction: options.tileLoadFunction, url: url }); diff --git a/src/ol/source/tilearcgisrestsource.js b/src/ol/source/tilearcgisrestsource.js index 3a105fe646..76ba2c336e 100644 --- a/src/ol/source/tilearcgisrestsource.js +++ b/src/ol/source/tilearcgisrestsource.js @@ -41,6 +41,7 @@ ol.source.TileArcGISRest = function(opt_options) { crossOrigin: options.crossOrigin, logo: options.logo, projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, tileUrlFunction: goog.bind(this.tileUrlFunction_, this), diff --git a/src/ol/source/tileimagesource.js b/src/ol/source/tileimagesource.js index fa92f0671d..964e7d7690 100644 --- a/src/ol/source/tileimagesource.js +++ b/src/ol/source/tileimagesource.js @@ -3,12 +3,16 @@ goog.provide('ol.source.TileImage'); goog.require('goog.asserts'); goog.require('goog.events'); goog.require('goog.events.EventType'); +goog.require('goog.object'); goog.require('ol.ImageTile'); +goog.require('ol.TileCache'); goog.require('ol.TileCoord'); goog.require('ol.TileLoadFunctionType'); goog.require('ol.TileState'); goog.require('ol.TileUrlFunction'); goog.require('ol.TileUrlFunctionType'); +goog.require('ol.proj'); +goog.require('ol.reproj.Tile'); goog.require('ol.source.Tile'); goog.require('ol.source.TileEvent'); @@ -69,6 +73,29 @@ ol.source.TileImage = function(options) { this.tileClass = options.tileClass !== undefined ? options.tileClass : ol.ImageTile; + /** + * @protected + * @type {Object.} + */ + this.tileCacheForProjection = {}; + + /** + * @protected + * @type {Object.} + */ + this.tileGridForProjection = {}; + + /** + * @private + * @type {number|undefined} + */ + this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold; + + /** + * @private + * @type {boolean} + */ + this.renderReprojectionEdges_ = false; }; goog.inherits(ol.source.TileImage, ol.source.Tile); @@ -82,11 +109,129 @@ ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) { }; +/** + * @inheritDoc + */ +ol.source.TileImage.prototype.canExpireCache = function() { + if (!ol.ENABLE_RASTER_REPROJECTION) { + return goog.base(this, 'canExpireCache'); + } + var canExpire = this.tileCache.canExpireCache(); + if (canExpire) { + return true; + } else { + return goog.object.some(this.tileCacheForProjection, function(tileCache) { + return tileCache.canExpireCache(); + }); + } +}; + + +/** + * @inheritDoc + */ +ol.source.TileImage.prototype.expireCache = function(projection, usedTiles) { + if (!ol.ENABLE_RASTER_REPROJECTION) { + goog.base(this, 'expireCache', projection, usedTiles); + return; + } + var usedTileCache = this.getTileCacheForProjection(projection); + + this.tileCache.expireCache(this.tileCache == usedTileCache ? usedTiles : {}); + goog.object.forEach(this.tileCacheForProjection, function(tileCache) { + tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {}); + }); +}; + + +/** + * @inheritDoc + */ +ol.source.TileImage.prototype.getTileGridForProjection = function(projection) { + if (!ol.ENABLE_RASTER_REPROJECTION) { + return goog.base(this, 'getTileGridForProjection', projection); + } + var thisProj = this.getProjection(); + if (this.tileGrid && + (!thisProj || ol.proj.equivalent(thisProj, projection))) { + return this.tileGrid; + } else { + var projKey = goog.getUid(projection).toString(); + if (!(projKey in this.tileGridForProjection)) { + this.tileGridForProjection[projKey] = + ol.tilegrid.getForProjection(projection); + } + return this.tileGridForProjection[projKey]; + } +}; + + +/** + * @inheritDoc + */ +ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) { + if (!ol.ENABLE_RASTER_REPROJECTION) { + return goog.base(this, 'getTileCacheForProjection', projection); + } + var thisProj = this.getProjection(); + if (!thisProj || ol.proj.equivalent(thisProj, projection)) { + return this.tileCache; + } else { + var projKey = goog.getUid(projection).toString(); + if (!(projKey in this.tileCacheForProjection)) { + this.tileCacheForProjection[projKey] = new ol.TileCache(); + } + return this.tileCacheForProjection[projKey]; + } +}; + + /** * @inheritDoc */ ol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) { + if (!ol.ENABLE_RASTER_REPROJECTION || + !this.getProjection() || + !projection || + ol.proj.equivalent(this.getProjection(), projection)) { + return this.getTileInternal(z, x, y, pixelRatio, projection); + } else { + var cache = this.getTileCacheForProjection(projection); + var tileCoordKey = this.getKeyZXY(z, x, y); + if (cache.containsKey(tileCoordKey)) { + return /** @type {!ol.Tile} */(cache.get(tileCoordKey)); + } else { + var sourceProjection = this.getProjection(); + var sourceTileGrid = this.getTileGridForProjection(sourceProjection); + var targetTileGrid = this.getTileGridForProjection(projection); + var tile = new ol.reproj.Tile( + sourceProjection, sourceTileGrid, + projection, targetTileGrid, + z, x, y, this.getTilePixelRatio(), + goog.bind(function(z, x, y, pixelRatio) { + return this.getTileInternal(z, x, y, pixelRatio, sourceProjection); + }, this), this.reprojectionErrorThreshold_, + this.renderReprojectionEdges_); + + cache.set(tileCoordKey, tile); + return tile; + } + } +}; + + +/** + * @param {number} z Tile coordinate z. + * @param {number} x Tile coordinate x. + * @param {number} y Tile coordinate y. + * @param {number} pixelRatio Pixel ratio. + * @param {ol.proj.Projection} projection Projection. + * @return {!ol.Tile} Tile. + * @protected + */ +ol.source.TileImage.prototype.getTileInternal = + function(z, x, y, pixelRatio, projection) { var tileCoordKey = this.getKeyZXY(z, x, y); if (this.tileCache.containsKey(tileCoordKey)) { return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); @@ -156,6 +301,50 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) { }; +/** + * Sets whether to render reprojection edges or not (usually for debugging). + * @param {boolean} render Render the edges. + * @api + */ +ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) { + if (!ol.ENABLE_RASTER_REPROJECTION || + this.renderReprojectionEdges_ == render) { + return; + } + this.renderReprojectionEdges_ = render; + goog.object.forEach(this.tileCacheForProjection, function(tileCache) { + tileCache.clear(); + }); + this.changed(); +}; + + +/** + * Sets the tile grid to use when reprojecting the tiles to the given + * projection instead of the default tile grid for the projection. + * + * This can be useful when the default tile grid cannot be created + * (e.g. projection has no extent defined) or + * for optimization reasons (custom tile size, resolutions, ...). + * + * @param {ol.proj.ProjectionLike} projection Projection. + * @param {ol.tilegrid.TileGrid} tilegrid Tile grid to use for the projection. + * @api + */ +ol.source.TileImage.prototype.setTileGridForProjection = + function(projection, tilegrid) { + if (ol.ENABLE_RASTER_REPROJECTION) { + var proj = ol.proj.get(projection); + if (proj) { + var projKey = goog.getUid(proj).toString(); + if (!(projKey in this.tileGridForProjection)) { + this.tileGridForProjection[projKey] = tilegrid; + } + } + } +}; + + /** * Set the tile load function of the source. * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function. @@ -163,6 +352,7 @@ ol.source.TileImage.prototype.handleTileChange_ = function(event) { */ ol.source.TileImage.prototype.setTileLoadFunction = function(tileLoadFunction) { this.tileCache.clear(); + this.tileCacheForProjection = {}; this.tileLoadFunction = tileLoadFunction; this.changed(); }; @@ -178,6 +368,7 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) { // FIXME cache. The tile URL function would need to be incorporated into the // FIXME cache key somehow. this.tileCache.clear(); + this.tileCacheForProjection = {}; this.tileUrlFunction = tileUrlFunction; this.changed(); }; @@ -186,9 +377,10 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) { /** * @inheritDoc */ -ol.source.TileImage.prototype.useTile = function(z, x, y) { +ol.source.TileImage.prototype.useTile = function(z, x, y, projection) { + var tileCache = this.getTileCacheForProjection(projection); var tileCoordKey = this.getKeyZXY(z, x, y); - if (this.tileCache.containsKey(tileCoordKey)) { - this.tileCache.get(tileCoordKey); + if (tileCache && tileCache.containsKey(tileCoordKey)) { + tileCache.get(tileCoordKey); } }; diff --git a/src/ol/source/tilejsonsource.js b/src/ol/source/tilejsonsource.js index e1c3f854fc..bf82741004 100644 --- a/src/ol/source/tilejsonsource.js +++ b/src/ol/source/tilejsonsource.js @@ -34,6 +34,7 @@ ol.source.TileJSON = function(options) { attributions: options.attributions, crossOrigin: options.crossOrigin, projection: ol.proj.get('EPSG:3857'), + reprojectionErrorThreshold: options.reprojectionErrorThreshold, state: ol.source.State.LOADING, tileLoadFunction: options.tileLoadFunction, wrapX: options.wrapX !== undefined ? options.wrapX : true diff --git a/src/ol/source/tilesource.js b/src/ol/source/tilesource.js index 111fb6cf18..e47afe0d3f 100644 --- a/src/ol/source/tilesource.js +++ b/src/ol/source/tilesource.js @@ -10,6 +10,7 @@ goog.require('ol.Extent'); goog.require('ol.TileCache'); goog.require('ol.TileRange'); goog.require('ol.TileState'); +goog.require('ol.proj'); goog.require('ol.size'); goog.require('ol.source.Source'); goog.require('ol.tilecoord'); @@ -97,14 +98,19 @@ ol.source.Tile.prototype.canExpireCache = function() { /** + * @param {ol.proj.Projection} projection Projection. * @param {Object.} usedTiles Used tiles. */ -ol.source.Tile.prototype.expireCache = function(usedTiles) { - this.tileCache.expireCache(usedTiles); +ol.source.Tile.prototype.expireCache = function(projection, usedTiles) { + var tileCache = this.getTileCacheForProjection(projection); + if (tileCache) { + tileCache.expireCache(usedTiles); + } }; /** + * @param {ol.proj.Projection} projection Projection. * @param {number} z Zoom level. * @param {ol.TileRange} tileRange Tile range. * @param {function(ol.Tile):(boolean|undefined)} callback Called with each @@ -112,15 +118,21 @@ ol.source.Tile.prototype.expireCache = function(usedTiles) { * considered loaded. * @return {boolean} The tile range is fully covered with loaded tiles. */ -ol.source.Tile.prototype.forEachLoadedTile = function(z, tileRange, callback) { +ol.source.Tile.prototype.forEachLoadedTile = + function(projection, z, tileRange, callback) { + var tileCache = this.getTileCacheForProjection(projection); + if (!tileCache) { + return false; + } + var covered = true; var tile, tileCoordKey, loaded; for (var x = tileRange.minX; x <= tileRange.maxX; ++x) { for (var y = tileRange.minY; y <= tileRange.maxY; ++y) { tileCoordKey = this.getKeyZXY(z, x, y); loaded = false; - if (this.tileCache.containsKey(tileCoordKey)) { - tile = /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); + if (tileCache.containsKey(tileCoordKey)) { + tile = /** @type {!ol.Tile} */ (tileCache.get(tileCoordKey)); loaded = tile.getState() === ol.TileState.LOADED; if (loaded) { loaded = (callback(tile) !== false); @@ -174,7 +186,7 @@ ol.source.Tile.prototype.getResolutions = function() { * @param {number} x Tile coordinate x. * @param {number} y Tile coordinate y. * @param {number} pixelRatio Pixel ratio. - * @param {ol.proj.Projection=} opt_projection Projection. + * @param {ol.proj.Projection} projection Projection. * @return {!ol.Tile} Tile. */ ol.source.Tile.prototype.getTile = goog.abstractMethod; @@ -203,6 +215,29 @@ ol.source.Tile.prototype.getTileGridForProjection = function(projection) { }; +/** + * @param {ol.proj.Projection} projection Projection. + * @return {ol.TileCache} Tile cache. + * @protected + */ +ol.source.Tile.prototype.getTileCacheForProjection = function(projection) { + var thisProj = this.getProjection(); + if (thisProj && !ol.proj.equivalent(thisProj, projection)) { + return null; + } else { + return this.tileCache; + } +}; + + +/** + * @return {number} Tile pixel ratio. + */ +ol.source.Tile.prototype.getTilePixelRatio = function() { + return this.tilePixelRatio_; +}; + + /** * @param {number} z Z. * @param {number} pixelRatio Pixel ratio. @@ -244,6 +279,7 @@ ol.source.Tile.prototype.getTileCoordForTileUrlFunction = * @param {number} z Tile coordinate z. * @param {number} x Tile coordinate x. * @param {number} y Tile coordinate y. + * @param {ol.proj.Projection} projection Projection. */ ol.source.Tile.prototype.useTile = ol.nullFunction; diff --git a/src/ol/source/tilewmssource.js b/src/ol/source/tilewmssource.js index e3ad0ed0d3..265b607a2d 100644 --- a/src/ol/source/tilewmssource.js +++ b/src/ol/source/tilewmssource.js @@ -45,6 +45,7 @@ ol.source.TileWMS = function(opt_options) { logo: options.logo, opaque: !transparent, projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileGrid: options.tileGrid, tileLoadFunction: options.tileLoadFunction, tileUrlFunction: goog.bind(this.tileUrlFunction_, this), diff --git a/src/ol/source/wmtssource.js b/src/ol/source/wmtssource.js index c80e745752..854f1062d5 100644 --- a/src/ol/source/wmtssource.js +++ b/src/ol/source/wmtssource.js @@ -185,6 +185,7 @@ ol.source.WMTS = function(options) { crossOrigin: options.crossOrigin, logo: options.logo, projection: options.projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileClass: options.tileClass, tileGrid: tileGrid, tileLoadFunction: options.tileLoadFunction, diff --git a/src/ol/source/xyzsource.js b/src/ol/source/xyzsource.js index 1863cdb574..17c1f46ed7 100644 --- a/src/ol/source/xyzsource.js +++ b/src/ol/source/xyzsource.js @@ -49,6 +49,7 @@ ol.source.XYZ = function(options) { crossOrigin: options.crossOrigin, logo: options.logo, projection: projection, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileGrid: tileGrid, tileLoadFunction: options.tileLoadFunction, tilePixelRatio: options.tilePixelRatio, diff --git a/src/ol/source/zoomifysource.js b/src/ol/source/zoomifysource.js index f9533a74b0..bcddc08af1 100644 --- a/src/ol/source/zoomifysource.js +++ b/src/ol/source/zoomifysource.js @@ -124,6 +124,7 @@ ol.source.Zoomify = function(opt_options) { attributions: options.attributions, crossOrigin: options.crossOrigin, logo: options.logo, + reprojectionErrorThreshold: options.reprojectionErrorThreshold, tileClass: ol.source.ZoomifyTile_, tileGrid: tileGrid, tileUrlFunction: tileUrlFunction diff --git a/test/spec/ol/math.test.js b/test/spec/ol/math.test.js index f299028b8d..ba781f75da 100644 --- a/test/spec/ol/math.test.js +++ b/test/spec/ol/math.test.js @@ -91,6 +91,45 @@ describe('ol.math.roundUpToPowerOfTwo', function() { }); +describe('ol.math.solveLinearSystem', function() { + it('calculates correctly', function() { + var result = ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [6, 8, 18, 5] + ]); + expect(result[0]).to.roughlyEqual(0.3, 1e-9); + expect(result[1]).to.roughlyEqual(0.4, 1e-9); + expect(result[2]).to.roughlyEqual(0, 1e-9); + }); + it('can handle singular matrix', function() { + var result = ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [2, 1, 3, 1] + ]); + expect(result).to.be(null); + }); + it('raises an exception when the matrix is malformed', function() { + expect(function() { + ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [6, 8, 18] + ]); + }).to.throwException(); + + expect(function() { + ol.math.solveLinearSystem([ + [2, 1, 3, 1], + [2, 6, 8, 3], + [6, 8, 18, 5, 0] + ]); + }).to.throwException(); + }); +}); + + describe('ol.math.toDegrees', function() { it('returns the correct value at -π', function() { expect(ol.math.toDegrees(-Math.PI)).to.be(-180); diff --git a/test/spec/ol/reproj/image.test.js b/test/spec/ol/reproj/image.test.js new file mode 100644 index 0000000000..13d8a8627a --- /dev/null +++ b/test/spec/ol/reproj/image.test.js @@ -0,0 +1,60 @@ +goog.provide('ol.test.reproj.Image'); + +describe('ol.reproj.Image', function() { + function createImage(pixelRatio) { + return new ol.reproj.Image( + ol.proj.get('EPSG:3857'), ol.proj.get('EPSG:4326'), + [-180, -85, 180, 85], 10, pixelRatio, + function(extent, resolution, pixelRatio) { + return new ol.Image(extent, resolution, pixelRatio, [], + 'data:image/gif;base64,' + + 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=', null, + function(image, src) { + image.getImage().src = src; + }); + }); + } + + it('changes state as expected', function(done) { + var image = createImage(1); + expect(image.getState()).to.be(ol.ImageState.IDLE); + image.listen('change', function() { + if (image.getState() == ol.ImageState.LOADED) { + done(); + } + }); + image.load(); + }); + + it('returns correct canvas size', function(done) { + var image = createImage(1); + image.listen('change', function() { + if (image.getState() == ol.ImageState.LOADED) { + var canvas = image.getImage(); + expect(canvas.width).to.be(36); + expect(canvas.height).to.be(17); + done(); + } + }); + image.load(); + }); + + it('respects pixelRatio', function(done) { + var image = createImage(2); + image.listen('change', function() { + if (image.getState() == ol.ImageState.LOADED) { + var canvas = image.getImage(); + expect(canvas.width).to.be(72); + expect(canvas.height).to.be(34); + done(); + } + }); + image.load(); + }); +}); + + +goog.require('ol.Image'); +goog.require('ol.ImageState'); +goog.require('ol.proj'); +goog.require('ol.reproj.Image'); diff --git a/test/spec/ol/reproj/reproj.test.js b/test/spec/ol/reproj/reproj.test.js new file mode 100644 index 0000000000..44283a86c1 --- /dev/null +++ b/test/spec/ol/reproj/reproj.test.js @@ -0,0 +1,45 @@ +goog.provide('ol.test.reproj'); + +describe('ol.reproj', function() { + + describe('#calculateSourceResolution', function() { + var proj3857 = ol.proj.get('EPSG:3857'); + var proj4326 = ol.proj.get('EPSG:4326'); + var origin = [0, 0]; + var point3857 = [50, 40]; + var point4326 = ol.proj.transform(point3857, proj3857, proj4326); + + it('is identity for identical projection', function() { + var result; + var resolution = 500; + result = ol.reproj.calculateSourceResolution( + proj3857, proj3857, origin, resolution); + expect(result).to.be(resolution); + + result = ol.reproj.calculateSourceResolution( + proj3857, proj3857, point3857, resolution); + expect(result).to.be(resolution); + + result = ol.reproj.calculateSourceResolution( + proj4326, proj4326, point4326, resolution); + expect(result).to.be(resolution); + }); + + it('calculates correctly', function() { + var resolution4326 = 5; + + var resolution3857 = ol.reproj.calculateSourceResolution( + proj3857, proj4326, point4326, resolution4326); + expect(resolution3857).not.to.be(resolution4326); + expect(resolution3857).to.roughlyEqual(555974.3714343394, 1e-6); + + var result = ol.reproj.calculateSourceResolution( + proj4326, proj3857, point3857, resolution3857); + expect(result).to.be(resolution4326); + }); + }); +}); + + +goog.require('ol.reproj'); +goog.require('ol.proj'); diff --git a/test/spec/ol/reproj/tile.test.js b/test/spec/ol/reproj/tile.test.js new file mode 100644 index 0000000000..1354b804fb --- /dev/null +++ b/test/spec/ol/reproj/tile.test.js @@ -0,0 +1,100 @@ +goog.provide('ol.test.reproj.Tile'); + +describe('ol.reproj.Tile', function() { + beforeEach(function() { + proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 ' + + '+k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); + var proj27700 = ol.proj.get('EPSG:27700'); + proj27700.setExtent([0, 0, 700000, 1300000]); + }); + + afterEach(function() { + delete proj4.defs['EPSG:27700']; + }); + + + function createTile(pixelRatio, opt_tileSize) { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj3857 = ol.proj.get('EPSG:3857'); + return new ol.reproj.Tile( + proj3857, ol.tilegrid.createForProjection(proj3857), proj4326, + ol.tilegrid.createForProjection(proj4326, 3, opt_tileSize), + 3, 2, -2, pixelRatio, function(z, x, y, pixelRatio) { + return new ol.ImageTile([z, x, y], ol.TileState.IDLE, + 'data:image/gif;base64,' + + 'R0lGODlhAQABAIAAAP///wAAACwAAAAAAQABAAACAkQBADs=', null, + function(tile, src) { + tile.getImage().src = src; + }); + }); + } + + it('changes state as expected', function(done) { + var tile = createTile(1); + expect(tile.getState()).to.be(ol.TileState.IDLE); + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + done(); + } + }); + tile.load(); + }); + + it('is empty when outside target tile grid', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj3857 = ol.proj.get('EPSG:3857'); + var tile = new ol.reproj.Tile( + proj3857, ol.tilegrid.createForProjection(proj3857), + proj4326, ol.tilegrid.createForProjection(proj4326), + 0, -1, 0, 1, function() { + expect().fail('No tiles should be required'); + }); + expect(tile.getState()).to.be(ol.TileState.EMPTY); + }); + + it('is empty when outside source tile grid', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj27700 = ol.proj.get('EPSG:27700'); + var tile = new ol.reproj.Tile( + proj27700, ol.tilegrid.createForProjection(proj27700), + proj4326, ol.tilegrid.createForProjection(proj4326), + 3, 2, -2, 1, function() { + expect().fail('No tiles should be required'); + }); + expect(tile.getState()).to.be(ol.TileState.EMPTY); + }); + + it('respects tile size of target tile grid', function(done) { + var tile = createTile(1, [100, 40]); + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + var canvas = tile.getImage(); + expect(canvas.width).to.be(100); + expect(canvas.height).to.be(40); + done(); + } + }); + tile.load(); + }); + + it('respects pixelRatio', function(done) { + var tile = createTile(3, [60, 20]); + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + var canvas = tile.getImage(); + expect(canvas.width).to.be(180); + expect(canvas.height).to.be(60); + done(); + } + }); + tile.load(); + }); +}); + + +goog.require('ol.ImageTile'); +goog.require('ol.TileState'); +goog.require('ol.proj'); +goog.require('ol.reproj.Tile'); diff --git a/test/spec/ol/reproj/triangulation.test.js b/test/spec/ol/reproj/triangulation.test.js new file mode 100644 index 0000000000..3142498ff9 --- /dev/null +++ b/test/spec/ol/reproj/triangulation.test.js @@ -0,0 +1,44 @@ +goog.provide('ol.test.reproj.Triangulation'); + +describe('ol.reproj.Triangulation', function() { + beforeEach(function() { + proj4.defs('EPSG:27700', '+proj=tmerc +lat_0=49 +lon_0=-2 ' + + '+k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy ' + + '+towgs84=446.448,-125.157,542.06,0.15,0.247,0.842,-20.489 ' + + '+units=m +no_defs'); + var proj27700 = ol.proj.get('EPSG:27700'); + proj27700.setExtent([0, 0, 700000, 1300000]); + }); + + afterEach(function() { + delete proj4.defs['EPSG:27700']; + }); + + describe('constructor', function() { + it('is trivial for identity', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var triangulation = new ol.reproj.Triangulation(proj4326, proj4326, + [20, 20, 30, 30], [-180, -90, 180, 90], 0); + expect(triangulation.getTriangles().length).to.be(2); + }); + + it('is empty when outside source extent', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var proj27700 = ol.proj.get('EPSG:27700'); + var triangulation = new ol.reproj.Triangulation(proj27700, proj4326, + [0, 0, 10, 10], proj27700.getExtent(), 0); + expect(triangulation.getTriangles().length).to.be(0); + }); + + it('can handle null source extent', function() { + var proj4326 = ol.proj.get('EPSG:4326'); + var triangulation = new ol.reproj.Triangulation(proj4326, proj4326, + [20, 20, 30, 30], null, 0); + expect(triangulation.getTriangles().length).to.be(2); + }); + }); +}); + + +goog.require('ol.proj'); +goog.require('ol.reproj.Triangulation'); diff --git a/test/spec/ol/source/tileimagesource.test.js b/test/spec/ol/source/tileimagesource.test.js new file mode 100644 index 0000000000..2b90a23717 --- /dev/null +++ b/test/spec/ol/source/tileimagesource.test.js @@ -0,0 +1,94 @@ +goog.provide('ol.test.source.TileImageSource'); + +describe('ol.source.TileImage', function() { + function createSource(opt_proj, opt_tileGrid) { + var proj = opt_proj || 'EPSG:3857'; + return new ol.source.TileImage({ + projection: proj, + tileGrid: opt_tileGrid || + ol.tilegrid.createForProjection(proj, undefined, [2, 2]), + tileUrlFunction: ol.TileUrlFunction.createFromTemplate( + 'data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA=') + }); + } + + describe('#setTileGridForProjection', function() { + it('uses the tilegrid for given projection', function() { + var source = createSource(); + var tileGrid = ol.tilegrid.createForProjection('EPSG:4326', 3, [10, 20]); + source.setTileGridForProjection('EPSG:4326', tileGrid); + var retrieved = source.getTileGridForProjection(ol.proj.get('EPSG:4326')); + expect(retrieved).to.be(tileGrid); + }); + }); + + describe('#getTile', function() { + it('does not do reprojection for identity', function() { + var source3857 = createSource('EPSG:3857'); + var tile3857 = source3857.getTile(0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(tile3857).to.be.a(ol.ImageTile); + expect(tile3857).not.to.be.a(ol.reproj.Tile); + + var projXXX = new ol.proj.Projection({ + code: 'XXX', + units: 'degrees' + }); + var sourceXXX = createSource(projXXX); + var tileXXX = sourceXXX.getTile(0, 0, -1, 1, projXXX); + expect(tileXXX).to.be.a(ol.ImageTile); + expect(tileXXX).not.to.be.a(ol.reproj.Tile); + }); + + beforeEach(function() { + proj4.defs('4326_noextentnounits', '+proj=longlat +datum=WGS84 +no_defs'); + }); + + afterEach(function() { + delete proj4.defs['4326_noextentnounits']; + }); + + it('can handle source projection without extent and units', function(done) { + var source = createSource('4326_noextentnounits', ol.tilegrid.createXYZ({ + extent: [-180, -90, 180, 90], + tileSize: [2, 2] + })); + var tile = source.getTile(0, 0, -1, 1, ol.proj.get('EPSG:3857')); + expect(tile).to.be.a(ol.reproj.Tile); + + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + done(); + } + }); + tile.load(); + }); + + it('can handle target projection without extent and units', function(done) { + var proj = ol.proj.get('4326_noextentnounits'); + var source = createSource(); + source.setTileGridForProjection(proj, + ol.tilegrid.createXYZ({ + extent: [-180, -90, 180, 90], + tileSize: [2, 2] + })); + var tile = source.getTile(0, 0, -1, 1, proj); + expect(tile).to.be.a(ol.reproj.Tile); + + tile.listen('change', function() { + if (tile.getState() == ol.TileState.LOADED) { + done(); + } + }); + tile.load(); + }); + }); +}); + +goog.require('ol.ImageTile'); +goog.require('ol.Tile'); +goog.require('ol.TileState'); +goog.require('ol.TileUrlFunction'); +goog.require('ol.proj'); +goog.require('ol.proj.Projection'); +goog.require('ol.reproj.Tile'); +goog.require('ol.source.TileImage'); diff --git a/test/spec/ol/source/tilesource.test.js b/test/spec/ol/source/tilesource.test.js index 59d8fbbc4d..e660f2f520 100644 --- a/test/spec/ol/source/tilesource.test.js +++ b/test/spec/ol/source/tilesource.test.js @@ -26,7 +26,7 @@ describe('ol.source.Tile', function() { var zoom = 3; var range = grid.getTileRangeForExtentAndZ(extent, zoom); - source.forEachLoadedTile(zoom, range, callback); + source.forEachLoadedTile(source.getProjection(), zoom, range, callback); expect(callback.callCount).to.be(0); }); @@ -38,7 +38,7 @@ describe('ol.source.Tile', function() { var zoom = 3; var range = grid.getTileRangeForExtentAndZ(extent, zoom); - source.forEachLoadedTile(zoom, range, callback); + source.forEachLoadedTile(source.getProjection(), zoom, range, callback); expect(source.getTile.callCount).to.be(0); source.getTile.restore(); }); @@ -55,7 +55,7 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - source.forEachLoadedTile(zoom, range, callback); + source.forEachLoadedTile(source.getProjection(), zoom, range, callback); expect(callback.callCount).to.be(3); }); @@ -71,9 +71,10 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - var covered = source.forEachLoadedTile(zoom, range, function() { - return true; - }); + var covered = source.forEachLoadedTile(source.getProjection(), zoom, + range, function() { + return true; + }); expect(covered).to.be(true); }); @@ -89,9 +90,10 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - var covered = source.forEachLoadedTile(zoom, range, function() { - return true; - }); + var covered = source.forEachLoadedTile(source.getProjection(), zoom, + range, function() { + return true; + }); expect(covered).to.be(false); }); @@ -107,9 +109,10 @@ describe('ol.source.Tile', function() { var zoom = 1; var range = new ol.TileRange(0, 1, 0, 1); - var covered = source.forEachLoadedTile(zoom, range, function() { - return false; - }); + var covered = source.forEachLoadedTile(source.getProjection(), zoom, + range, function() { + return false; + }); expect(covered).to.be(false); }); diff --git a/test/test-extensions.js b/test/test-extensions.js index 70b7c2dbdc..7d7551264e 100644 --- a/test/test-extensions.js +++ b/test/test-extensions.js @@ -390,6 +390,7 @@ done(); }); }; + global.resembleCanvas = resembleCanvas; function expectResembleCanvas(map, referenceImage, tolerance, done) { map.render(); diff --git a/test_rendering/spec/ol/data/tiles/4326/0/0/0.png b/test_rendering/spec/ol/data/tiles/4326/0/0/0.png new file mode 100644 index 0000000000..0b99038d5a Binary files /dev/null and b/test_rendering/spec/ol/data/tiles/4326/0/0/0.png differ diff --git a/test_rendering/spec/ol/data/tiles/osm-512x256/5/3/12.png b/test_rendering/spec/ol/data/tiles/osm-512x256/5/3/12.png new file mode 100644 index 0000000000..b25a05ace1 Binary files /dev/null and b/test_rendering/spec/ol/data/tiles/osm-512x256/5/3/12.png differ diff --git a/test_rendering/spec/ol/data/tiles/osm/5/5/13.png b/test_rendering/spec/ol/data/tiles/osm/5/5/13.png new file mode 100644 index 0000000000..84a6879b3e Binary files /dev/null and b/test_rendering/spec/ol/data/tiles/osm/5/5/13.png differ diff --git a/test_rendering/spec/ol/data/tiles/osm/5/6/12.png b/test_rendering/spec/ol/data/tiles/osm/5/6/12.png new file mode 100644 index 0000000000..54b877726e Binary files /dev/null and b/test_rendering/spec/ol/data/tiles/osm/5/6/12.png differ diff --git a/test_rendering/spec/ol/data/tiles/osm/5/6/13.png b/test_rendering/spec/ol/data/tiles/osm/5/6/13.png new file mode 100644 index 0000000000..9fe6d7bc55 Binary files /dev/null and b/test_rendering/spec/ol/data/tiles/osm/5/6/13.png differ diff --git a/test_rendering/spec/ol/reproj/expected/4326-to-3857.png b/test_rendering/spec/ol/reproj/expected/4326-to-3857.png new file mode 100644 index 0000000000..8374eb0028 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/4326-to-3857.png differ diff --git a/test_rendering/spec/ol/reproj/expected/512x256-to-64x128.png b/test_rendering/spec/ol/reproj/expected/512x256-to-64x128.png new file mode 100644 index 0000000000..7823071688 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/512x256-to-64x128.png differ diff --git a/test_rendering/spec/ol/reproj/expected/dateline-merc-180.png b/test_rendering/spec/ol/reproj/expected/dateline-merc-180.png new file mode 100644 index 0000000000..eaec26b428 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/dateline-merc-180.png differ diff --git a/test_rendering/spec/ol/reproj/expected/dateline-pole.png b/test_rendering/spec/ol/reproj/expected/dateline-pole.png new file mode 100644 index 0000000000..30ff7dcf12 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/dateline-pole.png differ diff --git a/test_rendering/spec/ol/reproj/expected/image-3857-to-4326.png b/test_rendering/spec/ol/reproj/expected/image-3857-to-4326.png new file mode 100644 index 0000000000..a32f5c0264 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/image-3857-to-4326.png differ diff --git a/test_rendering/spec/ol/reproj/expected/image-dateline-merc-180.png b/test_rendering/spec/ol/reproj/expected/image-dateline-merc-180.png new file mode 100644 index 0000000000..b99cee8343 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/image-dateline-merc-180.png differ diff --git a/test_rendering/spec/ol/reproj/expected/image-dateline-pole.png b/test_rendering/spec/ol/reproj/expected/image-dateline-pole.png new file mode 100644 index 0000000000..ee93205125 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/image-dateline-pole.png differ diff --git a/test_rendering/spec/ol/reproj/expected/osm4326.png b/test_rendering/spec/ol/reproj/expected/osm4326.png new file mode 100644 index 0000000000..af1a03b9be Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/osm4326.png differ diff --git a/test_rendering/spec/ol/reproj/expected/osm5070.png b/test_rendering/spec/ol/reproj/expected/osm5070.png new file mode 100644 index 0000000000..2de3fa8a3e Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/osm5070.png differ diff --git a/test_rendering/spec/ol/reproj/expected/osm54009.png b/test_rendering/spec/ol/reproj/expected/osm54009.png new file mode 100644 index 0000000000..655e02eb20 Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/osm54009.png differ diff --git a/test_rendering/spec/ol/reproj/expected/stitch-osm3740.png b/test_rendering/spec/ol/reproj/expected/stitch-osm3740.png new file mode 100644 index 0000000000..9f67ee4acf Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/stitch-osm3740.png differ diff --git a/test_rendering/spec/ol/reproj/expected/stitch-osm4326.png b/test_rendering/spec/ol/reproj/expected/stitch-osm4326.png new file mode 100644 index 0000000000..d0b67e4e0f Binary files /dev/null and b/test_rendering/spec/ol/reproj/expected/stitch-osm4326.png differ diff --git a/test_rendering/spec/ol/reproj/image.test.js b/test_rendering/spec/ol/reproj/image.test.js new file mode 100644 index 0000000000..2413e945dc --- /dev/null +++ b/test_rendering/spec/ol/reproj/image.test.js @@ -0,0 +1,61 @@ +goog.provide('ol.test.rendering.reproj.Image'); + +describe('ol.rendering.reproj.Image', function() { + + function testSingleImage(source, targetProj, + targetExtent, targetResolution, pixelRatio, expectedUrl, done) { + var sourceProj = source.getProjection(); + + var imagesRequested = 0; + + var image = new ol.reproj.Image(sourceProj, ol.proj.get(targetProj), + targetExtent, targetResolution, pixelRatio, + function(extent, resolution, pixelRatio) { + imagesRequested++; + return source.getImage(extent, resolution, pixelRatio, sourceProj); + }); + if (image.getState() == ol.ImageState.IDLE) { + image.listen('change', function(e) { + if (image.getState() == ol.ImageState.LOADED) { + expect(imagesRequested).to.be(1); + resembleCanvas(image.getImage(), expectedUrl, IMAGE_TOLERANCE, done); + } + }); + image.load(); + } + } + + var source; + + describe('image reprojections from EPSG:3857', function() { + beforeEach(function() { + source = new ol.source.ImageStatic({ + url: 'spec/ol/data/tiles/osm/5/5/12.png', + imageExtent: ol.tilegrid.createXYZ().getTileCoordExtent([5, 5, -13]), + projection: ol.proj.get('EPSG:3857') + }); + }); + + it('works for identity reprojection', function(done) { + testSingleImage(source, 'EPSG:3857', + ol.tilegrid.createXYZ().getTileCoordExtent([5, 5, -13]), + 2 * ol.proj.EPSG3857.HALF_SIZE / (256 * (1 << 5)), 1, + 'spec/ol/data/tiles/osm/5/5/12.png', done); + }); + + it('to EPSG:4326', function(done) { + testSingleImage(source, 'EPSG:4326', + ol.tilegrid.createForProjection('EPSG:4326'). + getTileCoordExtent([6, 10, -10]), + 360 / (256 * (1 << 4)), 1, + 'spec/ol/reproj/expected/image-3857-to-4326.png', done); + }); + }); +}); + +goog.require('ol.extent'); +goog.require('ol.proj'); +goog.require('ol.proj.EPSG3857'); +goog.require('ol.reproj.Image'); +goog.require('ol.source.ImageStatic'); +goog.require('ol.ImageState'); diff --git a/test_rendering/spec/ol/reproj/tile.test.js b/test_rendering/spec/ol/reproj/tile.test.js new file mode 100644 index 0000000000..2102b2f7f4 --- /dev/null +++ b/test_rendering/spec/ol/reproj/tile.test.js @@ -0,0 +1,178 @@ +goog.provide('ol.test.rendering.reproj.Tile'); + +describe('ol.rendering.reproj.Tile', function() { + + function testSingleTile(source, targetProjection, targetTileGrid, z, x, y, + pixelRatio, expectedUrl, expectedRequests, done) { + var sourceProjection = source.getProjection(); + + var tilesRequested = 0; + + var tile = new ol.reproj.Tile(sourceProjection, source.getTileGrid(), + ol.proj.get(targetProjection), targetTileGrid, z, x, y, pixelRatio, + function(z, x, y, pixelRatio) { + tilesRequested++; + return source.getTile(z, x, y, pixelRatio, sourceProjection); + }); + if (tile.getState() == ol.TileState.IDLE) { + tile.listen('change', function(e) { + if (tile.getState() == ol.TileState.LOADED) { + expect(tilesRequested).to.be(expectedRequests); + resembleCanvas(tile.getImage(), expectedUrl, + IMAGE_TOLERANCE, done); + } + }); + tile.load(); + } + } + + var source; + + describe('single tile reprojections from EPSG:3857', function() { + beforeEach(function() { + source = new ol.source.XYZ({ + projection: 'EPSG:3857', + url: 'spec/ol/data/tiles/osm/{z}/{x}/{y}.png' + }); + }); + + it('works for identity reprojection', function(done) { + testSingleTile(source, 'EPSG:3857', source.getTileGrid(), 5, 5, -13, 1, + 'spec/ol/data/tiles/osm/5/5/12.png', 1, done); + }); + + it('to EPSG:4326', function(done) { + var tileGrid = ol.tilegrid.createForProjection('EPSG:4326', 7, [64, 64]); + testSingleTile(source, 'EPSG:4326', tileGrid, 7, 21, -20, 1, + 'spec/ol/reproj/expected/osm4326.png', 1, done); + }); + + it('to EPSG:5070', function(done) { + proj4.defs('EPSG:5070', + '+proj=aea +lat_1=29.5 +lat_2=45.5 +lat_0=23 +lon_0=-96 +x_0=0 ' + + '+y_0=0 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs'); + var proj5070 = ol.proj.get('EPSG:5070'); + proj5070.setExtent([-6e6, 0, 4e6, 6e6]); + + var tileGrid = ol.tilegrid.createForProjection('EPSG:5070', 5, [64, 64]); + testSingleTile(source, 'EPSG:5070', tileGrid, 5, 13, -15, 1, + 'spec/ol/reproj/expected/osm5070.png', 1, done); + }); + + it('to ESRI:54009', function(done) { + proj4.defs('ESRI:54009', + '+proj=moll +lon_0=0 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + var proj54009 = ol.proj.get('ESRI:54009'); + proj54009.setExtent([-18e6, -9e6, 18e6, 9e6]); + + var tileGrid = ol.tilegrid.createForProjection('ESRI:54009', 7, [64, 64]); + testSingleTile(source, 'ESRI:54009', tileGrid, 7, 27, -16, 1, + 'spec/ol/reproj/expected/osm54009.png', 1, done); + }); + }); + + describe('stitching several tiles from EPSG:3857', function() { + beforeEach(function() { + source = new ol.source.XYZ({ + projection: 'EPSG:3857', + url: 'spec/ol/data/tiles/osm/{z}/{x}/{y}.png' + }); + }); + + it('to EPSG:4326', function(done) { + var tileGrid = ol.tilegrid.createForProjection('EPSG:4326', 7, [64, 64]); + testSingleTile(source, 'EPSG:4326', tileGrid, 7, 23, -21, 1, + 'spec/ol/reproj/expected/stitch-osm4326.png', 2, done); + }); + + it('to EPSG:3740', function(done) { + proj4.defs('EPSG:3740', + '+proj=utm +zone=10 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 ' + + '+units=m +no_defs'); + var proj3740 = ol.proj.get('EPSG:3740'); + proj3740.setExtent([318499.05, 2700792.39, 4359164.89, 7149336.98]); + + var tileGrid = ol.tilegrid.createForProjection('EPSG:3740', 4, [64, 64]); + testSingleTile(source, 'EPSG:3740', tileGrid, 4, 4, -13, 1, + 'spec/ol/reproj/expected/stitch-osm3740.png', 4, done); + }); + }); + + describe('tile projection from EPSG:4326', function() { + beforeEach(function() { + source = new ol.source.XYZ({ + projection: 'EPSG:4326', + maxZoom: 0, + url: 'spec/ol/data/tiles/4326/{z}/{x}/{y}.png' + }); + }); + + it('works for identity reprojection', function(done) { + testSingleTile(source, 'EPSG:4326', source.getTileGrid(), 0, 0, -1, 1, + 'spec/ol/data/tiles/4326/0/0/0.png', 1, done); + }); + + it('to EPSG:3857', function(done) { + var tileGrid = ol.tilegrid.createForProjection('EPSG:3857', 0, [64, 64]); + testSingleTile(source, 'EPSG:3857', tileGrid, 0, 0, -1, 1, + 'spec/ol/reproj/expected/4326-to-3857.png', 1, done); + }); + }); + + describe('non-square source tiles', function() { + beforeEach(function() { + source = new ol.source.XYZ({ + projection: 'EPSG:3857', + url: 'spec/ol/data/tiles/osm-512x256/{z}/{x}/{y}.png', + tileSize: [512, 256] + }); + }); + + it('works for identity reprojection', function(done) { + testSingleTile(source, 'EPSG:3857', source.getTileGrid(), 5, 3, -13, 1, + 'spec/ol/data/tiles/osm-512x256/5/3/12.png', 1, done); + }); + + it('to 64x128 EPSG:4326', function(done) { + var tileGrid = ol.tilegrid.createForProjection('EPSG:4326', 7, [64, 128]); + testSingleTile(source, 'EPSG:4326', tileGrid, 7, 27, -10, 1, + 'spec/ol/reproj/expected/512x256-to-64x128.png', 1, done); + }); + }); + + describe('dateline wrapping', function() { + beforeEach(function() { + source = new ol.source.XYZ({ + projection: 'EPSG:4326', + maxZoom: 0, + url: 'spec/ol/data/tiles/4326/{z}/{x}/{y}.png' + }); + }); + + it('wraps X when prime meridian is shifted', function(done) { + proj4.defs('merc_180', '+proj=merc +lon_0=180 +units=m +no_defs'); + var proj_ = ol.proj.get('merc_180'); + proj_.setExtent([-20026376.39, -20048966.10, 20026376.39, 20048966.10]); + + var tileGrid = ol.tilegrid.createForProjection('merc_180', 0, [64, 64]); + testSingleTile(source, 'merc_180', tileGrid, 0, 0, -1, 1, + 'spec/ol/reproj/expected/dateline-merc-180.png', 2, done); + }); + + it('displays north pole correctly (EPSG:3413)', function(done) { + proj4.defs('EPSG:3413', '+proj=stere +lat_0=90 +lat_ts=70 +lon_0=-45 ' + + '+k=1 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs'); + var proj3413 = ol.proj.get('EPSG:3413'); + proj3413.setExtent([-4194304, -4194304, 4194304, 4194304]); + + var tileGrid = ol.tilegrid.createForProjection('EPSG:3413', 0, [64, 64]); + testSingleTile(source, 'EPSG:3413', tileGrid, 0, 0, -1, 1, + 'spec/ol/reproj/expected/dateline-pole.png', 2, done); + }); + }); +}); + +goog.require('ol.proj'); +goog.require('ol.reproj.Tile'); +goog.require('ol.source.XYZ'); +goog.require('ol.TileState');