diff --git a/README_light.md b/README_light.md index 2e019ff..c969679 100644 --- a/README_light.md +++ b/README_light.md @@ -28,8 +28,8 @@ docker build -t tileserver-gl-light . [Download from OpenMapTiles.com](https://openmaptiles.com/downloads/planet/) or [create](https://github.com/openmaptiles/openmaptiles) your vector tile, and run following in directory contains your *.mbtiles. ``` -docker run --rm -it -v $(pwd):/data -p 8000:80 tileserver-gl-light +docker run --rm -it -v $(pwd):/data -p 8080:8080 tileserver-gl-light ``` ## Documentation -You can read full documentation of this project at https://tileserver.readthedocs.io/. \ No newline at end of file +You can read full documentation of this project at https://maptiler-tileserver.readthedocs.io/ diff --git a/docs/config.rst b/docs/config.rst index 9cd8620..9078d99 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -132,6 +132,19 @@ If you have plenty of memory, try setting these equal to or slightly above your If you need to conserve memory, try lower values for scale factors that are less common. Default is ``[16, 8, 4]``. +``pbfAlias`` +------------------------ + +Some CDNs did not handle .pbf extension as a static file correctly. +The default URLs (with .pbf) are always available, but an alternative can be set. +An example extension suffix would be ".pbf.pict". + +``serveAllFonts`` +------------------------ + +If this option is enabled, all the fonts from the ``paths.fonts`` will be served. +Otherwise only the fonts referenced by available styles will be served. + ``serveAllStyles`` ------------------------ @@ -139,10 +152,16 @@ If this option is enabled, all the styles from the ``paths.styles`` will be serv The process will also watch for changes in this directory and remove/add more styles dynamically. It is recommended to also use the ``serveAllFonts`` option when using this option. +``serveStaticMaps`` +------------------------ + +If this option is enabled, all the static map endpoints will be served. +Default is ``true``. + ``watermark`` ----------- -Optional string to be rendered into the raster tiles (and static maps) as watermark (bottom-left corner). +Optional string to be rendered into the raster tiles and static maps as watermark (bottom-left corner). Not used by default. ``staticAttributionText`` diff --git a/docs/endpoints.rst b/docs/endpoints.rst index cfed7f0..d025e46 100644 --- a/docs/endpoints.rst +++ b/docs/endpoints.rst @@ -52,14 +52,14 @@ Static images * can be provided multiple times - * ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat`` - * ``fill`` - color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``) - * ``stroke`` - color of the path stroke - * ``width`` - width of the stroke - * ``linecap`` - rendering style for the start and end points of the path - * ``linejoin`` - rendering style for overlapping segments of the path with differing directions - * ``border`` - color of the optional border path stroke - * ``borderwidth`` - width of the border stroke (default 10% of width) + * ``latlng`` - indicates coordinates are in ``lat,lng`` order rather than the usual ``lng,lat`` for paths and markers + * ``fill`` - default color to use as the fill (e.g. ``red``, ``rgba(255,255,255,0.5)``, ``#0000ff``) for all paths + * ``stroke`` - default color of the path stroke for all paths + * ``width`` - default width of the stroke for all paths + * ``linecap`` - rendering style for the start and end points of all paths - see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap + * ``linejoin`` - rendering style for joining successive segments of all paths when the direction changes - see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin + * ``border`` - color of the optional border stroke for all paths ; the border is like a halo around the stroke + * ``borderwidth`` - width of the border stroke (default 10% of stroke width) for all paths * ``marker`` - Marker in format ``lng,lat|iconPath|option|option|...`` * Will be rendered with the bottom center at the provided location diff --git a/package-lock.json b/package-lock.json index 5ccad80..a53c851 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "@mapbox/vector-tile": "1.3.1", "@maplibre/maplibre-gl-native": "5.2.0", "@maplibre/maplibre-gl-style-spec": "18.0.0", - "@sindresorhus/fnv1a": "3.0.0", + "@sindresorhus/fnv1a": "3.1.0", "advanced-pool": "0.3.3", "axios": "^1.6.2", "canvas": "2.11.2", @@ -40,12 +40,12 @@ "tileserver-gl": "src/main.js" }, "devDependencies": { - "@commitlint/cli": "^18.4.1", - "@commitlint/config-conventional": "^18.4.0", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", + "@commitlint/cli": "^18.4.3", + "@commitlint/config-conventional": "^18.4.3", + "@typescript-eslint/eslint-plugin": "^6.12.0", + "@typescript-eslint/parser": "^6.12.0", "chai": "4.3.10", - "eslint": "^8.53.0", + "eslint": "^8.54.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jsdoc": "^46.9.0", "eslint-plugin-prettier": "^5.0.1", @@ -207,16 +207,16 @@ } }, "node_modules/@commitlint/cli": { - "version": "18.4.1", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-18.4.1.tgz", - "integrity": "sha512-4+jljfd29Udw9RDDyigavLO9LvdbmB8O9xjDzVZ0R3lJuG7nCeyHgnKWIVpFaN590isZMV/cMeQK0gH7hRF40A==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-18.4.3.tgz", + "integrity": "sha512-zop98yfB3A6NveYAZ3P1Mb6bIXuCeWgnUfVNkH4yhIMQpQfzFwseadazOuSn0OOfTt0lWuFauehpm9GcqM5lww==", "dev": true, "dependencies": { - "@commitlint/format": "^18.4.0", - "@commitlint/lint": "^18.4.0", - "@commitlint/load": "^18.4.1", - "@commitlint/read": "^18.4.0", - "@commitlint/types": "^18.4.0", + "@commitlint/format": "^18.4.3", + "@commitlint/lint": "^18.4.3", + "@commitlint/load": "^18.4.3", + "@commitlint/read": "^18.4.3", + "@commitlint/types": "^18.4.3", "execa": "^5.0.0", "lodash.isfunction": "^3.0.9", "resolve-from": "5.0.0", @@ -231,9 +231,9 @@ } }, "node_modules/@commitlint/config-conventional": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-18.4.0.tgz", - "integrity": "sha512-vArwCZopsZs0FnGsh9AR7uUTPZ5oVGk8+qnEZWq2KTsMjrE0k80b+oZ32GSQmXQT2iMKVrDC8pKX5uKNkCe9Sw==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-18.4.3.tgz", + "integrity": "sha512-729eRRaNta7JZF07qf6SAGSghoDEp9mH7yHU0m7ff0q89W97wDrWCyZ3yoV3mcQJwbhlmVmZPTkPcm7qiAu8WA==", "dev": true, "dependencies": { "conventional-changelog-conventionalcommits": "^7.0.2" @@ -243,12 +243,12 @@ } }, "node_modules/@commitlint/config-validator": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-18.4.0.tgz", - "integrity": "sha512-1y6qHMU3o4cYQSK+Y9EnmH6H1GRiwQGjnLIUOIKlekrmfc8MrMk1ByNmb8od4vK3qHJAaL/77/5n+1uyyIF5dA==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-18.4.3.tgz", + "integrity": "sha512-FPZZmTJBARPCyef9ohRC9EANiQEKSWIdatx5OlgeHKu878dWwpyeFauVkhzuBRJFcCA4Uvz/FDtlDKs008IHcA==", "dev": true, "dependencies": { - "@commitlint/types": "^18.4.0", + "@commitlint/types": "^18.4.3", "ajv": "^8.11.0" }, "engines": { @@ -256,12 +256,12 @@ } }, "node_modules/@commitlint/ensure": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-18.4.0.tgz", - "integrity": "sha512-N5cJo/n61ULSwz3W5Iz/IZJ0I9H/PaHc+OMcF2XcRVbLa6B3YwzEW66XGCRKVULlsBNSrIH6tk5un9ayXAXIdw==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-18.4.3.tgz", + "integrity": "sha512-MI4fwD9TWDVn4plF5+7JUyLLbkOdzIRBmVeNlk4dcGlkrVA+/l5GLcpN66q9LkFsFv6G2X31y89ApA3hqnqIFg==", "dev": true, "dependencies": { - "@commitlint/types": "^18.4.0", + "@commitlint/types": "^18.4.3", "lodash.camelcase": "^4.3.0", "lodash.kebabcase": "^4.1.1", "lodash.snakecase": "^4.1.1", @@ -273,21 +273,21 @@ } }, "node_modules/@commitlint/execute-rule": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-18.4.0.tgz", - "integrity": "sha512-g013SWki6ZWhURBLOSXTaVQGWHdA0QlPJGiW4a+YpThezmJOemvc4LiKVpn13AjSKQ40QnmBqpBrxujOaSo+3A==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-18.4.3.tgz", + "integrity": "sha512-t7FM4c+BdX9WWZCPrrbV5+0SWLgT3kCq7e7/GhHCreYifg3V8qyvO127HF796vyFql75n4TFF+5v1asOOWkV1Q==", "dev": true, "engines": { "node": ">=v18" } }, "node_modules/@commitlint/format": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-18.4.0.tgz", - "integrity": "sha512-MiAe4D5/ahty38CzULdQbpRa3ReKZtx0kyigOWcntq+N5uqez+Ac4/MO7H+3j1kC4G7nfJVfBu6TqcXeyNvhCQ==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-18.4.3.tgz", + "integrity": "sha512-8b+ItXYHxAhRAXFfYki5PpbuMMOmXYuzLxib65z2XTqki59YDQJGpJ/wB1kEE5MQDgSTQWtKUrA8n9zS/1uIDQ==", "dev": true, "dependencies": { - "@commitlint/types": "^18.4.0", + "@commitlint/types": "^18.4.3", "chalk": "^4.1.0" }, "engines": { @@ -295,12 +295,12 @@ } }, "node_modules/@commitlint/is-ignored": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-18.4.0.tgz", - "integrity": "sha512-vyBKBj3Q4N3Xe4ZQcJXW9ef6gVrDL9Fl2HXnnC3F0Qt/F6E4runhJkEuUh5DB3WCXTJUHIJkByKPqrnz4RNrZw==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-18.4.3.tgz", + "integrity": "sha512-ZseOY9UfuAI32h9w342Km4AIaTieeFskm2ZKdrG7r31+c6zGBzuny9KQhwI9puc0J3GkUquEgKJblCl7pMnjwg==", "dev": true, "dependencies": { - "@commitlint/types": "^18.4.0", + "@commitlint/types": "^18.4.3", "semver": "7.5.4" }, "engines": { @@ -308,30 +308,30 @@ } }, "node_modules/@commitlint/lint": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-18.4.0.tgz", - "integrity": "sha512-Wkkf1DPVeLdHYGqtzMBfWoMbUtCojvlzDR89OKVic1rid41iZbb0FzTcwgMYs/1TNWNxoIq9PVVwY7ovLX1aJQ==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-18.4.3.tgz", + "integrity": "sha512-18u3MRgEXNbnYkMOWoncvq6QB8/90m9TbERKgdPqVvS+zQ/MsuRhdvHYCIXGXZxUb0YI4DV2PC4bPneBV/fYuA==", "dev": true, "dependencies": { - "@commitlint/is-ignored": "^18.4.0", - "@commitlint/parse": "^18.4.0", - "@commitlint/rules": "^18.4.0", - "@commitlint/types": "^18.4.0" + "@commitlint/is-ignored": "^18.4.3", + "@commitlint/parse": "^18.4.3", + "@commitlint/rules": "^18.4.3", + "@commitlint/types": "^18.4.3" }, "engines": { "node": ">=v18" } }, "node_modules/@commitlint/load": { - "version": "18.4.1", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-18.4.1.tgz", - "integrity": "sha512-o/plBiPJQgbSq/4ipDpsq4HCmURjBAEjr1EO/p2falr3VhwV0WGXTvb8NlihgI8xtSyO6lHvtycrE535GMLQbA==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-18.4.3.tgz", + "integrity": "sha512-v6j2WhvRQJrcJaj5D+EyES2WKTxPpxENmNpNG3Ww8MZGik3jWRXtph0QTzia5ZJyPh2ib5aC/6BIDymkUUM58Q==", "dev": true, "dependencies": { - "@commitlint/config-validator": "^18.4.0", - "@commitlint/execute-rule": "^18.4.0", - "@commitlint/resolve-extends": "^18.4.0", - "@commitlint/types": "^18.4.0", + "@commitlint/config-validator": "^18.4.3", + "@commitlint/execute-rule": "^18.4.3", + "@commitlint/resolve-extends": "^18.4.3", + "@commitlint/types": "^18.4.3", "@types/node": "^18.11.9", "chalk": "^4.1.0", "cosmiconfig": "^8.3.6", @@ -346,22 +346,22 @@ } }, "node_modules/@commitlint/message": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-18.4.0.tgz", - "integrity": "sha512-3kg6NQO6pJ+VdBTWi51KInT8ngkxPJaW+iI7URtUALjKcO9K4XY3gf80ZPmS1hDessrjb7qCr1lau8eWMINAQw==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-18.4.3.tgz", + "integrity": "sha512-ddJ7AztWUIoEMAXoewx45lKEYEOeOlBVWjk8hDMUGpprkuvWULpaXczqdjwVtjrKT3JhhN+gMs8pm5G3vB2how==", "dev": true, "engines": { "node": ">=v18" } }, "node_modules/@commitlint/parse": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-18.4.0.tgz", - "integrity": "sha512-SxTCSUZH8CJNYWOlFg18YUQ2RLz8ubXKbpHUIiSNwCbiQx7UDCydp1JnhoB4sOYOxgV8d3nuDwYluRU5KnEY4A==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-18.4.3.tgz", + "integrity": "sha512-eoH7CXM9L+/Me96KVcfJ27EIIbA5P9sqw3DqjJhRYuhaULIsPHFs5S5GBDCqT0vKZQDx0DgxhMpW6AQbnKrFtA==", "dev": true, "dependencies": { - "@commitlint/types": "^18.4.0", - "conventional-changelog-angular": "^6.0.0", + "@commitlint/types": "^18.4.3", + "conventional-changelog-angular": "^7.0.0", "conventional-commits-parser": "^5.0.0" }, "engines": { @@ -369,13 +369,13 @@ } }, "node_modules/@commitlint/read": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-18.4.0.tgz", - "integrity": "sha512-IpnABCbDeOw5npZ09SZZGLfd3T7cFtsxUYm6wT3aGmIB2fXKE3fMeuj3jxXjMibiGIyA3Z5voCMuOcKWpkNySA==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-18.4.3.tgz", + "integrity": "sha512-H4HGxaYA6OBCimZAtghL+B+SWu8ep4X7BwgmedmqWZRHxRLcX2q0bWBtUm5FsMbluxbOfrJwOs/Z0ah4roP/GQ==", "dev": true, "dependencies": { - "@commitlint/top-level": "^18.4.0", - "@commitlint/types": "^18.4.0", + "@commitlint/top-level": "^18.4.3", + "@commitlint/types": "^18.4.3", "fs-extra": "^11.0.0", "git-raw-commits": "^2.0.11", "minimist": "^1.2.6" @@ -385,13 +385,13 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-18.4.0.tgz", - "integrity": "sha512-qhgU6ach+S6sJMD9NjCYiEycOObGhxzWQLQzqlScJCv9zkPs15Bg0ffLXTQ3z7ipXv46XEKYMnSJzjLRw2Tlkg==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-18.4.3.tgz", + "integrity": "sha512-30sk04LZWf8+SDgJrbJCjM90gTg2LxsD9cykCFeFu+JFHvBFq5ugzp2eO/DJGylAdVaqxej3c7eTSE64hR/lnw==", "dev": true, "dependencies": { - "@commitlint/config-validator": "^18.4.0", - "@commitlint/types": "^18.4.0", + "@commitlint/config-validator": "^18.4.3", + "@commitlint/types": "^18.4.3", "import-fresh": "^3.0.0", "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0", @@ -402,15 +402,15 @@ } }, "node_modules/@commitlint/rules": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-18.4.0.tgz", - "integrity": "sha512-T3ChRxQZ6g0iNCpVLc6KeQId0/86TnyQA8PFkng+dWElO2DAA5km/yirgKZV1Xlc+gF7Rf6d+a0ottxdKpOY+w==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-18.4.3.tgz", + "integrity": "sha512-8KIeukDf45BiY+Lul1T0imSNXF0sMrlLG6JpLLKolkmYVQ6PxxoNOriwyZ3UTFFpaVbPy0rcITaV7U9JCAfDTA==", "dev": true, "dependencies": { - "@commitlint/ensure": "^18.4.0", - "@commitlint/message": "^18.4.0", - "@commitlint/to-lines": "^18.4.0", - "@commitlint/types": "^18.4.0", + "@commitlint/ensure": "^18.4.3", + "@commitlint/message": "^18.4.3", + "@commitlint/to-lines": "^18.4.3", + "@commitlint/types": "^18.4.3", "execa": "^5.0.0" }, "engines": { @@ -418,18 +418,18 @@ } }, "node_modules/@commitlint/to-lines": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-18.4.0.tgz", - "integrity": "sha512-bZXuCtfBPjNgtEnG3gwJrveIgfKK2UdhIhFvKpMTrQl/gAwoto/3mzmE7qGAHwmuP4eZ2U8X7iwMnqIlWmv2Tw==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-18.4.3.tgz", + "integrity": "sha512-fy1TAleik4Zfru1RJ8ZU6cOSvgSVhUellxd3WZV1D5RwHZETt1sZdcA4mQN2y3VcIZsUNKkW0Mq8CM9/L9harQ==", "dev": true, "engines": { "node": ">=v18" } }, "node_modules/@commitlint/top-level": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-18.4.0.tgz", - "integrity": "sha512-TfulcA8UHF7MZ6tm4Ci3aqZgMBZa1OoCg4prccWHvwG/hsHujZ7+0FKbeKqDbcSli/YWm4NJwEjl4uh5itIJeA==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-18.4.3.tgz", + "integrity": "sha512-E6fJPBLPFL5R8+XUNSYkj4HekIOuGMyJo3mIx2PkYc3clel+pcWQ7TConqXxNWW4x1ugigiIY2RGot55qUq1hw==", "dev": true, "dependencies": { "find-up": "^5.0.0" @@ -439,9 +439,9 @@ } }, "node_modules/@commitlint/types": { - "version": "18.4.0", - "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-18.4.0.tgz", - "integrity": "sha512-MKeaFxt0I9fhqUb2E+YIzX/gZtmkuodJET/XKiZIMvXUff8Ee4Ih86eLg+yAm2jf1pwGBmU02uNOp0y094w2Uw==", + "version": "18.4.3", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-18.4.3.tgz", + "integrity": "sha512-cvzx+vtY/I2hVBZHCLrpoh+sA0hfuzHwDc+BAFPimYLjJkpHnghQM+z8W/KyLGkygJh3BtI3xXXq+dKjnSWEmA==", "dev": true, "dependencies": { "chalk": "^4.1.0" @@ -556,9 +556,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz", - "integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz", + "integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -969,9 +969,9 @@ } }, "node_modules/@sindresorhus/fnv1a": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-3.0.0.tgz", - "integrity": "sha512-M6pmbdZqAryzjZ4ELAzrdCMoMZk5lH/fshKrapfSeXdf2W+GDqZvPmfXaNTZp43//FVbSwkTPwpEMnehSyskkQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/fnv1a/-/fnv1a-3.1.0.tgz", + "integrity": "sha512-KV321z5m/0nuAg83W1dPLy85HpHDk7Sdi4fJbwvacWsEhAh+rZUW4ZfGcXmUIvjZg4ss2bcwNlRhJ7GBEUG08w==", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -1005,9 +1005,9 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==" }, "node_modules/@types/node": { - "version": "18.18.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.9.tgz", - "integrity": "sha512-0f5klcuImLnG4Qreu9hPj/rEfFq6YRc5n2mAjSsH+ec/mJL+3voBH0+8T7o8RpFjH7ovc+TRsL/c7OYIQsPTfQ==", + "version": "18.18.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.11.tgz", + "integrity": "sha512-c1vku6qnTeujJneYH94/4aq73XrVcsJe35UPyAsSok1ijiKrkRzK+AxQPSpNMUnC03roWBBwJx/9I8V7lQoxmA==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -1025,16 +1025,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz", - "integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz", + "integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.5.1", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/type-utils": "6.11.0", - "@typescript-eslint/utils": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.12.0", + "@typescript-eslint/type-utils": "6.12.0", + "@typescript-eslint/utils": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0", "debug": "^4.3.4", "graphemer": "^1.4.0", "ignore": "^5.2.4", @@ -1060,15 +1060,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz", - "integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz", + "integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/scope-manager": "6.12.0", + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0", "debug": "^4.3.4" }, "engines": { @@ -1088,13 +1088,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz", - "integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz", + "integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0" + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0" }, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1105,13 +1105,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz", - "integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz", + "integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "6.11.0", - "@typescript-eslint/utils": "6.11.0", + "@typescript-eslint/typescript-estree": "6.12.0", + "@typescript-eslint/utils": "6.12.0", "debug": "^4.3.4", "ts-api-utils": "^1.0.1" }, @@ -1132,9 +1132,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz", - "integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz", + "integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==", "dev": true, "engines": { "node": "^16.0.0 || >=18.0.0" @@ -1145,13 +1145,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz", - "integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz", + "integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/visitor-keys": "6.11.0", + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/visitor-keys": "6.12.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1172,17 +1172,17 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz", - "integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz", + "integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@types/json-schema": "^7.0.12", "@types/semver": "^7.5.0", - "@typescript-eslint/scope-manager": "6.11.0", - "@typescript-eslint/types": "6.11.0", - "@typescript-eslint/typescript-estree": "6.11.0", + "@typescript-eslint/scope-manager": "6.12.0", + "@typescript-eslint/types": "6.12.0", + "@typescript-eslint/typescript-estree": "6.12.0", "semver": "^7.5.4" }, "engines": { @@ -1197,12 +1197,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz", - "integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz", + "integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "6.11.0", + "@typescript-eslint/types": "6.12.0", "eslint-visitor-keys": "^3.4.1" }, "engines": { @@ -2117,15 +2117,15 @@ } }, "node_modules/conventional-changelog-angular": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-6.0.0.tgz", - "integrity": "sha512-6qLgrBF4gueoC7AFVHu51nHL9pF9FRjXrH+ceVf7WmAfH3gs+gEYOkvxhjMPjZu57I4AGUGoNTY8V7Hrgf1uqg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", "dev": true, "dependencies": { "compare-func": "^2.0.0" }, "engines": { - "node": ">=14" + "node": ">=16" } }, "node_modules/conventional-changelog-conventionalcommits": { @@ -2809,15 +2809,15 @@ } }, "node_modules/eslint": { - "version": "8.53.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz", - "integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==", + "version": "8.54.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz", + "integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.3", - "@eslint/js": "8.53.0", + "@eslint/js": "8.54.0", "@humanwhocodes/config-array": "^0.11.13", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", diff --git a/package.json b/package.json index 37966ed..0127b42 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "bin": "src/main.js", "type": "module", "scripts": { - "test": "mocha test/**.js --timeout 10000", + "test": "mocha test/**.js --timeout 10000 --exit", "lint:yml": "yamllint --schema=CORE_SCHEMA *.{yml,yaml}", "lint:js": "npm run lint:eslint && npm run lint:prettier", "lint:js:fix": "npm run lint:eslint:fix && npm run lint:prettier:fix", @@ -14,7 +14,7 @@ "lint:eslint:fix": "eslint --fix \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs}\" --ignore-path .gitignore", "lint:prettier": "prettier --check \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore", "lint:prettier:fix": "prettier --write \"{,!(node_modules|dist|static|public)/**/}*.{js,ts,cjs,mjs,json}\" --ignore-path .gitignore", - "docker": "docker build -f Dockerfile . && docker run --rm -i -p 8080:8080 $(docker build -q .)", + "docker": "docker build . && docker run --rm -i -p 8080:8080 $(docker build -q .)", "prepare": "node -e \"if (process.env.NODE_ENV !== 'production'){ process.exit(1) } \" || husky install" }, "dependencies": { @@ -25,7 +25,7 @@ "@mapbox/vector-tile": "1.3.1", "@maplibre/maplibre-gl-native": "5.2.0", "@maplibre/maplibre-gl-style-spec": "18.0.0", - "@sindresorhus/fnv1a": "3.0.0", + "@sindresorhus/fnv1a": "3.1.0", "advanced-pool": "0.3.3", "axios": "^1.6.2", "canvas": "2.11.2", @@ -46,12 +46,12 @@ "tileserver-gl-styles": "2.0.0" }, "devDependencies": { - "@commitlint/cli": "^18.4.1", - "@commitlint/config-conventional": "^18.4.0", - "@typescript-eslint/eslint-plugin": "^6.11.0", - "@typescript-eslint/parser": "^6.11.0", + "@commitlint/cli": "^18.4.3", + "@commitlint/config-conventional": "^18.4.3", + "@typescript-eslint/eslint-plugin": "^6.12.0", + "@typescript-eslint/parser": "^6.12.0", "chai": "4.3.10", - "eslint": "^8.53.0", + "eslint": "^8.54.0", "eslint-config-prettier": "^9.0.0", "eslint-plugin-jsdoc": "^46.9.0", "eslint-plugin-prettier": "^5.0.1", diff --git a/public/templates/index.tmpl b/public/templates/index.tmpl index 8708881..2de6634 100644 --- a/public/templates/index.tmpl +++ b/public/templates/index.tmpl @@ -82,9 +82,6 @@
type: {{#is_vector}}vector{{/is_vector}}{{^is_vector}}raster{{/is_vector}} data {{#if source_type}} | ext: {{source_type}}{{/if}}

services: TileJSON - {{#if wmts_link}} - | WMTS - {{/if}} {{#if xyz_link}} | XYZ diff --git a/src/healthcheck.js b/src/healthcheck.js index ef3c3d7..b3a0ab0 100644 --- a/src/healthcheck.js +++ b/src/healthcheck.js @@ -1,9 +1,9 @@ import * as http from 'http'; -var options = { +const options = { timeout: 2000, }; -var url = 'http://localhost:8080/health'; -var request = http.request(url, options, (res) => { +const url = 'http://localhost:8080/health'; +const request = http.request(url, options, (res) => { console.log(`STATUS: ${res.statusCode}`); if (res.statusCode == 200) { process.exit(0); diff --git a/src/render.js b/src/render.js new file mode 100644 index 0000000..a7489f7 --- /dev/null +++ b/src/render.js @@ -0,0 +1,303 @@ +'use strict'; + +import { createCanvas, Image } from 'canvas'; + +import SphericalMercator from '@mapbox/sphericalmercator'; + +const mercator = new SphericalMercator(); + +/** + * Transforms coordinates to pixels. + * @param {List[Number]} ll Longitude/Latitude coordinate pair. + * @param {number} zoom Map zoom level. + */ +const precisePx = (ll, zoom) => { + const px = mercator.px(ll, 20); + const scale = Math.pow(2, zoom - 20); + return [px[0] * scale, px[1] * scale]; +}; + +/** + * Draws a marker in canvas context. + * @param {object} ctx Canvas context object. + * @param {object} marker Marker object parsed by extractMarkersFromQuery. + * @param {number} z Map zoom level. + */ +const drawMarker = (ctx, marker, z) => { + return new Promise((resolve) => { + const img = new Image(); + const pixelCoords = precisePx(marker.location, z); + + const getMarkerCoordinates = (imageWidth, imageHeight, scale) => { + // Images are placed with their top-left corner at the provided location + // within the canvas but we expect icons to be centered and above it. + + // Substract half of the images width from the x-coordinate to center + // the image in relation to the provided location + let xCoordinate = pixelCoords[0] - imageWidth / 2; + // Substract the images height from the y-coordinate to place it above + // the provided location + let yCoordinate = pixelCoords[1] - imageHeight; + + // Since image placement is dependent on the size offsets have to be + // scaled as well. Additionally offsets are provided as either positive or + // negative values so we always add them + if (marker.offsetX) { + xCoordinate = xCoordinate + marker.offsetX * scale; + } + if (marker.offsetY) { + yCoordinate = yCoordinate + marker.offsetY * scale; + } + + return { + x: xCoordinate, + y: yCoordinate, + }; + }; + + const drawOnCanvas = () => { + // Check if the images should be resized before beeing drawn + const defaultScale = 1; + const scale = marker.scale ? marker.scale : defaultScale; + + // Calculate scaled image sizes + const imageWidth = img.width * scale; + const imageHeight = img.height * scale; + + // Pass the desired sizes to get correlating coordinates + const coords = getMarkerCoordinates(imageWidth, imageHeight, scale); + + // Draw the image on canvas + if (scale != defaultScale) { + ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight); + } else { + ctx.drawImage(img, coords.x, coords.y); + } + // Resolve the promise when image has been drawn + resolve(); + }; + + img.onload = drawOnCanvas; + img.onerror = (err) => { + throw err; + }; + img.src = marker.icon; + }); +}; + +/** + * Draws a list of markers onto a canvas. + * Wraps drawing of markers into list of promises and awaits them. + * It's required because images are expected to load asynchronous in canvas js + * even when provided from a local disk. + * @param {object} ctx Canvas context object. + * @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery. + * @param {number} z Map zoom level. + */ +const drawMarkers = async (ctx, markers, z) => { + const markerPromises = []; + + for (const marker of markers) { + // Begin drawing marker + markerPromises.push(drawMarker(ctx, marker, z)); + } + + // Await marker drawings before continuing + await Promise.all(markerPromises); +}; + +/** + * Draws a list of coordinates onto a canvas and styles the resulting path. + * @param {object} ctx Canvas context object. + * @param {List[Number]} path List of coordinates. + * @param {object} query Request query parameters. + * @param {string} pathQuery Path query parameter. + * @param {number} z Map zoom level. + */ +const drawPath = (ctx, path, query, pathQuery, z) => { + const splitPaths = pathQuery.split('|'); + + if (!path || path.length < 2) { + return null; + } + + ctx.beginPath(); + + // Transform coordinates to pixel on canvas and draw lines between points + for (const pair of path) { + const px = precisePx(pair, z); + ctx.lineTo(px[0], px[1]); + } + + // Check if first coordinate matches last coordinate + if ( + path[0][0] === path[path.length - 1][0] && + path[0][1] === path[path.length - 1][1] + ) { + ctx.closePath(); + } + + // Optionally fill drawn shape with a rgba color from query + const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0; + if (query.fill !== undefined || pathHasFill) { + if ('fill' in query) { + ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)'; + } + if (pathHasFill) { + ctx.fillStyle = splitPaths + .find((x) => x.startsWith('fill:')) + .replace('fill:', ''); + } + ctx.fill(); + } + + // Get line width from query and fall back to 1 if not provided + const pathHasWidth = + splitPaths.filter((x) => x.startsWith('width')).length > 0; + if (query.width !== undefined || pathHasWidth) { + let lineWidth = 1; + // Get line width from query + if ('width' in query) { + lineWidth = Number(query.width); + } + // Get line width from path in query + if (pathHasWidth) { + lineWidth = Number( + splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''), + ); + } + // Get border width from query and fall back to 10% of line width + const borderWidth = + query.borderwidth !== undefined + ? parseFloat(query.borderwidth) + : lineWidth * 0.1; + + // Set rendering style for the start and end points of the path + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap + ctx.lineCap = query.linecap || 'butt'; + + // Set rendering style for overlapping segments of the path with differing directions + // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin + ctx.lineJoin = query.linejoin || 'miter'; + + // In order to simulate a border we draw the path two times with the first + // beeing the wider border part. + if (query.border !== undefined && borderWidth > 0) { + // We need to double the desired border width and add it to the line width + // in order to get the desired border on each side of the line. + ctx.lineWidth = lineWidth + borderWidth * 2; + // Set border style as rgba + ctx.strokeStyle = query.border; + ctx.stroke(); + } + ctx.lineWidth = lineWidth; + } + + const pathHasStroke = + splitPaths.filter((x) => x.startsWith('stroke')).length > 0; + if (query.stroke !== undefined || pathHasStroke) { + if ('stroke' in query) { + ctx.strokeStyle = query.stroke; + } + // Path Stroke gets higher priority + if (pathHasStroke) { + ctx.strokeStyle = splitPaths + .find((x) => x.startsWith('stroke:')) + .replace('stroke:', ''); + } + } else { + ctx.strokeStyle = 'rgba(0,64,255,0.7)'; + } + ctx.stroke(); +}; + +export const renderOverlay = async ( + z, + x, + y, + bearing, + pitch, + w, + h, + scale, + paths, + markers, + query, +) => { + if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) { + return null; + } + + const center = precisePx([x, y], z); + + const mapHeight = 512 * (1 << z); + const maxEdge = center[1] + h / 2; + const minEdge = center[1] - h / 2; + if (maxEdge > mapHeight) { + center[1] -= maxEdge - mapHeight; + } else if (minEdge < 0) { + center[1] -= minEdge; + } + + const canvas = createCanvas(scale * w, scale * h); + const ctx = canvas.getContext('2d'); + ctx.scale(scale, scale); + if (bearing) { + ctx.translate(w / 2, h / 2); + ctx.rotate((-bearing / 180) * Math.PI); + ctx.translate(-center[0], -center[1]); + } else { + // optimized path + ctx.translate(-center[0] + w / 2, -center[1] + h / 2); + } + + // Draw provided paths if any + paths.forEach((path, i) => { + const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path; + drawPath(ctx, path, query, pathQuery, z); + }); + + // Await drawing of markers before rendering the canvas + await drawMarkers(ctx, markers, z); + + return canvas.toBuffer(); +}; + +export const renderWatermark = (width, height, scale, text) => { + const canvas = createCanvas(scale * width, scale * height); + const ctx = canvas.getContext('2d'); + ctx.scale(scale, scale); + + ctx.font = '10px sans-serif'; + ctx.strokeWidth = '1px'; + ctx.strokeStyle = 'rgba(255,255,255,.4)'; + ctx.strokeText(text, 5, height - 5); + ctx.fillStyle = 'rgba(0,0,0,.4)'; + ctx.fillText(text, 5, height - 5); + + return canvas; +}; + +export const renderAttribution = (width, height, scale, text) => { + const canvas = createCanvas(scale * width, scale * height); + const ctx = canvas.getContext('2d'); + ctx.scale(scale, scale); + + ctx.font = '10px sans-serif'; + const textMetrics = ctx.measureText(text); + const textWidth = textMetrics.width; + const textHeight = 14; + + const padding = 6; + ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; + ctx.fillRect( + width - textWidth - padding, + height - textHeight - padding, + textWidth + padding, + textHeight + padding, + ); + ctx.fillStyle = 'rgba(0,0,0,.8)'; + ctx.fillText(text, width - textWidth - padding / 2, height - textHeight + 8); + + return canvas; +}; diff --git a/src/serve_font.js b/src/serve_font.js index f03d8f5..e21bf22 100644 --- a/src/serve_font.js +++ b/src/serve_font.js @@ -1,10 +1,8 @@ 'use strict'; import express from 'express'; -import fs from 'node:fs'; -import path from 'path'; -import { getFontsPbf } from './utils.js'; +import { getFontsPbf, listFonts } from './utils.js'; export const serve_font = (options, allowedFonts) => { const app = express().disable('x-powered-by'); @@ -14,29 +12,6 @@ export const serve_font = (options, allowedFonts) => { const fontPath = options.paths.fonts; const existingFonts = {}; - const fontListingPromise = new Promise((resolve, reject) => { - fs.readdir(options.paths.fonts, (err, files) => { - if (err) { - reject(err); - return; - } - for (const file of files) { - fs.stat(path.join(fontPath, file), (err, stats) => { - if (err) { - reject(err); - return; - } - if ( - stats.isDirectory() && - fs.existsSync(path.join(fontPath, file, '0-255.pbf')) - ) { - existingFonts[path.basename(file)] = true; - } - }); - } - resolve(); - }); - }); app.get('/fonts/:fontstack/:range([\\d]+-[\\d]+).pbf', (req, res, next) => { const fontstack = decodeURI(req.params.fontstack); @@ -65,5 +40,8 @@ export const serve_font = (options, allowedFonts) => { ); }); - return fontListingPromise.then(() => app); + return listFonts(options.paths.fonts).then((fonts) => { + Object.assign(existingFonts, fonts); + return app; + }); }; diff --git a/src/serve_rendered.js b/src/serve_rendered.js index b10fe4c..22025a6 100644 --- a/src/serve_rendered.js +++ b/src/serve_rendered.js @@ -6,7 +6,7 @@ import path from 'path'; import url from 'url'; import util from 'util'; import zlib from 'zlib'; -import { createCanvas, Image } from 'canvas'; +import { renderOverlay, renderWatermark, renderAttribution } from './render.js'; import sharp from 'sharp'; // sharp has to be required before node-canvas on linux but after it on windows. see https://github.com/lovell/sharp/issues/371 import clone from 'clone'; import Color from 'color'; @@ -20,6 +20,7 @@ import proj4 from 'proj4'; import axios from 'axios'; import { getFontsPbf, + listFonts, getTileUrls, isValidHttpUrl, fixTileJSONCenter, @@ -330,263 +331,6 @@ const extractMarkersFromQuery = (query, options, transformer) => { return markers; }; -/** - * Transforms coordinates to pixels. - * @param {List[Number]} ll Longitude/Latitude coordinate pair. - * @param {number} zoom Map zoom level. - */ -const precisePx = (ll, zoom) => { - const px = mercator.px(ll, 20); - const scale = Math.pow(2, zoom - 20); - return [px[0] * scale, px[1] * scale]; -}; - -/** - * Draws a marker in cavans context. - * @param {object} ctx Canvas context object. - * @param {object} marker Marker object parsed by extractMarkersFromQuery. - * @param {number} z Map zoom level. - */ -const drawMarker = (ctx, marker, z) => { - return new Promise((resolve) => { - const img = new Image(); - const pixelCoords = precisePx(marker.location, z); - - const getMarkerCoordinates = (imageWidth, imageHeight, scale) => { - // Images are placed with their top-left corner at the provided location - // within the canvas but we expect icons to be centered and above it. - - // Substract half of the images width from the x-coordinate to center - // the image in relation to the provided location - let xCoordinate = pixelCoords[0] - imageWidth / 2; - // Substract the images height from the y-coordinate to place it above - // the provided location - let yCoordinate = pixelCoords[1] - imageHeight; - - // Since image placement is dependent on the size offsets have to be - // scaled as well. Additionally offsets are provided as either positive or - // negative values so we always add them - if (marker.offsetX) { - xCoordinate = xCoordinate + marker.offsetX * scale; - } - if (marker.offsetY) { - yCoordinate = yCoordinate + marker.offsetY * scale; - } - - return { - x: xCoordinate, - y: yCoordinate, - }; - }; - - const drawOnCanvas = () => { - // Check if the images should be resized before beeing drawn - const defaultScale = 1; - const scale = marker.scale ? marker.scale : defaultScale; - - // Calculate scaled image sizes - const imageWidth = img.width * scale; - const imageHeight = img.height * scale; - - // Pass the desired sizes to get correlating coordinates - const coords = getMarkerCoordinates(imageWidth, imageHeight, scale); - - // Draw the image on canvas - if (scale != defaultScale) { - ctx.drawImage(img, coords.x, coords.y, imageWidth, imageHeight); - } else { - ctx.drawImage(img, coords.x, coords.y); - } - // Resolve the promise when image has been drawn - resolve(); - }; - - img.onload = drawOnCanvas; - img.onerror = (err) => { - throw err; - }; - img.src = marker.icon; - }); -}; - -/** - * Draws a list of markers onto a canvas. - * Wraps drawing of markers into list of promises and awaits them. - * It's required because images are expected to load asynchronous in canvas js - * even when provided from a local disk. - * @param {object} ctx Canvas context object. - * @param {List[Object]} markers Marker objects parsed by extractMarkersFromQuery. - * @param {number} z Map zoom level. - */ -const drawMarkers = async (ctx, markers, z) => { - const markerPromises = []; - - for (const marker of markers) { - // Begin drawing marker - markerPromises.push(drawMarker(ctx, marker, z)); - } - - // Await marker drawings before continuing - await Promise.all(markerPromises); -}; - -/** - * Draws a list of coordinates onto a canvas and styles the resulting path. - * @param {object} ctx Canvas context object. - * @param {List[Number]} path List of coordinates. - * @param {object} query Request query parameters. - * @param {string} pathQuery Path query parameter. - * @param {number} z Map zoom level. - */ -const drawPath = (ctx, path, query, pathQuery, z) => { - const splitPaths = pathQuery.split('|'); - - if (!path || path.length < 2) { - return null; - } - - ctx.beginPath(); - - // Transform coordinates to pixel on canvas and draw lines between points - for (const pair of path) { - const px = precisePx(pair, z); - ctx.lineTo(px[0], px[1]); - } - - // Check if first coordinate matches last coordinate - if ( - path[0][0] === path[path.length - 1][0] && - path[0][1] === path[path.length - 1][1] - ) { - ctx.closePath(); - } - - // Optionally fill drawn shape with a rgba color from query - const pathHasFill = splitPaths.filter((x) => x.startsWith('fill')).length > 0; - if (query.fill !== undefined || pathHasFill) { - if ('fill' in query) { - ctx.fillStyle = query.fill || 'rgba(255,255,255,0.4)'; - } - if (pathHasFill) { - ctx.fillStyle = splitPaths - .find((x) => x.startsWith('fill:')) - .replace('fill:', ''); - } - ctx.fill(); - } - - // Get line width from query and fall back to 1 if not provided - const pathHasWidth = - splitPaths.filter((x) => x.startsWith('width')).length > 0; - if (query.width !== undefined || pathHasWidth) { - let lineWidth = 1; - // Get line width from query - if ('width' in query) { - lineWidth = Number(query.width); - } - // Get line width from path in query - if (pathHasWidth) { - lineWidth = Number( - splitPaths.find((x) => x.startsWith('width:')).replace('width:', ''), - ); - } - // Get border width from query and fall back to 10% of line width - const borderWidth = - query.borderwidth !== undefined - ? parseFloat(query.borderwidth) - : lineWidth * 0.1; - - // Set rendering style for the start and end points of the path - // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap - ctx.lineCap = query.linecap || 'butt'; - - // Set rendering style for overlapping segments of the path with differing directions - // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin - ctx.lineJoin = query.linejoin || 'miter'; - - // In order to simulate a border we draw the path two times with the first - // beeing the wider border part. - if (query.border !== undefined && borderWidth > 0) { - // We need to double the desired border width and add it to the line width - // in order to get the desired border on each side of the line. - ctx.lineWidth = lineWidth + borderWidth * 2; - // Set border style as rgba - ctx.strokeStyle = query.border; - ctx.stroke(); - } - ctx.lineWidth = lineWidth; - } - - const pathHasStroke = - splitPaths.filter((x) => x.startsWith('stroke')).length > 0; - if (query.stroke !== undefined || pathHasStroke) { - if ('stroke' in query) { - ctx.strokeStyle = query.stroke; - } - // Path Stroke gets higher priority - if (pathHasStroke) { - ctx.strokeStyle = splitPaths - .find((x) => x.startsWith('stroke:')) - .replace('stroke:', ''); - } - } else { - ctx.strokeStyle = 'rgba(0,64,255,0.7)'; - } - ctx.stroke(); -}; - -const renderOverlay = async ( - z, - x, - y, - bearing, - pitch, - w, - h, - scale, - paths, - markers, - query, -) => { - if ((!paths || paths.length === 0) && (!markers || markers.length === 0)) { - return null; - } - - const center = precisePx([x, y], z); - - const mapHeight = 512 * (1 << z); - const maxEdge = center[1] + h / 2; - const minEdge = center[1] - h / 2; - if (maxEdge > mapHeight) { - center[1] -= maxEdge - mapHeight; - } else if (minEdge < 0) { - center[1] -= minEdge; - } - - const canvas = createCanvas(scale * w, scale * h); - const ctx = canvas.getContext('2d'); - ctx.scale(scale, scale); - if (bearing) { - ctx.translate(w / 2, h / 2); - ctx.rotate((-bearing / 180) * Math.PI); - ctx.translate(-center[0], -center[1]); - } else { - // optimized path - ctx.translate(-center[0] + w / 2, -center[1] + h / 2); - } - - // Draw provided paths if any - paths.forEach((path, i) => { - const pathQuery = Array.isArray(query.path) ? query.path.at(i) : query.path; - drawPath(ctx, path, query, pathQuery, z); - }); - - // Await drawing of markers before rendering the canvas - await drawMarkers(ctx, markers, z); - - return canvas.toBuffer(); -}; - const calcZForBBox = (bbox, w, h, query) => { let z = 25; @@ -608,32 +352,179 @@ const calcZForBBox = (bbox, w, h, query) => { return z; }; +const respondImage = ( + options, + item, + z, + lon, + lat, + bearing, + pitch, + width, + height, + scale, + format, + res, + overlay = null, + mode = 'tile', +) => { + if ( + Math.abs(lon) > 180 || + Math.abs(lat) > 85.06 || + lon !== lon || + lat !== lat + ) { + return res.status(400).send('Invalid center'); + } + + if ( + Math.min(width, height) <= 0 || + Math.max(width, height) * scale > (options.maxSize || 2048) || + width !== width || + height !== height + ) { + return res.status(400).send('Invalid size'); + } + + if (format === 'png' || format === 'webp') { + } else if (format === 'jpg' || format === 'jpeg') { + format = 'jpeg'; + } else { + return res.status(400).send('Invalid format'); + } + + const tileMargin = Math.max(options.tileMargin || 0, 0); + let pool; + if (mode === 'tile' && tileMargin === 0) { + pool = item.map.renderers[scale]; + } else { + pool = item.map.renderers_static[scale]; + } + pool.acquire((err, renderer) => { + const mlglZ = Math.max(0, z - 1); + const params = { + zoom: mlglZ, + center: [lon, lat], + bearing: bearing, + pitch: pitch, + width: width, + height: height, + }; + + if (z === 0) { + params.width *= 2; + params.height *= 2; + } + + if (z > 2 && tileMargin > 0) { + params.width += tileMargin * 2; + params.height += tileMargin * 2; + } + + renderer.render(params, (err, data) => { + pool.release(renderer); + if (err) { + console.error(err); + return res.status(500).header('Content-Type', 'text/plain').send(err); + } + + // Fix semi-transparent outlines on raw, premultiplied input + // https://github.com/maptiler/tileserver-gl/issues/350#issuecomment-477857040 + for (let i = 0; i < data.length; i += 4) { + const alpha = data[i + 3]; + const norm = alpha / 255; + if (alpha === 0) { + data[i] = 0; + data[i + 1] = 0; + data[i + 2] = 0; + } else { + data[i] = data[i] / norm; + data[i + 1] = data[i + 1] / norm; + data[i + 2] = data[i + 2] / norm; + } + } + + const image = sharp(data, { + raw: { + width: params.width * scale, + height: params.height * scale, + channels: 4, + }, + }); + + if (z > 2 && tileMargin > 0) { + const [_, y] = mercator.px(params.center, z); + let yoffset = Math.max( + Math.min(0, y - 128 - tileMargin), + y + 128 + tileMargin - Math.pow(2, z + 8), + ); + image.extract({ + left: tileMargin * scale, + top: (tileMargin + yoffset) * scale, + width: width * scale, + height: height * scale, + }); + } + + if (z === 0) { + // HACK: when serving zoom 0, resize the 0 tile from 512 to 256 + image.resize(width * scale, height * scale); + } + + const composite_array = []; + if (overlay) { + composite_array.push({ input: overlay }); + } + if (item.watermark) { + const canvas = renderWatermark(width, height, scale, item.watermark); + + composite_array.push({ input: canvas.toBuffer() }); + } + + if (mode === 'static' && item.staticAttributionText) { + const canvas = renderAttribution( + width, + height, + scale, + item.staticAttributionText, + ); + + composite_array.push({ input: canvas.toBuffer() }); + } + + if (composite_array.length > 0) { + image.composite(composite_array); + } + + const formatQuality = (options.formatQuality || {})[format]; + + if (format === 'png') { + image.png({ adaptiveFiltering: false }); + } else if (format === 'jpeg') { + image.jpeg({ quality: formatQuality || 80 }); + } else if (format === 'webp') { + image.webp({ quality: formatQuality || 90 }); + } + image.toBuffer((err, buffer, info) => { + if (!buffer) { + return res.status(404).send('Not found'); + } + + res.set({ + 'Last-Modified': item.lastModified, + 'Content-Type': `image/${format}`, + }); + return res.status(200).send(buffer); + }); + }); + }); +}; + const existingFonts = {}; let maxScaleFactor = 2; export const serve_rendered = { init: (options, repo) => { - const fontListingPromise = new Promise((resolve, reject) => { - fs.readdir(options.paths.fonts, (err, files) => { - if (err) { - reject(err); - return; - } - for (const file of files) { - fs.stat(path.join(options.paths.fonts, file), (err, stats) => { - if (err) { - reject(err); - return; - } - if (stats.isDirectory()) { - existingFonts[path.basename(file)] = true; - } - }); - } - resolve(); - }); - }); - maxScaleFactor = Math.min(Math.floor(options.maxScaleFactor || 3), 9); let scalePattern = ''; for (let i = 2; i <= maxScaleFactor; i++) { @@ -643,203 +534,6 @@ export const serve_rendered = { const app = express().disable('x-powered-by'); - const respondImage = ( - item, - z, - lon, - lat, - bearing, - pitch, - width, - height, - scale, - format, - res, - next, - opt_overlay, - opt_mode = 'tile', - ) => { - if ( - Math.abs(lon) > 180 || - Math.abs(lat) > 85.06 || - lon !== lon || - lat !== lat - ) { - return res.status(400).send('Invalid center'); - } - - if ( - Math.min(width, height) <= 0 || - Math.max(width, height) * scale > (options.maxSize || 2048) || - width !== width || - height !== height - ) { - return res.status(400).send('Invalid size'); - } - - if (format === 'png' || format === 'webp') { - } else if (format === 'jpg' || format === 'jpeg') { - format = 'jpeg'; - } else { - return res.status(400).send('Invalid format'); - } - - const tileMargin = Math.max(options.tileMargin || 0, 0); - let pool; - if (opt_mode === 'tile' && tileMargin === 0) { - pool = item.map.renderers[scale]; - } else { - pool = item.map.renderers_static[scale]; - } - pool.acquire((err, renderer) => { - const mlglZ = Math.max(0, z - 1); - const params = { - zoom: mlglZ, - center: [lon, lat], - bearing: bearing, - pitch: pitch, - width: width, - height: height, - }; - - if (z === 0) { - params.width *= 2; - params.height *= 2; - } - - if (z > 2 && tileMargin > 0) { - params.width += tileMargin * 2; - params.height += tileMargin * 2; - } - - renderer.render(params, (err, data) => { - pool.release(renderer); - if (err) { - console.error(err); - return res - .status(500) - .header('Content-Type', 'text/plain') - .send(err); - } - - // Fix semi-transparent outlines on raw, premultiplied input - // https://github.com/maptiler/tileserver-gl/issues/350#issuecomment-477857040 - for (let i = 0; i < data.length; i += 4) { - const alpha = data[i + 3]; - const norm = alpha / 255; - if (alpha === 0) { - data[i] = 0; - data[i + 1] = 0; - data[i + 2] = 0; - } else { - data[i] = data[i] / norm; - data[i + 1] = data[i + 1] / norm; - data[i + 2] = data[i + 2] / norm; - } - } - - const image = sharp(data, { - raw: { - width: params.width * scale, - height: params.height * scale, - channels: 4, - }, - }); - - if (z > 2 && tileMargin > 0) { - const [_, y] = mercator.px(params.center, z); - let yoffset = Math.max( - Math.min(0, y - 128 - tileMargin), - y + 128 + tileMargin - Math.pow(2, z + 8), - ); - image.extract({ - left: tileMargin * scale, - top: (tileMargin + yoffset) * scale, - width: width * scale, - height: height * scale, - }); - } - - if (z === 0) { - // HACK: when serving zoom 0, resize the 0 tile from 512 to 256 - image.resize(width * scale, height * scale); - } - - var composite_array = []; - if (opt_overlay) { - composite_array.push({ input: opt_overlay }); - } - if (item.watermark) { - const canvas = createCanvas(scale * width, scale * height); - const ctx = canvas.getContext('2d'); - ctx.scale(scale, scale); - ctx.font = '10px sans-serif'; - ctx.strokeWidth = '1px'; - ctx.strokeStyle = 'rgba(255,255,255,.4)'; - ctx.strokeText(item.watermark, 5, height - 5); - ctx.fillStyle = 'rgba(0,0,0,.4)'; - ctx.fillText(item.watermark, 5, height - 5); - - composite_array.push({ input: canvas.toBuffer() }); - } - - if (opt_mode === 'static' && item.staticAttributionText) { - const canvas = createCanvas(scale * width, scale * height); - const ctx = canvas.getContext('2d'); - ctx.scale(scale, scale); - - ctx.font = '10px sans-serif'; - const text = item.staticAttributionText; - const textMetrics = ctx.measureText(text); - const textWidth = textMetrics.width; - const textHeight = 14; - - const padding = 6; - ctx.fillStyle = 'rgba(255, 255, 255, 0.8)'; - ctx.fillRect( - width - textWidth - padding, - height - textHeight - padding, - textWidth + padding, - textHeight + padding, - ); - ctx.fillStyle = 'rgba(0,0,0,.8)'; - ctx.fillText( - item.staticAttributionText, - width - textWidth - padding / 2, - height - textHeight + 8, - ); - - composite_array.push({ input: canvas.toBuffer() }); - } - - if (composite_array.length > 0) { - image.composite(composite_array); - } - - const formatQuality = (options.formatQuality || {})[format]; - - if (format === 'png') { - image.png({ adaptiveFiltering: false }); - } else if (format === 'jpeg') { - image.jpeg({ quality: formatQuality || 80 }); - } else if (format === 'webp') { - image.webp({ quality: formatQuality || 90 }); - } - image.toBuffer((err, buffer, info) => { - if (!buffer) { - return res.status(404).send('Not found'); - } - - res.set({ - 'Last-Modified': item.lastModified, - 'Content-Type': `image/${format}`, - }); - return res.status(200).send(buffer); - }); - }); - }); - }; - app.get( `/:id/:z(\\d+)/:x(\\d+)/:y(\\d+):scale(${scalePattern})?.:format([\\w]+)`, (req, res, next) => { @@ -880,6 +574,7 @@ export const serve_rendered = { z, ); return respondImage( + options, item, z, tileCenter[0], @@ -891,7 +586,6 @@ export const serve_rendered = { scale, format, res, - next, ); }, ); @@ -962,6 +656,7 @@ export const serve_rendered = { ); return respondImage( + options, item, z, x, @@ -973,7 +668,6 @@ export const serve_rendered = { scale, format, res, - next, overlay, 'static', ); @@ -1043,6 +737,7 @@ export const serve_rendered = { req.query, ); return respondImage( + options, item, z, x, @@ -1054,7 +749,6 @@ export const serve_rendered = { scale, format, res, - next, overlay, 'static', ); @@ -1177,6 +871,7 @@ export const serve_rendered = { ); return respondImage( + options, item, z, x, @@ -1188,7 +883,6 @@ export const serve_rendered = { scale, format, res, - next, overlay, 'static', ); @@ -1215,7 +909,10 @@ export const serve_rendered = { return res.send(info); }); - return Promise.all([fontListingPromise]).then(() => app); + return listFonts(options.paths.fonts).then((fonts) => { + Object.assign(existingFonts, fonts); + return app; + }); }, add: async (options, repo, params, id, publicUrl, dataResolver) => { const map = { @@ -1618,7 +1315,7 @@ export const serve_rendered = { } }); - return Promise.all([renderersReadyPromise]); + return renderersReadyPromise; }, remove: (repo, id) => { const item = repo[id]; diff --git a/src/serve_style.js b/src/serve_style.js index de981c7..de4ccdb 100644 --- a/src/serve_style.js +++ b/src/serve_style.js @@ -98,9 +98,7 @@ export const serve_style = { const validationErrors = validate(styleFileData); if (validationErrors.length > 0) { - console.log( - `The file "${params.style}" is not valid a valid style file:`, - ); + console.log(`The file "${params.style}" is not a valid style file:`); for (const err of validationErrors) { console.log(`${err.line}: ${err.message}`); } diff --git a/src/utils.js b/src/utils.js index 123fed6..3f5cb0d 100644 --- a/src/utils.js +++ b/src/utils.js @@ -163,6 +163,35 @@ export const getFontsPbf = ( return Promise.all(queue).then((values) => glyphCompose.combine(values)); }; +export const listFonts = async (fontPath) => { + const existingFonts = {}; + const fontListingPromise = new Promise((resolve, reject) => { + fs.readdir(fontPath, (err, files) => { + if (err) { + reject(err); + return; + } + for (const file of files) { + fs.stat(path.join(fontPath, file), (err, stats) => { + if (err) { + reject(err); + return; + } + if ( + stats.isDirectory() && + fs.existsSync(path.join(fontPath, file, '0-255.pbf')) + ) { + existingFonts[path.basename(file)] = true; + } + }); + } + resolve(); + }); + }); + await fontListingPromise; + return existingFonts; +}; + export const isValidHttpUrl = (string) => { let url; diff --git a/test/setup.js b/test/setup.js index bae30ea..34fba67 100644 --- a/test/setup.js +++ b/test/setup.js @@ -24,6 +24,5 @@ after(function () { console.log('global teardown'); global.server.close(function () { console.log('Done'); - process.exit(); }); });