├── .github └── workflows │ ├── codeql.yml │ ├── deploy-lambda.yml │ ├── node.js.yml │ └── sonarcloud.yml ├── .gitignore ├── .travis.yml ├── DEVELOPMENT.md ├── LICENSE ├── README.md ├── docs ├── architecture-v2.drawio ├── architecture.drawio ├── architecture.odg ├── benchmark.ods ├── icon.svg ├── icon.svg.png ├── img │ ├── CloudFront-static.png │ ├── CloudFront-tiles-simple.png │ ├── CloudFront-tiles.png │ ├── CodeBuild-Docker.png │ ├── architecture.png │ ├── benchmark.png │ ├── map_screenshot.png │ ├── map_screenshot2.png │ ├── stack-with-timing.png │ └── stack.png └── stack.odg ├── html ├── cyclemap-style.json ├── favicon.ico ├── gpx.svg ├── index.html ├── json.svg ├── local.html ├── localhost-style.json ├── mountain.svg ├── serve.sh ├── shadow-style.json ├── sprites │ ├── cyclemap.json │ ├── cyclemap.png │ ├── cyclemap@2x.json │ ├── cyclemap@2x.png │ ├── cyclemap@4x.json │ └── cyclemap@4x.png ├── togeojson.js ├── togpx.js ├── transport-style.json └── xray-style.json ├── jest.config.js ├── local └── local.ts ├── package-lock.json ├── package.json ├── sonar-project.properties ├── sprites ├── bar.svg ├── bicycle.svg ├── bus_stop.svg ├── camp_site.svg ├── caravan_site.svg ├── church.svg ├── circle-black.svg ├── circle-blue.svg ├── circle-red.svg ├── climbing.svg ├── hospital.svg ├── hotel.svg ├── information.svg ├── miniature_golf.svg ├── nautilid.svg ├── no-access.svg ├── parking.garage.svg ├── parking.svg ├── peak.svg ├── playground.svg ├── pub.svg ├── restaurant.svg ├── ring-black.svg ├── roundedsquare.svg ├── shelter.svg ├── shield-3.blue.svg ├── shield-3.yellow.svg ├── shield-4.blue.svg ├── shield-4.yellow.svg ├── shield-5.blue.svg ├── shield-5.yellow.svg ├── shop.svg ├── shop1.svg ├── shop2.svg ├── square.svg ├── square.white.svg ├── star.white.svg ├── station.blue.svg ├── station.red.svg ├── stripes.svg ├── subway.svg ├── supermarket.svg ├── tower.svg ├── viewpoint.svg ├── wetland.svg ├── zoo.svg └── zoo2.svg ├── src ├── index.ts ├── projection.ts ├── sources.json ├── sources.toml └── tileserver.ts ├── terraform ├── .terraform.lock.hcl ├── main.tf ├── terraform-state.tf ├── tileserver-apigateway.tf ├── tileserver-cert.tf ├── tileserver-cloudfront.tf ├── tileserver-lambda.tf ├── tileserver-networking.tf ├── tileserver-route53.tf ├── tileserver-s3.tf └── variables.tf ├── test ├── database.test.ts ├── env.test.ts ├── fixtures │ ├── duplicate_layername.toml │ ├── local_14_8691_5677.js │ ├── simple.toml │ ├── simple_dbconfig.toml │ └── simple_z13.sql ├── handler.test.ts ├── layer.test.ts ├── logger.test.ts ├── parser.test.ts ├── projection.test.ts ├── queries.test.ts └── vectortile.test.ts ├── tileserver-openapi.yaml ├── tileserver_layer └── nodejs │ ├── package-lock.json │ └── package.json ├── tools ├── benchmark.sh ├── gensprites.js ├── toml2json.js └── toml2json.ts ├── tsconfig.json └── typedoc.json /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | schedule: 9 | - cron: "55 3 * * 6" 10 | 11 | jobs: 12 | analyze: 13 | name: Analyze 14 | runs-on: ubuntu-latest 15 | permissions: 16 | actions: read 17 | contents: read 18 | security-events: write 19 | 20 | strategy: 21 | fail-fast: false 22 | matrix: 23 | language: [ javascript ] 24 | 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@v3 28 | 29 | - name: Initialize CodeQL 30 | uses: github/codeql-action/init@v2 31 | with: 32 | languages: ${{ matrix.language }} 33 | queries: +security-and-quality 34 | 35 | - name: Autobuild 36 | uses: github/codeql-action/autobuild@v2 37 | 38 | - name: Perform CodeQL Analysis 39 | uses: github/codeql-action/analyze@v2 40 | with: 41 | category: "/language:${{ matrix.language }}" 42 | -------------------------------------------------------------------------------- /.github/workflows/deploy-lambda.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Nodejs Lambda 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | tags: [ '*' ] 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@main 14 | - name: Use Node.js 20.x 15 | uses: actions/setup-node@main 16 | with: 17 | node-version: 20.x 18 | cache: 'npm' 19 | - name: predeploy 20 | run: | 21 | npm ci 22 | npm run predeploy 23 | - name: Setup AWS CLI 24 | uses: aws-actions/configure-aws-credentials@main 25 | with: 26 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 27 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 28 | aws-region: eu-central-1 29 | - name: deploy to AWS lambda 30 | run: aws lambda update-function-code --function-name tileserver --zip-file fileb://dist/function.zip 31 | #run: aws lambda create-function --dry-run --function-name tileserver --description "Deploy commit ${{github.sha}} by ${{github.actor}}" --runtime nodejs18.x --handler handler --role ${{ secrets.AWS_LAMBDA_TILESERVER_ROLE }} --zip-file fileb://dist/function.zip -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 18.x, 20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@main 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@main 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - name: test 30 | run: | 31 | npm ci 32 | npm run test:solo 33 | 34 | - name: Coveralls 35 | uses: coverallsapp/github-action@master 36 | with: 37 | github-token: ${{ secrets.GITHUB_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/sonarcloud.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow helps you trigger a SonarCloud analysis of your code and populates 7 | # GitHub Code Scanning alerts with the vulnerabilities found. 8 | # Free for open source project. 9 | 10 | # 1. Login to SonarCloud.io using your GitHub account 11 | 12 | # 2. Import your project on SonarCloud 13 | # * Add your GitHub organization first, then add your repository as a new project. 14 | # * Please note that many languages are eligible for automatic analysis, 15 | # which means that the analysis will start automatically without the need to set up GitHub Actions. 16 | # * This behavior can be changed in Administration > Analysis Method. 17 | # 18 | # 3. Follow the SonarCloud in-product tutorial 19 | # * a. Copy/paste the Project Key and the Organization Key into the args parameter below 20 | # (You'll find this information in SonarCloud. Click on "Information" at the bottom left) 21 | # 22 | # * b. Generate a new token and add it to your Github repository's secrets using the name SONAR_TOKEN 23 | # (On SonarCloud, click on your avatar on top-right > My account > Security 24 | # or go directly to https://sonarcloud.io/account/security/) 25 | 26 | # Feel free to take a look at our documentation (https://docs.sonarcloud.io/getting-started/github/) 27 | # or reach out to our community forum if you need some help (https://community.sonarsource.com/c/help/sc/9) 28 | 29 | name: SonarCloud analysis 30 | 31 | on: 32 | push: 33 | branches: [ "master" ] 34 | pull_request: 35 | branches: [ "master" ] 36 | workflow_dispatch: 37 | 38 | permissions: 39 | pull-requests: read # allows SonarCloud to decorate PRs with analysis results 40 | 41 | jobs: 42 | Analysis: 43 | runs-on: ubuntu-latest 44 | 45 | steps: 46 | - name: Analyze with SonarCloud 47 | 48 | # You can pin the exact commit or the version. 49 | # uses: SonarSource/sonarcloud-github-action@de2e56b42aa84d0b1c5b622644ac17e505c9a049 50 | uses: SonarSource/sonarcloud-github-action@master 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information 53 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} # Generate a token on Sonarcloud.io, add it to the secrets of this repo with the name SONAR_TOKEN (Settings > Secrets > Actions > add new repository secret) 54 | with: 55 | # Additional arguments for the sonarcloud scanner 56 | args: 57 | # Unique keys of your project and organization. You can find them in SonarCloud > Information (bottom-left menu) 58 | # mandatory 59 | -Dsonar.projectKey=henrythasler_cloud-tileserver 60 | -Dsonar.organization=henrythasler 61 | # Comma-separated paths to directories containing main source files. 62 | #-Dsonar.sources= # optional, default is project base directory 63 | # When you need the analysis to take place in a directory other than the one from which it was launched 64 | #-Dsonar.projectBaseDir= # optional, default is . 65 | # Comma-separated paths to directories containing test source files. 66 | #-Dsonar.tests= # optional. For more info about Code Coverage, please refer to https://docs.sonarcloud.io/enriching/test-coverage/overview/ 67 | # Adds more detail to both client and server-side analysis logs, activating DEBUG mode for the scanner, and adding client-side environment variables and system properties to the server-side log of analysis report processing. 68 | #-Dsonar.verbose= # optional, default is false 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # terraform stuff 64 | .terraform/ 65 | secret.auto.tfvars 66 | *.tfstate* 67 | 68 | # generated typedocs (for now) 69 | docs/out 70 | .coveralls.yml 71 | dist 72 | local/local.js 73 | src/projection.js 74 | src/tileserver.js 75 | tools/*.csv 76 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: jammy 3 | node_js: 4 | - 18 5 | #jdk: openjdk17 6 | 7 | addons: 8 | sonarcloud: 9 | organization: "henrythasler" 10 | 11 | script: 12 | - npm run test 13 | # - sonar-scanner 14 | 15 | before_deploy: 16 | - npm run predeploy 17 | 18 | # see `https://docs.travis-ci.com/user/deployment/lambda/` for instructions 19 | # see `terraform/tileserver-lambda.tf` for values 20 | deploy: 21 | provider: lambda 22 | edge: 23 | source: travis-ci/dpl 24 | branch: qa-add-lambda-runtime 25 | function_name: "tileserver" 26 | region: "eu-central-1" 27 | role: "arn:aws:iam::324094553422:role/tileserver_role" 28 | runtime: "nodejs18.x" 29 | handler_name: "handler" 30 | zip: "dist/function.zip" 31 | on: 32 | branch: master 33 | tags: true 34 | # edge: true # force dpl v2 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 henrythasler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloud-Tileserver 2 | 3 | [![Build Status](https://github.com/henrythasler/cloud-tileserver/actions/workflows/node.js.yml/badge.svg)](https://github.com/henrythasler/cloud-tileserver/actions/workflows/node.js.yml) [![Coverage Status](https://coveralls.io/repos/github/henrythasler/cloud-tileserver/badge.svg?branch=master)](https://coveralls.io/github/henrythasler/cloud-tileserver?branch=master) 4 | [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=henrythasler_cloud-tileserver&metric=alert_status)](https://sonarcloud.io/dashboard?id=henrythasler_cloud-tileserver) 5 | [![CodeQL](https://github.com/henrythasler/cloud-tileserver/workflows/CodeQL/badge.svg)](https://github.com/henrythasler/cloud-tileserver/actions/workflows/codeql.yml) 6 | [![Known Vulnerabilities](https://snyk.io//test/github/henrythasler/cloud-tileserver/badge.svg?targetFile=package.json)](https://snyk.io//test/github/henrythasler/cloud-tileserver?targetFile=package.json) 7 | 8 | Serve mapbox vectortiles via AWS stack. Please visit the [Wiki](https://github.com/henrythasler/cloud-tileserver/wiki) for installation instructions. 9 | 10 | ## Goals 11 | 12 | These are the main project goals: 13 | 14 | ``` 15 | [x] Setup the AWS infrastructure with terraform 16 | [x] Create an AWS lambda function to handle vectortile queries via REST 17 | [x] Create mapbox vectortiles directly with postgis using ST_AsMvtGeom() and ST_AsMVT() 18 | [x] Write a parser to read config-files that define the vectortiles layout 19 | [x] Create fully automated deployment pipeline. 20 | [x] Use some caching mechanism for vectortiles 21 | [x] Use Typescript and typed interfaces where possible 22 | [x] Have module tests with tsjest/chai 23 | [ ] Generate useful documentation with typedocs 24 | [ ] Learn more about AWS, terraform and typescript 25 | [ ] Use free-tier if possible. 26 | [x] Have fun 27 | ``` 28 | Checked items are already fulfilled. 29 | 30 | ## Overall Architecture 31 | 32 | 1. Client requests tile from CloudFront/S3 . 33 | 2. Missing tiles are created via API Gateway and Lambda. 34 | 35 | ![](docs/img/CloudFront-tiles-simple.png) 36 | 37 | A more detailled description can be found in [DEVELOPMENT.md](DEVELOPMENT.md) 38 | 39 | ## Screenshots, Live Demo 40 | 41 | The Live-Demo is available at: [cyclemap.link](https://cyclemap.link) 42 | 43 | [![](docs/img/map_screenshot.png)](https://cyclemap.link/#15/48.17374/11.54676) 44 | 45 | [![](docs/img/map_screenshot2.png)](https://cyclemap.link/#14/47.01863/11.5046) 46 | -------------------------------------------------------------------------------- /docs/architecture-v2.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/architecture.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /docs/architecture.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/architecture.odg -------------------------------------------------------------------------------- /docs/benchmark.ods: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/benchmark.ods -------------------------------------------------------------------------------- /docs/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 23 | 25 | 32 | 35 | 39 | 40 | 41 | 63 | 65 | 66 | 68 | image/svg+xml 69 | 71 | 72 | 73 | 74 | 75 | 80 | 90 | 100 | 110 | 111 | 112 | -------------------------------------------------------------------------------- /docs/icon.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/icon.svg.png -------------------------------------------------------------------------------- /docs/img/CloudFront-static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/CloudFront-static.png -------------------------------------------------------------------------------- /docs/img/CloudFront-tiles-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/CloudFront-tiles-simple.png -------------------------------------------------------------------------------- /docs/img/CloudFront-tiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/CloudFront-tiles.png -------------------------------------------------------------------------------- /docs/img/CodeBuild-Docker.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/CodeBuild-Docker.png -------------------------------------------------------------------------------- /docs/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/architecture.png -------------------------------------------------------------------------------- /docs/img/benchmark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/benchmark.png -------------------------------------------------------------------------------- /docs/img/map_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/map_screenshot.png -------------------------------------------------------------------------------- /docs/img/map_screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/map_screenshot2.png -------------------------------------------------------------------------------- /docs/img/stack-with-timing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/stack-with-timing.png -------------------------------------------------------------------------------- /docs/img/stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/img/stack.png -------------------------------------------------------------------------------- /docs/stack.odg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/docs/stack.odg -------------------------------------------------------------------------------- /html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/html/favicon.ico -------------------------------------------------------------------------------- /html/gpx.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 15 | 17 | 18 | 20 | image/svg+xml 21 | 23 | 24 | 25 | 26 | 27 | 30 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /html/mountain.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /html/serve.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | python3 -m http.server 8082 -------------------------------------------------------------------------------- /html/sprites/cyclemap.json: -------------------------------------------------------------------------------- 1 | {"wetland":{"width":32,"height":32,"x":0,"y":0,"pixelRatio":1},"church":{"width":16,"height":26,"x":32,"y":0,"pixelRatio":1},"shelter":{"width":17,"height":26,"x":0,"y":32,"pixelRatio":1},"shield-3.blue":{"width":39,"height":26,"x":17,"y":32,"pixelRatio":1},"shield-4.blue":{"width":46,"height":26,"x":56,"y":32,"pixelRatio":1},"shield-5.blue":{"width":54,"height":26,"x":48,"y":0,"pixelRatio":1},"tower":{"width":16,"height":26,"x":102,"y":32,"pixelRatio":1},"viewpoint":{"width":29,"height":26,"x":0,"y":58,"pixelRatio":1},"bar":{"width":24,"height":24,"x":29,"y":58,"pixelRatio":1},"bicycle":{"width":24,"height":24,"x":53,"y":58,"pixelRatio":1},"camp_site":{"width":24,"height":24,"x":77,"y":58,"pixelRatio":1},"caravan_site":{"width":24,"height":24,"x":101,"y":58,"pixelRatio":1},"hotel":{"width":24,"height":24,"x":102,"y":0,"pixelRatio":1},"no-access":{"width":24,"height":24,"x":0,"y":84,"pixelRatio":1},"pub":{"width":24,"height":24,"x":24,"y":84,"pixelRatio":1},"restaurant":{"width":24,"height":24,"x":48,"y":84,"pixelRatio":1},"shop2":{"width":24,"height":24,"x":72,"y":84,"pixelRatio":1},"supermarket":{"width":24,"height":24,"x":96,"y":84,"pixelRatio":1},"shield-3.yellow":{"width":39,"height":22,"x":120,"y":84,"pixelRatio":1},"shield-4.yellow":{"width":48,"height":22,"x":159,"y":84,"pixelRatio":1},"shield-5.yellow":{"width":57,"height":22,"x":118,"y":32,"pixelRatio":1},"hospital":{"width":19,"height":20,"x":207,"y":84,"pixelRatio":1},"information":{"width":13,"height":19,"x":226,"y":84,"pixelRatio":1},"bus_stop":{"width":18,"height":18,"x":175,"y":32,"pixelRatio":1},"climbing":{"width":18,"height":18,"x":193,"y":32,"pixelRatio":1},"miniature_golf":{"width":18,"height":18,"x":211,"y":32,"pixelRatio":1},"parking":{"width":18,"height":18,"x":229,"y":32,"pixelRatio":1},"parking.garage":{"width":18,"height":18,"x":125,"y":58,"pixelRatio":1},"playground":{"width":18,"height":18,"x":143,"y":58,"pixelRatio":1},"station.red":{"width":18,"height":18,"x":161,"y":58,"pixelRatio":1},"subway":{"width":18,"height":18,"x":179,"y":58,"pixelRatio":1},"circle-black":{"width":15,"height":15,"x":239,"y":84,"pixelRatio":1},"circle-blue":{"width":15,"height":15,"x":197,"y":58,"pixelRatio":1},"circle-red":{"width":15,"height":15,"x":212,"y":58,"pixelRatio":1},"nautilid":{"width":12,"height":12,"x":227,"y":58,"pixelRatio":1},"peak":{"width":12,"height":12,"x":239,"y":58,"pixelRatio":1},"roundedsquare":{"width":12,"height":12,"x":126,"y":0,"pixelRatio":1},"shop":{"width":12,"height":12,"x":138,"y":0,"pixelRatio":1},"shop1":{"width":12,"height":12,"x":150,"y":0,"pixelRatio":1},"square":{"width":12,"height":12,"x":162,"y":0,"pixelRatio":1},"square.white":{"width":12,"height":12,"x":174,"y":0,"pixelRatio":1},"star.white":{"width":12,"height":12,"x":186,"y":0,"pixelRatio":1},"station.blue":{"width":12,"height":12,"x":198,"y":0,"pixelRatio":1},"stripes":{"width":12,"height":12,"x":210,"y":0,"pixelRatio":1},"zoo":{"width":12,"height":12,"x":222,"y":0,"pixelRatio":1},"zoo2":{"width":12,"height":12,"x":234,"y":0,"pixelRatio":1},"ring-black":{"width":8,"height":8,"x":247,"y":32,"pixelRatio":1}} -------------------------------------------------------------------------------- /html/sprites/cyclemap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/html/sprites/cyclemap.png -------------------------------------------------------------------------------- /html/sprites/cyclemap@2x.json: -------------------------------------------------------------------------------- 1 | {"wetland":{"width":64,"height":64,"x":0,"y":0,"pixelRatio":2},"church":{"width":33,"height":53,"x":64,"y":0,"pixelRatio":2},"tower":{"width":33,"height":53,"x":0,"y":64,"pixelRatio":2},"shelter":{"width":35,"height":52,"x":33,"y":64,"pixelRatio":2},"shield-3.blue":{"width":79,"height":52,"x":68,"y":64,"pixelRatio":2},"shield-4.blue":{"width":92,"height":52,"x":147,"y":64,"pixelRatio":2},"shield-5.blue":{"width":108,"height":52,"x":97,"y":0,"pixelRatio":2},"viewpoint":{"width":58,"height":52,"x":0,"y":117,"pixelRatio":2},"bar":{"width":48,"height":48,"x":58,"y":117,"pixelRatio":2},"bicycle":{"width":48,"height":48,"x":106,"y":117,"pixelRatio":2},"camp_site":{"width":48,"height":48,"x":154,"y":117,"pixelRatio":2},"caravan_site":{"width":48,"height":48,"x":202,"y":117,"pixelRatio":2},"hotel":{"width":48,"height":48,"x":205,"y":0,"pixelRatio":2},"no-access":{"width":48,"height":48,"x":0,"y":169,"pixelRatio":2},"pub":{"width":48,"height":48,"x":48,"y":169,"pixelRatio":2},"restaurant":{"width":48,"height":48,"x":96,"y":169,"pixelRatio":2},"shop2":{"width":48,"height":48,"x":144,"y":169,"pixelRatio":2},"supermarket":{"width":48,"height":48,"x":192,"y":169,"pixelRatio":2},"shield-3.yellow":{"width":79,"height":44,"x":240,"y":169,"pixelRatio":2},"shield-4.yellow":{"width":96,"height":44,"x":319,"y":169,"pixelRatio":2},"shield-5.yellow":{"width":114,"height":44,"x":250,"y":117,"pixelRatio":2},"hospital":{"width":39,"height":40,"x":415,"y":169,"pixelRatio":2},"information":{"width":26,"height":39,"x":454,"y":169,"pixelRatio":2},"bus_stop":{"width":36,"height":36,"x":364,"y":117,"pixelRatio":2},"climbing":{"width":36,"height":36,"x":400,"y":117,"pixelRatio":2},"miniature_golf":{"width":36,"height":36,"x":436,"y":117,"pixelRatio":2},"parking":{"width":36,"height":36,"x":472,"y":117,"pixelRatio":2},"parking.garage":{"width":36,"height":36,"x":239,"y":64,"pixelRatio":2},"playground":{"width":36,"height":36,"x":275,"y":64,"pixelRatio":2},"station.red":{"width":36,"height":36,"x":311,"y":64,"pixelRatio":2},"subway":{"width":36,"height":36,"x":347,"y":64,"pixelRatio":2},"circle-black":{"width":30,"height":30,"x":480,"y":169,"pixelRatio":2},"circle-blue":{"width":30,"height":30,"x":383,"y":64,"pixelRatio":2},"circle-red":{"width":30,"height":30,"x":413,"y":64,"pixelRatio":2},"nautilid":{"width":24,"height":24,"x":443,"y":64,"pixelRatio":2},"peak":{"width":24,"height":24,"x":467,"y":64,"pixelRatio":2},"roundedsquare":{"width":24,"height":24,"x":253,"y":0,"pixelRatio":2},"shop":{"width":24,"height":24,"x":277,"y":0,"pixelRatio":2},"shop1":{"width":24,"height":24,"x":301,"y":0,"pixelRatio":2},"square":{"width":24,"height":24,"x":325,"y":0,"pixelRatio":2},"square.white":{"width":24,"height":24,"x":349,"y":0,"pixelRatio":2},"star.white":{"width":24,"height":24,"x":373,"y":0,"pixelRatio":2},"station.blue":{"width":24,"height":24,"x":397,"y":0,"pixelRatio":2},"stripes":{"width":24,"height":24,"x":421,"y":0,"pixelRatio":2},"zoo":{"width":24,"height":24,"x":445,"y":0,"pixelRatio":2},"zoo2":{"width":24,"height":24,"x":469,"y":0,"pixelRatio":2},"ring-black":{"width":16,"height":16,"x":491,"y":64,"pixelRatio":2}} -------------------------------------------------------------------------------- /html/sprites/cyclemap@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/html/sprites/cyclemap@2x.png -------------------------------------------------------------------------------- /html/sprites/cyclemap@4x.json: -------------------------------------------------------------------------------- 1 | {"wetland":{"width":128,"height":128,"x":0,"y":0,"pixelRatio":4},"church":{"width":67,"height":106,"x":128,"y":0,"pixelRatio":4},"tower":{"width":67,"height":106,"x":0,"y":128,"pixelRatio":4},"shelter":{"width":71,"height":105,"x":67,"y":128,"pixelRatio":4},"shield-3.blue":{"width":159,"height":104,"x":138,"y":128,"pixelRatio":4},"shield-4.blue":{"width":184,"height":104,"x":297,"y":128,"pixelRatio":4},"shield-5.blue":{"width":216,"height":104,"x":195,"y":0,"pixelRatio":4},"viewpoint":{"width":116,"height":104,"x":0,"y":234,"pixelRatio":4},"bar":{"width":96,"height":96,"x":116,"y":234,"pixelRatio":4},"bicycle":{"width":96,"height":96,"x":212,"y":234,"pixelRatio":4},"camp_site":{"width":96,"height":96,"x":308,"y":234,"pixelRatio":4},"caravan_site":{"width":96,"height":96,"x":404,"y":234,"pixelRatio":4},"hotel":{"width":96,"height":96,"x":411,"y":0,"pixelRatio":4},"no-access":{"width":96,"height":96,"x":0,"y":338,"pixelRatio":4},"pub":{"width":96,"height":96,"x":96,"y":338,"pixelRatio":4},"restaurant":{"width":96,"height":96,"x":192,"y":338,"pixelRatio":4},"shop2":{"width":96,"height":96,"x":288,"y":338,"pixelRatio":4},"supermarket":{"width":96,"height":96,"x":384,"y":338,"pixelRatio":4},"shield-3.yellow":{"width":158,"height":88,"x":480,"y":338,"pixelRatio":4},"shield-4.yellow":{"width":193,"height":88,"x":638,"y":338,"pixelRatio":4},"shield-5.yellow":{"width":228,"height":88,"x":500,"y":234,"pixelRatio":4},"hospital":{"width":79,"height":80,"x":831,"y":338,"pixelRatio":4},"information":{"width":52,"height":79,"x":910,"y":338,"pixelRatio":4},"bus_stop":{"width":72,"height":72,"x":728,"y":234,"pixelRatio":4},"climbing":{"width":72,"height":72,"x":800,"y":234,"pixelRatio":4},"miniature_golf":{"width":72,"height":72,"x":872,"y":234,"pixelRatio":4},"parking":{"width":72,"height":72,"x":944,"y":234,"pixelRatio":4},"parking.garage":{"width":72,"height":72,"x":481,"y":128,"pixelRatio":4},"playground":{"width":72,"height":72,"x":553,"y":128,"pixelRatio":4},"station.red":{"width":72,"height":72,"x":625,"y":128,"pixelRatio":4},"subway":{"width":72,"height":72,"x":697,"y":128,"pixelRatio":4},"circle-black":{"width":60,"height":60,"x":962,"y":338,"pixelRatio":4},"circle-blue":{"width":60,"height":60,"x":769,"y":128,"pixelRatio":4},"circle-red":{"width":60,"height":60,"x":829,"y":128,"pixelRatio":4},"nautilid":{"width":48,"height":48,"x":889,"y":128,"pixelRatio":4},"peak":{"width":48,"height":48,"x":937,"y":128,"pixelRatio":4},"roundedsquare":{"width":48,"height":48,"x":507,"y":0,"pixelRatio":4},"shop":{"width":48,"height":48,"x":555,"y":0,"pixelRatio":4},"shop1":{"width":48,"height":48,"x":603,"y":0,"pixelRatio":4},"square":{"width":48,"height":48,"x":651,"y":0,"pixelRatio":4},"square.white":{"width":48,"height":48,"x":699,"y":0,"pixelRatio":4},"star.white":{"width":48,"height":48,"x":747,"y":0,"pixelRatio":4},"station.blue":{"width":48,"height":48,"x":795,"y":0,"pixelRatio":4},"stripes":{"width":48,"height":48,"x":843,"y":0,"pixelRatio":4},"zoo":{"width":48,"height":48,"x":891,"y":0,"pixelRatio":4},"zoo2":{"width":48,"height":48,"x":939,"y":0,"pixelRatio":4},"ring-black":{"width":32,"height":32,"x":985,"y":128,"pixelRatio":4}} -------------------------------------------------------------------------------- /html/sprites/cyclemap@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/henrythasler/cloud-tileserver/4adbb0eabfbccec16873cbf932f4829df09d07e4/html/sprites/cyclemap@4x.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | coverageReporters: ["json", "lcov", "text", "clover"], 5 | testPathIgnorePatterns: ["/dist/", "/node_modules/"] 6 | }; -------------------------------------------------------------------------------- /local/local.ts: -------------------------------------------------------------------------------- 1 | import { Tileserver, Config } from "../src/tileserver"; 2 | import * as http from "http" 3 | import { readFileSync } from "fs"; 4 | import { parse } from "@iarna/toml"; 5 | 6 | const fixturesPath = "src/"; 7 | 8 | const config = parse(readFileSync(`${fixturesPath}sources.toml`, "utf8")) as unknown as Config; 9 | 10 | const gzip = process.env.GZIP?process.env.GZIP!=="false":true; 11 | const logLevel = (process.env.LOG_LEVEL)?parseInt(process.env.LOG_LEVEL):2; 12 | const tileserver = new Tileserver(config, "", logLevel, gzip); 13 | 14 | // docker run --rm -ti -p 5432:5432 -v /media/mapdata/pgdata_mvt:/pgdata -v $(pwd)/postgis.conf:/etc/postgresql/postgresql.conf -e PGDATA=/pgdata img-postgis:0.9 -c 'config_file=/etc/postgresql/postgresql.conf' 15 | process.env.PGPASSWORD = ""; 16 | process.env.PGUSER = "postgres"; 17 | 18 | async function listener(req: http.IncomingMessage, res: http.ServerResponse): Promise { 19 | let path = req.url?req.url:"/"; 20 | let vectortile = await tileserver.getVectortile(path); 21 | if ((vectortile.res >= 0) && (vectortile.data)) { 22 | res.writeHead(200, { 23 | 'Content-Type': 'application/vnd.mapbox-vector-tile', 24 | 'Content-Encoding': (gzip) ? "gzip" : "identity", 25 | 'Content-Length' : `${vectortile.data.byteLength}`, 26 | 'access-control-allow-origin': '*' 27 | }); 28 | res.end(vectortile.data); 29 | } 30 | else { 31 | res.writeHead(500, { 'Content-Type': 'text/html' }); 32 | res.end(JSON.stringify(vectortile)); 33 | } 34 | } 35 | 36 | const webserver = http.createServer(); 37 | webserver.on('request', listener); 38 | webserver.listen(8000); 39 | console.log(`(Nodejs ${process.version}) awaiting connections...`); 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloud-tileserver", 3 | "version": "1.3.1", 4 | "description": "AWS lambda function to handle vectortile queries via REST", 5 | "main": "index.js", 6 | "scripts": { 7 | "tools": "tsc tools/toml2json.ts", 8 | "gen:sources": "node tools/toml2json.js < src/sources.toml > src/sources.json", 9 | "gen:sprites": "node tools/gensprites.js && mv sprites/cyclemap* html/sprites", 10 | "test": "LOG_LEVEL=3 jest --coverage && coveralls < coverage/lcov.info", 11 | "test:solo": "LOG_LEVEL=1 jest --coverage", 12 | "test:single": "LOG_LEVEL=1 jest --coverage vectortile.test.ts", 13 | "layer": "cd tileserver_layer/nodejs && npm i", 14 | "html": "aws s3 cp ./html/ s3://cyclemap.link/ --recursive", 15 | "docs": "node node_modules/typedoc/bin/typedoc", 16 | "sim": "npm run predeploy && LOG_LEVEL=5 PGUSER=postgres PGHOST=127.0.0.1 PGPORT=5432 node node_modules/lambda-local/build/cli.js -l dist/index.js -h handler -e test/fixtures/local_14_8691_5677.js", 17 | "local": "tsc local/local.ts && LOG_LEVEL=3 node local/local.js", 18 | "predeploy": "rm -rf ./dist/* && npm run gen:sources && tsc && cp ./src/sources.json ./dist && zip -j ./dist/function.zip ./dist/*.js* && cd tileserver_layer/nodejs && npm i && cd .. && zip -qr ../dist/tileserver_layer.zip nodejs", 19 | "deploy": "aws lambda update-function-code --function-name tileserver --zip-file fileb://dist/function.zip" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/henrythasler/cloud-tileserver.git" 24 | }, 25 | "keywords": [ 26 | "vectortiles", 27 | "postgis", 28 | "lambda", 29 | "mapbox", 30 | "mvt", 31 | "terraform", 32 | "aws", 33 | "cloud" 34 | ], 35 | "author": "Henry Thasler", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/henrythasler/cloud-tileserver/issues" 39 | }, 40 | "homepage": "https://github.com/henrythasler/cloud-tileserver#readme", 41 | "devDependencies": { 42 | "@iarna/toml": "^2.2.5", 43 | "@mapbox/spritezero": "^8.0.3", 44 | "@types/aws-lambda": "^8.10.119", 45 | "@types/chai": "^4.3.5", 46 | "@types/jest": "^29.5.3", 47 | "@types/node": "^14.18.54", 48 | "@types/pg": "^8.10.2", 49 | "chai": "^4.3.7", 50 | "coveralls": "^3.1.1", 51 | "jest": "^29.6.2", 52 | "lambda-local": "^2.1.1", 53 | "ts-jest": "^29.1.1", 54 | "typedoc": "^0.24.8", 55 | "typescript": "^5.1.6" 56 | }, 57 | "dependencies": { 58 | "@aws-sdk/client-s3": "^3.395.0", 59 | "aws-lambda": "^1.0.7", 60 | "pg": "^8.11.3" 61 | }, 62 | "np": { 63 | "publish": false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=henrythasler_cloud-tileserver 2 | sonar.sources=src 3 | sonar.typescript.lcov.reportPaths=coverage/lcov.info -------------------------------------------------------------------------------- /sprites/bus_stop.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 41 | 50 | 51 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 73 | 78 | 81 | 86 | 91 | 96 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /sprites/camp_site.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 75 | 78 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /sprites/church.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 57 | 62 | 67 | 72 | 77 | 82 | 87 | 88 | -------------------------------------------------------------------------------- /sprites/circle-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 76 | 79 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /sprites/circle-blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 76 | 79 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /sprites/circle-red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 76 | 79 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /sprites/climbing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 75 | 78 | 84 | 85 | 86 | -------------------------------------------------------------------------------- /sprites/hospital.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 47 | 56 | 57 | 59 | 60 | 62 | image/svg+xml 63 | 65 | 66 | 67 | 68 | 69 | 74 | 77 | 86 | 96 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /sprites/information.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 64 | 69 | 74 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /sprites/miniature_golf.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 46 | 55 | 56 | 58 | 59 | 61 | image/svg+xml 62 | 64 | 65 | 66 | 67 | 72 | 77 | 80 | 86 | 93 | 98 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /sprites/parking.garage.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 75 | 80 | 84 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /sprites/parking.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 75 | 80 | 83 | 84 | 89 | 90 | -------------------------------------------------------------------------------- /sprites/peak.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 42 | 44 | 45 | 47 | image/svg+xml 48 | 50 | 51 | 52 | 53 | 54 | 59 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /sprites/playground.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 75 | 78 | 82 | 87 | 94 | 95 | 101 | 102 | 103 | -------------------------------------------------------------------------------- /sprites/ring-black.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 76 | 79 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /sprites/roundedsquare.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /sprites/shelter.svg: -------------------------------------------------------------------------------- 1 | 2 | 16 | 18 | 42 | 53 | 54 | 56 | 57 | 59 | image/svg+xml 60 | 62 | 63 | 64 | 65 | 66 | 71 | 76 | 82 | 88 | 97 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /sprites/shield-3.blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 49 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 70 | 79 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /sprites/shield-3.yellow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 49 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /sprites/shield-4.blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 49 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 70 | 79 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /sprites/shield-4.yellow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 49 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /sprites/shield-5.blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 49 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 70 | 79 | 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /sprites/shield-5.yellow.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 44 | 49 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /sprites/square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /sprites/square.white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 76 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /sprites/star.white.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 71 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /sprites/station.blue.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 50 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 73 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /sprites/station.red.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 75 | 80 | 81 | 82 | -------------------------------------------------------------------------------- /sprites/stripes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | 23 | 26 | 30 | 31 | 34 | 38 | 39 | 42 | 46 | 47 | 48 | 71 | 78 | 79 | 81 | 82 | 84 | image/svg+xml 85 | 87 | 88 | 89 | 90 | 91 | 96 | 99 | 105 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /sprites/subway.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 43 | 52 | 53 | 55 | 56 | 58 | image/svg+xml 59 | 61 | 62 | 63 | 64 | 65 | 70 | 78 | 84 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /sprites/tower.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 20 | 21 | 23 | image/svg+xml 24 | 26 | 27 | 28 | 29 | 30 | 32 | 57 | 62 | 67 | 72 | 77 | 78 | -------------------------------------------------------------------------------- /sprites/zoo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 41 | 48 | 49 | 51 | 53 | 54 | 56 | image/svg+xml 57 | 59 | 60 | 61 | 62 | 63 | 68 | 73 | 76 | 81 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /sprites/zoo2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 41 | 48 | 49 | 51 | 61 | 66 | 71 | 77 | 81 | 82 | 83 | 85 | 86 | 88 | image/svg+xml 89 | 91 | 92 | 93 | 94 | 95 | 100 | 105 | 108 | 113 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { Handler, Context } from "aws-lambda"; 2 | import { Tileserver, Vectortile } from "./tileserver"; 3 | import configJSON from "./sources.json"; 4 | 5 | 6 | const cacheBucketName = process.env.CACHE_BUCKET || ""; 7 | const gzip = process.env.GZIP?process.env.GZIP!=="false":true; 8 | const logLevel = (process.env.LOG_LEVEL)?parseInt(process.env.LOG_LEVEL):undefined; 9 | const tileserver: Tileserver = new Tileserver(configJSON, cacheBucketName, logLevel, gzip); 10 | 11 | interface Event { 12 | path?: string // used by API-Gateway 13 | rawPath?: string // used by Lambda function URLs 14 | } 15 | 16 | export const handler: Handler = async (event: Event, context: Context): Promise => { 17 | let response; 18 | const vectortile: Vectortile = await tileserver.getVectortile(event.path ?? event.rawPath ?? ""); 19 | if ((vectortile.res >= 0) && (vectortile.data)) { 20 | response = { 21 | statusCode: 200, 22 | headers: { 23 | 'Content-Type': 'application/vnd.mapbox-vector-tile', 24 | 'Content-Encoding': (gzip) ? "gzip" : "identity", 25 | 'access-control-allow-origin': '*' 26 | }, 27 | body: vectortile.data.toString('base64'), 28 | isBase64Encoded: true 29 | } 30 | } 31 | else { 32 | response = { 33 | statusCode: 500, 34 | headers: { 35 | 'Content-Type': 'text/html', 36 | 'access-control-allow-origin': '*', 37 | 'Content-Encoding': 'identity' 38 | }, 39 | body: JSON.stringify(vectortile), 40 | isBase64Encoded: false 41 | } 42 | } 43 | return Promise.resolve(response) 44 | } 45 | -------------------------------------------------------------------------------- /src/projection.ts: -------------------------------------------------------------------------------- 1 | export interface Wgs84 { 2 | /** in degrees */ 3 | lng: number, 4 | /** in degrees */ 5 | lat: number 6 | } 7 | 8 | export interface Mercator { 9 | /** in meters */ 10 | x: number, 11 | /** in meters */ 12 | y: number 13 | } 14 | 15 | export interface Vector { 16 | x: number, 17 | y: number 18 | } 19 | 20 | export interface Tile { 21 | z: number, 22 | x: number, 23 | y: number 24 | } 25 | 26 | export interface TileList extends Array { } 27 | 28 | export interface WGS84BoundingBox { 29 | leftbottom: Wgs84, 30 | righttop: Wgs84 31 | } 32 | 33 | export interface MercatorBoundingBox { 34 | leftbottom: Mercator, 35 | righttop: Mercator 36 | } 37 | 38 | export class Projection { 39 | protected originShift = 2 * Math.PI * 6378137 / 2.0; 40 | 41 | /** Converts XY point from Pseudo-Mercator (https://epsg.io/3857) to WGS84 (https://epsg.io/4326) */ 42 | getWGS84FromMercator(pos: Mercator): Wgs84 { 43 | const lon = (pos.x / this.originShift) * 180.0; 44 | let lat = (pos.y / this.originShift) * 180.0; 45 | lat = 180 / Math.PI * (2 * Math.atan(Math.exp(lat * Math.PI / 180.0)) - Math.PI / 2.0) 46 | return ({ lng: lon % 360, lat: lat % 180 } as Wgs84) 47 | } 48 | 49 | /** Converts pixel coordinates (Origin is top-left) in given zoom level of pyramid to EPSG:900913 */ 50 | getMercatorFromPixels(pos: Vector, zoom: number, tileSize = 256): Mercator { 51 | // zoom = Math.max(0, zoom + 1 - tileSize / 256) 52 | const res = 2 * Math.PI * 6378137 / tileSize / Math.pow(2, zoom); 53 | return ({ x: pos.x * res - this.originShift, y: this.originShift - pos.y * res } as Mercator) 54 | } 55 | 56 | /** Returns bounds of the given tile in Pseudo-Mercator (https://epsg.io/3857) coordinates */ 57 | getMercatorTileBounds(tile: Tile, tileSize = 256): MercatorBoundingBox { 58 | const leftbottom = this.getMercatorFromPixels({ x: tile.x * tileSize, y: (tile.y + 1) * tileSize } as Vector, tile.z, tileSize); 59 | const righttop = this.getMercatorFromPixels({ x: (tile.x + 1) * tileSize, y: tile.y * tileSize } as Vector, tile.z, tileSize); 60 | return ({ leftbottom, righttop } as MercatorBoundingBox) 61 | } 62 | 63 | /** Returns bounds of the given tile in WGS84 (https://epsg.io/4326) coordinates */ 64 | getWGS84TileBounds(tile: Tile, tileSize = 256): WGS84BoundingBox { 65 | const bounds: MercatorBoundingBox = this.getMercatorTileBounds(tile, tileSize); 66 | return ({ 67 | leftbottom: this.getWGS84FromMercator(bounds.leftbottom), 68 | righttop: this.getWGS84FromMercator(bounds.righttop) 69 | } as WGS84BoundingBox) 70 | } 71 | 72 | /** Returns center of the given tile in WGS84 (https://epsg.io/4326) coordinates */ 73 | getWGS84TileCenter(tile: Tile, tileSize = 256): Wgs84 { 74 | const bounds: WGS84BoundingBox = this.getWGS84TileBounds(tile, tileSize); 75 | return ({ 76 | lng: (bounds.righttop.lng + bounds.leftbottom.lng) / 2, 77 | lat: (bounds.righttop.lat + bounds.leftbottom.lat) / 2, 78 | } as Wgs84) 79 | } 80 | 81 | /** Return a list of zxy-Tilecoordinates `depth`-levels below the given tile 82 | * @param tile Top-level tile to start the pyramid; will also be part of the return value 83 | * @param depth How many levels the resulting pyramid will have. 84 | * @return An array of tiles 85 | */ 86 | getTilePyramid(tile: Tile, depth = 1): TileList { 87 | const list: TileList = []; 88 | depth = Math.max(0, depth); // do not allow negative values 89 | for (let zoom = 0; zoom <= depth; zoom++) { 90 | for (let y = tile.y * 2 ** zoom; y < (tile.y + 1) * 2 ** zoom; y++) { 91 | for (let x = tile.x * 2 ** zoom; x < (tile.x + 1) * 2 ** zoom; x++) { 92 | list.push({ 93 | x, 94 | y, 95 | z: tile.z + zoom 96 | } as Tile) 97 | } 98 | } 99 | } 100 | return list 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /terraform/.terraform.lock.hcl: -------------------------------------------------------------------------------- 1 | # This file is maintained automatically by "terraform init". 2 | # Manual edits may be lost in future updates. 3 | 4 | provider "registry.terraform.io/hashicorp/aws" { 5 | version = "5.13.1" 6 | constraints = "~> 5.13" 7 | hashes = [ 8 | "h1:uYJsEJhxGO/dFeK2MsC65wV/NKu7XxnTikh5rbrizBo=", 9 | "zh:0d107e410ecfbd5d2fb5ff9793f88e2ce03ae5b3bda4e3b772b5d146cdd859d8", 10 | "zh:1080cf6a402939ec4ad393380f2ab2dfdc0e175903e08ed796aa22eb95868847", 11 | "zh:300420d642c3ada48cfe633444eafa7bcd410cd6a8503de2384f14ac54dc3ce3", 12 | "zh:4e0121014a8d6ef0b1ab4634877545737bb54e951340f1b67ffea8cd22b2d252", 13 | "zh:59b401bbf95dc8c6bea58085ff286543380f176271251193eac09cb7fcf619b7", 14 | "zh:5dfaf51e979131710ce8e1572e6012564e68c7c842e3d9caaaeb0fe6af15c351", 15 | "zh:84bb75dafca056d7c3783be5185187fdd3294f902e9d72f7655f2efb5e066650", 16 | "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", 17 | "zh:aa4e2b9f699d497041679bc05ca34ac21a5184298eb1813f35455b1883977910", 18 | "zh:b51a4f08d84b071128df68a95cfa5114301a50bf8ab8e655dcf7e384e5bc6458", 19 | "zh:bce284ac6ebb65053d9b703048e3157bf4175782ea9dbaec9f64dc2b6fcefb0c", 20 | "zh:c748f78b79b354794098b06b18a02aefdb49be144dff8876c9204083980f7de0", 21 | "zh:ee69d9aef5ca532392cdc26499096f3fa6e55f8622387f78194ebfaadb796aef", 22 | "zh:ef561bee58e4976474bc056649095737fa3b2bcb74602032415d770bfc620c1f", 23 | "zh:f696d8416c57c31f144d432779ba34764560a95937db3bb3dd2892a791a6d5a7", 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /terraform/main.tf: -------------------------------------------------------------------------------- 1 | provider "aws" { 2 | profile = "default" 3 | region = "${var.region}" 4 | } 5 | 6 | provider "aws" { 7 | profile = "default" 8 | region = "us-east-1" 9 | alias = "us-east-1" 10 | } 11 | 12 | terraform { 13 | required_version = ">= 1.5" 14 | backend "s3" { 15 | bucket = "terraform-state-0000" 16 | key = "cyclemap.link/terraform.tfstate" 17 | region = "eu-central-1" 18 | dynamodb_table = "terraform-state-lock" 19 | encrypt = true 20 | } 21 | 22 | required_providers { 23 | aws = { 24 | source = "hashicorp/aws" 25 | version = "~> 5.13" 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /terraform/terraform-state.tf: -------------------------------------------------------------------------------- 1 | resource "aws_s3_bucket" "terraform_state" { 2 | bucket = "terraform-state-0000" 3 | } 4 | 5 | resource "aws_s3_bucket_versioning" "terraform_state_versioning" { 6 | bucket = aws_s3_bucket.terraform_state.id 7 | versioning_configuration { 8 | status = "Enabled" 9 | } 10 | } 11 | 12 | resource "aws_s3_bucket_server_side_encryption_configuration" "example" { 13 | bucket = aws_s3_bucket.terraform_state.id 14 | rule { 15 | apply_server_side_encryption_by_default { 16 | sse_algorithm = "AES256" 17 | } 18 | } 19 | } 20 | 21 | resource "aws_dynamodb_table" "terraform_locks" { 22 | name = "terraform-state-lock" 23 | billing_mode = "PAY_PER_REQUEST" 24 | hash_key = "LockID" 25 | attribute { 26 | name = "LockID" 27 | type = "S" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /terraform/tileserver-apigateway.tf: -------------------------------------------------------------------------------- 1 | resource "aws_api_gateway_rest_api" "tileserver" { 2 | name = "${var.tileserver_prefix}${var.domain}" 3 | description = "Vectortiles Server" 4 | binary_media_types = [ 5 | "*/*" 6 | ] 7 | minimum_compression_size = 5000 8 | endpoint_configuration { 9 | types = [ 10 | "REGIONAL" 11 | ] 12 | } 13 | } 14 | 15 | resource "aws_api_gateway_resource" "proxy" { 16 | rest_api_id = "${aws_api_gateway_rest_api.tileserver.id}" 17 | parent_id = "${aws_api_gateway_rest_api.tileserver.root_resource_id}" 18 | path_part = "{proxy+}" 19 | } 20 | 21 | resource "aws_api_gateway_method" "proxy" { 22 | rest_api_id = "${aws_api_gateway_rest_api.tileserver.id}" 23 | resource_id = "${aws_api_gateway_resource.proxy.id}" 24 | http_method = "GET" 25 | authorization = "NONE" 26 | } 27 | 28 | resource "aws_api_gateway_integration" "lambda" { 29 | rest_api_id = "${aws_api_gateway_rest_api.tileserver.id}" 30 | resource_id = "${aws_api_gateway_method.proxy.resource_id}" 31 | http_method = "${aws_api_gateway_method.proxy.http_method}" 32 | 33 | integration_http_method = "POST" 34 | type = "AWS_PROXY" 35 | uri = "${aws_lambda_function.tileserver.invoke_arn}" 36 | } 37 | 38 | resource "aws_api_gateway_method" "proxy_root" { 39 | rest_api_id = "${aws_api_gateway_rest_api.tileserver.id}" 40 | resource_id = "${aws_api_gateway_rest_api.tileserver.root_resource_id}" 41 | http_method = "GET" 42 | authorization = "NONE" 43 | } 44 | 45 | resource "aws_api_gateway_integration" "lambda_root" { 46 | rest_api_id = "${aws_api_gateway_rest_api.tileserver.id}" 47 | resource_id = "${aws_api_gateway_method.proxy_root.resource_id}" 48 | http_method = "${aws_api_gateway_method.proxy_root.http_method}" 49 | 50 | integration_http_method = "POST" 51 | type = "AWS_PROXY" 52 | uri = "${aws_lambda_function.tileserver.invoke_arn}" 53 | } 54 | 55 | resource "aws_api_gateway_deployment" "testing" { 56 | depends_on = [ 57 | aws_api_gateway_integration.lambda, 58 | aws_api_gateway_integration.lambda_root, 59 | ] 60 | 61 | rest_api_id = "${aws_api_gateway_rest_api.tileserver.id}" 62 | stage_name = "testing" 63 | } 64 | 65 | 66 | resource "aws_api_gateway_domain_name" "tileserver_domain" { 67 | certificate_arn = "${data.aws_acm_certificate.acm_certificate.arn}" 68 | domain_name = "${aws_api_gateway_rest_api.tileserver.name}" 69 | # security_policy = "TLS_1_2" 70 | endpoint_configuration { 71 | types = ["EDGE"] 72 | } 73 | } 74 | 75 | resource "aws_api_gateway_base_path_mapping" "tileserver_mapping" { 76 | api_id = "${aws_api_gateway_rest_api.tileserver.id}" 77 | stage_name = "${aws_api_gateway_deployment.testing.stage_name}" 78 | domain_name = "${aws_api_gateway_domain_name.tileserver_domain.domain_name}" 79 | } 80 | 81 | # output "base_url" { 82 | # value = "${aws_api_gateway_deployment.testing.invoke_url}" 83 | # } -------------------------------------------------------------------------------- /terraform/tileserver-cert.tf: -------------------------------------------------------------------------------- 1 | data "aws_acm_certificate" "acm_certificate" { 2 | provider = aws.us-east-1 3 | domain = "${var.domain}" 4 | statuses = ["ISSUED"] 5 | } 6 | -------------------------------------------------------------------------------- /terraform/tileserver-cloudfront.tf: -------------------------------------------------------------------------------- 1 | resource "aws_cloudfront_distribution" "website_distribution" { 2 | origin { 3 | domain_name = "${aws_s3_bucket.website.bucket_regional_domain_name}" 4 | origin_id = "S3-Website" 5 | } 6 | 7 | enabled = true 8 | is_ipv6_enabled = true 9 | price_class = "PriceClass_100" 10 | default_root_object = "index.html" 11 | 12 | aliases = ["cyclemap.link", "www.cyclemap.link"] 13 | 14 | default_cache_behavior { 15 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 16 | cached_methods = ["GET", "HEAD"] 17 | target_origin_id = "S3-Website" 18 | 19 | forwarded_values { 20 | query_string = false 21 | 22 | cookies { 23 | forward = "none" 24 | } 25 | } 26 | 27 | viewer_protocol_policy = "redirect-to-https" 28 | compress = true 29 | min_ttl = 0 30 | default_ttl = 86400 31 | max_ttl = 604800 32 | } 33 | 34 | restrictions { 35 | geo_restriction { 36 | restriction_type = "none" 37 | } 38 | } 39 | 40 | viewer_certificate { 41 | acm_certificate_arn = "${data.aws_acm_certificate.acm_certificate.arn}" 42 | minimum_protocol_version = "TLSv1.2_2018" 43 | ssl_support_method = "sni-only" 44 | } 45 | } 46 | 47 | 48 | resource "aws_cloudfront_distribution" "tiles" { 49 | origin { 50 | domain_name = "${aws_s3_bucket_website_configuration.tilecache_config.website_endpoint}" 51 | origin_id = "S3-Tilecache" 52 | custom_origin_config { 53 | http_port = 80 54 | https_port = 443 55 | origin_keepalive_timeout = 5 56 | origin_read_timeout = 30 57 | origin_protocol_policy = "http-only" 58 | origin_ssl_protocols = ["TLSv1.2", "TLSv1.1", "TLSv1"] 59 | } 60 | } 61 | 62 | 63 | enabled = true 64 | is_ipv6_enabled = true 65 | price_class = "PriceClass_100" 66 | 67 | aliases = ["tiles.cyclemap.link"] 68 | 69 | default_cache_behavior { 70 | allowed_methods = ["GET", "HEAD", "OPTIONS"] 71 | cached_methods = ["GET", "HEAD"] 72 | target_origin_id = "S3-Tilecache" 73 | 74 | forwarded_values { 75 | query_string = false 76 | headers = ["origin", "access-control-request-headers", "access-control-request-method"] 77 | 78 | cookies { 79 | forward = "none" 80 | } 81 | } 82 | 83 | viewer_protocol_policy = "redirect-to-https" 84 | min_ttl = 1 85 | default_ttl = 86400 86 | max_ttl = 220752000 87 | } 88 | 89 | restrictions { 90 | geo_restriction { 91 | restriction_type = "none" 92 | } 93 | } 94 | 95 | viewer_certificate { 96 | acm_certificate_arn = "${data.aws_acm_certificate.acm_certificate.arn}" 97 | minimum_protocol_version = "TLSv1.2_2018" 98 | ssl_support_method = "sni-only" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /terraform/tileserver-lambda.tf: -------------------------------------------------------------------------------- 1 | resource "aws_iam_role" "tileserver_role" { 2 | name = "tileserver_role" 3 | 4 | assume_role_policy = < ({ 14 | Client: class { 15 | connect = jest.fn().mockResolvedValue(null) 16 | query = mockQuery 17 | end = jest.fn().mockResolvedValue(null) 18 | } 19 | })); 20 | 21 | const fixturesPath = "test/fixtures/"; 22 | const testOutputPath = "test/out/"; 23 | 24 | describe("getClientConfig", function () { 25 | it("empty config", function () { 26 | let config = parse(readFileSync(`${fixturesPath}simple.toml`, "utf8")) as unknown as Config; 27 | let server = new Tileserver(config, "testBucket"); 28 | let pgconfig: ClientConfig = server.getClientConfig("local"); 29 | expect(pgconfig).to.be.empty; 30 | }); 31 | 32 | it("full database config", function () { 33 | let config = parse(readFileSync(`${fixturesPath}simple_dbconfig.toml`, "utf8")) as unknown as Config; 34 | let server = new Tileserver(config, "testBucket"); 35 | let pgconfig: ClientConfig = server.getClientConfig("local"); 36 | expect(pgconfig).to.deep.equal({ 37 | host: "localhost", 38 | port: 5432, 39 | user: "user", 40 | password: "secret", 41 | database: "local" 42 | }); 43 | }); 44 | 45 | it("source not found", function () { 46 | let config = parse(readFileSync(`${fixturesPath}simple_dbconfig.toml`, "utf8")) as unknown as Config; 47 | let server = new Tileserver(config, "testBucket"); 48 | let pgconfig: ClientConfig = server.getClientConfig("unknown"); 49 | expect(pgconfig).to.be.empty; 50 | }); 51 | 52 | }); 53 | 54 | 55 | describe("fetchTileFromDatabase", function () { 56 | beforeEach(() => { 57 | mockQuery.mockReset(); 58 | }); 59 | 60 | it("regular response", async function () { 61 | mockQuery.mockResolvedValue({ rows: [{ mvt: Buffer.from("data") }, { mvt: Buffer.from("something") }] }) 62 | let config = parse(readFileSync(`${fixturesPath}simple.toml`, "utf8")) as unknown as Config; 63 | let server = new Tileserver(config, "testBucket"); 64 | let pgconfig: ClientConfig = server.getClientConfig("local"); 65 | 66 | let res = await server.fetchTileFromDatabase("SELECT true", pgconfig); 67 | expect(mockQuery.mock.calls.length).to.be.equal(1); 68 | expect(res.toString()).to.equal("data"); 69 | }); 70 | 71 | it("row not found", async function () { 72 | mockQuery.mockResolvedValue({ rows: [{ wrong: Buffer.from("data") }, { mvt: Buffer.from("something") }] }) 73 | let config = parse(readFileSync(`${fixturesPath}simple.toml`, "utf8")) as unknown as Config; 74 | let server = new Tileserver(config, "testBucket"); 75 | let pgconfig: ClientConfig = server.getClientConfig("local"); 76 | 77 | try { 78 | await server.fetchTileFromDatabase("SELECT true", pgconfig) 79 | } catch (e) { 80 | expect(e).to.be.an("Error"); 81 | expect(e).to.have.property("message", "Property \'mvt\' does not exist in res.rows[0]"); 82 | } 83 | expect(mockQuery.mock.calls.length).to.be.equal(1); 84 | }); 85 | }); -------------------------------------------------------------------------------- /test/env.test.ts: -------------------------------------------------------------------------------- 1 | import { Context } from 'aws-lambda'; 2 | import { expect } from "chai"; 3 | 4 | import { gzip } from "zlib"; 5 | import { promisify } from "util"; 6 | const asyncgzip = promisify(gzip); 7 | 8 | import "jest"; 9 | 10 | /** Setup mocks for postgres */ 11 | jest.mock('pg', () => ({ 12 | Client: class { 13 | connect = jest.fn().mockResolvedValue(this); 14 | query = jest.fn().mockResolvedValue({ rows: [{ mvt: Buffer.from("data") }, { mvt: Buffer.from("something") }] }); 15 | end = jest.fn().mockResolvedValue(this); 16 | } 17 | })); 18 | 19 | /** Setup mocks for aws */ 20 | const mockPutObject = jest.fn().mockResolvedValue(null); 21 | jest.mock('@aws-sdk/client-s3', () => { 22 | return { 23 | S3Client: jest.fn(() => ({ 24 | send: mockPutObject 25 | })), 26 | PutObjectCommand: jest.fn(), 27 | }; 28 | }); 29 | 30 | 31 | // fake Context 32 | const ctx: Context = { 33 | callbackWaitsForEmptyEventLoop: true, 34 | functionName: "", 35 | functionVersion: "", 36 | invokedFunctionArn: "", 37 | memoryLimitInMB: "128", 38 | awsRequestId: "", 39 | logGroupName: "", 40 | logStreamName: "", 41 | 42 | getRemainingTimeInMillis: () => { return 3000; }, 43 | done: () => { }, 44 | fail: () => { }, 45 | succeed: () => { } 46 | } 47 | 48 | describe('environmental variables', () => { 49 | const OLD_ENV = process.env; 50 | 51 | beforeEach(() => { 52 | mockPutObject.mockClear() 53 | jest.resetModules() // this is important - it clears the cache 54 | process.env = { ...OLD_ENV }; 55 | delete process.env.LOG_LEVEL; // as this was specified in package.json for test:* 56 | }); 57 | 58 | afterEach(() => { 59 | process.env = OLD_ENV; 60 | }); 61 | 62 | it("no environmental variables set", async function () { 63 | const index = require('../src/index'); 64 | const response = await index.handler({ path: "/local/14/8691/5677.mvt" }, ctx, () => { }); 65 | const gzipped = await asyncgzip("data") as Buffer; 66 | expect(response).to.deep.equal({ 67 | statusCode: 200, 68 | headers: { 69 | 'Content-Type': 'application/vnd.mapbox-vector-tile', 70 | 'Content-Encoding': 'gzip', 71 | 'access-control-allow-origin': '*' 72 | }, 73 | body: gzipped.toString('base64'), 74 | isBase64Encoded: true 75 | }); 76 | expect(mockPutObject.mock.calls.length).to.be.equal(0); 77 | }); 78 | 79 | it("LOG_LEVEL=1", async function () { 80 | process.env.LOG_LEVEL = "1"; 81 | const index = require('../src/index'); 82 | const response = await index.handler({ path: "/local/14/8691/5677.mvt" }, ctx, () => { }); 83 | const gzipped = await asyncgzip("data") as Buffer; 84 | expect(response).to.deep.equal({ 85 | statusCode: 200, 86 | headers: { 87 | 'Content-Type': 'application/vnd.mapbox-vector-tile', 88 | 'Content-Encoding': 'gzip', 89 | 'access-control-allow-origin': '*' 90 | }, 91 | body: gzipped.toString('base64'), 92 | isBase64Encoded: true 93 | }); 94 | expect(mockPutObject.mock.calls.length).to.be.equal(0); 95 | }); 96 | 97 | it("CACHE_BUCKET=sampleBucket", async function () { 98 | process.env.CACHE_BUCKET = "sampleBucket"; 99 | const index = require('../src/index'); 100 | const response = await index.handler({ path: "/local/14/8691/5677.mvt" }, ctx, () => { }); 101 | const gzipped = await asyncgzip("data") as Buffer; 102 | expect(response).to.deep.equal({ 103 | statusCode: 200, 104 | headers: { 105 | 'Content-Type': 'application/vnd.mapbox-vector-tile', 106 | 'Content-Encoding': 'gzip', 107 | 'access-control-allow-origin': '*' 108 | }, 109 | body: gzipped.toString('base64'), 110 | isBase64Encoded: true 111 | }); 112 | expect(mockPutObject.mock.calls.length).to.be.equal(1); 113 | }); 114 | 115 | it("GZIP=false", async function () { 116 | process.env.GZIP = "false"; 117 | const index = require('../src/index'); 118 | const response = await index.handler({ path: "/local/14/8691/5677.mvt" }, ctx, () => { }); 119 | expect(response).to.deep.equal({ 120 | statusCode: 200, 121 | headers: { 122 | 'Content-Type': 'application/vnd.mapbox-vector-tile', 123 | 'Content-Encoding': 'identity', 124 | 'access-control-allow-origin': '*' 125 | }, 126 | body: Buffer.from('data').toString('base64'), 127 | isBase64Encoded: true 128 | }); 129 | expect(mockPutObject.mock.calls.length).to.be.equal(0); 130 | }); 131 | 132 | }); -------------------------------------------------------------------------------- /test/fixtures/duplicate_layername.toml: -------------------------------------------------------------------------------- 1 | [[sources]] 2 | name = "local" 3 | 4 | [[sources.layers]] 5 | name = "landuse" 6 | minzoom = 8 7 | table = "import.landuse_gen8" 8 | 9 | [[sources.layers]] 10 | name = "landuse" 11 | minzoom = 9 12 | table = "import.landuse_gen9" 13 | -------------------------------------------------------------------------------- /test/fixtures/local_14_8691_5677.js: -------------------------------------------------------------------------------- 1 | // Sample event data 2 | module.exports = { 3 | path: "/local/14/8686/5691.mvt", 4 | }; -------------------------------------------------------------------------------- /test/fixtures/simple.toml: -------------------------------------------------------------------------------- 1 | [[sources]] 2 | name = "local" 3 | namespace = "import" 4 | 5 | [[sources.layers]] 6 | name = "landuse" 7 | minzoom = 8 8 | table = "landuse_gen8" 9 | -------------------------------------------------------------------------------- /test/fixtures/simple_dbconfig.toml: -------------------------------------------------------------------------------- 1 | [[sources]] 2 | name = "local" 3 | host = "localhost" 4 | port = 5432 5 | database = "local" 6 | user = "user" 7 | password = "secret" 8 | 9 | [[sources.layers]] 10 | name = "landuse" 11 | minzoom = 8 12 | namespace = "import" 13 | table = "landuse_gen8" 14 | -------------------------------------------------------------------------------- /test/fixtures/simple_z13.sql: -------------------------------------------------------------------------------- 1 | SELECT ( (SELECT ST_AsMVT(q, 'landuse', 4096, 'geom') AS l FROM 2 | (SELECT ST_AsMvtGeom( 3 | geometry, 4 | ST_Transform(ST_MakeEnvelope(!BBOX!, 4326), 3857), 5 | 4096, 6 | 64, 7 | true 8 | ) AS geom 9 | FROM import.landuse_gen8 WHERE (geometry && ST_Transform(ST_MakeEnvelope(!BBOX!, 4326), 3857))) AS q) ) AS mvt -------------------------------------------------------------------------------- /test/logger.test.ts: -------------------------------------------------------------------------------- 1 | import { Log, LogLevels } from "../src/tileserver"; 2 | import "jest"; 3 | 4 | describe("logger", function () { 5 | it("regular constructor", function () { 6 | let log = new Log(LogLevels.INFO); 7 | log.show("log", LogLevels.TRACE); 8 | }); 9 | 10 | it("use default value in constructor", function () { 11 | let log = new Log(); 12 | log.show("log", LogLevels.TRACE); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/parser.test.ts: -------------------------------------------------------------------------------- 1 | import { Tile } from "../src/projection"; 2 | import { Tileserver } from "../src/tileserver"; 3 | import { expect } from "chai"; 4 | 5 | import "jest"; 6 | 7 | const tileserver = new Tileserver({ sources: [] }, "testBucket") 8 | 9 | describe("Parsing functions", function () { 10 | it("extractTile regular #1 - simple path", function () { 11 | let tile: Tile | null = tileserver.extractTile("/local/0/0/0.mvt"); 12 | expect(tile).to.be.an('object'); 13 | expect(tile).to.deep.equal({ "x": 0, "y": 0, "z": 0 }); 14 | }); 15 | it("extractTile regular #2 - complex path", function () { 16 | let tile: Tile | null = tileserver.extractTile("/local/11/1087/714.mvt"); 17 | expect(tile).to.be.an('object'); 18 | expect(tile).to.deep.equal({ "x": 1087, "y": 714, "z": 11 }); 19 | }); 20 | it("extractTile regular #3 - strange path", function () { 21 | let tile: Tile | null = tileserver.extractTile("/local/1337/something/11/1087/714.mvt"); 22 | expect(tile).to.be.an('object'); 23 | expect(tile).to.deep.equal({ "x": 1087, "y": 714, "z": 11 }); 24 | }); 25 | 26 | it("extractTile negative #1 - y-value missing", function () { 27 | let tile: Tile | null = tileserver.extractTile("/local/1087/714.mvt"); 28 | expect(tile).to.be.null; 29 | }); 30 | it("extractTile negative #2 - wrong extension", function () { 31 | let tile: Tile | null = tileserver.extractTile("/local/11/1087/714.pbf"); 32 | expect(tile).to.be.null; 33 | }); 34 | it("extractTile negative #3 - totally useless request", function () { 35 | let tile: Tile | null = tileserver.extractTile("foo"); 36 | expect(tile).to.be.null; 37 | }); 38 | it("extractTile negative #4 - invalid extension", function () { 39 | let tile: Tile | null = tileserver.extractTile("/local/14/8691/5677.mvtinvalid"); 40 | expect(tile).to.be.null; 41 | }); 42 | it("extractTile negative #5 - oversized input", function () { 43 | const longString = '9'.repeat(1024); 44 | let tile: Tile | null = tileserver.extractTile(longString); 45 | expect(tile).to.be.null; 46 | }); 47 | 48 | 49 | it("extractSource regular #1 - simple path", function () { 50 | let source: string | null = tileserver.extractSource("/local/0/0/0.mvt"); 51 | expect(source).to.be.equal('local'); 52 | }); 53 | it("extractSource regular #2 - strange path", function () { 54 | let source: string | null = tileserver.extractSource("/foo/13bar37/global/11/1087/714.mvt/foo2/local/11/1087/714.mvt"); 55 | expect(source).to.be.equal('local'); 56 | }); 57 | 58 | it("extractSource negative #1 - incomplete path", function () { 59 | let source: string | null = tileserver.extractSource("/local/"); 60 | expect(source).to.be.null; 61 | }); 62 | it("extractSource negative #2 - totally useless request", function () { 63 | let source: string | null = tileserver.extractSource("foo"); 64 | expect(source).to.be.null; 65 | }); 66 | it("extractSource negative #3 - input length limit exceeded", function () { 67 | const longString = '9'.repeat(1024); 68 | let source: string | null = tileserver.extractSource(longString); 69 | expect(source).to.be.null; 70 | }); 71 | it("extractSource SQL-Injection #1 - `select now()`", function () { 72 | let source: string | null = tileserver.extractSource("/select+now%28%29/0/0/0.mvt"); 73 | expect(source).to.be.equal('29'); 74 | }); 75 | 76 | }) 77 | -------------------------------------------------------------------------------- /tileserver-openapi.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: cyclemap.link 4 | description: | 5 | generate mapbox vectortiles 6 | contact: {} 7 | version: 1.2.3 8 | 9 | paths: 10 | /local/{zoom}/{x}/{y}.mvt: 11 | parameters: 12 | - name: zoom 13 | in: path 14 | required: true 15 | schema: 16 | type: integer 17 | minimum: 1 18 | maximum: 20 19 | - name: x 20 | in: path 21 | required: true 22 | schema: 23 | type: integer 24 | minimum: 0 25 | - name: y 26 | in: path 27 | required: true 28 | schema: 29 | type: integer 30 | minimum: 0 31 | get: 32 | summary: gets a tile from the local layer 33 | description: cool endpoint 34 | operationId: ewe 35 | tags: 36 | - vectortile 37 | responses: 38 | "200": 39 | description: "OK" 40 | content: 41 | application/vnd.mapbox-vector-tile: 42 | schema: 43 | type: string 44 | format: binary 45 | tags: 46 | - name: vectortile 47 | description: a mapbox vectortile 48 | externalDocs: 49 | url: http://mapbox.github.io/vector-tile-spec/ 50 | servers: 51 | - url: https://4vci3n7djxnwdhltigbeptswua0vzafy.lambda-url.eu-central-1.on.aws 52 | description: Temporary Lambda Function URL for testing purposes 53 | - url: https://tileserver.cyclemap.link 54 | description: Lambda Function Endpoint 55 | -------------------------------------------------------------------------------- /tileserver_layer/nodejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pg_layer", 3 | "version": "1.0.0", 4 | "description": "Layer for tileserver lambda-function", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/henrythasler/cloud-tileserver.git" 12 | }, 13 | "author": "Henry Thasler", 14 | "license": "MIT", 15 | "dependencies": { 16 | "pg": "^8.11.3" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tools/benchmark.sh: -------------------------------------------------------------------------------- 1 | # ENDPOINT=https://4vci3n7djxnwdhltigbeptswua0vzafy.lambda-url.eu-central-1.on.aws/ 2 | ENDPOINT=https://tileserver.cyclemap.link/ 3 | FILE=local/14/8691/5677.mvt 4 | 5 | printf "run;response_code;http_version;size_download;time_namelookup;time_connect;time_appconnect;time_starttransfer;time_total\n" 6 | for i in {1..50}; do 7 | curl -s -w "${i};%{response_code};%{http_version};%{size_download};%{time_namelookup};%{time_connect};%{time_appconnect};%{time_starttransfer};%{time_total}\n" -o /dev/null ${ENDPOINT}${FILE} 8 | done -------------------------------------------------------------------------------- /tools/gensprites.js: -------------------------------------------------------------------------------- 1 | var spritezero = require('@mapbox/spritezero'); 2 | var fs = require('fs'); 3 | var glob = require('glob'); 4 | var path = require('path'); 5 | 6 | var spritePath = "sprites"; 7 | 8 | [1].forEach(function(pxRatio) { 9 | var svgs = glob.sync(path.resolve(path.join(spritePath, '*.svg'))) 10 | .map(function(f) { 11 | return { 12 | svg: fs.readFileSync(f), 13 | id: path.basename(f).replace('.svg', '') 14 | }; 15 | }); 16 | var pngPath = path.resolve(path.join(spritePath, 'cyclemap.png')); 17 | var jsonPath = path.resolve(path.join(spritePath, 'cyclemap.json')); 18 | 19 | // Pass `true` in the layout parameter to generate a data layout 20 | // suitable for exporting to a JSON sprite manifest file. 21 | spritezero.generateLayout({ imgs: svgs, pixelRatio: pxRatio, format: true }, function(err, dataLayout) { 22 | if (err) return; 23 | fs.writeFileSync(jsonPath, JSON.stringify(dataLayout)); 24 | }); 25 | 26 | // Pass `false` in the layout parameter to generate an image layout 27 | // suitable for exporting to a PNG sprite image file. 28 | spritezero.generateLayout({ imgs: svgs, pixelRatio: pxRatio, format: false }, function(err, imageLayout) { 29 | spritezero.generateImage(imageLayout, function(err, image) { 30 | if (err) return; 31 | fs.writeFileSync(pngPath, image); 32 | }); 33 | }); 34 | }); 35 | 36 | [2, 4].forEach(function(pxRatio) { 37 | var svgs = glob.sync(path.resolve(path.join(spritePath, '*.svg'))) 38 | .map(function(f) { 39 | return { 40 | svg: fs.readFileSync(f), 41 | id: path.basename(f).replace('.svg', '') 42 | }; 43 | }); 44 | var pngPath = path.resolve(path.join(spritePath, 'cyclemap@' + pxRatio + 'x.png')); 45 | var jsonPath = path.resolve(path.join(spritePath, 'cyclemap@' + pxRatio + 'x.json')); 46 | 47 | // Pass `true` in the layout parameter to generate a data layout 48 | // suitable for exporting to a JSON sprite manifest file. 49 | spritezero.generateLayout({ imgs: svgs, pixelRatio: pxRatio, format: true }, function(err, dataLayout) { 50 | if (err) return; 51 | fs.writeFileSync(jsonPath, JSON.stringify(dataLayout)); 52 | }); 53 | 54 | // Pass `false` in the layout parameter to generate an image layout 55 | // suitable for exporting to a PNG sprite image file. 56 | spritezero.generateLayout({ imgs: svgs, pixelRatio: pxRatio, format: false }, function(err, imageLayout) { 57 | spritezero.generateImage(imageLayout, function(err, image) { 58 | if (err) return; 59 | fs.writeFileSync(pngPath, image); 60 | }); 61 | }); 62 | }); -------------------------------------------------------------------------------- /tools/toml2json.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | exports.__esModule = true; 3 | var toml_1 = require("@iarna/toml"); 4 | var fs_1 = require("fs"); 5 | console.log(JSON.stringify(toml_1.parse(fs_1.readFileSync("/dev/stdin", "utf8")), null, 2)); 6 | -------------------------------------------------------------------------------- /tools/toml2json.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "@iarna/toml"; 2 | import { readFileSync } from "fs"; 3 | console.log(JSON.stringify(parse(readFileSync("/dev/stdin", "utf8")), null, 2)); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "ES2022", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 5 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 12 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 13 | // "outFile": "./", /* Concatenate and emit output to single file. */ 14 | "outDir": "./dist", /* Redirect output structure to the directory. */ 15 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 16 | // "composite": true, /* Enable project compilation */ 17 | // "incremental": true, /* Enable incremental compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": true, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ 47 | "types": ["node", "jest"], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | 52 | /* Source Map Options */ 53 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 54 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 55 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 56 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 57 | 58 | /* Experimental Options */ 59 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 60 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 61 | "resolveJsonModule": true 62 | }, 63 | "include": [ 64 | "src/**/*.ts" 65 | ] 66 | } 67 | -------------------------------------------------------------------------------- /typedoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "entryPoints": ["src"], 3 | "entryPointStrategy": "expand", 4 | "out": "docs/out", 5 | } --------------------------------------------------------------------------------