Merge pull request #4122 from klokantech/rasterreproj

Raster reprojection
This commit is contained in:
Andreas Hocevar
2015-10-16 21:47:02 +02:00
66 changed files with 2890 additions and 67 deletions

View File

@@ -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 <a href="http://epsg.io/">EPSG.io</a> database.
tags: "reprojection, projection, proj4js, mapquest, epsg.io"
resources:
- http://cdnjs.cloudflare.com/ajax/libs/proj4js/2.3.6/proj4.js
---
<div class="row-fluid">
<div class="span12">
<div id="map" class="map"></div>
</div>
<form class="form-inline">
<label for="epsg-query">Search projection:</label>
<input type="text" id="epsg-query" placeholder="4326, 27700, US National Atlas, Swiss, France, ..." class="form-control" size="50" />
<button id="epsg-search" class="btn">Search</button>
<span id="epsg-result"></span>
<div>
<label for="render-edges"><input type="checkbox" id="render-edges" />Render reprojection edges</label>
</div>
</form>
</div>

View File

@@ -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);
}
}
});
};

View File

@@ -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
---
<div class="row-fluid">
<div class="span12">
<div id="map" class="map"></div>
</div>
</div>

View File

@@ -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
})
});

View File

@@ -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"
---
<div class="row-fluid">
<div class="span12">
<div id="map" class="map"></div>
</div>
</div>

View File

@@ -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
})
});

View File

@@ -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
---
<div class="row-fluid">
<div class="span12">
<div id="map" class="map"></div>
</div>
<form class="form-inline">
<div class="col-md-3">
<label>Base map:</label>
<select id="base-layer">
<option value="mapquest">MapQuest (EPSG:3857)</option>
<option value="wms4326">WMS (EPSG:4326)</option>
</select>
</div>
<div class="col-md-4">
<label>Overlay map:</label>
<select id="overlay-layer">
<option value="bng">British National Grid (EPSG:27700)</option>
<option value="wms21781">Swisstopo WMS (EPSG:21781)</option>
<option value="wmts3413">NASA Arctic WMTS (EPSG:3413)</option>
<option value="grandcanyon">Grand Canyon HiDPI (EPSG:3857)</option>
<option value="states">United States (EPSG:3857)</option>
</select>
</div>
<div class="col-md-5">
<label>View projection:</label>
<select id="view-projection">
<option value="EPSG:3857">Spherical Mercator (EPSG:3857)</option>
<option value="EPSG:4326">WGS 84 (EPSG:4326)</option>
<option value="ESRI:54009">Mollweide (ESRI:54009)</option>
<option value="EPSG:27700">British National Grid (EPSG:27700)</option>
<option value="EPSG:23032">ED50 / UTM zone 32N (EPSG:23032)</option>
<option value="EPSG:2163">US National Atlas Equal Area (EPSG:2163)</option>
<option value="EPSG:3413">NSIDC Polar Stereographic North (EPSG:3413)</option>
<option value="EPSG:5479">RSRGD2000 / MSLC2000 (EPSG:5479)</option>
</select>
</div>
<label for="render-edges"><input type="checkbox" id="render-edges" />Render reprojection edges</label> (only displayed on reprojected data)
</form>
</div>

244
examples/reprojection.js Normal file
View File

@@ -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: '&copy; ' +
'<a href="http://www.geo.admin.ch/internet/geoportal/' +
'en/home.html">' +
'Pixelmap 1:1000000 / geo.admin.ch</a>'
})],
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 &copy; USGS, rendered with ' +
'<a href="http://www.maptiler.com/">MapTiler</a>'
})]
})
});
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);
});
};

View File

