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
+---
+
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');