Skip to content

Commit

Permalink
Add TileJSON Response
Browse files Browse the repository at this point in the history
  • Loading branch information
ingalls committed Dec 16, 2024
1 parent 0347594 commit 6a3ca30
Show file tree
Hide file tree
Showing 7 changed files with 70 additions and 62 deletions.
2 changes: 1 addition & 1 deletion cloudformation/lib/pmtiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export default {
PackageType: 'Image',
Environment: {
Variables: {
BUCKET: cf.join('-', [cf.stackName, cf.accountId, cf.region]),
ASSET_BUCKET: cf.ref('AssetBucket'),
APIROOT: cf.join(['https://tiles.', cf.ref('HostedURL')]),
SigningSecret: cf.sub('{{resolve:secretsmanager:${AWS::StackName}/api/secret:SecretString::AWSCURRENT}}')
}
Expand Down
26 changes: 4 additions & 22 deletions tasks/pmtiles/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions tasks/pmtiles/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@
"scripts": {
"check": "tsc --noEmit",
"lint": "eslint src/",
"build": "tsup --target es2022 --format esm src/index.ts"
"build": "tsup --target es2022 --format esm src/"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.213.0",
"@aws-sdk/node-http-handler": "^3.212.0",
"@mapbox/tilebelt": "^1.0.2",
"@mapbox/tilebelt": "^2.0.1",
"@mapbox/vtquery": "^0.6.0",
"@openaddresses/batch-error": "^2.9.0",
"@openaddresses/batch-schema": "^10.11.0",
Expand All @@ -25,7 +25,6 @@
"devDependencies": {
"@types/aws-lambda": "^8.10.108",
"@types/jsonwebtoken": "^9.0.2",
"@types/mapbox__tilebelt": "^1.0.0",
"@types/node": "^22.0.0",
"eslint": "^9.0.0",
"tsup": "^8.0.0",
Expand Down
37 changes: 21 additions & 16 deletions tasks/pmtiles/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import Err from '@openaddresses/batch-error';
import Schema from '@openaddresses/batch-schema';
import { Type } from '@sinclair/typebox'
import cors from 'cors';
import S3 from './lib/s3.js';
import * as pmtiles from 'pmtiles'
import { nativeDecompress, CACHE } from './lib/pmtiles.js';
import { S3Source, nativeDecompress, CACHE } from './lib/pmtiles.js';
import auth from './lib/auth.js';
import * as pmtiles from 'pmtiles';
import zlib from "zlib";
import vtquery from '@mapbox/vtquery';
import TB from '@mapbox/tilebelt';
import { pointToTile } from '@mapbox/tilebelt';
import serverless from 'serverless-http';

if (!process.env.SigningSecret) throw new Error('SigningSecret env var must be provided');
if (!process.env.ASSET_BUCKET) throw new Error('ASSET_BUCKET env var must be provided');
if (!process.env.APIROOT) process.env.APIROOT = 'http://localhost:5002';

const app = express();

Expand Down Expand Up @@ -55,23 +56,23 @@ schema.get('/tiles/profile/:username/:file', {
name: Type.String(),
description: Type.String(),
version: Type.Literal('1.0.0'),
scheme: Type.Literal('zxy'),
scheme: Type.Literal('xyz'),
tiles: Type.Array(Type.String()),
minzoom: Type.Integer(),
maxzoom: Type.Integer(),
bounds: Type.Array(Type.Number()),
meta: Type.Unknown(),
cetner: Type.Array(Type.Number())
center: Type.Array(Type.Number())
})
}, async (req, res) => {
try {
const username = auth(req.params.token);

if (username !== req.params.username) {
const token = auth(req.query.token);
if (token.email !== req.params.username || token.access !== 'profile') {
throw new Err(401, null, 'Unauthorized Access');
}

const p = new pmtiles.PMTiles(new S3Source(req.params.name), CACHE, nativeDecompress);
const path = `profile/${req.params.username}/${req.params.file}`;
const p = new pmtiles.PMTiles(new S3Source(path), CACHE, nativeDecompress);

const header = await p.getHeader();

Expand All @@ -87,11 +88,11 @@ schema.get('/tiles/profile/:username/:file', {

res.json({
"tilejson": "2.2.0",
"name": `${name}.pmtiles`,
"name": `${req.params.file}.pmtiles`,
"description": "Hosted by TAK-ETL",
"version": "1.0.0",
"scheme": "xyz",
"tiles": [ process.env.APIROOT + `/tiles${path}/{z}/{x}/{y}.${format}?token=${event.queryStringParameters.token}`],
"tiles": [ process.env.APIROOT + `/tiles${path}/{z}/{x}/{y}.${format}?token=${req.query.token}`],
"minzoom": header.minZoom,
"maxzoom": header.maxZoom,
"bounds": [ header.minLon, header.minLat, header.maxLon, header.maxLat ],
Expand Down Expand Up @@ -146,6 +147,11 @@ schema.get('/tiles/profile/:username/:file/query', {
})
}, async (req, res) => {
try {
const token = auth(req.query.token);
if (token.email !== req.params.username || token.access !== 'profile') {
throw new Err(401, null, 'Unauthorized Access');
}

const query: {
lnglat: [number, number],
zoom: number,
Expand All @@ -168,7 +174,7 @@ schema.get('/tiles/profile/:username/:file/query', {
if (query.zoom > header.maxZoom) throw new Err(400, null, "Above Layer MaxZoom");
if (query.zoom < header.minZoom) throw new Err(400, null, "Below Layer MinZoom");

const xyz = TB.pointToTile(query.lnglat[0], query.lnglat[1], query.zoom)
const xyz = pointToTile(query.lnglat[0], query.lnglat[1], query.zoom)
const tile = await p.getZxy(xyz[2], xyz[0], xyz[1]);

const meta = { x: xyz[0], y: xyz[1], z: xyz[2] };
Expand Down Expand Up @@ -218,9 +224,8 @@ schema.get('/tiles/profile/:username/:file/tiles/:z/:x/:y.:format', {
}),
}, async (req, res) => {
try {
const username = auth(req.params.token);

if (username !== req.params.username) {
const token = auth(req.query.token);
if (token.email !== req.params.username || token.access !== 'profile') {
throw new Err(401, null, 'Unauthorized Access');
}

Expand Down Expand Up @@ -261,7 +266,7 @@ schema.get('/tiles/profile/:username/:file/tiles/:z/:x/:y.:format', {
break;
}

let data = tile_result.data;
const data = tile_result.data;

// We need to force API Gateway to interpret the Lambda response as binary
// without depending on clients sending matching Accept: headers in the request.
Expand Down
10 changes: 7 additions & 3 deletions tasks/pmtiles/src/lib/auth.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import jwt from 'jsonwebtoken';
import Err from '@openaddresses/batch-error';

export type JWTToken = {
access: string
email: string
iat: number
}

export default function(token): string {
try {
const int = jwt.verify(token, process.env.SigningSecret);

console.error('AUTH', int);
return jwt.verify(token, process.env.SigningSecret) as JWTToken;
} catch (err) {
throw new Err(401, err, 'Invalid Token');
}
Expand Down
51 changes: 34 additions & 17 deletions tasks/pmtiles/src/lib/pmtiles.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import * as pmtiles from 'pmtiles';
import zlib from "zlib";
import Err from '@openaddresses/batch-error';
import {
GetObjectCommand
} from '@aws-sdk/client-s3';
import S3Client from './s3.js';

const s3client = S3Client();

export async function nativeDecompress(
buf: ArrayBuffer,
Expand All @@ -21,30 +28,40 @@ export class S3Source implements pmtiles.Source {
archive_name: string;

constructor(archive_name: string) {
this.archive_name = archive_name;
this.archive_name = archive_name + '.pmtiles';
}

getKey() {
return this.archive_name;
}

async getBytes(offset: number, length: number): Promise<pmtiles.RangeResponse> {
const resp = await s3client.send(
new S3.GetObjectCommand({
Bucket: process.env.BUCKET!,
Key: this.archive_name + '.pmtiles',
Range: "bytes=" + offset + "-" + (offset + length - 1),
})
);

const arr = await resp.Body!.transformToByteArray();

return {
data: arr.buffer,
etag: resp.ETag,
expires: resp.Expires?.toISOString(),
cacheControl: resp.CacheControl,
};
try {
console.error(process.env.ASSET_BUCKET, this.archive_name)

const resp = await s3client.send(
new GetObjectCommand({
Bucket: process.env.ASSET_BUCKET!,
Key: this.archive_name,
Range: "bytes=" + offset + "-" + (offset + length - 1),
})
);

const arr = await resp.Body!.transformToByteArray();

return {
data: arr.buffer,
etag: resp.ETag,
expires: resp.Expires?.toISOString(),
cacheControl: resp.CacheControl,
};
} catch (err) {
if (err instanceof Error && err.name === 'NoSuchKey') {
throw new Err(404, err, 'Key not found');
} else {
throw new Err(500, err, 'Internal Server Error');
}
}
}
}

Expand Down
1 change: 1 addition & 0 deletions tasks/pmtiles/src/lib/s3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { NodeHttpHandler } from "@aws-sdk/node-http-handler";
export default function s3client(): S3.S3Client {
// the region should default to the same one as the function
const s3config: S3ClientConfig = {
region: process.env.AWS_REGION,
requestHandler: new NodeHttpHandler({
connectionTimeout: 500,
socketTimeout: 500,
Expand Down

0 comments on commit 6a3ca30

Please sign in to comment.