You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gis/inc/staticmap.php

433 lines
16 KiB
PHP

<?php
/**
* staticMapLite 0.3.1
*
* Copyright 2009 Gerhard Koch
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* @author Gerhard Koch <gerhard.koch AT ymail.com>
*
* Adaptation pour SPIP
*
* @author b_b
*
*/
class staticMapGis
{
protected $maxWidth = _TAILLE_MAX_GIS_STATIC;
protected $maxHeight = _TAILLE_MAX_GIS_STATIC;
protected $tileSize = 256;
protected $tileSrcUrl = [
'openstreetmap_mapnik' => [
'url' => 'https://tile.openstreetmap.org/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors'
],
'openstreetmap_de' => [
'url' => 'https://tile.openstreetmap.de/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors'
],
'openstreetmap_fr' => [
'url' => 'https://a.tile.openstreetmap.fr/osmfr/{Z}/{X}/{Y}.png',
'credits' => '© Openstreetmap France | © OpenStreetMap contributors'
],
'openstreetmap_hot' => [
'url' => 'http://a.tile.openstreetmap.fr/hot/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France'
],
'opentopomap' => [
'url' => 'https://tile.opentopomap.org/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributorsn, SRTM | Map style: © OpenTopoMap (CC-BY-SA)'
],
'cartodb_positron' => [
'url' => 'https://basemaps.cartocdn.com/light_all/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors © CARTO'
],
'cartodb_positron_base' => [
'url' => 'https://basemaps.cartocdn.com/light_nolabels/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors © CARTO'
],
'cartodb_darkmatter' => [
'url' => 'https://basemaps.cartocdn.com/dark_all/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors © CARTO'
],
'cartodb_darkmatter_base' => [
'url' => 'https://basemaps.cartocdn.com/dark_nolabels/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors © CARTO'
],
'cartodb_voyager' => [
'url' => 'https://basemaps.cartocdn.com/rastertiles/voyager/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors © CARTO'
],
'cartodb_voyager_base' => [
'url' => 'https://basemaps.cartocdn.com/rastertiles/voyager_nolabels/{Z}/{X}/{Y}.png',
'credits' => '© OpenStreetMap contributors © CARTO'
],
'stamen_toner' => [
'url' => 'https://stamen-tiles-c.a.ssl.fastly.net/toner/{Z}/{X}/{Y}.png',
'credits' => 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors'
],
'stamen_tonerlite' => [
'url' => 'https://stamen-tiles-c.a.ssl.fastly.net/toner-lite/{Z}/{X}/{Y}.png',
'credits' => 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors'
],
'stamen_terrain' => [
'url' => 'https://stamen-tiles-c.a.ssl.fastly.net/terrain/{Z}/{X}/{Y}.png',
'credits' => 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors'
],
'stamen_watercolor' => [
'url' => 'https://stamen-tiles-c.a.ssl.fastly.net/watercolor/{Z}/{X}/{Y}.jpg',
'credits' => 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors'
],
'esri_worldstreetmap' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
],
'esri_delorme' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/Specialty/DeLorme_World_Base_Map/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Copyright: ©2012 DeLorme'
],
'esri_worldtopomap' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Esri, DeLorme, NAVTEQ, TomTom, Intermap, iPC, USGS, FAO, NPS, NRCAN, GeoBase, Kadaster NL, Ordnance Survey, Esri Japan, METI, Esri China (Hong Kong), and the GIS User Community'
],
'esri_worldimagery' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Source: Esri, i-cubed, USDA, USGS, AEX, GeoEye, Getmapping, Aerogrid, IGN, IGP, UPR-EGP, and the GIS User Community'
],
'esri_worldterrain' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Terrain_Base/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Source: USGS, Esri, TANA, DeLorme, and NPS'
],
'esri_worldshadedrelief' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Shaded_Relief/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Source: Esri'
],
'esri_worldphysical' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Physical_Map/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Source: US National Park Service'
],
'esri_oceanbasemap' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/Ocean_Basemap/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Sources: GEBCO, NOAA, CHS, OSU, UNH, CSUMB, National Geographic, DeLorme, NAVTEQ, and Esri'
],
'esri_natgeoworldmap' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — National Geographic, Esri, DeLorme, NAVTEQ, UNEP-WCMC, USGS, NASA, ESA, METI, NRCAN, GEBCO, NOAA, iPC'
],
'esri_worldgraycanvas' => [
'url' => 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{Z}/{Y}/{X}',
'credits' => 'Tiles © Esri — Esri, DeLorme, NAVTEQ'
]
];
protected $tileDefaultSrc = 'openstreetmap_mapnik';
protected $useTileCache = true;
protected $tileCacheBaseDir = '';
protected $useMapCache = true;
protected $mapCacheBaseDir = '';
protected $mapCacheID = '';
protected $mapCacheFile = '';
protected $mapCacheExtension = 'png';
protected $zoom, $lat, $lon, $width, $height, $markers, $image, $maptype;
protected $centerX, $centerY, $offsetX, $offsetY;
public function __construct($lat, $lon, $zoom, $width, $height, $maptype, $markers) {
$this->width = intval($width);
$this->height = intval($height);
if ($zoom) {
$this->zoom = intval($zoom);
}
$this->lat = floatval($lat);
$this->lon = floatval($lon);
$this->markers = $markers;
$this->maptype = $this->tileDefaultSrc;
$base = sous_repertoire(_DIR_VAR, 'cache-gis');
$this->tileCacheBaseDir = sous_repertoire($base, 'tiles');
$this->mapCacheBaseDir = sous_repertoire($base, 'maps');
if ($maptype and array_key_exists($maptype, $this->tileSrcUrl)) {
$this->maptype = $maptype;
}
}
public function lonToTile($long, $zoom) {
return (($long + 180) / 360) * 2 ** $zoom;
}
public function latToTile($lat, $zoom) {
return (1 - log(tan($lat * pi() / 180) + 1 / cos($lat * pi() / 180)) / pi()) / 2 * 2 ** $zoom;
}
public function latLngToPixels($lat, $lon, $zoom) {
return [
'x' => $this->lonToTile($lon, $zoom) * $this->tileSize,
'y' => $this->latToTile($lat, $zoom) * $this->tileSize
];
}
public function getBoundsZoom() {
// thx to https://github.com/esripdx/Static-Maps-API-PHP/blob/master/img.php#L166
$bounds = ['minLat' => 90, 'maxLat' => -90, 'minLon' => 180, 'maxLon' => -180];
foreach ($this->markers as $marker) {
if ($marker['lat'] < $bounds['minLat']) {
$bounds['minLat'] = $marker['lat'];
}
if ($marker['lat'] > $bounds['maxLat']) {
$bounds['maxLat'] = $marker['lat'];
}
if ($marker['lon'] < $bounds['minLon']) {
$bounds['minLon'] = $marker['lon'];
}
if ($marker['lon'] > $bounds['maxLon']) {
$bounds['maxLon'] = $marker['lon'];
}
}
$this->lat = $bounds['minLat'] + (($bounds['maxLat'] - $bounds['minLat']) / 2);
$this->lon = $bounds['minLon'] + (($bounds['maxLon'] - $bounds['minLon']) / 2);
if (!intval($this->zoom)) {
$fitZoom = 10;
$doesNotFit = true;
while ($fitZoom > 1 && $doesNotFit) {
$center = $this->latLngToPixels($this->lat, $this->lon, $fitZoom);
// check if the bounding rectangle fits within width/height
$sw = $this->latLngToPixels($bounds['minLat'], $bounds['minLon'], $fitZoom);
$ne = $this->latLngToPixels($bounds['maxLat'], $bounds['maxLon'], $fitZoom);
$fitHeight = abs($ne['y'] - $sw['y']);
$fitWidth = abs($ne['x'] - $sw['x']);
if ($fitHeight <= $this->height && $fitWidth <= $this->width) {
$doesNotFit = false;
}
$fitZoom--;
}
$this->zoom = $fitZoom;
}
}
public function initCoords() {
$this->centerX = $this->lonToTile($this->lon, $this->zoom);
$this->centerY = $this->latToTile($this->lat, $this->zoom);
$this->offsetX = floor((floor($this->centerX) - $this->centerX) * $this->tileSize);
$this->offsetY = floor((floor($this->centerY) - $this->centerY) * $this->tileSize);
}
public function createBaseMap() {
$this->image = imagecreatetruecolor($this->width, $this->height);
$startX = floor($this->centerX - ($this->width / $this->tileSize) / 2);
$startY = floor($this->centerY - ($this->height / $this->tileSize) / 2);
$endX = ceil($this->centerX + ($this->width / $this->tileSize) / 2);
$endY = ceil($this->centerY + ($this->height / $this->tileSize) / 2);
$this->offsetX = -floor(($this->centerX - floor($this->centerX)) * $this->tileSize);
$this->offsetY = -floor(($this->centerY - floor($this->centerY)) * $this->tileSize);
$this->offsetX += floor($this->width / 2);
$this->offsetY += floor($this->height / 2);
$this->offsetX += floor($startX - floor($this->centerX)) * $this->tileSize;
$this->offsetY += floor($startY - floor($this->centerY)) * $this->tileSize;
for ($x = $startX; $x <= $endX; $x++) {
// normalise x index for tile stitching at date crossing line
$xNorm = $x % (2 ** $this->zoom);
if ($xNorm < 0) {
$xNorm = $xNorm + (2 ** $this->zoom);
}
for ($y = $startY; $y <= $endY; $y++) {
$url = str_replace(['{Z}', '{X}', '{Y}'], [$this->zoom, $xNorm, $y], $this->tileSrcUrl[$this->maptype]['url']);
$tileData = $this->fetchTile($url);
if ($tileData) {
$tileImage = @imagecreatefromstring($tileData);
} else {
$tileImage = imagecreate($this->tileSize, $this->tileSize);
$color = imagecolorallocate($tileImage, 255, 255, 255);
@imagestring($tileImage, 1, 127, 127, 'err', $color);
}
$destX = ($x - $startX) * $this->tileSize + $this->offsetX;
$destY = ($y - $startY) * $this->tileSize + $this->offsetY;
@imagecopy($this->image, $tileImage, $destX, $destY, 0, 0, $this->tileSize, $this->tileSize);
}
}
}
public function placeMarkers() {
// loop thru marker array
foreach ($this->markers as $marker) {
// set some local variables
$markerLat = $marker['lat'];
$markerLon = $marker['lon'];
$markerUrl = $marker['url'] ?? '';
// clear variables from previous loops
$markerShadow = false;
// marker perso ou par défaut ?
if ($markerUrl) {
include_spip('inc/distant');
$markerPath = copie_locale($markerUrl);
[$h, $w] = taille_image($markerPath);
$markerOffsetX = -$w / 2;
$markerOffsetY = -$h;
} else {
$markerPath = sinon(find_in_path('images/marker_defaut.png'), find_in_path('lib/leaflet/dist/images/marker-icon.png'));
$markerShadow = sinon(find_in_path('images/marker_defaut_shadow.png'), find_in_path('lib/leaflet/dist/images/marker-shadow.png'));
$markerOffsetX = -12;
$markerOffsetY = -41;
}
// create img + shadow resource
$markerImg = imagecreatefrompng($markerPath);
$markerShadowImg = null;
if ($markerShadow) {
$markerShadowImg = imagecreatefrompng($markerShadow);
}
// calc position
$destX = floor(($this->width / 2) - $this->tileSize * ($this->centerX - $this->lonToTile($markerLon, $this->zoom)));
$destY = floor(($this->height / 2) - $this->tileSize * ($this->centerY - $this->latToTile($markerLat, $this->zoom)));
// copy shadow on basemap
if ($markerShadow && $markerShadowImg) {
imagecopy(
$this->image,
$markerShadowImg,
$destX + intval($markerOffsetX),
$destY + intval($markerOffsetY),
0,
0,
imagesx($markerShadowImg),
imagesy($markerShadowImg)
);
}
// copy marker on basemap above shadow
imagecopy(
$this->image,
$markerImg,
$destX + intval($markerOffsetX),
$destY + intval($markerOffsetY),
0,
0,
imagesx($markerImg),
imagesy($markerImg)
);
};
}
public function tileUrlToFilename($url) {
return $this->tileCacheBaseDir . '/' . str_replace(['http://','https://'], '', $url);
}
public function checkTileCache($url) {
$filename = $this->tileUrlToFilename($url);
if (file_exists($filename)) {
return file_get_contents($filename);
}
}
public function checkMapCache() {
$this->mapCacheID = md5($this->serializeParams());
$filename = $this->mapCacheIDToFilename();
if (file_exists($filename)) {
return true;
}
}
public function serializeParams() {
return join('&', [$this->zoom, $this->lat, $this->lon, $this->width, $this->height, serialize($this->markers), $this->maptype]);
}
public function mapCacheIDToFilename() {
if (!$this->mapCacheFile) {
$this->mapCacheFile = $this->mapCacheBaseDir . $this->maptype . '/' . $this->zoom . '/cache_' . substr($this->mapCacheID, 0, 2) . '/' . substr($this->mapCacheID, 2, 2) . '/' . substr($this->mapCacheID, 4);
}
return $this->mapCacheFile . '.' . $this->mapCacheExtension;
}
public function mkdir_recursive($pathname, $mode) {
is_dir(dirname($pathname)) || $this->mkdir_recursive(dirname($pathname), $mode);
return is_dir($pathname) || @mkdir($pathname, $mode);
}
public function writeTileToCache($url, $data) {
$filename = $this->tileUrlToFilename($url);
$this->mkdir_recursive(dirname($filename), 0777);
file_put_contents($filename, $data);
}
public function fetchTile($url) {
if ($this->useTileCache && ($cached = $this->checkTileCache($url))) {
return $cached;
}
$ch = curl_init();
curl_setopt($ch, CURLOPT_FAILONERROR, 1);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_USERAGENT, _INC_DISTANT_USER_AGENT);
curl_setopt($ch, CURLOPT_URL, $url);
$tile = curl_exec($ch);
curl_close($ch);
if ($tile && $this->useTileCache) {
$this->writeTileToCache($url, $tile);
}
return $tile;
}
public function copyrightNotice() {
$credits = $this->tileSrcUrl[$this->maptype]['credits'];
$largeur = $this->width * 2;
include_spip('inc/filtres_images_mini');
$logoImg = imagecreatefrompng(extraire_attribut(image_reduire_par(image_typo($credits, 'taille=20', 'padding=8', 'align=right', "largeur=$largeur", 'police=dustismo_bold.ttf'), 2), 'src'));
imagecopy($this->image, $logoImg, imagesx($this->image) - imagesx($logoImg), imagesy($this->image) - imagesy($logoImg), 0, 0, imagesx($logoImg), imagesy($logoImg));
}
public function makeMap() {
if (is_countable($this->markers) ? count($this->markers) : 0) {
$this->getBoundsZoom();
}
$this->initCoords();
$this->createBaseMap();
if (is_countable($this->markers) ? count($this->markers) : 0) {
$this->placeMarkers();
}
$this->copyrightNotice();
}
public function showMap() {
if ($this->useMapCache) {
// use map cache, so check cache for map
if (!$this->checkMapCache()) {
// map is not in cache, needs to be build
$this->makeMap();
$this->mkdir_recursive(dirname($this->mapCacheIDToFilename()), 0777);
imagepng($this->image, $this->mapCacheIDToFilename(), 9);
if (file_exists($this->mapCacheIDToFilename())) {
return $this->mapCacheIDToFilename();
} else {
return imagepng($this->image);
}
} else {
// map is in cache
return $this->mapCacheIDToFilename();
}
} else {
// no cache, make map, send headers and deliver png
$this->makeMap();
return imagepng($this->image);
}
}
}