Безлімітно за розміром, та безкоштовно генерувати тайли з великих зображень можна утилітою Zoomify. При цьому, буде згенерований також файл для показу у браузері, однак він буде дещо спрощений без можливості додати елементи навігації, легенди тощо.
В разі необхідності показу з допомогою Leaflet, потрібно враховувати деякі нюанси. Справа в тому, що команда Leaflet
з переходом на на версію скрипта 1.0, суттєво змінила його алгоритм, що унеможливлює роботу plugins
, до яких відносяться скрипти обробки тайлів Zoomify-формату. Повний код html-файла для відображення мапи за допомоги Leaflet версії 0.7.7
і скрипта від Bjorn Sandvik:
/* Zoomify - Leaflet ver.0.7.7 */
[HTML]
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="origin-when-cross-origin">
<title>Leaflet 0.7.7</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.css">
<style type="text/css">
html, body, #picture {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
background: #063B41;
}
</style>
</head>
<body>
<div id="picture"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/0.7.7/leaflet.js"></script>
<!-- Наступне можна винести окремо: <script type="text/javascript" src="L.TileLayer.Zoomify.js"></script> -->
<script type="text/javascript">
/*
* L.TileLayer.Zoomify display Zoomify tiles with Leaflet
*/
L.TileLayer.Zoomify = L.TileLayer.extend({
options: {
continuousWorld: true,
tolerance: 0.8
},
initialize: function (url, options) {
options = L.setOptions(this, options);
this._url = url;
var imageSize = L.point(options.width, options.height),
tileSize = options.tileSize;
this._imageSize = [imageSize];
this._gridSize = [this._getGridSize(imageSize)];
while (parseInt(imageSize.x) > tileSize || parseInt(imageSize.y) > tileSize) {
imageSize = imageSize.divideBy(2).floor();
this._imageSize.push(imageSize);
this._gridSize.push(this._getGridSize(imageSize));
}
this._imageSize.reverse();
this._gridSize.reverse();
this.options.maxZoom = this._gridSize.length - 1;
},
onAdd: function (map) {
L.TileLayer.prototype.onAdd.call(this, map);
var mapSize = map.getSize(),
zoom = this._getBestFitZoom(mapSize),
imageSize = this._imageSize[zoom],
center = map.options.crs.pointToLatLng(L.point(imageSize.x / 2, imageSize.y / 2), zoom);
map.setView(center, zoom, true);
},
_getGridSize: function (imageSize) {
var tileSize = this.options.tileSize;
return L.point(Math.ceil(imageSize.x / tileSize), Math.ceil(imageSize.y / tileSize));
},
_getBestFitZoom: function (mapSize) {
var tolerance = this.options.tolerance,
zoom = this._imageSize.length - 1,
imageSize, zoom;
while (zoom) {
imageSize = this._imageSize[zoom];
if (imageSize.x * tolerance < mapSize.x && imageSize.y * tolerance < mapSize.y) {
return zoom;
}
zoom--;
}
return zoom;
},
_tileShouldBeLoaded: function (tilePoint) {
var gridSize = this._gridSize[this._map.getZoom()];
return (tilePoint.x >= 0 && tilePoint.x < gridSize.x && tilePoint.y >= 0 && tilePoint.y < gridSize.y);
},
_addTile: function (tilePoint, container) {
var tilePos = this._getTilePos(tilePoint),
tile = this._getTile(),
zoom = this._map.getZoom(),
imageSize = this._imageSize[zoom],
gridSize = this._gridSize[zoom],
tileSize = this.options.tileSize;
if (tilePoint.x === gridSize.x - 1) {
tile.style.width = imageSize.x - (tileSize * (gridSize.x - 1)) + 'px';
}
if (tilePoint.y === gridSize.y - 1) {
tile.style.height = imageSize.y - (tileSize * (gridSize.y - 1)) + 'px';
}
L.DomUtil.setPosition(tile, tilePos, L.Browser.chrome || L.Browser.android23);
this._tiles[tilePoint.x + ':' + tilePoint.y] = tile;
this._loadTile(tile, tilePoint);
if (tile.parentNode !== this._tileContainer) {
container.appendChild(tile);
}
},
getTileUrl: function (tilePoint) {
return this._url + 'TileGroup' + this._getTileGroup(tilePoint) + '/' + this._map.getZoom() + '-' + tilePoint.x + '-' + tilePoint.y + '.jpg';
},
_getTileGroup: function (tilePoint) {
var zoom = this._map.getZoom(),
num = 0,
gridSize;
for (z = 0; z < zoom; z++) {
gridSize = this._gridSize[z];
num += gridSize.x * gridSize.y;
}
num += tilePoint.y * this._gridSize[zoom].x + tilePoint.x;
return Math.floor(num / 256);;
}
});
L.tileLayer.zoomify = function (url, options) {
return new L.TileLayer.Zoomify(url, options);
};
</script>
<script type="text/javascript">
var map = L.map('picture').setView(new L.LatLng(0,0), 0);
<!-- Цю частину можна згенерувати php-скриптом, вказавши місце зберігання тайлів, та прочитавши розмір скану width-height із ImageProperties.xml -->
L.tileLayer.zoomify('./dardanelles_by_rudometov/', {
width: 8090,
height: 5741,
tolerance: 0.8,
attribution: '0.7.7 | tile generator © <a href="http://zoomify.com">Zoomify</a> | with help <a href="https://github.com/turban/Leaflet.Zoomify">Bjorn Sandvik</a> | © 2018-2022 <a href="https://nvkarta.com">NVkarta</a> as user'
}).addTo(map);
</script>
</body>
</html>
В разі використання Leaflet версії 1.0.0
і вище, повний код html-файла і скрипта від Coen Mulders. Додатково, в код був добавлений скрипт для обмеження зумінгу, але його можна й виключити:
/* Zoomify - Leaflet ver.1.7.1 */
[HTML]
<!DOCTYPE html>
<html lang="uk">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="referrer" content="origin-when-cross-origin">
<title>Leaflet 1.7.1</title>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css" integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A==" crossorigin="" />
<style type="text/css">
html, body, #picture {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
background: #063B41;
}
</style>
</head>
<body>
<div id="picture"></div>
<script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js" integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA==" crossorigin=""></script>
<!-- Скрипт обмеження зумінгу: <script type="text/javascript" src="L.Map.mergeOptions.js"></script> -->
<script type="text/javascript">
// Limit leaflet map zoom to a list of variants
// Written by Ilya Zverev, licensed WTFPL
L.Map.mergeOptions({
zooms: [0,1,2,3,4,5] // array of integers
});
L.Map.include({
_limitZoom: function (zoom) {
var zooms = this.options.zooms;
if (!zooms || !('length' in zooms) || !zooms.length) {
var min = this.getMinZoom(),
max = this.getMaxZoom(),
snap = L.Browser.any3d ? this.options.zoomSnap : 1;
if (snap) {
zoom = Math.round(zoom / snap) * snap;
}
return Math.max(min, Math.min(max, zoom));
} else {
var z, d = 100, i, dist;
var dz = -1, dd = 100, dir = zoom - this._zoom;
var snap = L.Browser.any3d ? this.options.zoomSnap : 1;
if (snap) {
zoom = Math.round(zoom / snap) * snap;
}
for (i = 0; i < zooms.length; i++) {
dist = Math.abs(zooms[i] - zoom);
if (dist < d) {
z = zooms[i];
d = dist;
}
if (dir * (zooms[i] - this._zoom) > 0 && dist < dd) {
dz = zooms[i];
dd = dist;
}
}
return dz < 0 ? z : dz;
}
}
});
</script>
<!-- Наступне можна винести окремо: <script type="text/javascript" src="L.TileLayer.Zoomify.js"></script> -->
<script type="text/javascript">
/*
* L.TileLayer.Zoomify display Zoomify tiles with Leaflet
*
* Based on the Leaflet.Zoomify (https://github.com/turban/Leaflet.Zoomify)
* from turban (https://github.com/turban)
*
*/
L.TileLayer.Zoomify = L.TileLayer.extend({
options: {
width: -1, // Must be set by user, max zoom image width
height: -1, // Must be set by user, max zoom image height
tileGroupPrefix: 'TileGroup',
tilesPerTileGroup: 256
},
initialize: function (url, options) {
L.TileLayer.prototype.initialize.call(this, url, options);
// Replace with automatic loading from ImageProperties.xml
if (this.options.width < 0 || this.options.height < 0) {
throw new Error('The user must set the Width and Height of the Zoomify image');
}
},
beforeAdd: function (map) {
var imageSize = L.point(this.options.width, this.options.height);
// Build the zoom sizes of the pyramid and cache them in an array
this._imageSize = [imageSize];
this._gridSize = [this._getGridSize(imageSize)];
// Register the image size in pixels and the grid size in # of tiles for each zoom level
while (imageSize.x > this.options.tileSize || imageSize.y > this.options.tileSize) {
imageSize = imageSize.divideBy(2).ceil();
this._imageSize.push(imageSize);
this._gridSize.push(this._getGridSize(imageSize));
}
// We built the cache from bottom to top, but leaflet uses a top to bottom index for the zoomlevel,
// so reverse it for easy indexing by current zoomlevel
this._imageSize.reverse();
this._gridSize.reverse();
// Register our max supported zoom level
var maxNativeZoom = this._gridSize.length - 1;
this.options.maxNativeZoom = maxNativeZoom;
// Register our bounds for this zoomify layer based on the maximum zoom
var maxZoomGrid = this._gridSize[maxNativeZoom],
maxX = maxZoomGrid.x * this.options.tileSize,
maxY = maxZoomGrid.y * this.options.tileSize,
southEast = map.unproject([maxX, maxY], maxNativeZoom);
this.options.bounds = new L.LatLngBounds([[0, 0], southEast]);
L.TileLayer.prototype.beforeAdd.call(this, map);
},
// Calculate the grid size for a given image size (based on tile size)
_getGridSize: function (imageSize) {
var tileSize = this.options.tileSize;
return L.point(Math.ceil(imageSize.x / tileSize), Math.ceil(imageSize.y / tileSize));
},
// Extend the add tile function to update our arbitrary sized border tiles
_addTile: function (coords, container) {
// Load the tile via the original leaflet code
L.TileLayer.prototype._addTile.call(this, coords, container);
// Get out imagesize in pixels for this zoom level and our grid size
var imageSize = this._imageSize[this._getZoomForUrl()],
gridSize = this._gridSize[this._getZoomForUrl()];
// The real tile size (default:256) and the display tile size (if zoom > maxNativeZoom)
var realTileSize = L.GridLayer.prototype.getTileSize.call(this),
displayTileSize = L.TileLayer.prototype.getTileSize.call(this);
// Get the current tile to adjust
var key = this._tileCoordsToKey(coords),
tile = this._tiles[key].el;
// Calculate the required size of the border tiles
var scaleFactor = L.point( (imageSize.x % realTileSize.x),
(imageSize.y % realTileSize.y)).unscaleBy(realTileSize);
// Update tile dimensions if we are on a border
if ((imageSize.x % realTileSize.x) > 0 && coords.x === gridSize.x - 1) {
tile.style.width = displayTileSize.scaleBy(scaleFactor).x + 'px';
}
if ((imageSize.y % realTileSize.y) > 0 && coords.y === gridSize.y - 1) {
tile.style.height = displayTileSize.scaleBy(scaleFactor).y + 'px';
}
},
// Construct the tile url, by inserting our tilegroup before we template the url
getTileUrl: function (coords) {
// Set our TileGroup variable, and then let the original tile templater do the work
this.options.g = this.options.tileGroupPrefix + this._getTileGroup(coords);
// Call the original templater
return L.TileLayer.prototype.getTileUrl.call(this, coords);
},
// Calculates the TileGroup number, each group contains 256 tiles. The tiles are stored from topleft to bottomright
_getTileGroup: function (coords) {
var zoom = this._getZoomForUrl(),
num = 0,
gridSize;
// Get the total number of tiles from the lowest zoom level to our zoomlevel
for (var z = 0; z < zoom; z++) {
gridSize = this._gridSize[z];
num += gridSize.x * gridSize.y;
}
// Add the remaining tiles from this zoom layer to the running total of tiles
num += coords.y * this._gridSize[zoom].x + coords.x;
return Math.floor(num / this.options.tilesPerTileGroup);
},
getBounds: function () {
return this.options.bounds;
}
});
L.tileLayer.zoomify = function (url, options) {
return new L.TileLayer.Zoomify(url, options);
};
</script>
<script type="text/javascript">
<!-- Цю частину можна згенерувати php-скриптом, вказавши місце зберігання тайлів, та прочитавши розмір скану width-height із ImageProperties.xml -->
var map = L.map('picture', {
crs: L.CRS.Simple //Set a flat projection, as we are projecting an image
});
//Loading the Zoomify tile layer, notice the URL
var layer = L.tileLayer.zoomify('https://nvkarta.synology.me/history/maps/without-category/nvkarta-scan-temp/dardanelles_by_rudometov/{g}/{z}-{x}-{y}.jpg', {
width: 8090,
height: 5741,
attribution: '1.7.1 | tile generator © <a href="http://zoomify.com">Zoomify</a> | with help <a href="https://github.com/cmulders/Leaflet.Zoomify">Coen Mulders</a> | © 2018-2022 <a href="https://nvkarta.com">NVkarta</a> as user'
}).addTo(map);
//Setting the view to our layer bounds, set by our Zoomify plugin
map.fitBounds(layer.getBounds());
</script>
</body>
</html>