I've put together a simple example that loads a GeoTiff on the map directly in the browser. It doesn't do any optimizations, so it can be slow when the image is more than a few megabytes in size. Once this 26MB GeoTiff is loaded into the browser, it can take 30 seconds to a minute to convert it into an image and display it on the map. Note that for GeoTiff files larger than a few megabytes you should convert them into tiles on the server side rather than loading them directly into a browser. The download time alone would be slow (took me nearly 2 minutes to download with a fast internet connection).
To make this work I used the following library's/tools:
- https://github.com/geotiffjs/geotiff.js - JS library to read GeoTiff files.
- https://app.geotiff.io/ - Tool I used to test out the file to make user the geotiff.js library would work.
- https://github.com/proj4js/proj4js - JS Library to convert coordinate projections.
- https://github.com/matafokka/geotiff-geokeys-to-proj4/ - JS Library used to convert GeoTiff geokey information to proj4 projection information.
Here is the source code for a functional app that loads a GeoTiff image from a URL (if on a COR's enabled endpoint), or from a local file.
<!DOCTYPE html>
<html>
<head>
<title>Azure Maps GeoTiff Viewer</title>
<meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="IE=Edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<!-- Add references to the Azure Maps Map control JavaScript and CSS files. -->
<link rel="stylesheet" href="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.css" type="text/css" />
<script src="https://atlas.microsoft.com/sdk/javascript/mapcontrol/2/atlas.min.js"></script>
<!-- Load GeoTiff.js library -->
<script src="https://cdn.jsdelivr.net/npm/geotiff"></script>
<!-- Load proj4js library so we can convert bounding boxes to the required projection. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/proj4js/2.9.0/proj4.min.js"></script>
<!-- Load geotiff-geokeys-to-proj4 library to convert GeoTiff geokey information to proj4js projection information. -->
<script src="https://cdn.jsdelivr.net/npm/geotiff-geokeys-to-proj4@2022.9.7/main-dist.min.js"></script>
<script>
var map, imageLayer, datasource, loadingScreen;
var geoTiffUrl = 'https://ltimgpstorageacc.blob.core.windows.net/ltimgpblob/no-overviews.tif';
function GetMap() {
loadingScreen = document.getElementById('loadingScreen');
//Initialize a map instance.
map = new atlas.Map('myMap', {
view: 'Auto',
//Add authentication details for connecting to Azure Maps.
authOptions: {
authType: 'subscriptionKey',
subscriptionKey: '<Your Azure Maps Key>'
}
});
//Add some data to the map so that we can see how the rendering of data also changes.
map.events.add('ready', function () {
//Optionally load the style picker control.
map.controls.add(new atlas.control.StyleControl({
mapStyles: 'all'
}), {
position: 'top-right'
});
//Optionally, create a layer for displaying the bounding box of the GeoTiff.
datasource = new atlas.source.DataSource();
map.sources.add(datasource);
map.layers.add(new atlas.layer.LineLayer(datasource, null, {
strokeColor: 'red',
strokeWidth: 5
}));
});
}
async function loadRemoteGeoTiff() {
loadingScreen.style.display = '';
const response = await fetch(document.getElementById('urlInput').value);
const arrayBuffer = await response.arrayBuffer();
const tiff = await GeoTIFF.fromArrayBuffer(arrayBuffer);
await loadGeoTiff(tiff);
}
async function loadLocalGeoTiff() {
loadingScreen.style.display = '';
const input = document.getElementById('file');
const tiff = await GeoTIFF.fromBlob(input.files[0]);
await loadGeoTiff(tiff);
}
async function loadGeoTiff(tiff) {
//Remove any previously loaded image layer.
if (imageLayer) {
map.layers.remove(imageLayer);
datasource.clear();
}
//Get the image data for the tiff.
const image = await tiff.getImage();
const width = image.getWidth();
const height = image.getHeight();
//Get the bounding box of the image.
const bounds = image.getBoundingBox();
if (bounds == null) {
alert('No bounding box found in GeoTiff file.');
return;
}
//Convert bounding box projection.
const geoKeys = image.getGeoKeys();
const projObj = geokeysToProj4.toProj4(geoKeys);
const projection = proj4('WGS84', projObj.proj4);
var minXY = projection.inverse([bounds[0], bounds[1]]);
var maxXY = projection.inverse([bounds[2], bounds[3]]);
const bbox = [minXY[0], minXY[1], maxXY[0], maxXY[1]];
//Zoom the map into where the GeoTiff area.
map.setCamera({
bounds: bbox,
padding: 20
});
//Optionally, draw a polygon around the bounding box area.
datasource.setShapes(atlas.math.boundingBoxToPolygon(bbox));
//Use a decoder pool for faster processing of GeoTiff image.
const pool = new GeoTIFF.Pool();
//Load the RGB data from the GeoTiff file.
const rgb = await image.readRGB({
pool
});
//Convert GeoTiff to png by drawing the RGB data to a canvas.
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
let j = 0;
for (let i = 0; i < rgb.length; i += 3) {
data[j] = rgb[i];
data[j + 1] = rgb[i + 1];
data[j + 2] = rgb[i + 2];
data[j + 3] = 255;
j += 4;
}
ctx.putImageData(imageData, 0, 0);
const pngUrl = canvas.toDataURL();
//Get the corner coordinates of the image.
var corners = [
[minXY[0], maxXY[1]], //top left - North west
maxXY, //top right - North east
[maxXY[0], minXY[1]], //bottom right - South east
minXY //bottom left - South west
];
//Create an image layer and add it to the map below the labels.
imageLayer = new atlas.layer.ImageLayer({
url: pngUrl,
coordinates: corners
});
map.layers.add(imageLayer, 'labels');
setTimeout(() => {
loadingScreen.style.display = 'none';
}, 5000)
}
</script>
<style>
html, body, #myMap, #loadingScreen {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
.sidePanel {
position: absolute;
top: 10px;
left: 10px;
background-color: white;
border-radius: 5px;
padding: 5px;
}
h1 {
font-size: 16px;
}
#loadingScreen {
position: absolute;
top: 0;
left: 0;
background-color: rgba(0,0,0,0.5);
}
#loadingScreen span {
position: absolute;
top: 50%;
left: calc(50% - 30px);
font-size: 18px;
color: white;
}
</style>
</head>
<body onload="GetMap()">
<div id="myMap"></div>
<div class="sidePanel">
<h1>Azure Maps GeoTiff Viewer</h1>
<table>
<tr>
<td>From URL: </td>
<td><input id="urlInput" type="text" value=""/></td>
<td><input type="button" onclick="loadRemoteGeoTiff()" value="Load"/></td>
</tr>
<tr>
<td colspan="3" style="text-align:center">or</td>
</tr>
<tr>
<td>Local file: </td>
<td colspan="2" style="text-align:center">
<input type="file" id="file" onchange="loadLocalGeoTiff()">
</td>
</tr>
</table>
</div>
<div id="loadingScreen" style="display:none;">
<span>Loading...</span>
</div>
</body>
</html>
Here is a screenshot of the provide image overlaid on top the map.
There is a lot of potential improvements that could be made to this example to improve performance. Here are a couple of ideas:
- Move the image processing code to web workers. Split the work across multiple web workers (one per processer).
- Monitor the movement of the map and only load the image needed for the map view and only load at the resolution needed for the current zoom level. This would improve performance a lot but would be a lot of work.
- You could convert the image to tiles in the browser as well. There is an example using MapLibre here that might be worth looking into: https://qiita-com.translate.goog/Kanahiro/items/70b3b8b11bd26cbaf30e?_x_tr_sl=auto&_x_tr_tl=en&_x_tr_hl=en&_x_tr_pto=wapp