diff --git a/.gitignore b/.gitignore index 24c0e4318d..78d2adfe1c 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,8 @@ /build/src /build/phantomjs-*-windows /build/phantomjs-*-windows.zip +/build/proj4js-*.zip +/build/proj4js /examples/*.json /examples/*.combined.js /examples/example-list.js diff --git a/build.py b/build.py index 7a5f049e36..e2c5264019 100755 --- a/build.py +++ b/build.py @@ -22,14 +22,17 @@ if sys.platform == 'win32': variables.JSDOC = 'jsdoc' # FIXME variables.PYTHON = os.path.join(Python27, 'python.exe') PHANTOMJS_WINDOWS_ZIP = 'build/phantomjs-1.8.1-windows.zip' - PHANTOMJS = 'build/phantomjs-1.8.1-windows/phantomjs.exe' + # FIXME we should not need both a pake variable and a Python constant here + # FIXME this requires pake to be modified to lazily evaluate variables in target names + variables.PHANTOMJS = 'build/phantomjs-1.8.1-windows/phantomjs.exe' + PHANTOMJS = variables.PHANTOMJS else: variables.GIT = 'git' variables.GJSLINT = 'gjslint' variables.JAVA = 'java' variables.JSDOC = 'jsdoc' variables.PYTHON = 'python' - PHANTOMJS = 'phantomjs' + variables.PHANTOMJS = 'phantomjs' variables.BRANCH = output('%(GIT)s', 'rev-parse', '--abbrev-ref', 'HEAD').strip() @@ -74,6 +77,10 @@ SRC = [path PLOVR_JAR = 'bin/plovr-eba786b34df9.jar' PLOVR_JAR_MD5 = '20eac8ccc4578676511cf7ccbfc65100' +PROJ4JS = 'build/proj4js/lib/proj4js-combined.js' +PROJ4JS_ZIP = 'build/proj4js-1.1.0.zip' +PROJ4JS_ZIP_MD5 = '17caad64cf6ebc6e6fe62f292b134897' + def report_sizes(t): t.info('uncompressed: %d bytes', os.stat(t.name).st_size) @@ -335,12 +342,23 @@ def hostexamples(t): t.cp('examples/example-list.js', 'examples/example-list.xml', 'examples/Jugl.js', 'build/gh-pages/%(BRANCH)s/examples/') -@target('test', PHANTOMJS, INTERNAL_SRC, 'test/requireall.js', phony=True) -def test(t): - t.run(PHANTOMJS, 'test/phantom-jasmine/run_jasmine_test.coffee', 'test/ol.html') +@target(PROJ4JS, PROJ4JS_ZIP) +def proj4js(t): + from zipfile import ZipFile + ZipFile(PROJ4JS_ZIP).extractall('build') + + +@target(PROJ4JS_ZIP, clean=False) +def proj4js_zip(t): + t.download('http://download.osgeo.org/proj4js/' + os.path.basename(t.name), md5=PROJ4JS_ZIP_MD5) if sys.platform == 'win32': + @target('test', '%(PHANTOMJS)s', INTERNAL_SRC, PROJ4JS, 'test/requireall.js', phony=True) + def test(t): + t.run(PHANTOMJS, 'test/phantom-jasmine/run_jasmine_test.coffee', 'test/ol.html') + + # FIXME the PHANTOMJS should be a pake variable, not a constant @target(PHANTOMJS, PHANTOMJS_WINDOWS_ZIP, clean=False) def phantom_js(t): from zipfile import ZipFile @@ -351,7 +369,9 @@ if sys.platform == 'win32': t.download('http://phantomjs.googlecode.com/files/' + os.path.basename(t.name)) else: - virtual(PHANTOMJS) + @target('test', INTERNAL_SRC, PROJ4JS, 'test/requireall.js', phony=True) + def test(t): + t.run('%(PHANTOMJS)s', 'test/phantom-jasmine/run_jasmine_test.coffee', 'test/ol.html') @target('fixme', phony=True) diff --git a/examples/wms-custom-proj.js b/examples/wms-custom-proj.js index d389451935..fea7b563dd 100644 --- a/examples/wms-custom-proj.js +++ b/examples/wms-custom-proj.js @@ -10,7 +10,9 @@ goog.require('ol.Projection'); goog.require('ol.ProjectionUnits'); goog.require('ol.RendererHints'); goog.require('ol.View2D'); +goog.require('ol.layer.ImageLayer'); goog.require('ol.layer.TileLayer'); +goog.require('ol.source.SingleImageWMS'); goog.require('ol.source.TiledWMS'); @@ -24,6 +26,17 @@ var epsg21781 = new ol.Projection('EPSG:21781', ol.ProjectionUnits.METERS, new ol.Extent(485869.5728, 76443.1884, 837076.5648, 299941.7864)); ol.Projection.addProjection(epsg21781); +// We give the single image source a set of resolutions. This prevents the +// source from requesting images of arbitrary resolutions. +var projectionExtent = epsg21781.getExtent(); +var maxResolution = Math.max( + projectionExtent.maxX - projectionExtent.minX, + projectionExtent.maxY - projectionExtent.minY) / 256; +var resolutions = new Array(10); +for (var i = 0; i < 10; ++i) { + resolutions[i] = maxResolution / Math.pow(2.0, i); +} + var extent = new ol.Extent(420000, 30000, 900000, 350000); var layers = new ol.Collection([ new ol.layer.TileLayer({ @@ -41,8 +54,8 @@ var layers = new ol.Collection([ extent: extent }) }), - new ol.layer.TileLayer({ - source: new ol.source.TiledWMS({ + new ol.layer.ImageLayer({ + source: new ol.source.SingleImageWMS({ url: 'http://wms.geo.admin.ch/', attributions: [new ol.Attribution( '© ' + @@ -50,7 +63,7 @@ var layers = new ol.Collection([ 'National parks / geo.admin.ch')], params: {'LAYERS': 'ch.bafu.schutzgebiete-paerke_nationaler_bedeutung'}, projection: epsg21781, - extent: extent + resolutions: resolutions }) }) ]); diff --git a/src/imageurlfunction.js b/src/imageurlfunction.js new file mode 100644 index 0000000000..bb328819eb --- /dev/null +++ b/src/imageurlfunction.js @@ -0,0 +1,40 @@ +goog.provide('ol.ImageUrlFunction'); +goog.provide('ol.ImageUrlFunctionType'); + +goog.require('ol.Extent'); +goog.require('ol.Size'); + + +/** + * @typedef {function(ol.Extent, ol.Size): (string|undefined)} + */ +ol.ImageUrlFunctionType; + + +/** + * @param {string} baseUrl Base URL (may have query data). + * @return {ol.ImageUrlFunctionType} Image URL function. + */ +ol.ImageUrlFunction.createBboxParam = function(baseUrl) { + return function(extent, size) { + // FIXME Projection dependant axis order. + var bboxValue = [ + extent.minX, extent.minY, extent.maxX, extent.maxY + ].join(','); + return goog.uri.utils.appendParams(baseUrl, + 'BBOX', bboxValue, + 'HEIGHT', size.height, + 'WIDTH', size.width); + }; +}; + + +/** + * @param {ol.Extent} extent Extent. + * @param {ol.Size} size Size. + * @return {string|undefined} Image URL. + */ +ol.ImageUrlFunction.nullImageUrlFunction = + function(extent, size) { + return undefined; +}; diff --git a/src/objectliterals.exports b/src/objectliterals.exports index cf5e830f3f..0e24207a11 100644 --- a/src/objectliterals.exports +++ b/src/objectliterals.exports @@ -92,10 +92,29 @@ @exportObjectLiteralProperty ol.source.DebugTileSourceOptions.projection ol.Projection|undefined @exportObjectLiteralProperty ol.source.DebugTileSourceOptions.tileGrid ol.tilegrid.TileGrid|undefined +@exportObjectLiteral ol.source.SingleImageWMSOptions +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.attributions Array.|undefined +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.crossOrigin null|string|undefined +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.extent ol.Extent|undefined +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.params Object. +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.projection ol.Projection|undefined +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.resolutions Array.|undefined +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.url string|undefined +@exportObjectLiteralProperty ol.source.SingleImageWMSOptions.version string|undefined + @exportObjectLiteral ol.source.StamenOptions @exportObjectLiteralProperty ol.source.StamenOptions.flavor string|undefined @exportObjectLiteralProperty ol.source.StamenOptions.provider string +@exportObjectLiteral ol.source.StaticImageOptions +@exportObjectLiteralProperty ol.source.StaticImageOptions.attributions Array.|undefined +@exportObjectLiteralProperty ol.source.StaticImageOptions.crossOrigin null|string|undefined +@exportObjectLiteralProperty ol.source.StaticImageOptions.extent ol.Extent|undefined +@exportObjectLiteralProperty ol.source.StaticImageOptions.imageExtent ol.Extent|undefined +@exportObjectLiteralProperty ol.source.StaticImageOptions.imageSize ol.Size|undefined +@exportObjectLiteralProperty ol.source.StaticImageOptions.projection ol.Projection|undefined +@exportObjectLiteralProperty ol.source.StaticImageOptions.url string|undefined + @exportObjectLiteral ol.source.TiledWMSOptions @exportObjectLiteralProperty ol.source.TiledWMSOptions.attributions Array.|undefined @exportObjectLiteralProperty ol.source.TiledWMSOptions.params Object diff --git a/src/ol/control/attributioncontrol.js b/src/ol/control/attributioncontrol.js index d6f707ea56..731e2b9435 100644 --- a/src/ol/control/attributioncontrol.js +++ b/src/ol/control/attributioncontrol.js @@ -1,4 +1,5 @@ // FIXME handle date line wrap +// FIXME does not handle image sources goog.provide('ol.control.Attribution'); diff --git a/src/ol/ellipsoid.js b/src/ol/ellipsoid.js new file mode 100644 index 0000000000..1669c7de5f --- /dev/null +++ b/src/ol/ellipsoid.js @@ -0,0 +1,170 @@ +goog.provide('ol.Ellipsoid'); + +goog.require('goog.math'); +goog.require('ol.Coordinate'); + + + +/** + * @constructor + * @param {number} a Major radius. + * @param {number} flattening Flattening. + */ +ol.Ellipsoid = function(a, flattening) { + + /** + * @type {number} + */ + this.a = a; + + /** + * @type {number} + */ + this.flattening = flattening; + + /** + * @type {number} + */ + this.b = this.a * (1 - this.flattening); + +}; + + +/** + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 1. + * @param {number=} opt_minDeltaLambda Minimum delta lambda for convergence. + * @param {number=} opt_maxIterations Maximum iterations. + * @return {{distance: number, initialBearing: number, finalBearing: number}} + * Vincenty. + */ +ol.Ellipsoid.prototype.vincenty = + function(c1, c2, opt_minDeltaLambda, opt_maxIterations) { + var minDeltaLambda = goog.isDef(opt_minDeltaLambda) ? + opt_minDeltaLambda : 1e-12; + var maxIterations = goog.isDef(opt_maxIterations) ? + opt_maxIterations : 100; + var f = this.flattening; + var lat1 = goog.math.toRadians(c1.y); + var lat2 = goog.math.toRadians(c2.y); + var deltaLon = goog.math.toRadians(c2.x - c1.x); + var U1 = Math.atan((1 - f) * Math.tan(lat1)); + var cosU1 = Math.cos(U1); + var sinU1 = Math.sin(U1); + var U2 = Math.atan((1 - f) * Math.tan(lat2)); + var cosU2 = Math.cos(U2); + var sinU2 = Math.sin(U2); + var lambda = deltaLon; + var cosSquaredAlpha, sinAlpha; + var cosLambda, deltaLambda = Infinity, sinLambda; + var cos2SigmaM, cosSigma, sigma, sinSigma; + var i; + for (i = maxIterations; i > 0; --i) { + cosLambda = Math.cos(lambda); + sinLambda = Math.sin(lambda); + var x = cosU2 * sinLambda; + var y = cosU1 * sinU2 - sinU1 * cosU2 * cosLambda; + sinSigma = Math.sqrt(x * x + y * y); + if (sinSigma === 0) { + return { + distance: 0, + initialBearing: 0, + finalBearing: 0 + }; + } + cosSigma = sinU1 * sinU2 + cosU1 * cosU2 * cosLambda; + sigma = Math.atan2(sinSigma, cosSigma); + sinAlpha = cosU1 * cosU2 * sinLambda / sinSigma; + cosSquaredAlpha = 1 - sinAlpha * sinAlpha; + cos2SigmaM = cosSigma - 2 * sinU1 * sinU2 / cosSquaredAlpha; + if (isNaN(cos2SigmaM)) { + cos2SigmaM = 0; + } + var C = f / 16 * cosSquaredAlpha * (4 + f * (4 - 3 * cosSquaredAlpha)); + var lambdaPrime = deltaLon + (1 - C) * f * sinAlpha * (sigma + + C * sinSigma * (cos2SigmaM + + C * cosSigma * (2 * cos2SigmaM * cos2SigmaM - 1))); + deltaLambda = Math.abs(lambdaPrime - lambda); + lambda = lambdaPrime; + if (deltaLambda < minDeltaLambda) { + break; + } + } + if (i === 0) { + return { + distance: NaN, + finalBearing: NaN, + initialBearing: NaN + }; + } + var aSquared = this.a * this.a; + var bSquared = this.b * this.b; + var uSquared = cosSquaredAlpha * (aSquared - bSquared) / bSquared; + var A = 1 + uSquared / 16384 * + (4096 + uSquared * (uSquared * (320 - 175 * uSquared) - 768)); + var B = uSquared / 1024 * + (256 + uSquared * (uSquared * (74 - 47 * uSquared) - 128)); + var deltaSigma = B * sinSigma * (cos2SigmaM + B / 4 * + (cosSigma * (2 * cos2SigmaM * cos2SigmaM - 1) - + B / 6 * cos2SigmaM * (4 * sinSigma * sinSigma - 3) * + (4 * cos2SigmaM * cos2SigmaM - 3))); + cosLambda = Math.cos(lambda); + sinLambda = Math.sin(lambda); + var alpha1 = Math.atan2(cosU2 * sinLambda, + cosU1 * sinU2 - sinU1 * cosU2 * cosLambda); + var alpha2 = Math.atan2(cosU1 * sinLambda, + cosU1 * sinU2 * cosLambda - sinU1 * cosU2); + return { + distance: this.b * A * (sigma - deltaSigma), + initialBearing: goog.math.toDegrees(alpha1), + finalBearing: goog.math.toDegrees(alpha2) + }; +}; + + +/** + * Returns the distance from c1 to c2 using Vincenty. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 1. + * @param {number=} opt_minDeltaLambda Minimum delta lambda for convergence. + * @param {number=} opt_maxIterations Maximum iterations. + * @return {number} Vincenty distance. + */ +ol.Ellipsoid.prototype.vincentyDistance = + function(c1, c2, opt_minDeltaLambda, opt_maxIterations) { + var vincenty = this.vincenty(c1, c2, opt_minDeltaLambda, opt_maxIterations); + return vincenty.distance; +}; + + +/** + * Returns the final bearing from c1 to c2 using Vincenty. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 1. + * @param {number=} opt_minDeltaLambda Minimum delta lambda for convergence. + * @param {number=} opt_maxIterations Maximum iterations. + * @return {number} Initial bearing. + */ +ol.Ellipsoid.prototype.vincentyFinalBearing = + function(c1, c2, opt_minDeltaLambda, opt_maxIterations) { + var vincenty = this.vincenty(c1, c2, opt_minDeltaLambda, opt_maxIterations); + return vincenty.finalBearing; +}; + + +/** + * Returns the initial bearing from c1 to c2 using Vincenty. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 1. + * @param {number=} opt_minDeltaLambda Minimum delta lambda for convergence. + * @param {number=} opt_maxIterations Maximum iterations. + * @return {number} Initial bearing. + */ +ol.Ellipsoid.prototype.vincentyInitialBearing = + function(c1, c2, opt_minDeltaLambda, opt_maxIterations) { + var vincenty = this.vincenty(c1, c2, opt_minDeltaLambda, opt_maxIterations); + return vincenty.initialBearing; +}; diff --git a/src/ol/ellipsoid/wgs84.js b/src/ol/ellipsoid/wgs84.js new file mode 100644 index 0000000000..e5244b80d7 --- /dev/null +++ b/src/ol/ellipsoid/wgs84.js @@ -0,0 +1,10 @@ +goog.provide('ol.ellipsoid.WGS84'); + +goog.require('ol.Ellipsoid'); + + +/** + * @const + * @type {ol.Ellipsoid} + */ +ol.ellipsoid.WGS84 = new ol.Ellipsoid(6378137, 1 / 298.257223563); diff --git a/src/ol/extent.js b/src/ol/extent.js index 3d0ab215d6..3606dfc63c 100644 --- a/src/ol/extent.js +++ b/src/ol/extent.js @@ -46,17 +46,31 @@ ol.Extent.boundingExtent = function(var_args) { /** - * Checks if the given coordinate is contained or on the edge of the extent. + * Checks if the passed coordinate is contained or on the edge + * of the extent. * * @param {ol.Coordinate} coordinate Coordinate. * @return {boolean} Contains. */ -ol.Extent.prototype.contains = function(coordinate) { +ol.Extent.prototype.containsCoordinate = function(coordinate) { return this.minX <= coordinate.x && coordinate.x <= this.maxX && this.minY <= coordinate.y && coordinate.y <= this.maxY; }; +/** + * Checks if the passed extent is contained or on the edge of the + * extent. + * + * @param {ol.Extent} extent Extent. + * @return {boolean} Contains. + */ +ol.Extent.prototype.containsExtent = function(extent) { + return this.minX <= extent.minX && extent.maxX <= this.maxX && + this.minY <= extent.minY && extent.maxY <= this.maxY; +}; + + /** * @return {ol.Coordinate} Bottom left coordinate. */ diff --git a/src/ol/geolocation.js b/src/ol/geolocation.js index b75aaaf38c..99a0535382 100644 --- a/src/ol/geolocation.js +++ b/src/ol/geolocation.js @@ -5,6 +5,7 @@ goog.provide('ol.Geolocation'); goog.provide('ol.GeolocationProperty'); goog.require('goog.functions'); +goog.require('goog.math'); goog.require('ol.Coordinate'); goog.require('ol.Object'); goog.require('ol.Projection'); @@ -15,8 +16,12 @@ goog.require('ol.Projection'); */ ol.GeolocationProperty = { ACCURACY: 'accuracy', + ALTITUDE: 'altitude', + ALTITUDE_ACCURACY: 'altitudeAccuracy', + HEADING: 'heading', POSITION: 'position', - PROJECTION: 'projection' + PROJECTION: 'projection', + SPEED: 'speed' }; @@ -92,10 +97,19 @@ ol.Geolocation.isSupported = 'geolocation' in navigator; */ ol.Geolocation.prototype.positionChange_ = function(position) { var coords = position.coords; + this.set(ol.GeolocationProperty.ACCURACY, coords.accuracy); + this.set(ol.GeolocationProperty.ALTITUDE, + goog.isNull(coords.altitude) ? undefined : coords.altitude); + this.set(ol.GeolocationProperty.ALTITUDE_ACCURACY, + goog.isNull(coords.altitudeAccuracy) ? + undefined : coords.altitudeAccuracy); + this.set(ol.GeolocationProperty.HEADING, goog.isNull(coords.heading) ? + undefined : goog.math.toRadians(coords.heading)); this.position_ = new ol.Coordinate(coords.longitude, coords.latitude); this.set(ol.GeolocationProperty.POSITION, this.transformCoords_(this.position_)); - this.set(ol.GeolocationProperty.ACCURACY, coords.accuracy); + this.set(ol.GeolocationProperty.SPEED, + goog.isNull(coords.speed) ? undefined : coords.speed); }; @@ -107,20 +121,6 @@ ol.Geolocation.prototype.positionError_ = function(error) { }; -/** - * The position of the device. - * @return {ol.Coordinate|undefined} position. - */ -ol.Geolocation.prototype.getPosition = function() { - return /** @type {ol.Coordinate} */ ( - this.get(ol.GeolocationProperty.POSITION)); -}; -goog.exportProperty( - ol.Geolocation.prototype, - 'getPosition', - ol.Geolocation.prototype.getPosition); - - /** * The accuracy of the position in meters. * @return {number|undefined} accuracy. @@ -135,6 +135,59 @@ goog.exportProperty( ol.Geolocation.prototype.getAccuracy); +/** + * @return {number|undefined} Altitude. + */ +ol.Geolocation.prototype.getAltitude = function() { + return /** @type {number|undefined} */ ( + this.get(ol.GeolocationProperty.ALTITUDE)); +}; +goog.exportProperty( + ol.Geolocation.prototype, + 'getAltitude', + ol.Geolocation.prototype.getAltitude); + + +/** + * @return {number|undefined} Altitude accuracy. + */ +ol.Geolocation.prototype.getAltitudeAccuracy = function() { + return /** @type {number|undefined} */ ( + this.get(ol.GeolocationProperty.ALTITUDE_ACCURACY)); +}; +goog.exportProperty( + ol.Geolocation.prototype, + 'getAltitudeAccuracy', + ol.Geolocation.prototype.getAltitudeAccuracy); + + +/** + * @return {number|undefined} Heading. + */ +ol.Geolocation.prototype.getHeading = function() { + return /** @type {number|undefined} */ ( + this.get(ol.GeolocationProperty.HEADING)); +}; +goog.exportProperty( + ol.Geolocation.prototype, + 'getHeading', + ol.Geolocation.prototype.getHeading); + + +/** + * The position of the device. + * @return {ol.Coordinate|undefined} position. + */ +ol.Geolocation.prototype.getPosition = function() { + return /** @type {ol.Coordinate} */ ( + this.get(ol.GeolocationProperty.POSITION)); +}; +goog.exportProperty( + ol.Geolocation.prototype, + 'getPosition', + ol.Geolocation.prototype.getPosition); + + /** * @return {ol.Projection|undefined} projection. */ @@ -148,6 +201,19 @@ goog.exportProperty( ol.Geolocation.prototype.getProjection); +/** + * @return {number|undefined} Speed. + */ +ol.Geolocation.prototype.getSpeed = function() { + return /** @type {number|undefined} */ ( + this.get(ol.GeolocationProperty.SPEED)); +}; +goog.exportProperty( + ol.Geolocation.prototype, + 'getSpeed', + ol.Geolocation.prototype.getSpeed); + + /** * @param {ol.Projection} projection Projection. */ diff --git a/src/ol/image.js b/src/ol/image.js new file mode 100644 index 0000000000..9f7cbe0c47 --- /dev/null +++ b/src/ol/image.js @@ -0,0 +1,187 @@ +goog.provide('ol.Image'); +goog.provide('ol.ImageState'); + +goog.require('goog.array'); +goog.require('goog.events'); +goog.require('goog.events.EventTarget'); +goog.require('goog.events.EventType'); +goog.require('ol.Extent'); + + +/** + * @enum {number} + */ +ol.ImageState = { + IDLE: 0, + LOADING: 1, + LOADED: 2, + ERROR: 3 +}; + + + +/** + * @constructor + * @extends {goog.events.EventTarget} + * @param {ol.Extent} extent Extent. + * @param {number} resolution Resolution. + * @param {string} src Image source URI. + * @param {?string} crossOrigin Cross origin. + */ +ol.Image = function(extent, resolution, src, crossOrigin) { + + /** + * @private + * @type {ol.Extent} + */ + this.extent_ = extent; + + /** + * @private + * @type {string} + */ + this.src_ = src; + + /** + * @private + * @type {number} + */ + this.resolution_ = resolution; + + /** + * @private + * @type {Image} + */ + this.image_ = new Image(); + if (!goog.isNull(crossOrigin)) { + this.image_.crossOrigin = crossOrigin; + } + + /** + * @private + * @type {Object.} + */ + this.imageByContext_ = {}; + + /** + * @private + * @type {Array.} + */ + this.imageListenerKeys_ = null; + + /** + * @protected + * @type {ol.ImageState} + */ + this.state = ol.ImageState.IDLE; +}; +goog.inherits(ol.Image, goog.events.EventTarget); + + +/** + * @protected + */ +ol.Image.prototype.dispatchChangeEvent = function() { + this.dispatchEvent(goog.events.EventType.CHANGE); +}; + + +/** + * @return {ol.Extent} Extent. + */ +ol.Image.prototype.getExtent = function() { + return this.extent_; +}; + + +/** + * @param {Object=} opt_context Object. + * @return {HTMLCanvasElement|Image|HTMLVideoElement} Image. + */ +ol.Image.prototype.getImageElement = function(opt_context) { + if (goog.isDef(opt_context)) { + var image; + var key = goog.getUid(opt_context); + if (key in this.imageByContext_) { + return this.imageByContext_[key]; + } else if (goog.object.isEmpty(this.imageByContext_)) { + image = this.image_; + } else { + image = /** @type {Image} */ (this.image_.cloneNode(false)); + } + this.imageByContext_[key] = image; + return image; + } else { + return this.image_; + } +}; + + +/** + * @return {number} Resolution. + */ +ol.Image.prototype.getResolution = function() { + return this.resolution_; +}; + + +/** + * @return {ol.ImageState} State. + */ +ol.Image.prototype.getState = function() { + return this.state; +}; + + +/** + * Tracks loading or read errors. + * + * @private + */ +ol.Image.prototype.handleImageError_ = function() { + this.state = ol.ImageState.ERROR; + this.unlistenImage_(); + this.dispatchChangeEvent(); +}; + + +/** + * Tracks successful image load. + * + * @private + */ +ol.Image.prototype.handleImageLoad_ = function() { + this.state = ol.ImageState.LOADED; + this.unlistenImage_(); + this.dispatchChangeEvent(); +}; + + +/** + * Load not yet loaded URI. + */ +ol.Image.prototype.load = function() { + if (this.state == ol.ImageState.IDLE) { + this.state = ol.ImageState.LOADING; + goog.asserts.assert(goog.isNull(this.imageListenerKeys_)); + this.imageListenerKeys_ = [ + goog.events.listenOnce(this.image_, goog.events.EventType.ERROR, + this.handleImageError_, false, this), + goog.events.listenOnce(this.image_, goog.events.EventType.LOAD, + this.handleImageLoad_, false, this) + ]; + this.image_.src = this.src_; + } +}; + + +/** + * Discards event handlers which listen for load completion or errors. + * + * @private + */ +ol.Image.prototype.unlistenImage_ = function() { + goog.asserts.assert(!goog.isNull(this.imageListenerKeys_)); + goog.array.forEach(this.imageListenerKeys_, goog.events.unlistenByKey); + this.imageListenerKeys_ = null; +}; diff --git a/src/ol/interaction/dragpaninteraction.js b/src/ol/interaction/dragpaninteraction.js index e7228f8cf6..4281be71fb 100644 --- a/src/ol/interaction/dragpaninteraction.js +++ b/src/ol/interaction/dragpaninteraction.js @@ -106,7 +106,8 @@ ol.interaction.DragPan.prototype.handleDragStart = function(mapBrowserEvent) { var browserEvent = mapBrowserEvent.browserEvent; if (this.condition_(browserEvent)) { if (this.kinetic_) { - this.kinetic_.begin(browserEvent.clientX, browserEvent.clientY); + this.kinetic_.begin(); + this.kinetic_.update(browserEvent.clientX, browserEvent.clientY); } var map = mapBrowserEvent.map; map.requestRenderFrame(); diff --git a/src/ol/kinetic.js b/src/ol/kinetic.js index 31fcb8b9aa..e6b78526d2 100644 --- a/src/ol/kinetic.js +++ b/src/ol/kinetic.js @@ -63,14 +63,12 @@ ol.Kinetic = function(decay, minVelocity, delay) { /** - * @param {number} x X. - * @param {number} y Y. + * FIXME empty description for jsdoc */ -ol.Kinetic.prototype.begin = function(x, y) { +ol.Kinetic.prototype.begin = function() { this.points_.length = 0; this.angle_ = 0; this.initialVelocity_ = 0; - this.update(x, y); }; diff --git a/src/ol/layer/imagelayer.exports b/src/ol/layer/imagelayer.exports new file mode 100644 index 0000000000..f00c7ec5e5 --- /dev/null +++ b/src/ol/layer/imagelayer.exports @@ -0,0 +1 @@ +@exportClass ol.layer.ImageLayer ol.layer.LayerOptions diff --git a/src/ol/layer/imagelayer.js b/src/ol/layer/imagelayer.js new file mode 100644 index 0000000000..8b61efce0e --- /dev/null +++ b/src/ol/layer/imagelayer.js @@ -0,0 +1,24 @@ +goog.provide('ol.layer.ImageLayer'); + +goog.require('ol.layer.Layer'); +goog.require('ol.source.ImageSource'); + + + +/** + * @constructor + * @extends {ol.layer.Layer} + * @param {ol.layer.LayerOptions} layerOptions Layer options. + */ +ol.layer.ImageLayer = function(layerOptions) { + goog.base(this, layerOptions); +}; +goog.inherits(ol.layer.ImageLayer, ol.layer.Layer); + + +/** + * @return {ol.source.ImageSource} Single image source. + */ +ol.layer.ImageLayer.prototype.getImageSource = function() { + return /** @type {ol.source.ImageSource} */ (this.getSource()); +}; diff --git a/src/ol/rectangle.js b/src/ol/rectangle.js index d15f1d7a18..9d7a27af5e 100644 --- a/src/ol/rectangle.js +++ b/src/ol/rectangle.js @@ -41,6 +41,16 @@ ol.Rectangle = function(minX, minY, maxX, maxY) { }; +/** + * @param {ol.Rectangle} rectangle Rectangle. + * @return {boolean} Equals. + */ +ol.Rectangle.prototype.equals = function(rectangle) { + return this.minX == rectangle.minX && this.minY == rectangle.minY && + this.maxX == rectangle.maxX && this.maxY == rectangle.maxY; +}; + + /** * @param {ol.Rectangle} rectangle Rectangle. */ @@ -114,3 +124,16 @@ ol.Rectangle.prototype.normalize = function(coordinate) { ol.Rectangle.prototype.toString = function() { return '(' + [this.minX, this.minY, this.maxX, this.maxY].join(', ') + ')'; }; + + +/** + * @param {number} value Value. + */ +ol.Rectangle.prototype.scaleFromCenter = function(value) { + var deltaX = (this.getWidth() / 2.0) * (value - 1); + var deltaY = (this.getHeight() / 2.0) * (value - 1); + this.minX -= deltaX; + this.minY -= deltaY; + this.maxX += deltaX; + this.maxY += deltaY; +}; diff --git a/src/ol/renderer/canvas/canvasimagelayerrenderer.js b/src/ol/renderer/canvas/canvasimagelayerrenderer.js new file mode 100644 index 0000000000..273f22d49f --- /dev/null +++ b/src/ol/renderer/canvas/canvasimagelayerrenderer.js @@ -0,0 +1,115 @@ +goog.provide('ol.renderer.canvas.ImageLayer'); + +goog.require('goog.vec.Mat4'); +goog.require('ol.Image'); +goog.require('ol.ImageState'); +goog.require('ol.ViewHint'); +goog.require('ol.layer.ImageLayer'); +goog.require('ol.renderer.Map'); +goog.require('ol.renderer.canvas.Layer'); + + + +/** + * @constructor + * @extends {ol.renderer.canvas.Layer} + * @param {ol.renderer.Map} mapRenderer Map renderer. + * @param {ol.layer.ImageLayer} imageLayer Single image layer. + */ +ol.renderer.canvas.ImageLayer = function(mapRenderer, imageLayer) { + + goog.base(this, mapRenderer, imageLayer); + + /** + * @private + * @type {?ol.Image} + */ + this.image_ = null; + + /** + * @private + * @type {!goog.vec.Mat4.Number} + */ + this.transform_ = goog.vec.Mat4.createNumber(); + +}; +goog.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.Layer); + + +/** + * @inheritDoc + */ +ol.renderer.canvas.ImageLayer.prototype.getImage = function() { + return goog.isNull(this.image_) ? + null : this.image_.getImageElement(this); +}; + + +/** + * @return {ol.layer.ImageLayer} Single image layer. + */ +ol.renderer.canvas.ImageLayer.prototype.getImageLayer = function() { + return /** @type {ol.layer.ImageLayer} */ (this.getLayer()); +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.ImageLayer.prototype.getTransform = function() { + return this.transform_; +}; + + +/** + * @inheritDoc + */ +ol.renderer.canvas.ImageLayer.prototype.renderFrame = + function(frameState, layerState) { + + var view2DState = frameState.view2DState; + var viewCenter = view2DState.center; + var viewResolution = view2DState.resolution; + var viewRotation = view2DState.rotation; + + var image; + var imageLayer = this.getImageLayer(); + var imageSource = imageLayer.getImageSource(); + + var hints = frameState.viewHints; + + if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.PANNING]) { + image = imageSource.getImage(frameState.extent, viewResolution); + if (!goog.isNull(image)) { + var imageState = image.getState(); + if (imageState == ol.ImageState.IDLE) { + goog.events.listenOnce(image, goog.events.EventType.CHANGE, + this.handleImageChange, false, this); + image.load(); + } else if (imageState == ol.ImageState.LOADED) { + this.image_ = image; + } + } + } + + if (!goog.isNull(this.image_)) { + image = this.image_; + var imageExtent = image.getExtent(); + var imageResolution = image.getResolution(); + var transform = this.transform_; + goog.vec.Mat4.makeIdentity(transform); + goog.vec.Mat4.translate(transform, + frameState.size.width / 2, frameState.size.height / 2, 0); + goog.vec.Mat4.rotateZ(transform, viewRotation); + goog.vec.Mat4.scale( + transform, + imageResolution / viewResolution, + imageResolution / viewResolution, + 1); + goog.vec.Mat4.translate( + transform, + (imageExtent.minX - viewCenter.x) / imageResolution, + (viewCenter.y - imageExtent.maxY) / imageResolution, + 0); + } +}; diff --git a/src/ol/renderer/canvas/canvasmaprenderer.js b/src/ol/renderer/canvas/canvasmaprenderer.js index f9bde65550..083003196d 100644 --- a/src/ol/renderer/canvas/canvasmaprenderer.js +++ b/src/ol/renderer/canvas/canvasmaprenderer.js @@ -7,9 +7,11 @@ goog.require('goog.dom'); goog.require('goog.style'); goog.require('goog.vec.Mat4'); goog.require('ol.Size'); +goog.require('ol.layer.ImageLayer'); goog.require('ol.layer.TileLayer'); goog.require('ol.layer.Vector'); goog.require('ol.renderer.Map'); +goog.require('ol.renderer.canvas.ImageLayer'); goog.require('ol.renderer.canvas.TileLayer'); goog.require('ol.renderer.canvas.VectorLayer'); @@ -61,7 +63,9 @@ goog.inherits(ol.renderer.canvas.Map, ol.renderer.Map); * @inheritDoc */ ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) { - if (layer instanceof ol.layer.TileLayer) { + if (layer instanceof ol.layer.ImageLayer) { + return new ol.renderer.canvas.ImageLayer(this, layer); + } else if (layer instanceof ol.layer.TileLayer) { return new ol.renderer.canvas.TileLayer(this, layer); } else if (layer instanceof ol.layer.Vector) { return new ol.renderer.canvas.VectorLayer(this, layer); diff --git a/src/ol/renderer/dom/domimagelayerrenderer.js b/src/ol/renderer/dom/domimagelayerrenderer.js new file mode 100644 index 0000000000..b3f5f9f224 --- /dev/null +++ b/src/ol/renderer/dom/domimagelayerrenderer.js @@ -0,0 +1,122 @@ +goog.provide('ol.renderer.dom.ImageLayer'); + +goog.require('goog.dom'); +goog.require('goog.vec.Mat4'); +goog.require('ol.Image'); +goog.require('ol.ImageState'); +goog.require('ol.ViewHint'); +goog.require('ol.dom'); +goog.require('ol.layer.ImageLayer'); +goog.require('ol.renderer.dom.Layer'); + + + +/** + * @constructor + * @extends {ol.renderer.dom.Layer} + * @param {ol.renderer.Map} mapRenderer Map renderer. + * @param {ol.layer.ImageLayer} imageLayer Image layer. + */ +ol.renderer.dom.ImageLayer = function(mapRenderer, imageLayer) { + var target = goog.dom.createElement(goog.dom.TagName.DIV); + target.className = 'ol-layer-image'; + target.style.position = 'absolute'; + + goog.base(this, mapRenderer, imageLayer, target); + + /** + * The last rendered image. + * @private + * @type {?ol.Image} + */ + this.image_ = null; + + /** + * @private + * @type {goog.vec.Mat4.AnyType} + */ + this.transform_ = goog.vec.Mat4.createNumberIdentity(); + +}; +goog.inherits(ol.renderer.dom.ImageLayer, ol.renderer.dom.Layer); + + +/** + * @return {ol.layer.ImageLayer} Image layer. + */ +ol.renderer.dom.ImageLayer.prototype.getImageLayer = function() { + return /** @type {ol.layer.ImageLayer} */ (this.getLayer()); +}; + + +/** + * @inheritDoc + */ +ol.renderer.dom.ImageLayer.prototype.renderFrame = + function(frameState, layerState) { + + var view2DState = frameState.view2DState; + var viewCenter = view2DState.center; + var viewResolution = view2DState.resolution; + var viewRotation = view2DState.rotation; + + var image = this.image_; + var imageLayer = this.getImageLayer(); + var imageSource = imageLayer.getImageSource(); + + var hints = frameState.viewHints; + + if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.PANNING]) { + var image_ = imageSource.getImage(frameState.extent, viewResolution); + if (!goog.isNull(image_)) { + var imageState = image_.getState(); + if (imageState == ol.ImageState.IDLE) { + goog.events.listenOnce(image_, goog.events.EventType.CHANGE, + this.handleImageChange, false, this); + image_.load(); + } else if (imageState == ol.ImageState.LOADED) { + image = image_; + } + } + } + + if (!goog.isNull(image)) { + var imageExtent = image.getExtent(); + var imageResolution = image.getResolution(); + var transform = goog.vec.Mat4.createNumber(); + goog.vec.Mat4.makeIdentity(transform); + goog.vec.Mat4.translate(transform, + frameState.size.width / 2, frameState.size.height / 2, 0); + goog.vec.Mat4.rotateZ(transform, viewRotation); + goog.vec.Mat4.scale( + transform, + imageResolution / viewResolution, + imageResolution / viewResolution, + 1); + goog.vec.Mat4.translate( + transform, + (imageExtent.minX - viewCenter.x) / imageResolution, + (viewCenter.y - imageExtent.maxY) / imageResolution, + 0); + if (image != this.image_) { + var imageElement = image.getImageElement(this); + imageElement.style.position = 'absolute'; + goog.dom.removeChildren(this.target); + goog.dom.appendChild(this.target, imageElement); + this.image_ = image; + } + this.setTransform(transform); + } + +}; + + +/** + * @param {goog.vec.Mat4.AnyType} transform Transform. + */ +ol.renderer.dom.ImageLayer.prototype.setTransform = function(transform) { + if (!goog.vec.Mat4.equals(transform, this.transform_)) { + ol.dom.transformElement2D(this.target, transform, 6); + goog.vec.Mat4.setFromArray(this.transform_, transform); + } +}; diff --git a/src/ol/renderer/dom/dommaprenderer.js b/src/ol/renderer/dom/dommaprenderer.js index 18700d9b25..a833d5fd91 100644 --- a/src/ol/renderer/dom/dommaprenderer.js +++ b/src/ol/renderer/dom/dommaprenderer.js @@ -5,8 +5,10 @@ goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.style'); +goog.require('ol.layer.ImageLayer'); goog.require('ol.layer.TileLayer'); goog.require('ol.renderer.Map'); +goog.require('ol.renderer.dom.ImageLayer'); goog.require('ol.renderer.dom.TileLayer'); @@ -57,14 +59,15 @@ ol.renderer.dom.Map.prototype.addLayer = function(layer) { * @inheritDoc */ ol.renderer.dom.Map.prototype.createLayerRenderer = function(layer) { + var layerRenderer; if (layer instanceof ol.layer.TileLayer) { - var layerRenderer = new ol.renderer.dom.TileLayer(this, layer); - goog.dom.appendChild(this.layersPane_, layerRenderer.getTarget()); - return layerRenderer; - } else { - goog.asserts.assert(false); - return null; + layerRenderer = new ol.renderer.dom.TileLayer(this, layer); + } else if (layer instanceof ol.layer.ImageLayer) { + layerRenderer = new ol.renderer.dom.ImageLayer(this, layer); } + goog.asserts.assert(goog.isDef(layerRenderer)); + goog.dom.appendChild(this.layersPane_, layerRenderer.getTarget()); + return layerRenderer; }; diff --git a/src/ol/renderer/layerrenderer.js b/src/ol/renderer/layerrenderer.js index fab64889fc..bfe98600c4 100644 --- a/src/ol/renderer/layerrenderer.js +++ b/src/ol/renderer/layerrenderer.js @@ -3,6 +3,8 @@ goog.provide('ol.renderer.Layer'); goog.require('goog.events'); goog.require('goog.events.EventType'); goog.require('ol.FrameState'); +goog.require('ol.Image'); +goog.require('ol.ImageState'); goog.require('ol.Object'); goog.require('ol.Tile'); goog.require('ol.TileCoord'); @@ -119,6 +121,19 @@ ol.renderer.Layer.prototype.handleLayerContrastChange = goog.nullFunction; ol.renderer.Layer.prototype.handleLayerHueChange = goog.nullFunction; +/** + * Handle changes in image state. + * @param {goog.events.Event} event Image change event. + * @protected + */ +ol.renderer.Layer.prototype.handleImageChange = function(event) { + var image = /** @type {ol.Image} */ (event.target); + if (image.getState() === ol.ImageState.LOADED) { + this.getMap().requestRenderFrame(); + } +}; + + /** * @protected */ diff --git a/src/ol/renderer/webgl/webglimagelayerrenderer.js b/src/ol/renderer/webgl/webglimagelayerrenderer.js new file mode 100644 index 0000000000..6449a7c178 --- /dev/null +++ b/src/ol/renderer/webgl/webglimagelayerrenderer.js @@ -0,0 +1,234 @@ +goog.provide('ol.renderer.webgl.ImageLayer'); + +goog.require('goog.vec.Mat4'); +goog.require('ol.Coordinate'); +goog.require('ol.Extent'); +goog.require('ol.Image'); +goog.require('ol.ImageState'); +goog.require('ol.ViewHint'); +goog.require('ol.layer.ImageLayer'); +goog.require('ol.renderer.webgl.Layer'); + + + +/** + * @constructor + * @extends {ol.renderer.webgl.Layer} + * @param {ol.renderer.Map} mapRenderer Map renderer. + * @param {ol.layer.ImageLayer} imageLayer Tile layer. + */ +ol.renderer.webgl.ImageLayer = function(mapRenderer, imageLayer) { + + goog.base(this, mapRenderer, imageLayer); + + /** + * The last rendered image. + * @private + * @type {?ol.Image} + */ + this.image_ = null; + + /** + * The last rendered texture. + * @private + * @type {WebGLTexture} + */ + this.texture_ = null; + + /** + * @private + * @type {!goog.vec.Mat4.Number} + */ + this.texCoordMatrix_ = goog.vec.Mat4.createNumberIdentity(); + + /** + * @private + * @type {!goog.vec.Mat4.Number} + */ + this.vertexCoordMatrix_ = goog.vec.Mat4.createNumber(); + +}; +goog.inherits(ol.renderer.webgl.ImageLayer, ol.renderer.webgl.Layer); + + +/** + * @private + * @param {ol.Image} image Image. + * @return {WebGLTexture} Texture. + */ +ol.renderer.webgl.ImageLayer.prototype.createTexture_ = function(image) { + + // We meet the conditions to work with non-power of two textures. + // http://www.khronos.org/webgl/wiki/WebGL_and_OpenGL_Differences#Non-Power_of_Two_Texture_Support + // http://learningwebgl.com/blog/?p=2101 + + var imageElement = image.getImageElement(this); + var gl = this.getMapRenderer().getGL(); + + var texture = gl.createTexture(); + + gl.bindTexture(goog.webgl.TEXTURE_2D, texture); + gl.texImage2D(goog.webgl.TEXTURE_2D, 0, goog.webgl.RGBA, + goog.webgl.RGBA, goog.webgl.UNSIGNED_BYTE, imageElement); + + gl.texParameteri( + goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_S, + goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri( + goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_T, + goog.webgl.CLAMP_TO_EDGE); + gl.texParameteri( + goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MIN_FILTER, goog.webgl.LINEAR); + gl.texParameteri( + goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MAG_FILTER, goog.webgl.LINEAR); + + return texture; +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.ImageLayer.prototype.disposeInternal = function() { + var mapRenderer = this.getMapRenderer(); + var gl = mapRenderer.getGL(); + if (!gl.isContextLost()) { + gl.deleteTexture(this.texture_); + } + goog.base(this, 'disposeInternal'); +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.ImageLayer.prototype.getTexCoordMatrix = function() { + return this.texCoordMatrix_; +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.ImageLayer.prototype.getTexture = function() { + return this.texture_; +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.ImageLayer.prototype.getVertexCoordMatrix = function() { + return this.vertexCoordMatrix_; +}; + + +/** + * @return {ol.layer.ImageLayer} Tile layer. + */ +ol.renderer.webgl.ImageLayer.prototype.getImageLayer = function() { + return /** @type {ol.layer.ImageLayer} */ (this.getLayer()); +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.ImageLayer.prototype.handleWebGLContextLost = function() { + this.texture_ = null; +}; + + +/** + * @inheritDoc + */ +ol.renderer.webgl.ImageLayer.prototype.renderFrame = + function(frameState, layerState) { + + var gl = this.getMapRenderer().getGL(); + + var view2DState = frameState.view2DState; + var viewCenter = view2DState.center; + var viewResolution = view2DState.resolution; + var viewRotation = view2DState.rotation; + + var image = this.image_; + var texture = this.texture_; + var imageLayer = this.getImageLayer(); + var imageSource = imageLayer.getImageSource(); + + var hints = frameState.viewHints; + + if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.PANNING]) { + var image_ = imageSource.getImage(frameState.extent, viewResolution); + if (!goog.isNull(image_)) { + var imageState = image_.getState(); + if (imageState == ol.ImageState.IDLE) { + goog.events.listenOnce(image_, goog.events.EventType.CHANGE, + this.handleImageChange, false, this); + image_.load(); + } else if (imageState == ol.ImageState.LOADED) { + image = image_; + texture = this.createTexture_(image_); + if (!goog.isNull(this.texture_)) { + frameState.postRenderFunctions.push( + goog.partial(function(gl, texture) { + if (!gl.isContextLost()) { + gl.deleteTexture(texture); + } + }, gl, this.texture_)); + } + } + } + } + + if (!goog.isNull(image)) { + goog.asserts.assert(!goog.isNull(texture)); + + var canvas = this.getMapRenderer().getCanvas(); + + this.updateVertexCoordMatrix_(canvas.width, canvas.height, + viewCenter, viewResolution, viewRotation, image.getExtent()); + + // Translate and scale to flip the Y coord. + var texCoordMatrix = this.texCoordMatrix_; + goog.vec.Mat4.makeIdentity(texCoordMatrix); + goog.vec.Mat4.scale(texCoordMatrix, 1, -1, 1); + goog.vec.Mat4.translate(texCoordMatrix, 0, -1, 0); + + this.image_ = image; + this.texture_ = texture; + } +}; + + +/** + * @private + * @param {number} canvasWidth Canvas width. + * @param {number} canvasHeight Canvas height. + * @param {ol.Coordinate} viewCenter View center. + * @param {number} viewResolution View resolution. + * @param {number} viewRotation View rotation. + * @param {ol.Extent} imageExtent Image extent. + */ +ol.renderer.webgl.ImageLayer.prototype.updateVertexCoordMatrix_ = + function(canvasWidth, canvasHeight, viewCenter, + viewResolution, viewRotation, imageExtent) { + + var canvasExtentWidth = canvasWidth * viewResolution; + var canvasExtentHeight = canvasHeight * viewResolution; + + var vertexCoordMatrix = this.vertexCoordMatrix_; + goog.vec.Mat4.makeIdentity(vertexCoordMatrix); + goog.vec.Mat4.scale(vertexCoordMatrix, + 2 / canvasExtentWidth, 2 / canvasExtentHeight, 1); + goog.vec.Mat4.rotateZ(vertexCoordMatrix, -viewRotation); + goog.vec.Mat4.translate(vertexCoordMatrix, + imageExtent.minX - viewCenter.x, + imageExtent.minY - viewCenter.y, + 0); + goog.vec.Mat4.scale(vertexCoordMatrix, + imageExtent.getWidth() / 2, imageExtent.getHeight() / 2, 1); + goog.vec.Mat4.translate(vertexCoordMatrix, 1, 1, 0); + +}; diff --git a/src/ol/renderer/webgl/webgllayerrenderer.js b/src/ol/renderer/webgl/webgllayerrenderer.js index 238462410f..067a4e054c 100644 --- a/src/ol/renderer/webgl/webgllayerrenderer.js +++ b/src/ol/renderer/webgl/webgllayerrenderer.js @@ -97,6 +97,12 @@ ol.renderer.webgl.Layer.prototype.getTexCoordMatrix = goog.abstractMethod; ol.renderer.webgl.Layer.prototype.getTexture = goog.abstractMethod; +/** + * @return {!goog.vec.Mat4.Number} Matrix. + */ +ol.renderer.webgl.Layer.prototype.getVertexCoordMatrix = goog.abstractMethod; + + /** * @inheritDoc */ diff --git a/src/ol/renderer/webgl/webglmaprenderer.js b/src/ol/renderer/webgl/webglmaprenderer.js index f7a553f3f2..62b03fc489 100644 --- a/src/ol/renderer/webgl/webglmaprenderer.js +++ b/src/ol/renderer/webgl/webglmaprenderer.js @@ -14,9 +14,11 @@ goog.require('goog.webgl'); goog.require('ol.FrameState'); goog.require('ol.Size'); goog.require('ol.Tile'); +goog.require('ol.layer.ImageLayer'); goog.require('ol.layer.TileLayer'); goog.require('ol.renderer.Map'); goog.require('ol.renderer.webgl.FragmentShader'); +goog.require('ol.renderer.webgl.ImageLayer'); goog.require('ol.renderer.webgl.TileLayer'); goog.require('ol.renderer.webgl.VertexShader'); goog.require('ol.structs.LRUCache'); @@ -79,11 +81,12 @@ ol.renderer.webgl.map.shader.Vertex = function() { 'attribute vec2 aTexCoord;', '', 'uniform mat4 uTexCoordMatrix;', + 'uniform mat4 uVertexCoordMatrix;', '', 'varying vec2 vTexCoord;', '', 'void main(void) {', - ' gl_Position = vec4(aPosition, 0., 1.);', + ' gl_Position = uVertexCoordMatrix * vec4(aPosition, 0., 1.);', ' vTexCoord = (uTexCoordMatrix * vec4(aTexCoord, 0., 1.)).st;', '}' ].join('\n')); @@ -159,7 +162,8 @@ ol.renderer.webgl.Map = function(container, map) { * uColorMatrix: WebGLUniformLocation, * uOpacity: WebGLUniformLocation, * uTexture: WebGLUniformLocation, - * uTexCoordMatrix: WebGLUniformLocation}|null} + * uTexCoordMatrix: WebGLUniformLocation, + * uVertexCoordMatrix: WebGLUniformLocation}|null} */ this.locations_ = null; @@ -270,12 +274,15 @@ ol.renderer.webgl.Map.prototype.bindTileTexture = * @inheritDoc */ ol.renderer.webgl.Map.prototype.createLayerRenderer = function(layer) { + var layerRenderer = null; if (layer instanceof ol.layer.TileLayer) { - return new ol.renderer.webgl.TileLayer(this, layer); + layerRenderer = new ol.renderer.webgl.TileLayer(this, layer); + } else if (layer instanceof ol.layer.ImageLayer) { + layerRenderer = new ol.renderer.webgl.ImageLayer(this, layer); } else { goog.asserts.assert(false); - return null; } + return layerRenderer; }; @@ -519,6 +526,7 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { aTexCoord: gl.getAttribLocation(program, 'aTexCoord'), uColorMatrix: gl.getUniformLocation(program, 'uColorMatrix'), uTexCoordMatrix: gl.getUniformLocation(program, 'uTexCoordMatrix'), + uVertexCoordMatrix: gl.getUniformLocation(program, 'uVertexCoordMatrix'), uOpacity: gl.getUniformLocation(program, 'uOpacity'), uTexture: gl.getUniformLocation(program, 'uTexture') }; @@ -555,6 +563,9 @@ ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) { gl.uniformMatrix4fv( this.locations_.uTexCoordMatrix, false, layerRenderer.getTexCoordMatrix()); + gl.uniformMatrix4fv( + this.locations_.uVertexCoordMatrix, false, + layerRenderer.getVertexCoordMatrix()); gl.uniformMatrix4fv( this.locations_.uColorMatrix, false, layerRenderer.getColorMatrix()); gl.uniform1f(this.locations_.uOpacity, layer.getOpacity()); diff --git a/src/ol/renderer/webgl/webgltilelayerrenderer.js b/src/ol/renderer/webgl/webgltilelayerrenderer.js index 8f1dab302a..360f8f8d82 100644 --- a/src/ol/renderer/webgl/webgltilelayerrenderer.js +++ b/src/ol/renderer/webgl/webgltilelayerrenderer.js @@ -141,6 +141,12 @@ ol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) { */ this.texCoordMatrix_ = goog.vec.Mat4.createNumber(); + /** + * @private + * @type {!goog.vec.Mat4.Number} + */ + this.vertexCoordMatrix_ = goog.vec.Mat4.createNumberIdentity(); + /** * @private * @type {ol.TileRange} @@ -237,6 +243,14 @@ ol.renderer.webgl.TileLayer.prototype.getTexture = function() { }; +/** + * @inheritDoc + */ +ol.renderer.webgl.TileLayer.prototype.getVertexCoordMatrix = function() { + return this.vertexCoordMatrix_; +}; + + /** * @return {ol.layer.TileLayer} Tile layer. */ diff --git a/src/ol/source/imagesource.js b/src/ol/source/imagesource.js new file mode 100644 index 0000000000..caad04b5e9 --- /dev/null +++ b/src/ol/source/imagesource.js @@ -0,0 +1,113 @@ +goog.provide('ol.source.ImageSource'); + +goog.require('goog.array'); +goog.require('ol.Attribution'); +goog.require('ol.Extent'); +goog.require('ol.Image'); +goog.require('ol.ImageUrlFunction'); +goog.require('ol.ImageUrlFunctionType'); +goog.require('ol.Projection'); +goog.require('ol.Size'); +goog.require('ol.array'); +goog.require('ol.source.Source'); + + +/** + * @typedef {{attributions: (Array.|undefined), + * crossOrigin: (null|string|undefined), + * extent: (null|ol.Extent|undefined), + * projection: (ol.Projection|undefined), + * resolutions: (Array.|undefined), + * imageUrlFunction: (ol.ImageUrlFunctionType| + * undefined)}} + */ +ol.source.ImageSourceOptions; + + + +/** + * @constructor + * @extends {ol.source.Source} + * @param {ol.source.ImageSourceOptions} options Single + * image source options. + */ +ol.source.ImageSource = function(options) { + + goog.base(this, { + attributions: options.attributions, + extent: options.extent, + projection: options.projection + }); + + /** + * @protected + * @type {ol.ImageUrlFunctionType} + */ + this.imageUrlFunction = + goog.isDef(options.imageUrlFunction) ? + options.imageUrlFunction : + ol.ImageUrlFunction.nullImageUrlFunction; + + /** + * @private + * @type {?string} + */ + this.crossOrigin_ = + goog.isDef(options.crossOrigin) ? options.crossOrigin : 'anonymous'; + + /** + * @private + * @type {Array.} + */ + this.resolutions_ = goog.isDef(options.resolutions) ? + options.resolutions : null; + goog.asserts.assert(goog.isNull(this.resolutions_) || + goog.array.isSorted(this.resolutions_, + function(a, b) { + return b - a; + }, true)); + +}; +goog.inherits(ol.source.ImageSource, ol.source.Source); + + +/** + * @protected + * @param {ol.Extent} extent Extent. + * @param {number} resolution Resolution. + * @param {ol.Size} size Size. + * @return {ol.Image} Single image. + */ +ol.source.ImageSource.prototype.createImage = + function(extent, resolution, size) { + var image = null; + var imageUrl = this.imageUrlFunction(extent, size); + if (goog.isDef(imageUrl)) { + image = new ol.Image( + extent, resolution, imageUrl, this.crossOrigin_); + } + return image; +}; + + +/** + * @protected + * @param {number} resolution Resolution. + * @return {number} Resolution. + */ +ol.source.ImageSource.prototype.findNearestResolution = + function(resolution) { + if (!goog.isNull(this.resolutions_)) { + var idx = ol.array.linearFindNearest(this.resolutions_, resolution); + resolution = this.resolutions_[idx]; + } + return resolution; +}; + + +/** + * @param {ol.Extent} extent Extent. + * @param {number} resolution Resolution. + * @return {ol.Image} Single image. + */ +ol.source.ImageSource.prototype.getImage = goog.abstractMethod; diff --git a/src/ol/source/singleimagewms.exports b/src/ol/source/singleimagewms.exports new file mode 100644 index 0000000000..4d79ecdabc --- /dev/null +++ b/src/ol/source/singleimagewms.exports @@ -0,0 +1 @@ +@exportSymbol ol.source.SingleImageWMS diff --git a/src/ol/source/singleimagewmssource.js b/src/ol/source/singleimagewmssource.js new file mode 100644 index 0000000000..3529678443 --- /dev/null +++ b/src/ol/source/singleimagewmssource.js @@ -0,0 +1,99 @@ +goog.provide('ol.source.SingleImageWMS'); + +goog.require('ol.Extent'); +goog.require('ol.Image'); +goog.require('ol.ImageUrlFunction'); +goog.require('ol.Projection'); +goog.require('ol.Size'); +goog.require('ol.source.ImageSource'); + + + +/** + * @constructor + * @extends {ol.source.ImageSource} + * @param {ol.source.SingleImageWMSOptions} options Options. + */ +ol.source.SingleImageWMS = function(options) { + + var projection = ol.Projection.createProjection( + options.projection, 'EPSG:3857'); + var projectionExtent = projection.getExtent(); + + var extent = goog.isDef(options.extent) ? + options.extent : projectionExtent; + + var version = goog.isDef(options.version) ? + options.version : '1.3'; + + var baseParams = { + 'SERVICE': 'WMS', + 'VERSION': version, + 'REQUEST': 'GetMap', + 'STYLES': '', + 'FORMAT': 'image/png', + 'TRANSPARENT': true + }; + baseParams[version >= '1.3' ? 'CRS' : 'SRS'] = projection.getCode(); + goog.object.extend(baseParams, options.params); + + var imageUrlFunction; + if (options.url) { + var url = goog.uri.utils.appendParamsFromMap( + options.url, baseParams); + imageUrlFunction = ol.ImageUrlFunction.createBboxParam(url); + } else { + imageUrlFunction = + ol.ImageUrlFunction.nullImageUrlFunction; + } + + goog.base(this, { + attributions: options.attributions, + crossOrigin: options.crossOrigin, + extent: extent, + projection: projection, + resolutions: options.resolutions, + imageUrlFunction: imageUrlFunction + }); + + /** + * @private + * @type {ol.Image} + */ + this.image_ = null; + + /** + * FIXME configurable? + * @private + * @type {number} + */ + this.ratio_ = 1.5; + +}; +goog.inherits(ol.source.SingleImageWMS, ol.source.ImageSource); + + +/** + * @inheritDoc + */ +ol.source.SingleImageWMS.prototype.getImage = + function(extent, resolution) { + resolution = this.findNearestResolution(resolution); + + var image = this.image_; + if (!goog.isNull(image) && + image.getResolution() == resolution && + image.getExtent().containsExtent(extent)) { + return image; + } + + extent = new ol.Extent(extent.minX, extent.minY, + extent.maxX, extent.maxY); + extent.scaleFromCenter(this.ratio_); + var width = extent.getWidth() / resolution; + var height = extent.getHeight() / resolution; + var size = new ol.Size(width, height); + + this.image_ = this.createImage(extent, resolution, size); + return this.image_; +}; diff --git a/src/ol/source/staticimage.exports b/src/ol/source/staticimage.exports new file mode 100644 index 0000000000..3f7b2c98a5 --- /dev/null +++ b/src/ol/source/staticimage.exports @@ -0,0 +1 @@ +@exportSymbol ol.source.StaticImage diff --git a/src/ol/source/staticimagesource.js b/src/ol/source/staticimagesource.js new file mode 100644 index 0000000000..eafa205eb1 --- /dev/null +++ b/src/ol/source/staticimagesource.js @@ -0,0 +1,61 @@ +goog.provide('ol.source.StaticImage'); + +goog.require('ol.Image'); +goog.require('ol.ImageUrlFunctionType'); +goog.require('ol.source.ImageSource'); + + + +/** + * @constructor + * @extends {ol.source.ImageSource} + * @param {ol.source.StaticImageOptions} options Options. + */ +ol.source.StaticImage = function(options) { + + var imageFunction = ol.source.StaticImage.createImageFunction( + options.url); + + var imageExtent = options.imageExtent; + var imageSize = options.imageSize; + var imageResolution = imageExtent.getHeight() / imageSize.height; + + goog.base(this, { + attributions: options.attributions, + crossOrigin: options.crossOrigin, + extent: options.extent, + projection: options.projection, + imageUrlFunction: imageFunction, + resolutions: [imageResolution] + }); + + /** + * @private + * @type {ol.Image} + */ + this.image_ = this.createImage(imageExtent, imageResolution, imageSize); + +}; +goog.inherits(ol.source.StaticImage, ol.source.ImageSource); + + +/** + * @inheritDoc + */ +ol.source.StaticImage.prototype.getImage = function(extent, resolution) { + if (extent.intersects(this.image_.getExtent())) { + return this.image_; + } + return null; +}; + + +/** + * @param {string|undefined} url URL. + * @return {ol.ImageUrlFunctionType} Function. + */ +ol.source.StaticImage.createImageFunction = function(url) { + return function(extent, size) { + return url; + }; +}; diff --git a/src/ol/sphere.js b/src/ol/sphere.js new file mode 100644 index 0000000000..70c154ca64 --- /dev/null +++ b/src/ol/sphere.js @@ -0,0 +1,189 @@ +/** + * @license + * Latitude/longitude spherical geodesy formulae taken from + * http://www.movable-type.co.uk/scripts/latlong.html + * Licenced under CC-BY-3.0. + */ + +// FIXME add intersection of two paths given start points and bearings +// FIXME add rhumb lines + +goog.provide('ol.Sphere'); + +goog.require('goog.math'); +goog.require('ol.Coordinate'); + + + +/** + * @constructor + * @param {number} radius Radius. + */ +ol.Sphere = function(radius) { + + /** + * @type {number} + */ + this.radius = radius; + +}; + + +/** + * Returns the distance from c1 to c2 using the spherical law of cosines. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 2. + * @return {number} Spherical law of cosines distance. + */ +ol.Sphere.prototype.cosineDistance = function(c1, c2) { + var lat1 = goog.math.toRadians(c1.y); + var lat2 = goog.math.toRadians(c2.y); + var deltaLon = goog.math.toRadians(c2.x - c1.x); + return this.radius * Math.acos( + Math.sin(lat1) * Math.sin(lat2) + + Math.cos(lat1) * Math.cos(lat2) * Math.cos(deltaLon)); +}; + + +/** + * Returns the distance of c3 from the great circle path defined by c1 and c2. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 2. + * @param {ol.Coordinate} c3 Coordinate 3. + * @return {number} Cross-track distance. + */ +ol.Sphere.prototype.crossTrackDistance = function(c1, c2, c3) { + var d12 = this.cosineDistance(c1, c2); + var d13 = this.cosineDistance(c1, c2); + var theta12 = goog.math.toRadians(this.initialBearing(c1, c2)); + var theta13 = goog.math.toRadians(this.initialBearing(c1, c3)); + return this.radius * + Math.asin(Math.sin(d13 / this.radius) * Math.sin(theta13 - theta12)); +}; + + +/** + * Returns the distance from c1 to c2 using Pythagoras's theorem on an + * equirectangular projection. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 2. + * @return {number} Equirectangular distance. + */ +ol.Sphere.prototype.equirectangularDistance = function(c1, c2) { + var lat1 = goog.math.toRadians(c1.y); + var lat2 = goog.math.toRadians(c2.y); + var deltaLon = goog.math.toRadians(c2.x - c1.x); + var x = deltaLon * Math.cos((lat1 + lat2) / 2); + var y = lat2 - lat1; + return this.radius * Math.sqrt(x * x + y * y); +}; + + +/** + * Returns the final bearing from c1 to c2. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 2. + * @return {number} Initial bearing. + */ +ol.Sphere.prototype.finalBearing = function(c1, c2) { + return (this.initialBearing(c2, c1) + 180) % 360; +}; + + +/** + * Returns the distance from c1 to c2 using the haversine formula. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 2. + * @return {number} Haversine distance. + */ +ol.Sphere.prototype.haversineDistance = function(c1, c2) { + var lat1 = goog.math.toRadians(c1.y); + var lat2 = goog.math.toRadians(c2.y); + var deltaLatBy2 = (lat2 - lat1) / 2; + var deltaLonBy2 = goog.math.toRadians(c2.x - c1.x) / 2; + var a = Math.sin(deltaLatBy2) * Math.sin(deltaLatBy2) + + Math.sin(deltaLonBy2) * Math.sin(deltaLonBy2) * + Math.cos(lat1) * Math.cos(lat2); + return 2 * this.radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); +}; + + +/** + * Returns the initial bearing from c1 to c2. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 2. + * @return {number} Initial bearing. + */ +ol.Sphere.prototype.initialBearing = function(c1, c2) { + var lat1 = goog.math.toRadians(c1.y); + var lat2 = goog.math.toRadians(c2.y); + var deltaLon = goog.math.toRadians(c2.x - c1.x); + var y = Math.sin(deltaLon) * Math.cos(lat2); + var x = Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(deltaLon); + return goog.math.toDegrees(Math.atan2(y, x)); +}; + + +/** + * Returns the maximum latitude of the great circle defined by bearing and + * latitude. + * + * @param {number} bearing Bearing. + * @param {number} latitude Latitude. + * @return {number} Maximum latitude. + */ +ol.Sphere.prototype.maximumLatitude = function(bearing, latitude) { + return Math.cos(Math.abs(Math.sin(goog.math.toRadians(bearing)) * + Math.cos(goog.math.toRadians(latitude)))); +}; + + +/** + * Returns the midpoint between c1 and c2. + * + * @param {ol.Coordinate} c1 Coordinate 1. + * @param {ol.Coordinate} c2 Coordinate 2. + * @return {ol.Coordinate} Midpoint. + */ +ol.Sphere.prototype.midpoint = function(c1, c2) { + var lat1 = goog.math.toRadians(c1.y); + var lat2 = goog.math.toRadians(c2.y); + var lon1 = goog.math.toRadians(c1.x); + var deltaLon = goog.math.toRadians(c2.x - c1.x); + var Bx = Math.cos(lat2) * Math.cos(deltaLon); + var By = Math.cos(lat2) * Math.sin(deltaLon); + var cosLat1PlusBx = Math.cos(lat1) + Bx; + var lat = Math.atan2(Math.sin(lat1) + Math.sin(lat2), + Math.sqrt(cosLat1PlusBx * cosLat1PlusBx + By * By)); + var lon = lon1 + Math.atan2(By, cosLat1PlusBx); + return new ol.Coordinate(goog.math.toDegrees(lon), goog.math.toDegrees(lat)); +}; + + +/** + * Returns the coordinate at the given distance and bearing from c. + * + * @param {ol.Coordinate} c1 Coordinate. + * @param {number} distance Distance. + * @param {number} bearing Bearing. + * @return {ol.Coordinate} Coordinate. + */ +ol.Sphere.prototype.offset = function(c1, distance, bearing) { + var lat1 = goog.math.toRadians(c1.y); + var lon1 = goog.math.toRadians(c1.x); + var dByR = distance / this.radius; + var lat = Math.asin( + Math.sin(lat1) * Math.cos(dByR) + + Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing)); + var lon = lon1 + Math.atan2( + Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1), + Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat)); + return new ol.Coordinate(goog.math.toDegrees(lon), goog.math.toDegrees(lat)); +}; diff --git a/src/ol/tilerange.js b/src/ol/tilerange.js index 7c402f00cb..e1baa169f8 100644 --- a/src/ol/tilerange.js +++ b/src/ol/tilerange.js @@ -61,16 +61,6 @@ ol.TileRange.prototype.containsTileRange = function(tileRange) { }; -/** - * @param {ol.TileRange} tileRange Tile range. - * @return {boolean} Equals. - */ -ol.TileRange.prototype.equals = function(tileRange) { - return this.minX == tileRange.minX && tileRange.maxX == this.maxX && - this.minY == tileRange.minY && tileRange.minY == this.minY; -}; - - /** * @inheritDoc * @return {number} Height. diff --git a/test/ol.html b/test/ol.html index d4e3191ac9..979397f83e 100644 --- a/test/ol.html +++ b/test/ol.html @@ -25,6 +25,7 @@ +