@@ -3602,6 +3602,7 @@ olx.source;
* key: string, * key: string,
* imagerySet: string, * imagerySet: string,
* maxZoom: (number|undefined), * maxZoom: (number|undefined),
* reprojectionErrorThreshold: (number|undefined),
* tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
* wrapX: (boolean|undefined)}} * wrapX: (boolean|undefined)}}
* @api * @api
@@ -3642,6 +3643,15 @@ olx.source.BingMapsOptions.prototype.imagerySet;
olx.source.BingMapsOptions.prototype.maxZoom; 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. * Optional function to load a tile given a URL.
* @type {ol.TileLoadFunctionType|undefined} * @type {ol.TileLoadFunctionType|undefined}
@@ -3761,6 +3771,7 @@ olx.source.TileUTFGridOptions.prototype.url;
* logo: (string|olx.LogoOptions|undefined), * logo: (string|olx.LogoOptions|undefined),
* opaque: (boolean|undefined), * opaque: (boolean|undefined),
* projection: ol.proj.ProjectionLike, * projection: ol.proj.ProjectionLike,
* reprojectionErrorThreshold: (number|undefined),
* state: (ol.source.State|string|undefined), * state: (ol.source.State|string|undefined),
* tileClass: (function(new: ol.ImageTile, ol.TileCoord, * tileClass: (function(new: ol.ImageTile, ol.TileCoord,
* ol.TileState, string, ?string, * ol.TileState, string, ?string,
@@ -3819,6 +3830,15 @@ olx.source.TileImageOptions.prototype.opaque;
olx.source.TileImageOptions.prototype.projection; 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. * Source state.
* @type {ol.source.State|string|undefined} * @type {ol.source.State|string|undefined}
@@ -4076,6 +4096,7 @@ olx.source.ImageMapGuideOptions.prototype.params;
/** /**
* @typedef {{layer: string, * @typedef {{layer: string,
* reprojectionErrorThreshold: (number|undefined),
* tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
* url: (string|undefined)}} * url: (string|undefined)}}
* @api * @api
@@ -4091,6 +4112,15 @@ olx.source.MapQuestOptions;
olx.source.MapQuestOptions.prototype.layer; 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. * Optional function to load a tile given a URL.
* @type {ol.TileLoadFunctionType|undefined} * @type {ol.TileLoadFunctionType|undefined}
@@ -4144,6 +4174,7 @@ olx.source.TileDebugOptions.prototype.wrapX;
* @typedef {{attributions: (Array.<ol.Attribution>|undefined), * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
* crossOrigin: (null|string|undefined), * crossOrigin: (null|string|undefined),
* maxZoom: (number|undefined), * maxZoom: (number|undefined),
* reprojectionErrorThreshold: (number|undefined),
* tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
* url: (string|undefined), * url: (string|undefined),
* wrapX: (boolean|undefined)}} * wrapX: (boolean|undefined)}}
@@ -4182,6 +4213,15 @@ olx.source.OSMOptions.prototype.crossOrigin;
olx.source.OSMOptions.prototype.maxZoom; 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. * Optional function to load a tile given a URL.
* @type {ol.TileLoadFunctionType|undefined} * @type {ol.TileLoadFunctionType|undefined}
@@ -4538,6 +4578,7 @@ olx.source.ImageWMSOptions.prototype.url;
* minZoom: (number|undefined), * minZoom: (number|undefined),
* maxZoom: (number|undefined), * maxZoom: (number|undefined),
* opaque: (boolean|undefined), * opaque: (boolean|undefined),
* reprojectionErrorThreshold: (number|undefined),
* tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
* url: (string|undefined)}} * url: (string|undefined)}}
* @api * @api
@@ -4577,6 +4618,15 @@ olx.source.StamenOptions.prototype.maxZoom;
olx.source.StamenOptions.prototype.opaque; 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. * Optional function to load a tile given a URL.
* @type {ol.TileLoadFunctionType|undefined} * @type {ol.TileLoadFunctionType|undefined}
@@ -4683,6 +4733,7 @@ olx.source.ImageStaticOptions.prototype.url;
* logo: (string|olx.LogoOptions|undefined), * logo: (string|olx.LogoOptions|undefined),
* tileGrid: (ol.tilegrid.TileGrid|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined),
* projection: ol.proj.ProjectionLike, * projection: ol.proj.ProjectionLike,
* reprojectionErrorThreshold: (number|undefined),
* tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
* url: (string|undefined), * url: (string|undefined),
* urls: (Array.<string>|undefined), * urls: (Array.<string>|undefined),
@@ -4754,6 +4805,15 @@ olx.source.TileArcGISRestOptions.prototype.tileGrid;
olx.source.TileArcGISRestOptions.prototype.projection; 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. * Optional function to load a tile given a URL.
* @type {ol.TileLoadFunctionType|undefined} * @type {ol.TileLoadFunctionType|undefined}
@@ -4791,6 +4851,7 @@ olx.source.TileArcGISRestOptions.prototype.urls;
/** /**
* @typedef {{attributions: (Array.<ol.Attribution>|undefined), * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
* crossOrigin: (null|string|undefined), * crossOrigin: (null|string|undefined),
* reprojectionErrorThreshold: (number|undefined),
* tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
* url: string, * url: string,
* wrapX: (boolean|undefined)}} * wrapX: (boolean|undefined)}}
@@ -4821,6 +4882,15 @@ olx.source.TileJSONOptions.prototype.attributions;
olx.source.TileJSONOptions.prototype.crossOrigin; 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. * Optional function to load a tile given a URL.
* @type {ol.TileLoadFunctionType|undefined} * @type {ol.TileLoadFunctionType|undefined}
@@ -4855,6 +4925,7 @@ olx.source.TileJSONOptions.prototype.wrapX;
* tileGrid: (ol.tilegrid.TileGrid|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined),
* maxZoom: (number|undefined), * maxZoom: (number|undefined),
* projection: ol.proj.ProjectionLike, * projection: ol.proj.ProjectionLike,
* reprojectionErrorThreshold: (number|undefined),
* serverType: (ol.source.wms.ServerType|string|undefined), * serverType: (ol.source.wms.ServerType|string|undefined),
* tileLoadFunction: (ol.TileLoadFunctionType|undefined), * tileLoadFunction: (ol.TileLoadFunctionType|undefined),
* url: (string|undefined), * url: (string|undefined),
@@ -4955,6 +5026,15 @@ olx.source.TileWMSOptions.prototype.maxZoom;
olx.source.TileWMSOptions.prototype.projection; 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 * The type of the remote WMS server. Currently only used when `hidpi` is
* `true`. Default is `undefined`. * `true`. Default is `undefined`.
@@ -5120,6 +5200,7 @@ olx.source.VectorOptions.prototype.wrapX;
* logo: (string|olx.LogoOptions|undefined), * logo: (string|olx.LogoOptions|undefined),
* tileGrid: ol.tilegrid.WMTS, * tileGrid: ol.tilegrid.WMTS,
* projection: ol.proj.ProjectionLike, * projection: ol.proj.ProjectionLike,
* reprojectionErrorThreshold: (number|undefined),
* requestEncoding: (ol.source.WMTSRequestEncoding|string|undefined), * requestEncoding: (ol.source.WMTSRequestEncoding|string|undefined),
* layer: string, * layer: string,
* style: string, * style: string,
@@ -5185,6 +5266,15 @@ olx.source.WMTSOptions.prototype.tileGrid;
olx.source.WMTSOptions.prototype.projection; 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`. * Request encoding. Default is `KVP`.
* @type {ol.source.WMTSRequestEncoding|string|undefined} * @type {ol.source.WMTSRequestEncoding|string|undefined}
@@ -5311,6 +5401,7 @@ olx.source.WMTSOptions.prototype.wrapX;
* crossOrigin: (null|string|undefined), * crossOrigin: (null|string|undefined),
* logo: (string|olx.LogoOptions|undefined), * logo: (string|olx.LogoOptions|undefined),
* projection: ol.proj.ProjectionLike, * projection: ol.proj.ProjectionLike,
* reprojectionErrorThreshold: (number|undefined),
* maxZoom: (number|undefined), * maxZoom: (number|undefined),
* minZoom: (number|undefined), * minZoom: (number|undefined),
* tileGrid: (ol.tilegrid.TileGrid|undefined), * tileGrid: (ol.tilegrid.TileGrid|undefined),
@@ -5362,6 +5453,15 @@ olx.source.XYZOptions.prototype.logo;
olx.source.XYZOptions.prototype.projection; 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`. * Optional max zoom level. Default is `18`.
* @type {number|undefined} * @type {number|undefined}
@@ -5452,6 +5552,7 @@ olx.source.XYZOptions.prototype.wrapX;
* @typedef {{attributions: (Array.<ol.Attribution>|undefined), * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
* crossOrigin: (null|string|undefined), * crossOrigin: (null|string|undefined),
* logo: (string|olx.LogoOptions|undefined), * logo: (string|olx.LogoOptions|undefined),
* reprojectionErrorThreshold: (number|undefined),
* url: !string, * url: !string,
* tierSizeCalculation: (string|undefined), * tierSizeCalculation: (string|undefined),
* size: ol.Size}} * size: ol.Size}}
@@ -5488,6 +5589,15 @@ olx.source.ZoomifyOptions.prototype.crossOrigin;
olx.source.ZoomifyOptions.prototype.logo; 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. * Prefix of URL template.
* @type {!string} * @type {!string}

View File

@@ -78,6 +78,69 @@ ol.math.squaredDistance = function(x1, y1, x2, y2) {
}; };
/**
* Solves system of linear equations using Gaussian elimination method.
*
* @param {Array.<Array.<number>>} mat Augmented matrix (n x n + 1 column)
* in row-major order.
* @return {Array.<number>} 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. * Converts radians to to degrees.
* *

View File

@@ -28,6 +28,13 @@ ol.DEFAULT_MAX_ZOOM = 42;
ol.DEFAULT_MIN_ZOOM = 0; 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. * @define {number} Default high water mark.
*/ */
@@ -94,6 +101,13 @@ ol.ENABLE_NAMED_COLORS = false;
ol.ENABLE_PROJ4JS = true; 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 * @define {boolean} Enable rendering of ol.layer.Tile based layers. Default is
* `true`. Setting this to false at compile time in advanced mode removes * `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; 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. * @define {number} Tolerance for geometry simplification in device pixels.
*/ */

View File

@@ -170,11 +170,13 @@ ol.renderer.canvas.ImageLayer.prototype.prepareFrame =
if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
!ol.extent.isEmpty(renderedExtent)) { !ol.extent.isEmpty(renderedExtent)) {
var projection = viewState.projection; var projection = viewState.projection;
var sourceProjection = imageSource.getProjection(); if (!ol.ENABLE_RASTER_REPROJECTION) {
if (sourceProjection) { var sourceProjection = imageSource.getProjection();
goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), if (sourceProjection) {
'projection and sourceProjection are equivalent'); goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
projection = sourceProjection; 'projection and sourceProjection are equivalent');
projection = sourceProjection;
}
} }
image = imageSource.getImage( image = imageSource.getImage(
renderedExtent, viewResolution, pixelRatio, projection); renderedExtent, viewResolution, pixelRatio, projection);

View File

@@ -309,7 +309,8 @@ ol.renderer.canvas.TileLayer.prototype.prepareFrame =
/** @type {Array.<ol.Tile>} */ /** @type {Array.<ol.Tile>} */
var tilesToClear = []; var tilesToClear = [];
var findLoadedTiles = this.createLoadedTileFinder(tileSource, tilesToDrawByZ); var findLoadedTiles = this.createLoadedTileFinder(
tileSource, projection, tilesToDrawByZ);
var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();

View File

@@ -101,11 +101,13 @@ ol.renderer.dom.ImageLayer.prototype.prepareFrame =
if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
!ol.extent.isEmpty(renderedExtent)) { !ol.extent.isEmpty(renderedExtent)) {
var projection = viewState.projection; var projection = viewState.projection;
var sourceProjection = imageSource.getProjection(); if (!ol.ENABLE_RASTER_REPROJECTION) {
if (sourceProjection) { var sourceProjection = imageSource.getProjection();
goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), if (sourceProjection) {
'projection and sourceProjection are equivalent'); goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
projection = sourceProjection; 'projection and sourceProjection are equivalent');
projection = sourceProjection;
}
} }
var image_ = imageSource.getImage(renderedExtent, viewResolution, var image_ = imageSource.getImage(renderedExtent, viewResolution,
frameState.pixelRatio, projection); frameState.pixelRatio, projection);

View File

@@ -121,7 +121,8 @@ ol.renderer.dom.TileLayer.prototype.prepareFrame =
var tilesToDrawByZ = {}; var tilesToDrawByZ = {};
tilesToDrawByZ[z] = {}; tilesToDrawByZ[z] = {};
var findLoadedTiles = this.createLoadedTileFinder(tileSource, tilesToDrawByZ); var findLoadedTiles = this.createLoadedTileFinder(
tileSource, projection, tilesToDrawByZ);
var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();

View File

@@ -87,6 +87,7 @@ ol.renderer.Layer.prototype.hasFeatureAtCoordinate = goog.functions.FALSE;
/** /**
* Create a function that adds loaded tiles to the tile lookup. * Create a function that adds loaded tiles to the tile lookup.
* @param {ol.source.Tile} source Tile source. * @param {ol.source.Tile} source Tile source.
* @param {ol.proj.Projection} projection Projection of the tiles.
* @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded * @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded
* tiles by zoom level. * tiles by zoom level.
* @return {function(number, ol.TileRange):boolean} A function that can be * @return {function(number, ol.TileRange):boolean} A function that can be
@@ -94,7 +95,8 @@ ol.renderer.Layer.prototype.hasFeatureAtCoordinate = goog.functions.FALSE;
* lookup. * lookup.
* @protected * @protected
*/ */
ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, tiles) { ol.renderer.Layer.prototype.createLoadedTileFinder =
function(source, projection, tiles) {
return ( return (
/** /**
* @param {number} zoom Zoom level. * @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. * @return {boolean} The tile range is fully loaded.
*/ */
function(zoom, tileRange) { function(zoom, tileRange) {
return source.forEachLoadedTile(zoom, tileRange, function(tile) { return source.forEachLoadedTile(projection, zoom,
if (!tiles[zoom]) { tileRange, function(tile) {
tiles[zoom] = {}; if (!tiles[zoom]) {
} tiles[zoom] = {};
tiles[zoom][tile.tileCoord.toString()] = tile; }
}); tiles[zoom][tile.tileCoord.toString()] = tile;
});
}); });
}; };
@@ -156,8 +159,9 @@ ol.renderer.Layer.prototype.loadImage = function(image) {
if (imageState == ol.ImageState.IDLE) { if (imageState == ol.ImageState.IDLE) {
image.load(); image.load();
imageState = image.getState(); imageState = image.getState();
goog.asserts.assert(imageState == ol.ImageState.LOADING, goog.asserts.assert(imageState == ol.ImageState.LOADING ||
'imageState is "loading"'); imageState == ol.ImageState.LOADED,
'imageState is "loading" or "loaded"');
} }
return imageState == ol.ImageState.LOADED; return imageState == ol.ImageState.LOADED;
}; };
@@ -191,7 +195,8 @@ ol.renderer.Layer.prototype.scheduleExpireCache =
*/ */
function(tileSource, map, frameState) { function(tileSource, map, frameState) {
var tileSourceKey = goog.getUid(tileSource).toString(); var tileSourceKey = goog.getUid(tileSource).toString();
tileSource.expireCache(frameState.usedTiles[tileSourceKey]); tileSource.expireCache(frameState.viewState.projection,
frameState.usedTiles[tileSourceKey]);
}, tileSource)); }, tileSource));
} }
}; };
@@ -324,7 +329,7 @@ ol.renderer.Layer.prototype.manageTilePyramid = function(
opt_tileCallback.call(opt_this, tile); opt_tileCallback.call(opt_this, tile);
} }
} else { } else {
tileSource.useTile(z, x, y); tileSource.useTile(z, x, y, projection);
} }
} }
} }

View File

@@ -125,11 +125,13 @@ ol.renderer.webgl.ImageLayer.prototype.prepareFrame =
if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] && if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
!ol.extent.isEmpty(renderedExtent)) { !ol.extent.isEmpty(renderedExtent)) {
var projection = viewState.projection; var projection = viewState.projection;
var sourceProjection = imageSource.getProjection(); if (!ol.ENABLE_RASTER_REPROJECTION) {
if (sourceProjection) { var sourceProjection = imageSource.getProjection();
goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection), if (sourceProjection) {
'projection and sourceProjection are equivalent'); goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
projection = sourceProjection; 'projection and sourceProjection are equivalent');
projection = sourceProjection;
}
} }
var image_ = imageSource.getImage(renderedExtent, viewResolution, var image_ = imageSource.getImage(renderedExtent, viewResolution,
pixelRatio, projection); pixelRatio, projection);

View File

@@ -105,6 +105,7 @@ ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() {
/** /**
* Create a function that adds loaded tiles to the tile lookup. * Create a function that adds loaded tiles to the tile lookup.
* @param {ol.source.Tile} source Tile source. * @param {ol.source.Tile} source Tile source.
* @param {ol.proj.Projection} projection Projection of the tiles.
* @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded * @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded
* tiles by zoom level. * tiles by zoom level.
* @return {function(number, ol.TileRange):boolean} A function that can be * @return {function(number, ol.TileRange):boolean} A function that can be
@@ -113,7 +114,7 @@ ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() {
* @protected * @protected
*/ */
ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder =
function(source, tiles) { function(source, projection, tiles) {
var mapRenderer = this.mapRenderer; var mapRenderer = this.mapRenderer;
return ( return (
@@ -123,16 +124,17 @@ ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder =
* @return {boolean} The tile range is fully loaded. * @return {boolean} The tile range is fully loaded.
*/ */
function(zoom, tileRange) { function(zoom, tileRange) {
return source.forEachLoadedTile(zoom, tileRange, function(tile) { return source.forEachLoadedTile(projection, zoom,
var loaded = mapRenderer.isTileTextureLoaded(tile); tileRange, function(tile) {
if (loaded) { var loaded = mapRenderer.isTileTextureLoaded(tile);
if (!tiles[zoom]) { if (loaded) {
tiles[zoom] = {}; if (!tiles[zoom]) {
} tiles[zoom] = {};
tiles[zoom][tile.tileCoord.toString()] = tile; }
} tiles[zoom][tile.tileCoord.toString()] = tile;
return loaded; }
}); return loaded;
});
}); });
}; };
@@ -239,7 +241,7 @@ ol.renderer.webgl.TileLayer.prototype.prepareFrame =
tilesToDrawByZ[z] = {}; tilesToDrawByZ[z] = {};
var findLoadedTiles = this.createLoadedTileFinder( var findLoadedTiles = this.createLoadedTileFinder(
tileSource, tilesToDrawByZ); tileSource, projection, tilesToDrawByZ);
var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError(); var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
var allTilesLoaded = true; var allTilesLoaded = true;

210
src/ol/reproj/image.js Normal file
View File

@@ -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;
};

258
src/ol/reproj/reproj.js Normal file
View File

@@ -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;
};

332
src/ol/reproj/tile.js Normal file
View File

@@ -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.<number, HTMLCanvasElement>}
*/
this.canvasByContext_ = {};
/**
* @private
* @type {ol.tilegrid.TileGrid}
*/
this.sourceTileGrid_ = sourceTileGrid;
/**
* @private
* @type {ol.tilegrid.TileGrid}
*/
this.targetTileGrid_ = targetTileGrid;
/**
* @private
* @type {!Array.<ol.Tile>}
*/
this.sourceTiles_ = [];
/**
* @private
* @type {Array.<goog.events.Key>}
*/
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;
};

View File

@@ -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.<ol.Coordinate>,
* target: Array.<ol.Coordinate>}}
*/
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.<string, ol.Coordinate>} */
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.<ol.reproj.Triangle>}
* @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.<ol.reproj.Triangle>} Array of the calculated triangles.
*/
ol.reproj.Triangulation.prototype.getTriangles = function() {
return this.triangles_;
};

View File

@@ -29,6 +29,7 @@ ol.source.BingMaps = function(options) {
crossOrigin: 'anonymous', crossOrigin: 'anonymous',
opaque: true, opaque: true,
projection: ol.proj.get('EPSG:3857'), projection: ol.proj.get('EPSG:3857'),
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
state: ol.source.State.LOADING, state: ol.source.State.LOADING,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
wrapX: options.wrapX !== undefined ? options.wrapX : true wrapX: options.wrapX !== undefined ? options.wrapX : true

View File

@@ -59,7 +59,7 @@ goog.inherits(ol.source.ImageCanvas, ol.source.Image);
/** /**
* @inheritDoc * @inheritDoc
*/ */
ol.source.ImageCanvas.prototype.getImage = ol.source.ImageCanvas.prototype.getImageInternal =
function(extent, resolution, pixelRatio, projection) { function(extent, resolution, pixelRatio, projection) {
resolution = this.findNearestResolution(resolution); resolution = this.findNearestResolution(resolution);

View File

@@ -126,7 +126,7 @@ ol.source.ImageMapGuide.prototype.getParams = function() {
/** /**
* @inheritDoc * @inheritDoc
*/ */
ol.source.ImageMapGuide.prototype.getImage = ol.source.ImageMapGuide.prototype.getImageInternal =
function(extent, resolution, pixelRatio, projection) { function(extent, resolution, pixelRatio, projection) {
resolution = this.findNearestResolution(resolution); resolution = this.findNearestResolution(resolution);
pixelRatio = this.hidpi_ ? pixelRatio : 1; pixelRatio = this.hidpi_ ? pixelRatio : 1;

View File

@@ -5,9 +5,11 @@ goog.require('goog.array');
goog.require('goog.asserts'); goog.require('goog.asserts');
goog.require('goog.events.Event'); goog.require('goog.events.Event');
goog.require('ol.Attribution'); goog.require('ol.Attribution');
goog.require('ol.Extent');
goog.require('ol.ImageState'); goog.require('ol.ImageState');
goog.require('ol.array'); goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.reproj.Image');
goog.require('ol.source.Source'); goog.require('ol.source.Source');
@@ -56,6 +58,20 @@ ol.source.Image = function(options) {
return b - a; return b - a;
}, true), 'resolutions must be null or sorted in descending order'); }, 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); goog.inherits(ol.source.Image, ol.source.Source);
@@ -90,7 +106,53 @@ ol.source.Image.prototype.findNearestResolution =
* @param {ol.proj.Projection} projection Projection. * @param {ol.proj.Projection} projection Projection.
* @return {ol.ImageBase} Single image. * @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;
/** /**

View File

@@ -62,7 +62,7 @@ goog.inherits(ol.source.ImageStatic, ol.source.Image);
/** /**
* @inheritDoc * @inheritDoc
*/ */
ol.source.ImageStatic.prototype.getImage = ol.source.ImageStatic.prototype.getImageInternal =
function(extent, resolution, pixelRatio, projection) { function(extent, resolution, pixelRatio, projection) {
if (ol.extent.intersects(extent, this.image_.getExtent())) { if (ol.extent.intersects(extent, this.image_.getExtent())) {
return this.image_; return this.image_;

View File

@@ -185,7 +185,7 @@ ol.source.ImageWMS.prototype.getParams = function() {
/** /**
* @inheritDoc * @inheritDoc
*/ */
ol.source.ImageWMS.prototype.getImage = ol.source.ImageWMS.prototype.getImageInternal =
function(extent, resolution, pixelRatio, projection) { function(extent, resolution, pixelRatio, projection) {
if (this.url_ === undefined) { if (this.url_ === undefined) {

View File

@@ -40,6 +40,7 @@ ol.source.MapQuest = function(opt_options) {
crossOrigin: 'anonymous', crossOrigin: 'anonymous',
logo: 'https://developer.mapquest.com/content/osm/mq_logo.png', logo: 'https://developer.mapquest.com/content/osm/mq_logo.png',
maxZoom: layerConfig.maxZoom, maxZoom: layerConfig.maxZoom,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
opaque: true, opaque: true,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
url: url url: url

View File

@@ -36,6 +36,7 @@ ol.source.OSM = function(opt_options) {
crossOrigin: crossOrigin, crossOrigin: crossOrigin,
opaque: true, opaque: true,
maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19, maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
url: url, url: url,
wrapX: options.wrapX wrapX: options.wrapX

View File

@@ -109,6 +109,7 @@ ol.source.Stamen = function(options) {
// FIXME uncomment the following when tilegrid supports minZoom // FIXME uncomment the following when tilegrid supports minZoom
//minZoom: providerConfig.minZoom, //minZoom: providerConfig.minZoom,
opaque: layerConfig.opaque, opaque: layerConfig.opaque,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
url: url url: url
}); });

View File

@@ -41,6 +41,7 @@ ol.source.TileArcGISRest = function(opt_options) {
crossOrigin: options.crossOrigin, crossOrigin: options.crossOrigin,
logo: options.logo, logo: options.logo,
projection: options.projection, projection: options.projection,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileGrid: options.tileGrid, tileGrid: options.tileGrid,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
tileUrlFunction: goog.bind(this.tileUrlFunction_, this), tileUrlFunction: goog.bind(this.tileUrlFunction_, this),

View File

@@ -3,12 +3,16 @@ goog.provide('ol.source.TileImage');
goog.require('goog.asserts'); goog.require('goog.asserts');
goog.require('goog.events'); goog.require('goog.events');
goog.require('goog.events.EventType'); goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol.ImageTile'); goog.require('ol.ImageTile');
goog.require('ol.TileCache');
goog.require('ol.TileCoord'); goog.require('ol.TileCoord');
goog.require('ol.TileLoadFunctionType'); goog.require('ol.TileLoadFunctionType');
goog.require('ol.TileState'); goog.require('ol.TileState');
goog.require('ol.TileUrlFunction'); goog.require('ol.TileUrlFunction');
goog.require('ol.TileUrlFunctionType'); goog.require('ol.TileUrlFunctionType');
goog.require('ol.proj');
goog.require('ol.reproj.Tile');
goog.require('ol.source.Tile'); goog.require('ol.source.Tile');
goog.require('ol.source.TileEvent'); goog.require('ol.source.TileEvent');
@@ -69,6 +73,29 @@ ol.source.TileImage = function(options) {
this.tileClass = options.tileClass !== undefined ? this.tileClass = options.tileClass !== undefined ?
options.tileClass : ol.ImageTile; options.tileClass : ol.ImageTile;
/**
* @protected
* @type {Object.<string, ol.TileCache>}
*/
this.tileCacheForProjection = {};
/**
* @protected
* @type {Object.<string, ol.tilegrid.TileGrid>}
*/
this.tileGridForProjection = {};
/**
* @private
* @type {number|undefined}
*/
this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold;
/**
* @private
* @type {boolean}
*/
this.renderReprojectionEdges_ = false;
}; };
goog.inherits(ol.source.TileImage, ol.source.Tile); 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 * @inheritDoc
*/ */
ol.source.TileImage.prototype.getTile = ol.source.TileImage.prototype.getTile =
function(z, x, y, pixelRatio, projection) { 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); var tileCoordKey = this.getKeyZXY(z, x, y);
if (this.tileCache.containsKey(tileCoordKey)) { if (this.tileCache.containsKey(tileCoordKey)) {
return /** @type {!ol.Tile} */ (this.tileCache.get(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. * Set the tile load function of the source.
* @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function. * @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) { ol.source.TileImage.prototype.setTileLoadFunction = function(tileLoadFunction) {
this.tileCache.clear(); this.tileCache.clear();
this.tileCacheForProjection = {};
this.tileLoadFunction = tileLoadFunction; this.tileLoadFunction = tileLoadFunction;
this.changed(); 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. The tile URL function would need to be incorporated into the
// FIXME cache key somehow. // FIXME cache key somehow.
this.tileCache.clear(); this.tileCache.clear();
this.tileCacheForProjection = {};
this.tileUrlFunction = tileUrlFunction; this.tileUrlFunction = tileUrlFunction;
this.changed(); this.changed();
}; };
@@ -186,9 +377,10 @@ ol.source.TileImage.prototype.setTileUrlFunction = function(tileUrlFunction) {
/** /**
* @inheritDoc * @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); var tileCoordKey = this.getKeyZXY(z, x, y);
if (this.tileCache.containsKey(tileCoordKey)) { if (tileCache && tileCache.containsKey(tileCoordKey)) {
this.tileCache.get(tileCoordKey); tileCache.get(tileCoordKey);
} }
}; };

View File

@@ -34,6 +34,7 @@ ol.source.TileJSON = function(options) {
attributions: options.attributions, attributions: options.attributions,
crossOrigin: options.crossOrigin, crossOrigin: options.crossOrigin,
projection: ol.proj.get('EPSG:3857'), projection: ol.proj.get('EPSG:3857'),
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
state: ol.source.State.LOADING, state: ol.source.State.LOADING,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
wrapX: options.wrapX !== undefined ? options.wrapX : true wrapX: options.wrapX !== undefined ? options.wrapX : true

View File

@@ -10,6 +10,7 @@ goog.require('ol.Extent');
goog.require('ol.TileCache'); goog.require('ol.TileCache');
goog.require('ol.TileRange'); goog.require('ol.TileRange');
goog.require('ol.TileState'); goog.require('ol.TileState');
goog.require('ol.proj');
goog.require('ol.size'); goog.require('ol.size');
goog.require('ol.source.Source'); goog.require('ol.source.Source');
goog.require('ol.tilecoord'); goog.require('ol.tilecoord');
@@ -97,14 +98,19 @@ ol.source.Tile.prototype.canExpireCache = function() {
/** /**
* @param {ol.proj.Projection} projection Projection.
* @param {Object.<string, ol.TileRange>} usedTiles Used tiles. * @param {Object.<string, ol.TileRange>} usedTiles Used tiles.
*/ */
ol.source.Tile.prototype.expireCache = function(usedTiles) { ol.source.Tile.prototype.expireCache = function(projection, usedTiles) {
this.tileCache.expireCache(usedTiles); var tileCache = this.getTileCacheForProjection(projection);
if (tileCache) {
tileCache.expireCache(usedTiles);
}
}; };
/** /**
* @param {ol.proj.Projection} projection Projection.
* @param {number} z Zoom level. * @param {number} z Zoom level.
* @param {ol.TileRange} tileRange Tile range. * @param {ol.TileRange} tileRange Tile range.
* @param {function(ol.Tile):(boolean|undefined)} callback Called with each * @param {function(ol.Tile):(boolean|undefined)} callback Called with each
@@ -112,15 +118,21 @@ ol.source.Tile.prototype.expireCache = function(usedTiles) {
* considered loaded. * considered loaded.
* @return {boolean} The tile range is fully covered with loaded tiles. * @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 covered = true;
var tile, tileCoordKey, loaded; var tile, tileCoordKey, loaded;
for (var x = tileRange.minX; x <= tileRange.maxX; ++x) { for (var x = tileRange.minX; x <= tileRange.maxX; ++x) {
for (var y = tileRange.minY; y <= tileRange.maxY; ++y) { for (var y = tileRange.minY; y <= tileRange.maxY; ++y) {
tileCoordKey = this.getKeyZXY(z, x, y); tileCoordKey = this.getKeyZXY(z, x, y);
loaded = false; loaded = false;
if (this.tileCache.containsKey(tileCoordKey)) { if (tileCache.containsKey(tileCoordKey)) {
tile = /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey)); tile = /** @type {!ol.Tile} */ (tileCache.get(tileCoordKey));
loaded = tile.getState() === ol.TileState.LOADED; loaded = tile.getState() === ol.TileState.LOADED;
if (loaded) { if (loaded) {
loaded = (callback(tile) !== false); loaded = (callback(tile) !== false);
@@ -174,7 +186,7 @@ ol.source.Tile.prototype.getResolutions = function() {
* @param {number} x Tile coordinate x. * @param {number} x Tile coordinate x.
* @param {number} y Tile coordinate y. * @param {number} y Tile coordinate y.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
* @param {ol.proj.Projection=} opt_projection Projection. * @param {ol.proj.Projection} projection Projection.
* @return {!ol.Tile} Tile. * @return {!ol.Tile} Tile.
*/ */
ol.source.Tile.prototype.getTile = goog.abstractMethod; 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} z Z.
* @param {number} pixelRatio Pixel ratio. * @param {number} pixelRatio Pixel ratio.
@@ -244,6 +279,7 @@ ol.source.Tile.prototype.getTileCoordForTileUrlFunction =
* @param {number} z Tile coordinate z. * @param {number} z Tile coordinate z.
* @param {number} x Tile coordinate x. * @param {number} x Tile coordinate x.
* @param {number} y Tile coordinate y. * @param {number} y Tile coordinate y.
* @param {ol.proj.Projection} projection Projection.
*/ */
ol.source.Tile.prototype.useTile = ol.nullFunction; ol.source.Tile.prototype.useTile = ol.nullFunction;

View File

@@ -45,6 +45,7 @@ ol.source.TileWMS = function(opt_options) {
logo: options.logo, logo: options.logo,
opaque: !transparent, opaque: !transparent,
projection: options.projection, projection: options.projection,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileGrid: options.tileGrid, tileGrid: options.tileGrid,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
tileUrlFunction: goog.bind(this.tileUrlFunction_, this), tileUrlFunction: goog.bind(this.tileUrlFunction_, this),

View File

@@ -185,6 +185,7 @@ ol.source.WMTS = function(options) {
crossOrigin: options.crossOrigin, crossOrigin: options.crossOrigin,
logo: options.logo, logo: options.logo,
projection: options.projection, projection: options.projection,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileClass: options.tileClass, tileClass: options.tileClass,
tileGrid: tileGrid, tileGrid: tileGrid,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,

View File

@@ -49,6 +49,7 @@ ol.source.XYZ = function(options) {
crossOrigin: options.crossOrigin, crossOrigin: options.crossOrigin,
logo: options.logo, logo: options.logo,
projection: projection, projection: projection,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileGrid: tileGrid, tileGrid: tileGrid,
tileLoadFunction: options.tileLoadFunction, tileLoadFunction: options.tileLoadFunction,
tilePixelRatio: options.tilePixelRatio, tilePixelRatio: options.tilePixelRatio,

View File

@@ -124,6 +124,7 @@ ol.source.Zoomify = function(opt_options) {
attributions: options.attributions, attributions: options.attributions,
crossOrigin: options.crossOrigin, crossOrigin: options.crossOrigin,
logo: options.logo, logo: options.logo,
reprojectionErrorThreshold: options.reprojectionErrorThreshold,
tileClass: ol.source.ZoomifyTile_, tileClass: ol.source.ZoomifyTile_,
tileGrid: tileGrid, tileGrid: tileGrid,
tileUrlFunction: tileUrlFunction tileUrlFunction: tileUrlFunction

View File

@@ -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() { describe('ol.math.toDegrees', function() {
it('returns the correct value at -π', function() { it('returns the correct value at -π', function() {
expect(ol.math.toDegrees(-Math.PI)).to.be(-180); expect(ol.math.toDegrees(-Math.PI)).to.be(-180);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -26,7 +26,7 @@ describe('ol.source.Tile', function() {
var zoom = 3; var zoom = 3;
var range = grid.getTileRangeForExtentAndZ(extent, zoom); var range = grid.getTileRangeForExtentAndZ(extent, zoom);
source.forEachLoadedTile(zoom, range, callback); source.forEachLoadedTile(source.getProjection(), zoom, range, callback);
expect(callback.callCount).to.be(0); expect(callback.callCount).to.be(0);
}); });
@@ -38,7 +38,7 @@ describe('ol.source.Tile', function() {
var zoom = 3; var zoom = 3;
var range = grid.getTileRangeForExtentAndZ(extent, zoom); 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); expect(source.getTile.callCount).to.be(0);
source.getTile.restore(); source.getTile.restore();
}); });
@@ -55,7 +55,7 @@ describe('ol.source.Tile', function() {
var zoom = 1; var zoom = 1;
var range = new ol.TileRange(0, 1, 0, 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); expect(callback.callCount).to.be(3);
}); });
@@ -71,9 +71,10 @@ describe('ol.source.Tile', function() {
var zoom = 1; var zoom = 1;
var range = new ol.TileRange(0, 1, 0, 1); var range = new ol.TileRange(0, 1, 0, 1);
var covered = source.forEachLoadedTile(zoom, range, function() { var covered = source.forEachLoadedTile(source.getProjection(), zoom,
return true; range, function() {
}); return true;
});
expect(covered).to.be(true); expect(covered).to.be(true);
}); });
@@ -89,9 +90,10 @@ describe('ol.source.Tile', function() {
var zoom = 1; var zoom = 1;
var range = new ol.TileRange(0, 1, 0, 1); var range = new ol.TileRange(0, 1, 0, 1);
var covered = source.forEachLoadedTile(zoom, range, function() { var covered = source.forEachLoadedTile(source.getProjection(), zoom,
return true; range, function() {
}); return true;
});
expect(covered).to.be(false); expect(covered).to.be(false);
}); });
@@ -107,9 +109,10 @@ describe('ol.source.Tile', function() {
var zoom = 1; var zoom = 1;
var range = new ol.TileRange(0, 1, 0, 1); var range = new ol.TileRange(0, 1, 0, 1);
var covered = source.forEachLoadedTile(zoom, range, function() { var covered = source.forEachLoadedTile(source.getProjection(), zoom,
return false; range, function() {
}); return false;
});
expect(covered).to.be(false); expect(covered).to.be(false);
}); });

View File

@@ -390,6 +390,7 @@
done(); done();
}); });
}; };
global.resembleCanvas = resembleCanvas;
function expectResembleCanvas(map, referenceImage, tolerance, done) { function expectResembleCanvas(map, referenceImage, tolerance, done) {
map.render(); map.render();

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

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

View File

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