Developer guide for integrating cloud tiles into your map application.
Welcome to ZoomRadar's tiles integration guide. This guide will help to integrate cloud tiles into your map applicaiton.
It will also demonstrate the integration on two popular map engines: OpenStreetMaps and GoogleMaps.
In order to use this guide you will need the following:
Please visit ZoomRadar for more information.
To acquire your key please visit Generating API Key page and fill in your information. Currently it's free of charge. Once submitted you will receive your key, save it.
Generate API KeyIn order to test your key replace YOUR_API_KEY with your API key in the below query and open it.
https://builder.zoomradar.net/zoomradar_weather_map/tilesApi.php?key=YOUR_API_KEY
If it returns an XML (you may need to view the source as some browsers display it as blank) then your key is working, else please contact support by emailing to zoomradar@gmail.com.
Use your generated API key to request latest tiles list using the bellow URL.
https://builder.zoomradar.net/zoomradar_weather_map/tilesApi.php?key=YOUR_API_KEY
This will return an XML array of images where each item contains a timestamps. Parse timestamps and save them for later use.
These timestamps represent most recent cloud frame images. In order to avoid synchrnoization issues remove latest two timestamps, since the servers might be still processing them.
If you're interested only in static image (no animation) then only the latest timestamp is necessary.
Below we'll use jQuery to load and parse the latest tile timestamp.
$.ajax( { type: "GET", url: "https://builder.zoomradar.net/zoomradar_weather_map/tilesApi.php?key=YOUR_API_KEY", dataType: "xml", crossDomain: true, error: function (xhr, text, thrown) { // error management }, success: function (xml) { var imageNames = []; $(xml).find('image').each(function () { var str = $(this).attr('path'); str = str.split("/"); if (str.length == 2) str = str[1].split("."); if (str.length == 2) { imageNames.push(str[0]); } }); //Remove last frame to give the server time to sync images imageNames.shift(); imageNames.pop(); //Output latest image console.log(imageNames[0]); } } );
The radar cloud is a large image that covers entire US map. Since displaying such a large image is practically impossible on any map engine our main goal is to split it into small tile pieces and only display necessary tiles.
This is why understanding basics behind the radar cloud and the tile separation is important.
Let's start from importing the cloud radar bounds:
var radarBounds: { feedMaxLat: 50.303131, feedMinLat: 22.063271, feedMaxLon: -64.575134, feedMinLon: -128.424866 };
And also let's import radar dimensions in pixels:
var radarSize = { width: 28431, height: 15919 };
The radar cloud is not just being sliced into tiles, but it's also being scaled for different zoom levels. This makes the usage of tiles more adapted to different zoom levels of map engine. Zoom levels start at 1 and grow with powers of 2 (1, 2, 4, 8, etc). The zoom level 1 represents tiles sliced without any scaling. The zoom level 2 represents entire cloud scaled down by 2x both horizontally and vertically, and then sliced into tiles.
It's important to know maximum amount of supported zoom levels:
var maxZoomLevel = 64;
Finally it's important to know each tiles size in pixels:
var tileSizes: { width: 350, height: 350 };
Additionally there's a need to import pre-calculcated geo-coordinates for each tiles at each zoom level. In order to do it import the bellow JSON.
http://builder.zoomradar.net/zoomradar_weather_map/national_tile_coordinates.json
With all these constants ready we can go to next step.
There are two type of methods which are necessary for radar clouds to be displayed on your map enginge.
Most engines support this natively. For example GoogleMaps already do this as displayed here.
However on other engines, such as on OpenStreetMaps you might need to slightly alter the existing overlay. Here's an example below:
LeafMap.prototype.newGroundOverlay = function (url, sw, ne, opacity, clickable) { var imageUrl = url, imageBounds = [{ lat: ne.lat(), lng: ne.lng() }, { lat: sw.lat(), lng: sw.lng() }]; var overlay = L.imageOverlay(imageUrl, imageBounds, { opacity: opacity, zIndex: 2, alt: 'Weather pattern', interactive: true, crossOrigin: false, errorOverlayUrl: './leaflet/error_image.png', // you would put an error url to an image here className: 'image-one' //css }).addTo(this.map); return overlay; }
In case your engine doesn't support image overlays at all but it supports general HTML overlays you might need to code a custom overlay on your own.
While most engines have a solution for this, usually some additional coding is necessary to prepare these methods.
GoogleMaps, for example, use so called "projection"-s to perform this conversion. Below are sample functions:
//Screen pixels to latitude/longitude GmapMap.prototype.pixToLL = function (point) { var ne = this.map.getBounds().getNorthEast(); var sw = this.map.getBounds().getSouthWest(); var projection = this.map.getProjection(); var topRight = projection.fromLatLngToPoint(ne); var bottomLeft = projection.fromLatLngToPoint(sw); var scale = 1 << this.map.getZoom(); var newLatlng = projection.fromPointToLatLng(new google.maps.Point(point.x / scale + bottomLeft.x, point.y / scale + topRight.y)); return newLatlng; } //Latitude/longitude to screen pixels GmapMap.prototype.llToPix = function (latLng) { var topRight = this.map.getProjection().fromLatLngToPoint(this.map.getBounds().getNorthEast()); var bottomLeft = this.map.getProjection().fromLatLngToPoint(this.map.getBounds().getSouthWest()); var scale = Math.pow(2, this.map.getZoom()); var worldPoint = this.map.getProjection().fromLatLngToPoint(latLng); return new google.maps.Point((worldPoint.x-bottomLeft.x)*scale, (worldPoint.y-topRight.y)*scale); }
OpenStreetMaps, on the other hand, support this directly. Below are sample functions:
//Screen pixels to latitude/longitude LeafMap.prototype.pixToLL = function (point) { var topRight = this.map.latLngToLayerPoint(this.map.getBounds().getNorthEast()); var bottomLeft = this.map.latLngToLayerPoint(this.map.getBounds().getSouthWest()); var scale = parseInt(this.map.getZoom()); var newLatlng = this.map.layerPointToLatLng( L.point(point.x / scale + bottomLeft.x, point.y / scale + topRight.y) ); return { lat: function () { return newLatlng.lat; }, lng: function () { return newLatlng.lng; } }; } //Latitude/longitude to screen pixels LeafMap.prototype.llToPix = function (latLng) { var topRight = this.map.latLngToLayerPoint(this.map.getBounds().getNorthEast()); var bottomLeft = this.map.latLngToLayerPoint(this.map.getBounds().getSouthWest()); var scale = parseInt(this.map.getZoom()); var worldPoint = this.map.latLngToLayerPoint({ lat: latLng.lat(), lng: latLng.lng() }); return L.point((worldPoint.x - bottomLeft.x) * scale, (worldPoint.y - topRight.y) * scale); }
With these methods and constants ready we can go to the next step.
Once we have constants and necessary methods ready it's time to identify visible tiles. In most situations, when user is zoomed in somewhere on the map, only small portion of the radar cloud is visible. And it is important to identify which tiles are located in the current view area.
Even more important is to detect which zoom level should be used for current view. For example if it's the entire US map then the most scaled down image tiles need to be used, on the other hand if it's zoomed to a street level then unscaled tiles need to be used.
Below we'll use mapEngine as a global variable containing all above constants and methods. Please change with corresponding variabls for your case.
//Identify current view bounds var topLeftCorner = mapEngine.llToPix( mapEngine.newLLPoint(mapEngine.radarBounds.feedMaxLat, mapEngine.radarBounds.feedMinLon) ); var bottomRightCorner = mapEngine.llToPix( mapEngine.newLLPoint(mapEngine.radarBounds.feedMinLat, mapEngine.radarBounds.feedMaxLon) ); //Identify target size (horizontal size of radar) var targetSize = Math.abs(bottomRightCorner.x - topLeftCorner.x); //Identify zoom level var zoomLevel = 1; while (targetSize < mapEngine.radarSize.width / zoomLevel / 2) zoomLevel *= 2; if(zoomLevel > mapEngine.maxZoomLevel) zoomLevel = mapEngine.maxZoomLevel;
This will identify the necessary zoom level for current view.
Next let's identify which tiles of the current zoom level are visible. That is which horizontal and vertical index ranges are visible.
//Identify bounds var mapURLat = mapEngine.getBoundsN(); var mapURLon = mapEngine.getBoundsE(); var mapLLLat = mapEngine.getBoundsS(); var mapLLLon = mapEngine.getBoundsW(); //Identify pixel bounds of current view var mapUL = mapEngine.llToPix( mapEngine.newLLPoint(mapURLat, mapLLLon) ); var mapLR = mapEngine.llToPix( mapEngine.newLLPoint(mapLLLat, mapURLon) ); //Identify radar sizes in current view var currentRadarWidth = mapEngine.radarSize.width / zoomLevel; var currentRadarHeight = mapEngine.radarSize.height / zoomLevel; var correctionRatio = currentRadarWidth / targetSize; //Identify pixel bounds of tiles var minX = (mapUL.x - topLeftCorner.x) * correctionRatio; var minY = (mapUL.y - topLeftCorner.y) * correctionRatio; var maxX = (mapLR.x - topLeftCorner.x) * correctionRatio; var maxY = (mapLR.y - topLeftCorner.y) * correctionRatio; //Identify tile indices var tileMinX = Math.floor(Math.max(minX - mapEngine.tileSizes.width, 0) / mapEngine.tileSizes.width); var tileMinY = Math.floor(Math.max(minY - mapEngine.tileSizes.height, 0) / mapEngine.tileSizes.height); var tileMaxX = Math.floor(Math.min(maxX + mapEngine.tileSizes.width, currentRadarWidth) / mapEngine.tileSizes.width); var tileMaxY = Math.floor(Math.min(maxY + mapEngine.tileSizes.height, currentRadarHeight) / mapEngine.tileSizes.height);
With these indices identified we now know which tiles need to be loaded and displayed on the map.
Once we have identified visible tile indices alongside with the zoomlevel we can proceed to loading and to displaying them.
For each tile there's a specific URL and it's important to understand how tile URLs are being constructed. Each tile URL looks like this:
http://miami.zoomradar.net/HiResNationalRadar4/us_radar_{timestamp}_zoom_{zoomlevel}_tile_{y}_{x}.png
Where following parameters represent:
A sample code would look like this:
//Indices count var xCnt = tileMaxX - tileMinX + 1; var yCnt = tileMaxY - tileMinY + 1; for (var x = 0; x < xCnt; x++) { for (var y = 0; y < yCnt; y++) { var tileUrl = "http://miami.zoomradar.net/HiResNationalRadar4/us_radar_" + mapEngine.imageNames[mapEngine.frameIndex] + "_zoom_" + zoomLevel + "_tile_" + (tileMinY + y) + "_" + (tileMinX + x) + ".png"; var tileCoordinate = mapEngine.tileCoordinates[zoomLevel][tileMinX + x][tileMinY + y]; mapEngine.newGroundOverlay(tileUrl, [tileCoordinate[2], tileCoordinate[1]], [tileCoordinate[0], tileCoordinate[3]]); } }
This will load and display necessary tiles on corresponding coordinates. Of course you might want to do some management by pushing them into an array.
It's important to refresh tiles on following actions/events:
In general any action leading to current view change should trigger new visible tiles detection and then load and display new tiles while clearing previous tiles from the screen to prevent memory leaks.
Also you might be interested in implementing an automatic timer (let's say 15 minutes) which will trigger tiles refreshment to keep them live. In order to do this the XML retreived by the API needs to be re-queried and all timestamps need to be updated.
So far we've used only the latest timestamp for creating a static image. However one can use more than one timestamp to create radar animations.
In order to achieve this all you have to do is to use a timer interval to loop through desired timestamps and replace tile images.
Finally, we want to share some source code which you're free to copy and to use in your projects.
OpenStreetMaps Source GoogleMaps SourceCode released under the Apache 2.0 License.
For more information about copyright and license check ZoomRadar.com or email to zoomradar@gmail.com.