1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

wip: local tileserver

This commit is contained in:
Zack Pollard 2024-07-27 00:01:12 +01:00
parent e748945b4f
commit fe84f1cc90
9 changed files with 6576 additions and 3 deletions

View file

@ -25,6 +25,7 @@ server/upload/
server/src/queries
server/dist/
server/www/
server/resources/v1.pmtiles
web/node_modules/
web/coverage/

View file

@ -24,6 +24,7 @@ services:
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
- ../server/resources/v1.pmtiles:/usr/src/app/resources/v1.pmtiles
env_file:
- .env
environment:

View file

@ -3216,6 +3216,74 @@
]
}
},
"/map/tiles.json": {
"get": {
"operationId": "getTilesJson",
"parameters": [],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object"
}
}
},
"description": ""
}
},
"tags": [
"Map"
]
}
},
"/map/tiles/{z}/{x}/{y}.{format}": {
"get": {
"operationId": "getTiles",
"parameters": [
{
"name": "format",
"required": true,
"in": "path",
"schema": {
"type": "string"
}
},
{
"name": "x",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "y",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
},
{
"name": "z",
"required": true,
"in": "path",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": ""
}
},
"tags": [
"Map"
]
}
},
"/memories": {
"get": {
"operationId": "searchMemories",

View file

@ -52,6 +52,7 @@
"openid-client": "^5.4.3",
"pg": "^8.11.3",
"picomatch": "^4.0.2",
"pmtiles": "^3.0.7",
"react": "^18.3.1",
"react-email": "^3.0.0",
"reflect-metadata": "^0.2.0",
@ -5300,6 +5301,15 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/leaflet": {
"version": "1.9.12",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz",
"integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==",
"license": "MIT",
"dependencies": {
"@types/geojson": "*"
}
},
"node_modules/@types/lodash": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
@ -8665,6 +8675,12 @@
"reusify": "^1.0.4"
}
},
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==",
"license": "MIT"
},
"node_modules/figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@ -11427,6 +11443,16 @@
"node": ">=4"
}
},
"node_modules/pmtiles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-3.2.0.tgz",
"integrity": "sha512-4v3Nw5xeMxaUReLZQTz3PyM4VM/Lx/Xp/rc2GGEWMl0nqAmcb+gjyi+eOTwfPu8LnB0ash36hz0dV76uYvih5A==",
"license": "BSD-3-Clause",
"dependencies": {
"@types/leaflet": "^1.9.8",
"fflate": "^0.8.0"
}
},
"node_modules/point-in-polygon-hao": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.1.0.tgz",
@ -18588,6 +18614,14 @@
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"@types/leaflet": {
"version": "1.9.12",
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.12.tgz",
"integrity": "sha512-BK7XS+NyRI291HIo0HCfE18Lp8oA30H1gpi1tf0mF3TgiCEzanQjOqNZ4x126SXzzi2oNSZhZ5axJp1k0iM6jg==",
"requires": {
"@types/geojson": "*"
}
},
"@types/lodash": {
"version": "4.17.7",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
@ -21126,6 +21160,11 @@
"reusify": "^1.0.4"
}
},
"fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
"integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="
},
"figures": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz",
@ -23150,6 +23189,15 @@
"integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==",
"dev": true
},
"pmtiles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/pmtiles/-/pmtiles-3.2.0.tgz",
"integrity": "sha512-4v3Nw5xeMxaUReLZQTz3PyM4VM/Lx/Xp/rc2GGEWMl0nqAmcb+gjyi+eOTwfPu8LnB0ash36hz0dV76uYvih5A==",
"requires": {
"@types/leaflet": "^1.9.8",
"fflate": "^0.8.0"
}
},
"point-in-polygon-hao": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/point-in-polygon-hao/-/point-in-polygon-hao-1.1.0.tgz",

View file

@ -78,6 +78,7 @@
"openid-client": "^5.4.3",
"pg": "^8.11.3",
"picomatch": "^4.0.2",
"pmtiles": "^3.0.7",
"react": "^18.3.1",
"react-email": "^3.0.0",
"reflect-metadata": "^0.2.0",

View file

@ -5,7 +5,7 @@
"sources": {
"protomaps": {
"type": "vector",
"url": "https://tiles.immich.cloud/v1.json"
"url": "http://localhost:2283/api/map/tiles.json"
}
},
"layers": [

View file

@ -5,7 +5,7 @@
"sources": {
"protomaps": {
"type": "vector",
"url": "https://tiles.immich.cloud/v1.json"
"url": "http://localhost:2283/api/map/tiles.json"
}
},
"layers": [

6367
server/resources/tiles.json Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,10 @@
import { Controller, Get, HttpCode, HttpStatus, Query } from '@nestjs/common';
import { Controller, Get, NotFoundException, Param, HttpCode, HttpStatus, Query, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import fsSync from 'node:fs';
import fs from 'node:fs/promises';
// @ts-expect-error test
import { PMTiles, RangeResponse, Source } from 'pmtiles';
import { AuthDto } from 'src/dtos/auth.dto';
import {
MapMarkerDto,
@ -16,6 +21,9 @@ import { MapService } from 'src/services/map.service';
export class MapController {
constructor(private service: MapService) {}
source = new FileSource('./resources/v1.pmtiles');
pmtiles = new PMTiles(this.source);
@Get('markers')
@Authenticated()
getMapMarkers(@Auth() auth: AuthDto, @Query() options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
@ -34,4 +42,83 @@ export class MapController {
reverseGeocode(@Query() dto: MapReverseGeocodeDto): Promise<MapReverseGeocodeResponseDto[]> {
return this.service.reverseGeocode(dto);
}
@Get('tiles.json')
async getTilesJson() {
// eslint-disable-next-line unicorn/no-await-expression-member
return JSON.parse((await fs.readFile(`./resources/tiles.json`)).toString());
}
@Get('tiles/:z/:x/:y.:format')
async getTiles(
@Param('z') z: number,
@Param('x') x: number,
@Param('y') y: number,
@Param('format') format: string,
@Res({ passthrough: true }) res: Response,
) {
// load data based on tile request
console.log('getting tile');
const tile = await this.pmtiles.getZxy(Number(z), Number(x), Number(y));
console.log('tile', tile);
if (!tile) {
throw new NotFoundException('Tile not found.');
}
console.log('getting');
const data = Buffer.from(tile.data);
console.log('got buffer');
// determine content-type header based on data
// (assume pbf for now)
console.log('getting header');
const header = await this.pmtiles.getHeader();
console.log('header', header);
switch (header.tileType) {
case 0: {
console.log('Unknown tile type.');
break;
}
case 1: {
res.setHeader('Content-Type', 'application/x-protobuf');
break;
}
}
res.status(200).send(data);
}
}
class FileSource implements Source {
filename: string;
fileDescriptor: number;
constructor(filename: string) {
this.filename = filename;
this.fileDescriptor = fsSync.openSync(filename, 'r');
}
getKey() {
return this.filename;
}
// helper async function to read in bytes from file
readBytesIntoBuffer = async (buffer: Buffer, offset: number) =>
new Promise<void>((resolve, reject) => {
fsSync.read(this.fileDescriptor, buffer, 0, buffer.length, offset, (err) => {
if (err) {
return reject(err);
}
resolve();
});
});
getBytes = async (offset: number, length: number) => {
// create buffer and read in byes from file
const buffer = Buffer.alloc(length);
await this.readBytesIntoBuffer(buffer, offset);
const data = buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
return { data } as RangeResponse;
};
}