From 2b7768933bb7ed28872959cdd7dab68a85d782b4 Mon Sep 17 00:00:00 2001 From: Julien Veyssier Date: Mon, 22 Jul 2024 00:54:12 +0200 Subject: [PATCH] proxy all vector tile requests through the server Signed-off-by: Julien Veyssier --- appinfo/routes.php | 7 +- lib/Controller/MapController.php | 121 ++++++++++++++++++++++++++- lib/Service/MapService.php | 128 +++++++++++++++++++++++++++++ src/components/map/MaplibreMap.vue | 8 +- src/tileServers.js | 26 +++--- 5 files changed, 272 insertions(+), 18 deletions(-) diff --git a/appinfo/routes.php b/appinfo/routes.php index cbccfb1bb..61873df91 100644 --- a/appinfo/routes.php +++ b/appinfo/routes.php @@ -26,7 +26,12 @@ ['name' => 'map#getRasterTile', 'url' => '/tiles/{service}/{x}/{y}/{z}', 'verb' => 'GET'], ['name' => 'map#nominatimSearch', 'url' => '/nominatim/search', 'verb' => 'GET'], - ['name' => 'map#getMapTilerFont', 'url' => '/fonts/{fontstack}/{range}.pbf', 'verb' => 'GET'], + ['name' => 'map#getMapTilerStyle', 'url' => '/maptiler/maps/{version}/style.json', 'verb' => 'GET'], + ['name' => 'map#getMapTilerFont', 'url' => '/maptiler/fonts/{fontstack}/{range}.pbf', 'verb' => 'GET'], + ['name' => 'map#getMapTilerTiles', 'url' => '/maptiler/tiles/{version}/tiles.json', 'verb' => 'GET'], + ['name' => 'map#getMapTilerTile', 'url' => '/maptiler/tiles/{version}/{z}/{x}/{y}.{ext}', 'verb' => 'GET'], + ['name' => 'map#getMapTilerSprite', 'url' => '/maptiler/maps/{version}/sprite.{ext}', 'verb' => 'GET'], + ['name' => 'map#getMapTilerResource', 'url' => '/maptiler/resources/{name}', 'verb' => 'GET'], ['name' => 'page#addDirectory', 'url' => '/directories', 'verb' => 'POST'], ['name' => 'page#updateDirectory', 'url' => '/directories/{id}', 'verb' => 'PUT'], diff --git a/lib/Controller/MapController.php b/lib/Controller/MapController.php index 734ea710f..8a97de9fa 100644 --- a/lib/Controller/MapController.php +++ b/lib/Controller/MapController.php @@ -18,6 +18,8 @@ use OCP\AppFramework\Http\Attribute\NoCSRFRequired; use OCP\AppFramework\Http\DataDisplayResponse; use OCP\AppFramework\Http\DataResponse; +use OCP\AppFramework\Http\JSONResponse; +use OCP\AppFramework\Http\Response; use OCP\DB\Exception; use OCP\IRequest; use Psr\Log\LoggerInterface; @@ -58,22 +60,135 @@ public function getRasterTile(string $service, int $x, int $y, int $z, ?string $ } } + /** + * @param string $version + * @param string|null $key + * @return Response + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function getMapTilerStyle(string $version, ?string $key = null): Response { + try { + $response = new JSONResponse($this->mapService->getMapTilerStyle($version, $key)); + $response->cacheFor(60 * 60 * 24); + return $response; + } catch (Exception | Throwable $e) { + $this->logger->debug('Style not found', ['exception' => $e]); + return new JSONResponse(['exception' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + } + /** * @param string $fontstack * @param string $range * @param string|null $key - * @return DataDisplayResponse + * @return Response */ #[NoAdminRequired] #[NoCSRFRequired] - public function getMapTilerFont(string $fontstack, string $range, ?string $key = null): DataDisplayResponse { + public function getMapTilerFont(string $fontstack, string $range, ?string $key = null): Response { try { $response = new DataDisplayResponse($this->mapService->getMapTilerFont($fontstack, $range, $key)); $response->cacheFor(60 * 60 * 24); return $response; } catch (Exception | Throwable $e) { $this->logger->debug('Font not found', ['exception' => $e]); - return new DataDisplayResponse($e->getMessage(), Http::STATUS_NOT_FOUND); + return new JSONResponse(['exception' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + } + + /** + * @param string $version + * @param string|null $key + * @return JSONResponse + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function getMapTilerTiles(string $version, ?string $key = null): JSONResponse { + try { + $response = new JSONResponse($this->mapService->getMapTilerTiles($version, $key)); + $response->cacheFor(60 * 60 * 24); + return $response; + } catch (Exception | Throwable $e) { + $this->logger->debug('Tiles not found', ['exception' => $e]); + return new JSONResponse(['exception' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + } + + /** + * @param string $version + * @param int $z + * @param int $x + * @param int $y + * @param string $ext + * @param string|null $key + * @return Response + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function getMapTilerTile(string $version, int $z, int $x, int $y, string $ext, ?string $key = null): Response { + try { + $tileResponse = $this->mapService->getMapTilerTile($version, $x, $y, $z, $ext, $key); + $response = new DataDisplayResponse( + $tileResponse['body'], + Http::STATUS_OK, + ['Content-Type' => $tileResponse['headers']['Content-Type'] ?? 'image/jpeg'] + ); + $response->cacheFor(60 * 60 * 24); + return $response; + } catch (Exception | Throwable $e) { + $this->logger->debug('Tile not found', ['exception' => $e]); + return new JSONResponse(['exception' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + } + + /** + * @param string $version + * @param string $ext + * @return Response + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function getMapTilerSprite(string $version, string $ext): Response { + try { + if ($ext === 'json') { + $sprite = $this->mapService->getMapTilerSpriteJson($version); + $response = new JSONResponse($sprite); + } else { + $sprite = $this->mapService->getMapTilerSpriteImage($version, $ext); + $response = new DataDisplayResponse( + $sprite['body'], + Http::STATUS_OK, + ['Content-Type' => $sprite['headers']['Content-Type'] ?? 'image/png'] + ); + } + $response->cacheFor(60 * 60 * 24); + return $response; + } catch (Exception | Throwable $e) { + $this->logger->debug('Sprite not found', ['exception' => $e]); + return new JSONResponse(['exception' => $e->getMessage()], Http::STATUS_NOT_FOUND); + } + } + + /** + * @param string $name + * @return Response + */ + #[NoAdminRequired] + #[NoCSRFRequired] + public function getMapTilerResource(string $name): Response { + try { + $resourceResponse = $this->mapService->getMapTilerResource($name); + $response = new DataDisplayResponse( + $resourceResponse['body'], + Http::STATUS_OK, + ['Content-Type' => $resourceResponse['headers']['Content-Type'] ?? 'image/png'] + ); + $response->cacheFor(60 * 60 * 24); + return $response; + } catch (Exception | Throwable $e) { + $this->logger->debug('Resource not found', ['exception' => $e]); + return new JSONResponse(['exception' => $e->getMessage()], Http::STATUS_NOT_FOUND); } } diff --git a/lib/Service/MapService.php b/lib/Service/MapService.php index ffb3a0358..7bea3ee90 100644 --- a/lib/Service/MapService.php +++ b/lib/Service/MapService.php @@ -21,6 +21,7 @@ use OCP\Http\Client\IClient; use OCP\Http\Client\IClientService; use OCP\IL10N; +use OCP\IURLGenerator; use Psr\Log\LoggerInterface; use Throwable; @@ -31,6 +32,7 @@ class MapService { public function __construct( IClientService $clientService, private LoggerInterface $logger, + private IURLGenerator $urlGenerator, private IL10N $l10n ) { $this->client = $clientService->newClient(); @@ -136,6 +138,33 @@ public function getRasterTile(string $service, int $x, int $y, int $z, ?string $ return $body; } + /** + * @param string $version + * @param string|null $key + * @return array + * @throws Exception + */ + public function getMapTilerStyle(string $version, ?string $key = null): array { + $url = 'https://api.maptiler.com/maps/' . $version . '/style.json'; + if ($key !== null) { + $url .= '?key=' . $key; + } + $body = $this->client->get($url)->getBody(); + if (is_resource($body)) { + $content = stream_get_contents($body); + } else { + $content = $body; + } + $replacementUrl = $this->urlGenerator->linkToRouteAbsolute(Application::APP_ID . '.page.index') . 'maptiler'; + $style = json_decode(preg_replace('/https:\/\/api\.maptiler\.com/', $replacementUrl, $content), true); + foreach ($style['layers'] as $i => $layer) { + if (is_array($layer['layout']) && empty($layer['layout'])) { + $style['layers'][$i]['layout'] = (object)[]; + } + } + return $style; + } + /** * @param string $fontstack * @param string $range @@ -159,6 +188,105 @@ public function getMapTilerFont(string $fontstack, string $range, ?string $key = return $body; } + /** + * @param string $version + * @param string|null $key + * @return array + * @throws Exception + */ + public function getMapTilerTiles(string $version, ?string $key = null): array { + $url = 'https://api.maptiler.com/tiles/' . $version . '/tiles.json'; + if ($key !== null) { + $url .= '?key=' . $key; + } + $body = $this->client->get($url)->getBody(); + if (is_resource($body)) { + $content = stream_get_contents($body); + if ($content === false) { + throw new Exception('No content'); + } + } else { + $content = $body; + } + $replacementUrl = $this->urlGenerator->linkToRouteAbsolute(Application::APP_ID . '.page.index') . 'maptiler'; + return json_decode(preg_replace('/https:\/\/api\.maptiler\.com/', $replacementUrl, $content), true); + } + + /** + * @param string $version + * @param int $x + * @param int $y + * @param int $z + * @param string $ext + * @param string|null $key + * @return array + * @throws Exception + */ + public function getMapTilerTile(string $version, int $x, int $y, int $z, string $ext, ?string $key = null): array { + $url = 'https://api.maptiler.com/tiles/' . $version . '/' . $z . '/' . $x . '/' . $y . '.' . $ext; + if ($key !== null) { + $url .= '?key=' . $key; + } + $response = $this->client->get($url); + $body = $response->getBody(); + $headers = $response->getHeaders(); + return [ + 'body' => $body, + 'headers' => $headers, + ]; + } + + /** + * @param string $version + * @return array + * @throws Exception + */ + public function getMapTilerSpriteJson(string $version): array { + $url = 'https://api.maptiler.com/maps/' . $version . '/sprite.json'; + $body = $this->client->get($url)->getBody(); + if (is_resource($body)) { + $content = stream_get_contents($body); + if ($content === false) { + throw new Exception('No content'); + } + return json_decode($content, true); + } + return json_decode($body, true); + } + + /** + * @param string $version + * @param string $ext + * @return array + * @throws Exception + */ + public function getMapTilerSpriteImage(string $version, string $ext): array { + $url = 'https://api.maptiler.com/maps/' . $version . '/sprite.' . $ext; + $response = $this->client->get($url); + $body = $response->getBody(); + $headers = $response->getHeaders(); + return [ + 'body' => $body, + 'headers' => $headers, + ]; + } + + /** + * @param string $name + * @return array + * @throws Exception + */ + public function getMapTilerResource(string $name): array { + $url = 'https://api.maptiler.com/resources/' . $name; + $response = $this->client->get($url); + $body = $response->getBody(); + $headers = $response->getHeaders(); + return [ + 'body' => $body, + 'headers' => $headers, + ]; + } + /** * Search items * diff --git a/src/components/map/MaplibreMap.vue b/src/components/map/MaplibreMap.vue index 47f117b3e..49e1005b8 100644 --- a/src/components/map/MaplibreMap.vue +++ b/src/components/map/MaplibreMap.vue @@ -2,7 +2,7 @@
- MapTiler logo
@@ -84,7 +84,7 @@ import '@maplibre/maplibre-gl-geocoder/dist/maplibre-gl-geocoder.css' import { subscribe, unsubscribe } from '@nextcloud/event-bus' import moment from '@nextcloud/moment' -import { imagePath } from '@nextcloud/router' +import { imagePath, generateUrl } from '@nextcloud/router' import { getRasterTileServers, @@ -180,6 +180,7 @@ export default { nonPersistentPopup: null, positionMarkerEnabled: false, positionMarkerLngLat: null, + logoUrl: generateUrl('/apps/gpxpod/maptiler/resources/logo.svg'), } }, @@ -485,7 +486,8 @@ export default { this.map.addSource('terrain', { type: 'raster-dem', - url: 'https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=' + this.settings.maptiler_api_key, + // url: 'https://api.maptiler.com/tiles/terrain-rgb/tiles.json?key=' + this.settings.maptiler_api_key, + url: generateUrl('/apps/gpxpod/maptiler/tiles/terrain-rgb-v2/tiles.json?key=' + this.settings.maptiler_api_key), }) // Setting up the terrain with a 0 exaggeration factor diff --git a/src/tileServers.js b/src/tileServers.js index affb16298..17702a46e 100644 --- a/src/tileServers.js +++ b/src/tileServers.js @@ -7,7 +7,7 @@ export function getRasterTileServers(apiKey) { version: 8, // required to display text, apparently vector styles get this but not raster ones // glyphs: 'https://api.maptiler.com/fonts/{fontstack}/{range}.pbf?key=' + apiKey, - glyphs: generateUrl('/apps/gpxpod/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, + glyphs: generateUrl('/apps/gpxpod/maptiler/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, sources: { 'osm-source': { type: 'raster', @@ -39,7 +39,7 @@ export function getRasterTileServers(apiKey) { title: 'OpenCycleMap raster', version: 8, // required to display text, apparently vector styles get this but not raster ones - glyphs: generateUrl('/apps/gpxpod/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, + glyphs: generateUrl('/apps/gpxpod/maptiler/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, sources: { 'ocm-source': { type: 'raster', @@ -70,7 +70,7 @@ export function getRasterTileServers(apiKey) { title: 'OpenStreetMap raster HighRes', version: 8, // required to display text, apparently vector styles get this but not raster ones - glyphs: generateUrl('/apps/gpxpod/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, + glyphs: generateUrl('/apps/gpxpod/maptiler/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, sources: { 'osm-highres-source': { type: 'raster', @@ -96,7 +96,7 @@ export function getRasterTileServers(apiKey) { title: 'OpenCycleMap raster HighRes', version: 8, // required to display text, apparently vector styles get this but not raster ones - glyphs: generateUrl('/apps/gpxpod/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, + glyphs: generateUrl('/apps/gpxpod/maptiler/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, sources: { 'ocm-highres-source': { type: 'raster', @@ -125,7 +125,7 @@ export function getRasterTileServers(apiKey) { esriTopo: { title: t('gpxpod', 'ESRI topo with relief'), version: 8, - glyphs: generateUrl('/apps/gpxpod/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, + glyphs: generateUrl('/apps/gpxpod/maptiler/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, sources: { 'esri-topo-source': { type: 'raster', @@ -154,7 +154,7 @@ export function getRasterTileServers(apiKey) { waterColor: { title: t('gpxpod', 'WaterColor'), version: 8, - glyphs: generateUrl('/apps/gpxpod/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, + glyphs: generateUrl('/apps/gpxpod/maptiler/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, sources: { 'watercolor-source': { type: 'raster', @@ -197,15 +197,18 @@ export function getVectorStyles(apiKey) { return { streets: { title: t('gpxpod', 'Streets'), - uri: 'https://api.maptiler.com/maps/streets-v2/style.json?key=' + apiKey, + // uri: 'https://api.maptiler.com/maps/streets-v2/style.json?key=' + apiKey, + uri: generateUrl('/apps/gpxpod/maptiler/') + 'maps/streets-v2/style.json?key=' + apiKey, }, satellite: { title: t('gpxpod', 'Satellite'), - uri: 'https://api.maptiler.com/maps/hybrid/style.json?key=' + apiKey, + // uri: 'https://api.maptiler.com/maps/hybrid/style.json?key=' + apiKey, + uri: generateUrl('/apps/gpxpod/maptiler/') + 'maps/hybrid/style.json?key=' + apiKey, }, outdoor: { title: t('gpxpod', 'Outdoor'), - uri: 'https://api.maptiler.com/maps/outdoor-v2/style.json?key=' + apiKey, + // uri: 'https://api.maptiler.com/maps/outdoor-v2/style.json?key=' + apiKey, + uri: generateUrl('/apps/gpxpod/maptiler/') + 'maps/outdoor-v2/style.json?key=' + apiKey, }, // does not work ATM // malformed style.json (extra space): @@ -218,7 +221,8 @@ export function getVectorStyles(apiKey) { */ dark: { title: t('gpxpod', 'Dark'), - uri: 'https://api.maptiler.com/maps/streets-dark/style.json?key=' + apiKey, + // uri: 'https://api.maptiler.com/maps/streets-dark/style.json?key=' + apiKey, + uri: generateUrl('/apps/gpxpod/maptiler/') + 'maps/streets-dark/style.json?key=' + apiKey, }, } } @@ -246,7 +250,7 @@ export function getExtraTileServers(tileServers, apiKey) { title: ts.name, version: 8, // required to display text, apparently vector styles get this but not raster ones - glyphs: generateUrl('/apps/gpxpod/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, + glyphs: generateUrl('/apps/gpxpod/maptiler/fonts/') + '{fontstack}/{range}.pbf?key=' + apiKey, sources: { [sourceId]: { type: 'raster',