diff --git a/.github/workflows/build-preview.yml b/.github/workflows/build-preview.yml index d1ffd9423..78cded610 100644 --- a/.github/workflows/build-preview.yml +++ b/.github/workflows/build-preview.yml @@ -43,12 +43,15 @@ jobs: with: node-version: 18.16.1 #18.17.0 is buggy - name: Install and Build 🔧 + # TODO: when we move shieldlib to its own repo, move shieldlib docs CI also run: | npm ci --include=dev npm run build npm run style npm run shields cp src/configs/config.aws.js src/config.js + mkdir -p dist/shield-docs + cp -r shieldlib/docs/* dist/shield-docs - name: Upload Build artifact uses: actions/upload-artifact@v3 with: diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 13b362704..5c1885188 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -25,12 +25,15 @@ jobs: with: node-version: 18.16.1 #18.17.0 is buggy - name: Install and Build 🔧 + # TODO: when we move shieldlib to its own repo, move shieldlib docs CI also run: | npm ci --include=dev cp src/configs/config.aws.js src/config.js npm run build npm run style npm run shields + mkdir -p dist/shield-docs + cp -r shieldlib/docs/* dist/shield-docs - name: Upload 🏗 uses: actions/upload-pages-artifact@v1 with: diff --git a/.gitignore b/.gitignore index 8448264f5..3c0c0b15a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ config.js dist download local.config.js +shieldlib/docs diff --git a/package-lock.json b/package-lock.json index 793f57612..6220c67ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -730,6 +730,12 @@ "node": ">=8" } }, + "node_modules/ansi-sequence-parser": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ansi-sequence-parser/-/ansi-sequence-parser-1.1.1.tgz", + "integrity": "sha512-vJXt3yiaUL4UU546s3rPXlsry/RnM730G1+HkpKE012AN0sx1eOrxSu95oKDIonskeLTijMgqWZ3uDEe3NFvyg==", + "dev": true + }, "node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -1975,9 +1981,9 @@ } }, "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", "dev": true, "engines": { "node": "*" @@ -2675,6 +2681,12 @@ "json5": "lib/cli.js" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/kdbush": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", @@ -2764,6 +2776,12 @@ "node": "14 || >=16.14" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -2831,6 +2849,18 @@ "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==" }, + "node_modules/marked": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.3.0.tgz", + "integrity": "sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, "node_modules/mdn-data": { "version": "2.0.14", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", @@ -4349,6 +4379,18 @@ "node": "*" } }, + "node_modules/shiki": { + "version": "0.14.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-0.14.3.tgz", + "integrity": "sha512-U3S/a+b0KS+UkTyMjoNojvTgrBHjgp7L6ovhFVZsXmBGnVdQ4K4U9oK0z63w538S91ATngv1vXigHCSWOwnr+g==", + "dev": true, + "dependencies": { + "ansi-sequence-parser": "^1.1.0", + "jsonc-parser": "^3.2.0", + "vscode-oniguruma": "^1.7.0", + "vscode-textmate": "^8.0.0" + } + }, "node_modules/shortid": { "version": "2.2.16", "resolved": "https://registry.npmjs.org/shortid/-/shortid-2.2.16.tgz", @@ -5036,6 +5078,42 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typedoc": { + "version": "0.24.8", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.24.8.tgz", + "integrity": "sha512-ahJ6Cpcvxwaxfu4KtjA8qZNqS43wYt6JL27wYiIgl1vd38WW/KWX11YuAeZhuz9v+ttrutSsgK+XO1CjL1kA3w==", + "dev": true, + "dependencies": { + "lunr": "^2.3.9", + "marked": "^4.3.0", + "minimatch": "^9.0.0", + "shiki": "^0.14.1" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 14.14" + }, + "peerDependencies": { + "typescript": "4.6.x || 4.7.x || 4.8.x || 4.9.x || 5.0.x || 5.1.x" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", @@ -5101,6 +5179,18 @@ "spdx-expression-parse": "^3.0.0" } }, + "node_modules/vscode-oniguruma": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz", + "integrity": "sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==", + "dev": true + }, + "node_modules/vscode-textmate": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vscode-textmate/-/vscode-textmate-8.0.0.tgz", + "integrity": "sha512-AFbieoL7a5LMqcnOF04ji+rpXadgOXnZsxQr//r83kLPr7biP7am3g9zbaZIaBGwBRWeSvoMD4mgPdX3e4NWBg==", + "dev": true + }, "node_modules/vt-pbf": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", @@ -5291,7 +5381,7 @@ }, "shieldlib": { "name": "@americana/maplibre-shield-generator", - "version": "0.0.3", + "version": "0.0.4", "license": "CC0-1.0", "dependencies": { "color-rgba": "^2.4.0", @@ -5299,12 +5389,14 @@ }, "devDependencies": { "@types/color-rgba": "^2.1.0", + "canvas": "^2.11.2", "esbuild": "^0.17.10", "npm-run-all": "^4.1.5", "prettier": "^2.8.4", "shx": "^0.3.4", "ts-mocha": "^10.0.0", "ts-node": "^10.9.1", + "typedoc": "^0.24.8", "typescript": "^4.9.5" } } diff --git a/package.json b/package.json index 192a64e38..dfa558db8 100644 --- a/package.json +++ b/package.json @@ -11,13 +11,13 @@ "shieldlib" ], "scripts": { - "build:shieldlib": "cd shieldlib && node scripts/build.js", + "build:shieldlib": "cd shieldlib && node scripts/build.js && npm run docs", "build:code": "exec ts-node scripts/build", "build": "run-s clean-build sprites build:shieldlib build:code taginfo status_map", "clean": "run-s clean:shieldlib clean:code clean-download clean-build", "clean-download": "shx rm -rf download", "clean-build": "shx rm -rf dist build", - "clean:shieldlib": "cd shieldlib && shx rm -rf dist", + "clean:shieldlib": "cd shieldlib && shx rm -rf dist docs", "clean:code": "shx rm -rf dist", "config": "shx cp src/configs/config.maptiler.js src/config.js", "code_format": "run-s code_format:prettier code_format:svgo", diff --git a/shieldlib/README.md b/shieldlib/README.md index bd41b6c26..1a85ff789 100644 --- a/shieldlib/README.md +++ b/shieldlib/README.md @@ -127,8 +127,8 @@ You should create one definition entry for each network. The entry key must matc "drawFunc": "pentagon", "params": { "pointUp": false, - "offset": 5, - "angle": 0, + "yOffset": 5, + "sideAngle": 0, "fillColor": "white", "strokeColor": "black", "radius1": 2, @@ -299,9 +299,9 @@ If `shapeBlank` is specified, the shield will be drawn as a shape. This needs to The following `params` options can be specified: -- `angle` - indicates angle (in degrees) at which side edges deviate from vertical. Applies to `trapezoid`, `pentagon`, `hexagonHorizontal`, `octagonVertical`. +- `sideAngle` - indicates angle (in degrees) at which side edges deviate from vertical. Applies to `trapezoid`, `pentagon`, `hexagonHorizontal`, `octagonVertical`. - `fill` - specifies the internal fill color. -- `offset` - indicates height (in pixels) at which the bottom and/or top edges deviate from horizontal. Applies to `escutcheon`, `pentagon`, `hexagonVertical`, `octagonVertical`. +- `yOffset` - indicates height (in pixels) at which the bottom and/or top edges deviate from horizontal. Applies to `escutcheon`, `pentagon`, `hexagonVertical`, `octagonVertical`. - `outline` - specifies the outline color. - `outlineWidth` - specifies the width of the outline. - `pointUp` - applies to several shape types and specifies whether the pointy side is up. @@ -313,3 +313,7 @@ The following `params` options can be specified: ### Custom shield graphics In addition to the stock drawing functions, a custom draw function can be specified. `paDot` and `branson` are included as examples of this, for rendering the [Allegheny County belt system](https://en.wikipedia.org/wiki/Allegheny_County_belt_system) and the Branson, Missouri colored route system. See the file `src/custom_shields.mjs` for an example of how this is done. + +## Documentation + +See [TypeDoc generated documentation](https://zelonewolf.github.io/openstreetmap-americana/shield-docs/index.html) for detailed API information. diff --git a/shieldlib/package.json b/shieldlib/package.json index e23b2ee09..112a26433 100644 --- a/shieldlib/package.json +++ b/shieldlib/package.json @@ -1,7 +1,7 @@ { "name": "@americana/maplibre-shield-generator", "description": "Generate highway shields for maplibre-gl-js maps", - "version": "0.0.3", + "version": "0.0.4", "author": "OpenStreetMap Americana Contributors", "type": "module", "keywords": [ @@ -19,21 +19,24 @@ "source": "src/index.ts", "devDependencies": { "@types/color-rgba": "^2.1.0", + "canvas": "^2.11.2", "esbuild": "^0.17.10", "npm-run-all": "^4.1.5", "prettier": "^2.8.4", "shx": "^0.3.4", "ts-mocha": "^10.0.0", "ts-node": "^10.9.1", + "typedoc": "^0.24.8", "typescript": "^4.9.5" }, "scripts": { "code_format": "run-s code_format:prettier", "code_format:prettier": "prettier --write --list-different .", - "clean": "shx rm -rf dist", + "clean": "shx rm -rf dist docs", + "docs": "npx typedoc src/index.ts", "test": "npm exec -- ts-mocha", "build:code": "node scripts/build.js", - "build": "run-s clean build:code", + "build": "run-s clean build:code docs", "preversion": "npm version --no-git-tag-version --preid alpha", "publish-alpha": "npm publish --access=public --tag alpha" }, @@ -51,5 +54,10 @@ }, "directories": { "test": "test" - } + }, + "files": [ + "dist/", + "docs/", + "README.md" + ] } diff --git a/shieldlib/src/custom_shields.mjs b/shieldlib/src/custom_shields.ts similarity index 78% rename from shieldlib/src/custom_shields.mjs rename to shieldlib/src/custom_shields.ts index c390e60a6..8eccf845b 100644 --- a/shieldlib/src/custom_shields.mjs +++ b/shieldlib/src/custom_shields.ts @@ -1,9 +1,15 @@ "use strict"; -import * as ShieldDraw from "./shield_canvas_draw.mjs"; +import * as ShieldDraw from "./shield_canvas_draw"; +import { ShieldRenderingContext } from "./shield_renderer"; +import { ShapeBlankParams } from "./types"; // Special case for Allegheny, PA Belt System -export function paBelt(r, ctx, params) { +export function paBelt( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams +) { ShieldDraw.roundedRectangle(r, ctx, { fillColor: "white", strokeColor: "black", @@ -36,7 +42,11 @@ export function paBelt(r, ctx, params) { } // Special case for Branson color-coded routes -export function bransonRoute(r, ctx, params) { +export function bransonRoute( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams +) { ShieldDraw.roundedRectangle(r, ctx, { fillColor: "#006747", strokeColor: "white", diff --git a/shieldlib/src/index.d.ts b/shieldlib/src/index.d.ts index 1fe27f46d..46116e4fa 100644 --- a/shieldlib/src/index.d.ts +++ b/shieldlib/src/index.d.ts @@ -1,14 +1,10 @@ -export { - Bounds, - GfxFactory, - RouteDefinition, - ShieldSpecification, - SpriteRepository, -} from "./types"; +export * from "./types"; export { transposeImageData } from "./screen_gfx"; export { URLShieldRenderer, ShieldRenderer, InMemorySpriteRepository, + AbstractShieldRenderer, } from "./shield_renderer"; export { getDOMPixelRatio } from "./document_graphics"; +export * from "./shield_helper"; diff --git a/shieldlib/src/index.ts b/shieldlib/src/index.ts index 9cd52f669..97b3a5991 100644 --- a/shieldlib/src/index.ts +++ b/shieldlib/src/index.ts @@ -1,10 +1,4 @@ -export { - Bounds, - GraphicsFactory, - RouteDefinition, - ShieldSpecification, - SpriteRepository, -} from "./types"; +export * from "./types"; export { transposeImageData } from "./screen_gfx"; @@ -12,6 +6,8 @@ export { URLShieldRenderer, ShieldRenderer, InMemorySpriteRepository, + AbstractShieldRenderer, } from "./shield_renderer"; export { getDOMPixelRatio } from "./document_graphics"; +export * from "./shield_helper"; diff --git a/shieldlib/src/screen_gfx.ts b/shieldlib/src/screen_gfx.ts index 225b43420..e3fe2b55c 100644 --- a/shieldlib/src/screen_gfx.ts +++ b/shieldlib/src/screen_gfx.ts @@ -3,7 +3,7 @@ import { StyleImage } from "maplibre-gl"; import rgba from "color-rgba"; const defaultFontFamily = '"sans-serif-condensed", "Arial Narrow", sans-serif'; -export const shieldFont = (size: string, fontFamily: string) => +export const shieldFont = (size: number, fontFamily: string) => `bold ${size}px ${fontFamily || defaultFontFamily}`; export const fontSizeThreshold = 12; diff --git a/shieldlib/src/shield.d.ts b/shieldlib/src/shield.d.ts index 77589c812..fb44bf80b 100644 --- a/shieldlib/src/shield.d.ts +++ b/shieldlib/src/shield.d.ts @@ -14,7 +14,8 @@ export function storeNoShield( export function missingIconLoader( renderContext: ShieldRenderingContext, - routeDef: RouteDefinition + routeDef: RouteDefinition, + spriteID: string ): void; export function romanizeRef(ref: string): string; diff --git a/shieldlib/src/shield.js b/shieldlib/src/shield.js index d0ccaa296..6594d4a95 100644 --- a/shieldlib/src/shield.js +++ b/shieldlib/src/shield.js @@ -1,7 +1,7 @@ "use strict"; import * as ShieldText from "./shield_text.mjs"; -import * as ShieldDraw from "./shield_canvas_draw.mjs"; +import * as ShieldDraw from "./shield_canvas_draw"; import * as Gfx from "./screen_gfx.js"; function drawBannerPart(r, ctx, shieldDef, drawFunc) { @@ -186,14 +186,14 @@ function drawShieldText(r, ctx, shieldDef, routeDef) { return ctx; } -export function missingIconLoader(r, routeDef) { +export function missingIconLoader(r, routeDef, spriteID) { let ctx = generateShieldCtx(r, routeDef); if (ctx == null) { // Want to return null here, but that gives a corrupted display. See #243 console.warn("Didn't produce a shield for", JSON.stringify(routeDef)); ctx = r.gfxFactory.createGraphics({ width: 1, height: 1 }); } - storeSprite(r, routeDef.spriteID, ctx); + storeSprite(r, spriteID, ctx); } function storeSprite(r, id, ctx) { diff --git a/shieldlib/src/shield_canvas_draw.mjs b/shieldlib/src/shield_canvas_draw.ts similarity index 83% rename from shieldlib/src/shield_canvas_draw.mjs rename to shieldlib/src/shield_canvas_draw.ts index 5b8236d6c..6cfa8154f 100644 --- a/shieldlib/src/shield_canvas_draw.mjs +++ b/shieldlib/src/shield_canvas_draw.ts @@ -5,20 +5,27 @@ */ import * as ShieldText from "./shield_text.mjs"; -import { loadCustomShields } from "./custom_shields.mjs"; +import { loadCustomShields } from "./custom_shields"; +import { ShapeDrawFunction, ShieldRenderingContext } from "./shield_renderer"; +import { ShapeBlankParams } from "./types"; const minGenericShieldWidth = 20; const maxGenericShieldWidth = 34; const genericShieldFontSize = 18; -export function computeWidth(r, params, ref, shape) { +export function computeWidth( + r: ShieldRenderingContext, + params: ShapeBlankParams, + ref: string, + shape?: string +) { if (fixedWidthDefinitions[shape] !== undefined) { return r.px(fixedWidthDefinitions[shape]); } let rectWidth = params.rectWidth == undefined ? null : params.rectWidth; - let angle = params.angle == undefined ? 0 : params.angle; - let tangent = Math.tan(angle); + let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; + let tangent = Math.tan(sideAngle); if (rectWidth == null) { let shieldWidth = @@ -29,8 +36,8 @@ export function computeWidth(r, params, ref, shape) { //Shape-specific width adjustments switch (shape) { case "pentagon": - let offset = params.offset == undefined ? 0 : params.offset; - shieldWidth += ((r.shieldSize() - r.px(offset)) * tangent) / 2; + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; + shieldWidth += ((r.shieldSize() - r.px(yOffset)) * tangent) / 2; break; case "trapezoid": shieldWidth += (r.shieldSize() * tangent) / 2; @@ -53,7 +60,12 @@ export function computeWidth(r, params, ref, shape) { } } -function ellipse(r, ctx, params, ref) { +function ellipse( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -71,6 +83,7 @@ function ellipse(r, ctx, params, ref) { radiusX, radiusY, 0, + 0, 2 * Math.PI, false ); @@ -84,7 +97,7 @@ function ellipse(r, ctx, params, ref) { return width; } -export function blank(r, ref) { +export function blank(r: ShieldRenderingContext, ref: string) { var shieldWidth = ShieldText.calculateTextWidth(r, ref, r.px(genericShieldFontSize)) + r.px(2); @@ -98,7 +111,12 @@ export function blank(r, ref) { }); } -export function roundedRectangle(r, ctx, params, ref) { +export function roundedRectangle( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref?: string +) { let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -140,8 +158,13 @@ export function roundedRectangle(r, ctx, params, ref) { return width; } -function escutcheon(r, ctx, params, ref) { - let offset = params.offset == undefined ? 0 : params.offset; +function escutcheon( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -152,7 +175,7 @@ function escutcheon(r, ctx, params, ref) { let lineThick = r.px(outlineWidth); let lineWidth = lineThick / 2; let drawRadius = r.px(radius); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x5 = width - lineWidth; @@ -192,7 +215,12 @@ function escutcheon(r, ctx, params, ref) { return width; } -function fishhead(r, ctx, params, ref) { +function fishhead( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { let pointUp = params.pointUp == undefined ? false : params.pointUp; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -244,7 +272,12 @@ function fishhead(r, ctx, params, ref) { } } -function triangle(r, ctx, params, ref) { +function triangle( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { let pointUp = params.pointUp == undefined ? false : params.pointUp; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -305,19 +338,24 @@ function triangle(r, ctx, params, ref) { return width; } -function trapezoid(r, ctx, params, ref) { +function trapezoid( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { let shortSideUp = params.shortSideUp == undefined ? false : params.shortSideUp; - let angle = params.angle == undefined ? 0 : params.angle; + let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; let outlineWidth = params.outlineWidth == undefined ? 1 : params.outlineWidth; let angleSign = shortSideUp ? -1 : 1; - let sine = Math.sin(angle); - let cosine = Math.cos(angle); - let tangent = Math.tan(angle); + let sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); let width = computeWidth(r, params, ref, "trapezoid"); @@ -362,7 +400,12 @@ function trapezoid(r, ctx, params, ref) { return width; } -function diamond(r, ctx, params, ref) { +function diamond( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -426,10 +469,15 @@ function diamond(r, ctx, params, ref) { return width; } -function pentagon(r, ctx, params, ref) { +function pentagon( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { let pointUp = params.pointUp == undefined ? true : params.pointUp; - let offset = params.offset == undefined ? 0 : params.offset; - let angle = params.angle == undefined ? 0 : params.angle; + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; + let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius1 = params.radius1 == undefined ? 0 : params.radius1; @@ -437,9 +485,9 @@ function pentagon(r, ctx, params, ref) { let outlineWidth = params.outlineWidth == undefined ? 1 : params.outlineWidth; let angleSign = pointUp ? -1 : 1; - let sine = Math.sin(angle); - let cosine = Math.cos(angle); - let tangent = Math.tan(angle); + let sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); let width = computeWidth(r, params, ref, "pentagon"); @@ -447,7 +495,7 @@ function pentagon(r, ctx, params, ref) { let lineWidth = lineThick / 2; let drawRadius1 = r.px(radius1); let drawRadius2 = r.px(radius2); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x8 = width - lineWidth; @@ -462,10 +510,10 @@ function pentagon(r, ctx, params, ref) { let offsetAngle = Math.atan(drawOffset / (x4 - x0)); - let halfComplementAngle1 = (Math.PI / 2 - offsetAngle + angle) / 2; + let halfComplementAngle1 = (Math.PI / 2 - offsetAngle + sideAngle) / 2; let halfComplementTangent1 = Math.tan(halfComplementAngle1); - let halfComplementAngle2 = (Math.PI / 2 - angle) / 2; + let halfComplementAngle2 = (Math.PI / 2 - sideAngle) / 2; let halfComplementTangent2 = Math.tan(halfComplementAngle2); let x1 = x0 + drawRadius1 * halfComplementTangent1 * sine; @@ -494,8 +542,13 @@ function pentagon(r, ctx, params, ref) { return width; } -function hexagonVertical(r, ctx, params, ref) { - let offset = params.offset == undefined ? 0 : params.offset; +function hexagonVertical( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -506,7 +559,7 @@ function hexagonVertical(r, ctx, params, ref) { let lineThick = r.px(outlineWidth); let lineWidth = lineThick / 2; let drawRadius = r.px(radius); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x2 = width - lineWidth; @@ -543,8 +596,13 @@ function hexagonVertical(r, ctx, params, ref) { return width; } -function hexagonHorizontal(r, ctx, params, ref) { - let angle = params.angle == undefined ? 0 : params.angle; +function hexagonHorizontal( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { + let angle = params.sideAngle == undefined ? 0 : params.sideAngle; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; @@ -604,24 +662,29 @@ function hexagonHorizontal(r, ctx, params, ref) { return width; } -function octagonVertical(r, ctx, params, ref) { - let offset = params.offset == undefined ? 0 : params.offset; - let angle = params.angle == undefined ? 0 : params.angle; +function octagonVertical( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { + let yOffset = params.yOffset == undefined ? 0 : params.yOffset; + let sideAngle = params.sideAngle == undefined ? 0 : params.sideAngle; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; let radius = params.radius == undefined ? 0 : params.radius; let outlineWidth = params.outlineWidth == undefined ? 1 : params.outlineWidth; - let sine = Math.sin(angle); - let cosine = Math.cos(angle); - let tangent = Math.tan(angle); + let sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); let width = computeWidth(r, params, ref); let lineThick = r.px(outlineWidth); let lineWidth = lineThick / 2; let drawRadius = r.px(radius); - let drawOffset = r.px(offset); + let drawOffset = r.px(yOffset); let x0 = lineWidth; let x10 = width - lineWidth; @@ -644,13 +707,15 @@ function octagonVertical(r, ctx, params, ref) { let offsetSine = Math.sin(offsetAngle); let offsetCosine = Math.cos(offsetAngle); - let halfComplementAngle = (Math.PI / 2 - angle - offsetAngle) / 2; + let halfComplementAngle = (Math.PI / 2 - sideAngle - offsetAngle) / 2; let halfComplementCosine = Math.cos(halfComplementAngle); let dx = - (drawRadius * Math.cos(angle + halfComplementAngle)) / halfComplementCosine; + (drawRadius * Math.cos(sideAngle + halfComplementAngle)) / + halfComplementCosine; let dy = - (drawRadius * Math.sin(angle + halfComplementAngle)) / halfComplementCosine; + (drawRadius * Math.sin(sideAngle + halfComplementAngle)) / + halfComplementCosine; let x2 = x3 + dx - drawRadius * cosine; let x4 = x3 + dx - drawRadius * offsetSine; @@ -684,7 +749,7 @@ function octagonVertical(r, ctx, params, ref) { return width; } -export function shapeHeight(r, name) { +export function shapeHeight(r: ShieldRenderingContext, name: string) { switch (name) { case "diamond": return r.shieldSize() + r.px(4); @@ -693,8 +758,14 @@ export function shapeHeight(r, name) { } } -export function draw(r, name, ctx, options, ref) { - return drawFunctions[name](r, ctx, options, ref); +export function draw( + r: ShieldRenderingContext, + name: string, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) { + return drawFunctions[name](r, ctx, params, ref); } //Register draw functions @@ -710,7 +781,11 @@ const fixedWidthDefinitions = {}; * @param {*} fxn callback to the implementing function. Takes two parameters, ref and options * @param {*} fixedWidth if set, indicates that this function draws to a fixed width */ -export function registerDrawFunction(name, fxn, fixedWidth) { +export function registerDrawFunction( + name: string, + fxn: ShapeDrawFunction, + fixedWidth?: number +) { drawFunctions[name] = fxn; if (fixedWidth !== undefined) { fixedWidthDefinitions[name] = fixedWidth; diff --git a/shieldlib/src/shield_helper.d.ts b/shieldlib/src/shield_helper.d.ts new file mode 100644 index 000000000..85c942bee --- /dev/null +++ b/shieldlib/src/shield_helper.d.ts @@ -0,0 +1,145 @@ +import { ShieldDefinition, TextLayout } from "./types"; + +export declare function roundedRectTextConstraint(radius: number): TextLayout; +export declare function textConstraint(fxn: string): TextLayout; +export declare function ovalShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number +): ShieldDefinition; + +export declare function circleShield( + fillColor: string, + strokeColor: string, + textColor: string +): ShieldDefinition; + +export declare function roundedRectShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number, + radius: number +): ShieldDefinition; + +export declare function escutcheonDownShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function fishheadDownShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number +): ShieldDefinition; + +export declare function triangleDownShield( + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function trapezoidDownShield( + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function trapezoidUpShield( + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function diamondShield( + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function pentagonUpShield( + yOffset: number, + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius1: number, + radius2: number, + rectWidth: number +): ShieldDefinition; + +export declare function homePlateDownShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius1: number, + radius2: number, + rectWidth: number +): ShieldDefinition; + +export declare function homePlateUpShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius1: number, + radius2: number, + rectWidth: number +): ShieldDefinition; + +export declare function hexagonVerticalShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function hexagonHorizontalShield( + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function octagonVerticalShield( + yOffset: number, + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition; + +export declare function pillShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number +): ShieldDefinition; + +export function banneredShield( + baseDef: ShieldDefinition, + banners: string[] +): ShieldDefinition; diff --git a/shieldlib/src/shield_helper.ts b/shieldlib/src/shield_helper.ts new file mode 100644 index 000000000..64b141e59 --- /dev/null +++ b/shieldlib/src/shield_helper.ts @@ -0,0 +1,757 @@ +import { ShieldDefinition, TextLayout } from "./types"; + +/** + * Constrain the text to a rounded rectangle + * + * @param radius 1x pixel radius of the constraint corners + * @returns a constraint definition + */ +export function roundedRectTextConstraint(radius: number): TextLayout { + return { + constraintFunc: "roundedRect", + options: { + radius: radius, + }, + }; +} + +/** + * Constrain the text to a specified constraint type + * + * @returns a constraint definition + */ +export function textConstraint(fxn: string): TextLayout { + return { + constraintFunc: fxn, + }; +} + +/** + * Draws a shield with an ellipse background + * + * @param {*} fillColor - Color of ellipse background fill + * @param {*} strokeColor - Color of ellipse outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} rectWidth - Width of ellipse (defaults to variable-width) + * @returns a shield definition object + */ +export function ovalShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + return { + shapeBlank: { + drawFunc: "ellipse", + params: { + fillColor, + strokeColor, + rectWidth, + }, + }, + textLayout: textConstraint("ellipse"), + padding: { + left: 2, + right: 2, + top: 2, + bottom: 2, + }, + textColor, + }; +} + +/** + * Draws a shield with circle background (special case of ovalShield) + * + * @param {*} fillColor - Color of circle background fill + * @param {*} strokeColor - Color of circle outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @returns a shield definition object + */ +export function circleShield( + fillColor: string, + strokeColor: string, + textColor: string +): ShieldDefinition { + return ovalShield(fillColor, strokeColor, textColor, 20); +} + +/** + * Draws a shield with a rectangle background + * + * @param {*} fillColor - Color of rectangle background fill + * @param {*} strokeColor - Color of rectangle outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} rectWidth - Width of rectangle (defaults to variable-width) + * @param {*} radius - Corner radius of rectangle (defaults to 2) + * @returns a shield definition object + */ +export function roundedRectShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number, + radius: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + radius = radius ?? 2; + return { + shapeBlank: { + drawFunc: "roundedRectangle", + params: { + fillColor, + strokeColor, + rectWidth, + radius, + }, + }, + textLayout: roundedRectTextConstraint(radius), + padding: { + left: 3, + right: 3, + top: 3, + bottom: 3, + }, + textColor, + }; +} + +/** + * Draws a shield with an escutcheon background, pointed downward + * + * @param {*} yOffset - Height of curved portion + * @param {*} fillColor - Color of escutcheon background fill + * @param {*} strokeColor - Color of escutcheon outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of escutcheon (defaults to 0) + * @param {*} rectWidth - Width of escutcheon (defaults to variable-width) + * @returns a shield definition object + */ +export function escutcheonDownShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + radius = radius ?? 0; + return { + shapeBlank: { + drawFunc: "escutcheon", + params: { + yOffset, + fillColor, + strokeColor, + rectWidth, + radius, + outlineWidth: 1, + }, + }, + textLayout: roundedRectTextConstraint(radius), + padding: { + left: 2, + right: 2, + top: 2, + bottom: 0 + yOffset / 2, + }, + textColor, + }; +} + +/** + * Draws a shield with a fishhead background, pointed downward + * + * @param {*} fillColor - Color of fishhead background fill + * @param {*} strokeColor - Color of fishhead outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} rectWidth - Width of fishhead (defaults to variable-width) + * @returns a shield definition object + */ +export function fishheadDownShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + return { + shapeBlank: { + drawFunc: "fishhead", + params: { + fillColor, + strokeColor, + rectWidth, + outlineWidth: 1, + }, + }, + textLayout: textConstraint("roundedRect"), + padding: { + left: 3, + right: 3, + top: 2, + bottom: 6, + }, + textColor, + }; +} + +/** + * Draws a shield with a triangle background, pointed downward + * + * @param {*} fillColor - Color of triangle background fill + * @param {*} strokeColor - Color of triangle outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of triangle (defaults to 2) + * @param {*} rectWidth - Width of triangle (defaults to variable-width) + * @returns a shield definition object + */ +export function triangleDownShield( + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + radius = radius ?? 2; + + return { + shapeBlank: { + drawFunc: "triangle", + params: { + pointUp: false, + fillColor, + strokeColor, + rectWidth, + radius, + }, + }, + textLayout: textConstraint("triangleDown"), + padding: { + left: 1, + right: 1, + top: 2, + bottom: 1, + }, + textColor, + }; +} + +/** + * Draws a shield with a trapezoid background, with the short side on bottom + * + * @param {*} sideAngle - Angle (in degrees) at which sides deviate from vertical + * @param {*} fillColor - Color of trapezoid background fill + * @param {*} strokeColor - Color of trapezoid outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of trapezoid (defaults to 0) + * @param {*} rectWidth - Width of trapezoid (defaults to variable-width) + * @returns a shield definition object + */ +export function trapezoidDownShield( + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + let angleInRadians = (sideAngle * Math.PI) / 180; + textColor = textColor ?? strokeColor; + radius = radius ?? 0; + + return { + shapeBlank: { + drawFunc: "trapezoid", + params: { + sideAngle: angleInRadians, + fillColor, + strokeColor, + rectWidth, + radius, + }, + }, + textLayout: roundedRectTextConstraint(radius), + padding: { + left: 2 + 10 * Math.tan(angleInRadians), + right: 2 + 10 * Math.tan(angleInRadians), + top: 2, + bottom: 4, + }, + textColor, + }; +} + +/** + * Draws a shield with a trapezoid background, with the short side on top + * + * @param {*} sideAngle - Angle (in degrees) at which sides deviate from vertical + * @param {*} fillColor - Color of trapezoid background fill + * @param {*} strokeColor - Color of trapezoid outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of trapezoid (defaults to 0) + * @param {*} rectWidth - Width of trapezoid (defaults to variable-width) + * @returns a shield definition object + */ +export function trapezoidUpShield( + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + let angleInRadians = (sideAngle * Math.PI) / 180; + textColor = textColor ?? strokeColor; + radius = radius ?? 0; + return { + shapeBlank: { + drawFunc: "trapezoid", + params: { + shortSideUp: true, + sideAngle: angleInRadians, + fillColor, + strokeColor, + rectWidth, + radius, + }, + }, + textLayout: roundedRectTextConstraint(radius), + padding: { + left: 2 + 10 * Math.tan(angleInRadians), + right: 2 + 10 * Math.tan(angleInRadians), + top: 4, + bottom: 2, + }, + textColor, + }; +} + +/** + * Draws a shield with a diamond background + * + * @param {*} fillColor - Color of diamond background fill + * @param {*} strokeColor - Color of diamond outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of diamond (defaults to 2) + * @param {*} rectWidth - Width of diamond (defaults to variable-width) + * @returns a shield definition object + */ +export function diamondShield( + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + radius = radius ?? 2; + return { + shapeBlank: { + drawFunc: "diamond", + params: { + fillColor, + strokeColor, + radius, + rectWidth, + }, + }, + textLayout: textConstraint("diamond"), + padding: { + left: 1, + right: 1, + top: 1, + bottom: 1, + }, + textColor, + }; +} + +/** + * Draws a shield with a pentagon background, pointed upward + * + * @param {*} yOffset - Height of diagonal edges + * @param {*} sideAngle - Angle (in degrees) at which sides deviate from vertical + * @param {*} fillColor - Color of pentagon background fill + * @param {*} strokeColor - Color of pentagon outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius1 - Corner radius of pointed side of pentagon (defaults to 2) + * @param {*} radius2 - Corner radius of flat side of pentagon (defaults to 0) + * @param {*} rectWidth - Width of pentagon (defaults to variable-width) + * @returns a shield definition object + */ +export function pentagonUpShield( + yOffset: number, + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius1: number, + radius2: number, + rectWidth: number +): ShieldDefinition { + let angleInRadians = (sideAngle * Math.PI) / 180; + textColor = textColor ?? strokeColor; + radius1 = radius1 ?? 2; + radius2 = radius2 ?? 0; + return { + shapeBlank: { + drawFunc: "pentagon", + params: { + yOffset, + sideAngle: angleInRadians, + fillColor, + strokeColor, + radius1, + radius2, + rectWidth, + }, + }, + textLayout: { + constraintFunc: "rect", + }, + padding: { + left: 2 + ((20 - yOffset) * Math.tan(angleInRadians)) / 2, + right: 2 + ((20 - yOffset) * Math.tan(angleInRadians)) / 2, + top: 1 + yOffset / 2, + bottom: 3, + }, + textColor, + }; +} + +/** + * Draws a shield with a home plate background, pointed downward + * + * @param {*} yOffset - Height of diagonal edges + * @param {*} fillColor - Color of home plate background fill + * @param {*} strokeColor - Color of home plate outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius1 - Corner radius of pointed side of home plate (defaults to 2) + * @param {*} radius2 - Corner radius of flat side of home plate (defaults to 2) + * @param {*} rectWidth - Width of home plate (defaults to variable-width) + * @returns a shield definition object + */ +export function homePlateDownShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius1: number, + radius2: number, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + radius1 = radius1 ?? 2; + radius2 = radius2 ?? 2; + return { + shapeBlank: { + drawFunc: "pentagon", + params: { + pointUp: false, + yOffset, + sideAngle: 0, + fillColor, + strokeColor, + radius1, + radius2, + rectWidth, + }, + }, + textLayout: roundedRectTextConstraint(radius2), + padding: { + left: 2, + right: 2, + top: 2, + bottom: 1 + yOffset, + }, + textColor, + }; +} + +/** + * Draws a shield with a home plate background, pointed upward + * + * @param {*} yOffset - Height of diagonal edges + * @param {*} fillColor - Color of home plate background fill + * @param {*} strokeColor - Color of home plate outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius1 - Corner radius of pointed side of home plate (defaults to 2) + * @param {*} radius2 - Corner radius of flat side of home plate (defaults to 2) + * @param {*} rectWidth - Width of home plate (defaults to variable-width) + * @returns a shield definition object + */ +export function homePlateUpShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius1: number, + radius2: number, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + radius1 = radius1 ?? 2; + radius2 = radius2 ?? 2; + return { + shapeBlank: { + drawFunc: "pentagon", + params: { + pointUp: true, + yOffset, + sideAngle: 0, + fillColor, + strokeColor, + radius1, + radius2, + rectWidth, + }, + }, + textLayout: roundedRectTextConstraint(radius2), + padding: { + left: 2, + right: 2, + top: 1 + yOffset, + bottom: 2, + }, + textColor, + }; +} + +/** + * Draws a shield with a vertically-aligned hexagon background + * + * @param {*} yOffset - Height of diagonal edges + * @param {*} fillColor - Color of hexagon background fill + * @param {*} strokeColor - Color of hexagon outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of hexagon (defaults to 2) + * @param {*} rectWidth - Width of hexagon (defaults to variable-width) + * @returns a shield definition object + */ +export function hexagonVerticalShield( + yOffset: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + radius = radius ?? 2; + return { + shapeBlank: { + drawFunc: "hexagonVertical", + params: { + yOffset, + fillColor, + strokeColor, + radius, + rectWidth, + }, + }, + textLayout: roundedRectTextConstraint(radius), + padding: { + left: 2, + right: 2, + top: 1 + yOffset, + bottom: 1 + yOffset, + }, + textColor, + }; +} + +/** + * Draws a shield with a horizontally-aligned hexagon background + * + * @param {*} sideAngle - Angle (in degrees) at which sides deviate from vertical + * @param {*} fillColor - Color of hexagon background fill + * @param {*} strokeColor - Color of hexagon outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of hexagon (defaults to 2) + * @param {*} rectWidth - Width of hexagon (defaults to variable-width) + * @returns a shield definition object + */ +export function hexagonHorizontalShield( + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + let angleInRadians = (sideAngle * Math.PI) / 180; + textColor = textColor ?? strokeColor; + radius = radius ?? 2; + return { + shapeBlank: { + drawFunc: "hexagonHorizontal", + params: { + sideAngle: angleInRadians, + fillColor, + strokeColor, + radius, + rectWidth, + }, + }, + textLayout: textConstraint("ellipse"), + padding: { + left: 3, + right: 3, + top: 2, + bottom: 2, + }, + textColor, + }; +} + +/** + * Draws a shield with an octagon background + * + * @param {*} yOffset - Height of diagonal edges + * @param {*} sideAngle - Angle (in degrees) at which sides deviate from vertical + * @param {*} fillColor - Color of octagon background fill + * @param {*} strokeColor - Color of octagon outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} radius - Corner radius of octagon (defaults to 2) + * @param {*} rectWidth - Width of octagon (defaults to variable-width) + * @returns a shield definition object + */ +export function octagonVerticalShield( + yOffset: number, + sideAngle: number, + fillColor: string, + strokeColor: string, + textColor: string, + radius: number, + rectWidth: number +): ShieldDefinition { + let angleInRadians = (sideAngle * Math.PI) / 180; + textColor = textColor ?? strokeColor; + radius = radius ?? 2; + return { + shapeBlank: { + drawFunc: "octagonVertical", + params: { + yOffset, + sideAngle: angleInRadians, + fillColor, + strokeColor, + radius, + rectWidth, + }, + }, + textLayout: textConstraint("ellipse"), + padding: { + left: 2, + right: 2, + top: 2, + bottom: 2, + }, + textColor, + }; +} + +/** + * Draws a shield with a pill-shaped background + * + * @param {*} fillColor - Color of pill background fill + * @param {*} strokeColor - Color of pill outline stroke + * @param {*} textColor - Color of text (defaults to strokeColor) + * @param {*} rectWidth - Width of pill (defaults to variable-width) + * @returns a shield definition object + */ +export function pillShield( + fillColor: string, + strokeColor: string, + textColor: string, + rectWidth: number +): ShieldDefinition { + textColor = textColor ?? strokeColor; + return { + shapeBlank: { + drawFunc: "roundedRectangle", + params: { + fillColor, + strokeColor, + rectWidth, + radius: 10, + }, + }, + textLayout: textConstraint("ellipse"), + padding: { + left: 2, + right: 2, + top: 2, + bottom: 2, + }, + textColor, + }; +} + +/** + * Adds banner text above a shield + * + * @param {*} baseDef - Shield definition object + * @param {*} banners - Array of strings to be displayed above shield + * @returns a shield definition object + */ +export function banneredShield( + baseDef: ShieldDefinition, + banners: string[] +): ShieldDefinition { + return { + banners, + ...baseDef, + }; +} + +/** + * Draws a circle icon inside a black-outlined white square shield + * + * @param {*} fillColor - Color of circle icon background fill + * @param {*} strokeColor - Color of circle icon outline + * @returns a shield definition object + */ +export function paBeltShield( + fillColor: string, + strokeColor: string +): ShieldDefinition { + return { + notext: true, + shapeBlank: { + drawFunc: "paBelt", + params: { + fillColor, + strokeColor, + }, + }, + }; +} + +/** + * Draws a Branson, Missouri route shield + * + * @param {*} fillColor - Color of rectangle icon background fill + * @param {*} strokeColor - Color of rectangle icon outline + * @returns a shield definition object + */ +export function bransonRouteShield( + fillColor: string, + strokeColor: string +): ShieldDefinition { + return { + notext: true, + shapeBlank: { + drawFunc: "branson", + params: { + fillColor, + strokeColor, + }, + }, + }; +} diff --git a/shieldlib/src/shield_renderer.d.ts b/shieldlib/src/shield_renderer.d.ts index 7b3fd26a9..fc9d9ac19 100644 --- a/shieldlib/src/shield_renderer.d.ts +++ b/shieldlib/src/shield_renderer.d.ts @@ -4,6 +4,7 @@ import { DebugOptions, GraphicsFactory, RouteParser, + ShapeBlankParams, ShieldDefinitions, ShieldOptions, ShieldSpecification, @@ -21,7 +22,13 @@ export declare class ShieldRenderingContext { px(pixels: number): number; shieldSize(): number; } -declare class AbstractShieldRenderer { +export declare type ShapeDrawFunction = ( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) => void; +export declare class AbstractShieldRenderer { private _shieldPredicate; private _networkPredicate; private _routeParser; diff --git a/shieldlib/src/shield_renderer.ts b/shieldlib/src/shield_renderer.ts index 1f421dc3a..570dc3aa6 100644 --- a/shieldlib/src/shield_renderer.ts +++ b/shieldlib/src/shield_renderer.ts @@ -5,6 +5,7 @@ import { GraphicsFactory, RouteDefinition, RouteParser, + ShapeBlankParams, ShieldDefinitions, ShieldOptions, ShieldSpecification, @@ -43,6 +44,13 @@ export class ShieldRenderingContext { } } +export type ShapeDrawFunction = ( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +) => void; + class MaplibreGLSpriteRepository implements SpriteRepository { map: Map; constructor(map: Map) { @@ -56,19 +64,23 @@ class MaplibreGLSpriteRepository implements SpriteRepository { } } -class AbstractShieldRenderer { +/** Base class for shield renderers. Shield renderers use a builder pattern to configure its options. */ +export class AbstractShieldRenderer { private _shieldPredicate: StringPredicate = () => true; private _networkPredicate: StringPredicate = () => true; private _routeParser: RouteParser; + /** @hidden */ private _renderContext: ShieldRenderingContext; private _shieldDefCallbacks = []; + /** Create a shield renderer */ constructor(routeParser: RouteParser) { this._routeParser = routeParser; this._renderContext = new ShieldRenderingContext(); this._renderContext.gfxFactory = new DOMGraphicsFactory(); } + /** Specify which shields to draw and with what graphics */ protected setShields(shieldSpec: ShieldSpecification) { this._renderContext.options = shieldSpec.options; this._renderContext.shieldDef = shieldSpec.networks; @@ -77,15 +89,18 @@ class AbstractShieldRenderer { ); } + /** Get the shield definitions */ public getShieldDefinitions(): ShieldDefinitions { return this._renderContext.shieldDef; } + /** Set debugging options */ public debugOptions(debugOptions: DebugOptions): AbstractShieldRenderer { this._renderContext.debugOptions = debugOptions; return this; } + /** Set which unhandled sprite IDs this renderer handles */ public filterImageID( shieldPredicate: StringPredicate ): AbstractShieldRenderer { @@ -93,6 +108,7 @@ class AbstractShieldRenderer { return this; } + /** Set which network values this renderer handles */ public filterNetwork( networkPredicate: StringPredicate ): AbstractShieldRenderer { @@ -100,17 +116,20 @@ class AbstractShieldRenderer { return this; } + /** Set which graphics context to draw shields to */ public graphicsFactory(gfxFactory: GraphicsFactory): AbstractShieldRenderer { this._renderContext.gfxFactory = gfxFactory; return this; } + /** Set which MaplibreGL map to handle shields for */ public renderOnMaplibreGL(map: Map): AbstractShieldRenderer { this.renderOnRepository(new MaplibreGLSpriteRepository(map)); map.on("styleimagemissing", this.getStyleImageMissingHandler()); return this; } + /** Set a callback that fires when shield definitions are loaded */ public onShieldDefLoad( callback: (shields: ShieldDefinitions) => void ): AbstractShieldRenderer { @@ -122,6 +141,7 @@ class AbstractShieldRenderer { return this; } + /** Set the storage location for existing and generated sprite images */ public renderOnRepository(repo: SpriteRepository): AbstractShieldRenderer { if (!this._renderContext.spriteRepo) { this._renderContext.spriteRepo = repo; @@ -129,6 +149,11 @@ class AbstractShieldRenderer { return this; } + /** + * Get the handler function for styleimagemissing event calls + * + * See [MapStyleImageMissingEvent](https://maplibre.org/maplibre-gl-js/docs/API/types/maplibregl.MapStyleImageMissingEvent/) for more details. + **/ public getStyleImageMissingHandler() { return (e: MapStyleImageMissingEvent) => { try { @@ -140,10 +165,8 @@ class AbstractShieldRenderer { storeNoShield(this._renderContext, e.id); return; } - routeDef.spriteID = e.id; //Original ID so we can store the sprite - this._renderContext.debugOptions = this.debugOptions; if (routeDef) { - missingIconLoader(this._renderContext, routeDef); + missingIconLoader(this._renderContext, routeDef, e.id); } } catch (err) { console.error(`Exception while loading image ‘${e?.id}’:\n`, err); @@ -151,28 +174,56 @@ class AbstractShieldRenderer { }; } + /** Get the graphic for a specified route */ public getGraphicForRoute(network: string, ref: string, name: string) { return generateShieldCtx(this._renderContext, { network, ref, name, - spriteID: this._routeParser.format(network, ref, name), }); } + /** Get a blank route shield sprite in the default size */ public emptySprite(): CanvasRenderingContext2D { return this._renderContext.emptySprite(); } + /** Get a blank route shield graphics context in a specified size */ public createGraphics(bounds: Bounds) { return this._renderContext.gfxFactory.createGraphics(bounds); } + /** Get the current pixel ration (1x/2x) */ public pixelRatio(): number { return this._renderContext.px(1); } } +/** + * A shield renderer configured from a JSON specification + * + * @example + * + * const shields = { + * "US:I": { + * textColor: Color.shields.white, + * spriteBlank: ["shield_us_interstate_2", "shield_us_interstate_3"], + * textLayout: textConstraint("southHalfEllipse"), + * padding: { + * left: 4, + * right: 4, + * top: 6, + * bottom: 5, + * }, + * } + * }; + * + * const shieldRenderer = new ShieldRenderer(shields, routeParser) + * .filterImageID(shieldPredicate) + * .filterNetwork(networkPredicate) + * .renderOnMaplibreGL(map) + * .onShieldDefLoad((shields) => afterShieldRendererLoads(shields)); //Post config + */ export class ShieldRenderer extends AbstractShieldRenderer { constructor(shieldSpec: ShieldSpecification, routeParser: RouteParser) { super(routeParser); @@ -180,13 +231,30 @@ export class ShieldRenderer extends AbstractShieldRenderer { } } +/** + * A shield renderer configured from a URL containing a JSON specification + * + * @example + * + * const shieldRenderer = new URLShieldRenderer("shields.json", routeParser) + * .filterImageID(shieldPredicate) + * .filterNetwork(networkPredicate) + * .renderOnMaplibreGL(map) + * .onShieldDefLoad((shields) => afterShieldRendererLoads(shields)); //Post config + **/ export class URLShieldRenderer extends AbstractShieldRenderer { - constructor(shieldsURL: URL, routeParser: RouteParser) { + constructor( + /** URL containing the JSON shield definition */ + shieldsURL: URL, + /** Function that extracts route identification from a sprite string */ + routeParser: RouteParser + ) { super(routeParser); this.setShieldURL(shieldsURL); } - public async setShieldURL(shieldsURL: URL) { + /** Set the URL containing the shield specification */ + private async setShieldURL(shieldsURL: URL) { await fetch(shieldsURL) .then((res) => res.json()) .then((json) => super.setShields(json)) @@ -194,6 +262,7 @@ export class URLShieldRenderer extends AbstractShieldRenderer { } } +/** @hidden Used for testing */ export class InMemorySpriteRepository implements SpriteRepository { sprites = {}; public getSprite(spriteID: string): StyleImage { diff --git a/shieldlib/src/shield_text.ts b/shieldlib/src/shield_text.ts new file mode 100644 index 000000000..e9dc755ef --- /dev/null +++ b/shieldlib/src/shield_text.ts @@ -0,0 +1,473 @@ +"use strict"; + +import * as Gfx from "./screen_gfx.js"; +import { ShieldRenderingContext } from "./shield_renderer.js"; +import { + BoxPadding, + ShieldDefinition, + TextLayout, + TextLayoutParameters, +} from "./types.js"; + +const VerticalAlignment = { + Middle: "middle", + Top: "top", + Bottom: "bottom", +} as const; + +type VerticalAlignmentType = + (typeof VerticalAlignment)[keyof typeof VerticalAlignment]; + +interface Dimension { + width: number; + height: number; +} + +type TextLayoutScaler = ( + availSize: Dimension, + textSize: Dimension, + options?: TextLayoutParameters +) => TextTransform; + +interface TextTransform { + scale: number; + valign: VerticalAlignmentType; +} + +interface TextPlacement { + xBaseline: number; + yBaseline: number; + fontPx: number; +} + +let noPadding: BoxPadding = { + top: 0, + bottom: 0, + left: 0, + right: 0, +}; + +let bannerLayout: TextLayout = { + constraintFunc: "rectangle", +}; + +function ellipseScale(spaceBounds: Dimension, textBounds: Dimension): number { + //Math derived from https://mathworld.wolfram.com/Ellipse-LineIntersection.html + var a = spaceBounds.width; + var b = spaceBounds.height; + + var x0 = textBounds.width; + var y0 = textBounds.height; + + return (a * b) / Math.sqrt(a * a * y0 * y0 + b * b * x0 * x0); +} + +function ellipseTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { + return { + scale: ellipseScale(spaceBounds, textBounds), + valign: VerticalAlignment.Middle, + }; +} + +function southHalfEllipseTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { + return { + scale: ellipseScale(spaceBounds, { + //Turn ellipse 90 degrees + height: textBounds.width / 2, + width: textBounds.height, + }), + valign: VerticalAlignment.Top, + }; +} + +function rectTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { + var scaleHeight = spaceBounds.height / textBounds.height; + var scaleWidth = spaceBounds.width / textBounds.width; + + return { + scale: Math.min(scaleWidth, scaleHeight), + valign: VerticalAlignment.Middle, + }; +} + +function roundedRectTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension, + options: TextLayoutParameters +): TextTransform { + //Shrink space bounds so that corners hit the arcs + let constraintRadius = 2; + if (options !== undefined && options.radius !== undefined) { + constraintRadius = options.radius; + } + + return rectTextConstraint( + { + width: spaceBounds.width - constraintRadius * (2 - Math.sqrt(2)), + height: spaceBounds.height - constraintRadius * (2 - Math.sqrt(2)), + }, + textBounds + ); +} + +function diamondTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { + let a = spaceBounds.width; + let b = spaceBounds.height; + + let x0 = textBounds.width; + let y0 = textBounds.height; + + return { + scale: (a * b) / (b * x0 + a * y0), + valign: VerticalAlignment.Middle, + }; +} + +function triangleDownTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { + return { + scale: diamondTextConstraint(spaceBounds, textBounds).scale, + valign: VerticalAlignment.Top, + }; +} + +/** + * Determines the position and font size to draw text so that it fits within + * a bounding box. + * + * @param {*} r - rendering context + * @param {*} text - text to draw + * @param {*} padding - top/bottom/left/right padding around text + * @param {*} bounds - size of the overall graphics area + * @param {*} textLayoutDef - algorithm definition for text scaling + * @param {*} maxFontSize - maximum font size + * @returns JOSN object containing (X,Y) draw position and font size + */ +function layoutShieldText( + r: ShieldRenderingContext, + text: string, + padding: BoxPadding, + bounds: Dimension, + textLayoutDef: TextLayout, + maxFontSize: number = 14 +): TextPlacement { + var padTop = r.px(padding.top) || 0; + var padBot = r.px(padding.bottom) || 0; + var padLeft = r.px(padding.left) || 0; + var padRight = r.px(padding.right) || 0; + + var maxFont = r.px(maxFontSize); + //Temporary canvas for text measurment + var ctx = r.gfxFactory.createGraphics(bounds); + + ctx.font = Gfx.shieldFont(Gfx.fontSizeThreshold, r.options.shieldFont); + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + + var metrics = ctx.measureText(text); + + var textWidth = metrics.width; + var textHeight = metrics.actualBoundingBoxDescent; + + var availHeight = bounds.height - padTop - padBot; + var availWidth = bounds.width - padLeft - padRight; + + var xBaseline = padLeft + availWidth / 2; + + let textLayoutFunc = drawTextFunctions[textLayoutDef.constraintFunc]; + + let textConstraint = textLayoutFunc( + { height: availHeight, width: availWidth }, + { height: textHeight, width: textWidth }, + textLayoutDef.options + ); + + //If size-to-fill shield text is too big, shrink it + var fontSize = Math.min( + maxFont, + Gfx.fontSizeThreshold * textConstraint.scale + ); + + ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont); + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + + metrics = ctx.measureText(text); + textHeight = metrics.actualBoundingBoxDescent; + + let yBaseline: number; + + switch (textConstraint.valign) { + case VerticalAlignment.Top: + yBaseline = padTop; + break; + case VerticalAlignment.Bottom: + yBaseline = padTop + availHeight - textHeight; + break; + case VerticalAlignment.Middle: + default: + yBaseline = padTop + (availHeight - textHeight) / 2; + break; + } + + return { + xBaseline, + yBaseline, + fontPx: fontSize, + }; +} + +const defaultDefForLayout: ShieldDefinition = { + padding: { + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + shapeBlank: { + drawFunc: "rectangle", + params: { + fillColor: "white", + strokeColor: "black", + }, + }, +}; + +/** + * Determines the position and font size to draw text so that it fits within + * a bounding box. + * + * @param {*} r - rendering context + * @param {*} text - text to draw + * @param {*} def - shield definition + * @param {*} bounds - size of the overall graphics area + * @returns JOSN object containing (X,Y) draw position and font size + */ +export function layoutShieldTextFromDef( + r: ShieldRenderingContext, + text: string, + def: ShieldDefinition, + bounds: Dimension +): TextPlacement { + //FIX + if (def == null) { + def = defaultDefForLayout; + } + + var padding = def.padding || noPadding; + + var textLayoutDef = { + constraintFunc: "rect", + }; + + var maxFontSize = 14; // default max size + + if (typeof def.textLayout != "undefined") { + textLayoutDef = def.textLayout; + } + + if (typeof def.maxFontSize != "undefined") { + maxFontSize = Math.min(maxFontSize, def.maxFontSize); // shield definition cannot set max size higher than default + } + + return layoutShieldText(r, text, padding, bounds, textLayoutDef, maxFontSize); +} + +/** + * Draw text on a shield + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} textLayout - location to draw text + */ +export function renderShieldText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + textLayout: TextPlacement +): void { + //Text color is set by fillStyle + configureShieldText(r, ctx, textLayout); + + ctx.fillText(text, textLayout.xBaseline, textLayout.yBaseline); +} + +/** + * Draw drop shadow for text on a shield + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} textLayout - location to draw text + */ +export function drawShieldHaloText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + textLayout: TextPlacement +): void { + //Stroke color is set by strokeStyle + configureShieldText(r, ctx, textLayout); + + ctx.shadowColor = ctx.strokeStyle.toString(); + ctx.shadowBlur = 0; + ctx.lineWidth = r.px(2); + + ctx.strokeText(text, textLayout.xBaseline, textLayout.yBaseline); + ctx.shadowColor = null; + ctx.shadowBlur = null; +} + +function configureShieldText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + textLayout: TextPlacement +): void { + ctx.textAlign = "center"; + ctx.textBaseline = "top"; + ctx.font = Gfx.shieldFont(textLayout.fontPx, r.options.shieldFont); +} + +/** + * Draw text on a modifier plate above a shield + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} bannerIndex - plate position to draw, 0=top, incrementing + */ +export function drawBannerText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number +): void { + drawBannerTextComponent(r, ctx, text, bannerIndex, true); +} + +/** + * Draw drop shadow for text on a modifier plate above a shield + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} bannerIndex - plate position to draw, 0=top, incrementing + */ +export function drawBannerHaloText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number +): void { + drawBannerTextComponent(r, ctx, text, bannerIndex, false); +} + +/** + * Banners are composed of two components: text on top, and a shadow beneath. + * + * @param {*} r - rendering context + * @param {*} ctx - graphics context to draw to + * @param {*} text - text to draw + * @param {*} bannerIndex - plate position to draw, 0=top, incrementing + * @param {*} textComponent - if true, draw the text. If false, draw the halo + */ +function drawBannerTextComponent( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number, + textComponent: boolean +): void { + const bannerPadding = { + top: r.options.bannerPadding, + bottom: 0, + left: 0, + right: 0, + }; + + let bannerBounds: Dimension = { + width: ctx.canvas.width, + height: r.px(r.options.bannerHeight - r.options.bannerPadding), + }; + + let textLayout: TextPlacement = layoutShieldText( + r, + text, + bannerPadding, + bannerBounds, + bannerLayout + ); + + ctx.font = Gfx.shieldFont(textLayout.fontPx, r.options.shieldFont); + ctx.textBaseline = "top"; + ctx.textAlign = "center"; + + if (textComponent) { + ctx.fillStyle = r.options.bannerTextColor; + ctx.fillText( + text, + textLayout.xBaseline, + textLayout.yBaseline + + bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + ); + } else { + ctx.strokeStyle = ctx.shadowColor = r.options.bannerTextHaloColor; + ctx.shadowBlur = 0; + ctx.lineWidth = r.px(2); + ctx.strokeText( + text, + textLayout.xBaseline, + textLayout.yBaseline + + bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + ); + + ctx.shadowColor = null; + ctx.shadowBlur = null; + } +} + +export function calculateTextWidth( + r: ShieldRenderingContext, + text: string, + fontSize: number +): number { + var ctx = r.emptySprite(); //dummy canvas + ctx.font = Gfx.shieldFont(fontSize, r.options.shieldFont); + return Math.ceil(ctx.measureText(text).width); +} + +//Register text draw functions +const drawTextFunctions = {}; + +/** + * Invoked by a style to implement a custom draw function + * + * @param {*} name name of the function as referenced by the shield definition + * @param {*} fxn callback to the implementing function. Takes two parameters, ref and options + */ +function registerDrawTextFunction(name: string, fxn: TextLayoutScaler): void { + drawTextFunctions[name] = fxn; +} + +//Built-in draw functions (standard shapes) +registerDrawTextFunction("diamond", diamondTextConstraint); +registerDrawTextFunction("ellipse", ellipseTextConstraint); +registerDrawTextFunction("rect", rectTextConstraint); +registerDrawTextFunction("roundedRect", roundedRectTextConstraint); +registerDrawTextFunction("southHalfEllipse", southHalfEllipseTextConstraint); +registerDrawTextFunction("triangleDown", triangleDownTextConstraint); diff --git a/shieldlib/src/types.d.ts b/shieldlib/src/types.d.ts index eb2a19e7d..363863df1 100644 --- a/shieldlib/src/types.d.ts +++ b/shieldlib/src/types.d.ts @@ -3,7 +3,6 @@ export interface RouteDefinition { network: string; ref: string; name: string; - spriteID?: string; } export interface ShieldDefinition { spriteBlank: string[]; diff --git a/shieldlib/src/types.ts b/shieldlib/src/types.ts index 170eef2bc..2c1038dbb 100644 --- a/shieldlib/src/types.ts +++ b/shieldlib/src/types.ts @@ -1,73 +1,214 @@ import { StyleImage } from "maplibre-gl"; +/** Defines the set of routes that a shield applies to */ export interface RouteDefinition { + /** Only match routes with this network value */ network: string; - ref: string; - name: string; - spriteID?: string; + /** If set, only match routes with this ref value */ + ref?: string; + /** If set, only match routes with this name value */ + name?: string; } -export interface ShieldDefinition { - spriteBlank: string[]; - textColor: string; - padding: { - left: number; - right: number; - top: number; - bottom: number; - }; +/** Enforce a requirement that one field OR another field must be specified, but not both */ +export type Exclusive = + | (T & { [P in keyof U]?: never }) + | (U & { [P in keyof T]?: never }); + +/** Parameters that apply to all types of shield definitions */ +export interface ShieldDefinitionBase { + /** Color of text drawn on a shield */ + textColor?: string; + /** Padding around shield text */ + padding?: BoxPadding; + /** Algorithm for expanding text to fill a shield background */ + textLayout?: TextLayout; + /** Banners to be drawn above a shield */ + banners?: string[]; + /** If true, no next should be drawn on this shield */ + notext?: boolean; + /** Maximum size of shield text */ + maxFontSize?: number; +} + +/** + * Define how the renderer should draw the shield for various routes + * + * @example + * const shieldsDefinition = { + * "US:I": { + * textColor: Color.shields.white, + * spriteBlank: ["shield_us_interstate_2", "shield_us_interstate_3"], + * textLayout: textConstraint("southHalfEllipse"), + * padding: { + * left: 4, + * right: 4, + * top: 6, + * bottom: 5, + * } + * }; + */ +export type ShieldDefinition = Exclusive< + { spriteBlank: string[] }, + { shapeBlank: ShapeDefinition } +> & + ShieldDefinitionBase; + +/** Define a shield which is created by drawing a shape, optionally with text on top */ +export interface ShapeDefinition { + /** Which shape to draw */ + drawFunc: string; + /** Parameters for drawing the shape */ + params: ShapeBlankParams; +} + +/** Rectangular padding values */ +export interface BoxPadding { + /** Minimum padding to the left of the text */ + left: number; + /** Minimum padding to the right of the text */ + right: number; + /** Minimum padding above the text */ + top: number; + /** Minimum padding below the text */ + bottom: number; } +/** Parameters for drawing shield shapes */ +export interface ShapeBlankParams { + /** Fill color of the shape */ + fillColor: string; + /** Stroke (border) color */ + strokeColor: string; + /** Width of the shape */ + rectWidth?: number; + /** Radius of the shape's corners */ + radius?: number; + /** Radius of the shape's first corner. This is used for shapes that can specify multiple radius values */ + radius1?: number; + /** Radius of the shape's second corner. This is used for shapes that can specify multiple radius values */ + radius2?: number; + /** Distance from top or bottom edge to vertices. Higher number means pointier top and/or bottom */ + yOffset?: number; + /** Width of the shape's outline */ + outlineWidth?: number; + /** Specify whether the pointy end of the shape is on top */ + pointUp?: boolean; + /** Specify whether the short side of the shape is on top */ + shortSideUp?: boolean; + /** Specify the angle at which the sides of the shape deviate from vertical. Higher number means pointier sides */ + sideAngle?: number; +} + +/** Definition for laying out text on a shield background */ +export interface TextLayout { + constraintFunc: string; + options?: TextLayoutParameters; +} + +/** Options for text layout on a shield */ +export interface TextLayoutParameters { + radius: number; +} + +/** + * A predicate which determines whether to draw a shield for a particular sprite ID. + * This allows the library to consume a subset of sprite IDs passed in from maplibre + * while allowing other code to handle other sprite IDs. + */ export type StringPredicate = (spriteID: string) => boolean; -// RouteParser unpacks a route definition from a sprite image string. +/** RouteParser unpacks a route definition from a sprite image string */ export interface RouteParser { parse(spriteID: string): RouteDefinition; format(network: string, ref: string, name: string): string; } -// SpriteProducer returns a sprite graphic based on an ID. +/** Retrieve a sprite graphic based on an ID */ export interface SpriteProducer { getSprite(spriteID: string): StyleImage; } -// SpriteConsumer stores a sprite graphic based on an ID. +/** Store a sprite graphic based on an ID */ export interface SpriteConsumer { putSprite(spriteID: string, image: ImageData, pixelRatio: number): void; } +/** Respository that can store and retrieve sprite graphics */ export type SpriteRepository = SpriteProducer & SpriteConsumer; -// ShieldDefinitions maps routes to visual appearances +/** Map of shield definitions that associates a network name to its rendering */ export interface ShieldDefinitions { shield: { [key: string]: ShieldDefinition; }; } -export interface DebugOptions {} +/** Additional debugging-only options */ +export interface DebugOptions { + /** If set, draw a colored box around shield text constraint */ + shieldTextBboxColor?: string; +} +/** Global options for shield rendering */ export interface ShieldOptions { + /** Height of each specified banner above the shield */ bannerHeight: number; + /** Padding between each banner */ bannerPadding: number; + /** Color of text on the banner */ bannerTextColor: string; + /** Color of halo on text on the banner */ bannerTextHaloColor: string; + /** Browser font for banner text */ shieldFont: string; + /** Default shield size in pixels at 1x */ shieldSize: number; } +/** + * A user-supplied specification for rendering shields + * + * @example + * + * const shieldsSpecification = { + * shields: { + * "US:I": { + * textColor: Color.shields.white, + * spriteBlank: ["shield_us_interstate_2", "shield_us_interstate_3"], + * textLayout: textConstraint("southHalfEllipse"), + * padding: { + * left: 4, + * right: 4, + * top: 6, + * bottom: 5, + * }, + * } + * }, + * options: { + * bannerTextColor: "#000", + * bannerTextHaloColor: "#FFF", + * bannerHeight: 9, + * bannerPadding: 1, + * shieldFont: '"sans-serif-condensed", "Arial Narrow", sans-serif', + * shieldSize: 20, + * } + * }; + * + */ export interface ShieldSpecification { + /** Shield definitions */ networks: ShieldDefinitions; + /** Shield options */ options: ShieldOptions; } +/** Rectangular bounds, in scaled pixels */ export type Bounds = { width: number; height: number; }; -// export type GfxFactory = (bounds: Bounds) => CanvasRenderingContext2D; - export interface GraphicsFactory { createGraphics(bounds: Bounds): CanvasRenderingContext2D; /** diff --git a/src/js/shield_defs.js b/src/js/shield_defs.js index e4534a85b..54b34f5e8 100644 --- a/src/js/shield_defs.js +++ b/src/js/shield_defs.js @@ -1,717 +1,28 @@ "use strict"; import * as Color from "../constants/color.js"; - -/** - * Draws a shield with an ellipse background - * - * @param {*} fillColor - Color of ellipse background fill - * @param {*} strokeColor - Color of ellipse outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} rectWidth - Width of ellipse (defaults to variable-width) - * @returns a shield definition object - */ -function ovalShield(fillColor, strokeColor, textColor, rectWidth) { - textColor = textColor ?? strokeColor; - return { - shapeBlank: { - drawFunc: "ellipse", - params: { - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - }, - }, - textLayout: textConstraint("ellipse"), - padding: { - left: 2, - right: 2, - top: 2, - bottom: 2, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with circle background (special case of ovalShield) - * - * @param {*} fillColor - Color of circle background fill - * @param {*} strokeColor - Color of circle outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @returns a shield definition object - */ -function circleShield(fillColor, strokeColor, textColor) { - return ovalShield(fillColor, strokeColor, textColor, 20); -} - -function roundedRectTextConstraint(radius) { - return { - constraintFunc: "roundedRect", - options: { - radius: radius, - }, - }; -} - -function textConstraint(fxn) { - return { - constraintFunc: fxn, - }; -} - -/** - * Draws a shield with a rectangle background - * - * @param {*} fillColor - Color of rectangle background fill - * @param {*} strokeColor - Color of rectangle outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} rectWidth - Width of rectangle (defaults to variable-width) - * @param {*} radius - Corner radius of rectangle (defaults to 2) - * @returns a shield definition object - */ -function roundedRectShield( - fillColor, - strokeColor, - textColor, - rectWidth, - radius -) { - textColor = textColor ?? strokeColor; - radius = radius ?? 2; - return { - shapeBlank: { - drawFunc: "roundedRectangle", - params: { - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - radius: radius, - }, - }, - textLayout: roundedRectTextConstraint(radius), - padding: { - left: 3, - right: 3, - top: 3, - bottom: 3, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with an escutcheon background, pointed downward - * - * @param {*} offset - Height of curved portion - * @param {*} fillColor - Color of escutcheon background fill - * @param {*} strokeColor - Color of escutcheon outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of escutcheon (defaults to 0) - * @param {*} rectWidth - Width of escutcheon (defaults to variable-width) - * @returns a shield definition object - */ -function escutcheonDownShield( - offset, - fillColor, - strokeColor, - textColor, - radius, - rectWidth -) { - textColor = textColor ?? strokeColor; - radius = radius ?? 0; - return { - shapeBlank: { - drawFunc: "escutcheon", - params: { - offset: offset, - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - radius: radius, - outlineWidth: 1, - }, - }, - textLayout: roundedRectTextConstraint(radius), - padding: { - left: 2, - right: 2, - top: 2, - bottom: 0 + offset / 2, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a fishhead background, pointed downward - * - * @param {*} fillColor - Color of fishhead background fill - * @param {*} strokeColor - Color of fishhead outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} rectWidth - Width of fishhead (defaults to variable-width) - * @returns a shield definition object - */ -function fishheadDownShield(fillColor, strokeColor, textColor, rectWidth) { - textColor = textColor ?? strokeColor; - return { - shapeBlank: { - drawFunc: "fishhead", - params: { - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - outlineWidth: 1, - }, - }, - textLayout: textConstraint("roundedRect"), - padding: { - left: 3, - right: 3, - top: 2, - bottom: 6, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a triangle background, pointed downward - * - * @param {*} fillColor - Color of triangle background fill - * @param {*} strokeColor - Color of triangle outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of triangle (defaults to 2) - * @param {*} rectWidth - Width of triangle (defaults to variable-width) - * @returns a shield definition object - */ -function triangleDownShield( - fillColor, - strokeColor, - textColor, - radius, - rectWidth -) { - textColor = textColor ?? strokeColor; - radius = radius ?? 2; - - return { - shapeBlank: { - drawFunc: "triangle", - params: { - pointUp: false, - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - radius: radius, - }, - }, - textLayout: textConstraint("triangleDown"), - padding: { - left: 1, - right: 1, - top: 2, - bottom: 1, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a trapezoid background, with the short side on bottom - * - * @param {*} angle - Angle (in degrees) at which sides deviate from vertical - * @param {*} fillColor - Color of trapezoid background fill - * @param {*} strokeColor - Color of trapezoid outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of trapezoid (defaults to 0) - * @param {*} rectWidth - Width of trapezoid (defaults to variable-width) - * @returns a shield definition object - */ -function trapezoidDownShield( - angle, - fillColor, - strokeColor, - textColor, - radius, - rectWidth -) { - let angleInRadians = (angle * Math.PI) / 180; - textColor = textColor ?? strokeColor; - radius = radius ?? 0; - - return { - shapeBlank: { - drawFunc: "trapezoid", - params: { - angle: angleInRadians, - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - radius: radius, - }, - }, - textLayout: roundedRectTextConstraint(radius), - padding: { - left: 2 + 10 * Math.tan(angleInRadians), - right: 2 + 10 * Math.tan(angleInRadians), - top: 2, - bottom: 4, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a trapezoid background, with the short side on top - * - * @param {*} angle - Angle (in degrees) at which sides deviate from vertical - * @param {*} fillColor - Color of trapezoid background fill - * @param {*} strokeColor - Color of trapezoid outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of trapezoid (defaults to 0) - * @param {*} rectWidth - Width of trapezoid (defaults to variable-width) - * @returns a shield definition object - */ -function trapezoidUpShield( - angle, - fillColor, - strokeColor, - textColor, - radius, - rectWidth -) { - let angleInRadians = (angle * Math.PI) / 180; - textColor = textColor ?? strokeColor; - radius = radius ?? 0; - return { - shapeBlank: { - drawFunc: "trapezoid", - params: { - shortSideUp: true, - angle: angleInRadians, - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - radius: radius, - }, - }, - textLayout: roundedRectTextConstraint(radius), - padding: { - left: 2 + 10 * Math.tan(angleInRadians), - right: 2 + 10 * Math.tan(angleInRadians), - top: 4, - bottom: 2, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a diamond background - * - * @param {*} fillColor - Color of diamond background fill - * @param {*} strokeColor - Color of diamond outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of diamond (defaults to 2) - * @param {*} rectWidth - Width of diamond (defaults to variable-width) - * @returns a shield definition object - */ -function diamondShield(fillColor, strokeColor, textColor, radius, rectWidth) { - textColor = textColor ?? strokeColor; - radius = radius ?? 2; - return { - shapeBlank: { - drawFunc: "diamond", - params: { - fillColor: fillColor, - strokeColor: strokeColor, - radius: radius, - rectWidth: rectWidth, - }, - }, - textLayout: textConstraint("diamond"), - padding: { - left: 1, - right: 1, - top: 1, - bottom: 1, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a pentagon background, pointed upward - * - * @param {*} offset - Height of diagonal edges - * @param {*} angle - Angle (in degrees) at which sides deviate from vertical - * @param {*} fillColor - Color of pentagon background fill - * @param {*} strokeColor - Color of pentagon outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius1 - Corner radius of pointed side of pentagon (defaults to 2) - * @param {*} radius2 - Corner radius of flat side of pentagon (defaults to 0) - * @param {*} rectWidth - Width of pentagon (defaults to variable-width) - * @returns a shield definition object - */ -function pentagonUpShield( - offset, - angle, - fillColor, - strokeColor, - textColor, - radius1, - radius2, - rectWidth -) { - let angleInRadians = (angle * Math.PI) / 180; - textColor = textColor ?? strokeColor; - radius1 = radius1 ?? 2; - radius2 = radius2 ?? 0; - return { - shapeBlank: { - drawFunc: "pentagon", - params: { - offset: offset, - angle: angleInRadians, - fillColor: fillColor, - strokeColor: strokeColor, - radius1: radius1, - radius2: radius2, - rectWidth: rectWidth, - }, - }, - textLayout: { - constraintFunc: "rect", - }, - padding: { - left: 2 + ((20 - offset) * Math.tan(angleInRadians)) / 2, - right: 2 + ((20 - offset) * Math.tan(angleInRadians)) / 2, - top: 1 + offset / 2, - bottom: 3, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a home plate background, pointed downward - * - * @param {*} offset - Height of diagonal edges - * @param {*} fillColor - Color of home plate background fill - * @param {*} strokeColor - Color of home plate outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius1 - Corner radius of pointed side of home plate (defaults to 2) - * @param {*} radius2 - Corner radius of flat side of home plate (defaults to 2) - * @param {*} rectWidth - Width of home plate (defaults to variable-width) - * @returns a shield definition object - */ -function homePlateDownShield( - offset, - fillColor, - strokeColor, - textColor, - radius1, - radius2, - rectWidth -) { - textColor = textColor ?? strokeColor; - radius1 = radius1 ?? 2; - radius2 = radius2 ?? 2; - return { - shapeBlank: { - drawFunc: "pentagon", - params: { - pointUp: false, - offset: offset, - angle: 0, - fillColor: fillColor, - strokeColor: strokeColor, - radius1: radius1, - radius2: radius2, - rectWidth: rectWidth, - }, - }, - textLayout: roundedRectTextConstraint(radius2), - padding: { - left: 2, - right: 2, - top: 2, - bottom: 1 + offset, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a home plate background, pointed upward - * - * @param {*} offset - Height of diagonal edges - * @param {*} fillColor - Color of home plate background fill - * @param {*} strokeColor - Color of home plate outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius1 - Corner radius of pointed side of home plate (defaults to 2) - * @param {*} radius2 - Corner radius of flat side of home plate (defaults to 2) - * @param {*} rectWidth - Width of home plate (defaults to variable-width) - * @returns a shield definition object - */ -function homePlateUpShield( - offset, - fillColor, - strokeColor, - textColor, - radius1, - radius2, - rectWidth -) { - textColor = textColor ?? strokeColor; - radius1 = radius1 ?? 2; - radius2 = radius2 ?? 2; - return { - shapeBlank: { - drawFunc: "pentagon", - params: { - pointUp: true, - offset: offset, - angle: 0, - fillColor: fillColor, - strokeColor: strokeColor, - radius1: radius1, - radius2: radius2, - rectWidth: rectWidth, - }, - }, - textLayout: roundedRectTextConstraint(radius2), - padding: { - left: 2, - right: 2, - top: 1 + offset, - bottom: 2, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a vertically-aligned hexagon background - * - * @param {*} offset - Height of diagonal edges - * @param {*} fillColor - Color of hexagon background fill - * @param {*} strokeColor - Color of hexagon outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of hexagon (defaults to 2) - * @param {*} rectWidth - Width of hexagon (defaults to variable-width) - * @returns a shield definition object - */ -function hexagonVerticalShield( - offset, - fillColor, - strokeColor, - textColor, - radius, - rectWidth -) { - textColor = textColor ?? strokeColor; - radius = radius ?? 2; - return { - shapeBlank: { - drawFunc: "hexagonVertical", - params: { - offset: offset, - fillColor: fillColor, - strokeColor: strokeColor, - radius: radius, - rectWidth: rectWidth, - }, - }, - textLayout: roundedRectTextConstraint(radius), - padding: { - left: 2, - right: 2, - top: 1 + offset, - bottom: 1 + offset, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a horizontally-aligned hexagon background - * - * @param {*} angle - Angle (in degrees) at which sides deviate from vertical - * @param {*} fillColor - Color of hexagon background fill - * @param {*} strokeColor - Color of hexagon outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of hexagon (defaults to 2) - * @param {*} rectWidth - Width of hexagon (defaults to variable-width) - * @returns a shield definition object - */ -function hexagonHorizontalShield( - angle, - fillColor, - strokeColor, - textColor, - radius, - rectWidth -) { - let angleInRadians = (angle * Math.PI) / 180; - textColor = textColor ?? strokeColor; - radius = radius ?? 2; - return { - shapeBlank: { - drawFunc: "hexagonHorizontal", - params: { - angle: angleInRadians, - fillColor: fillColor, - strokeColor: strokeColor, - radius: radius, - rectWidth: rectWidth, - }, - }, - textLayout: textConstraint("ellipse"), - padding: { - left: 3, - right: 3, - top: 2, - bottom: 2, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with an octagon background - * - * @param {*} offset - Height of diagonal edges - * @param {*} angle - Angle (in degrees) at which sides deviate from vertical - * @param {*} fillColor - Color of octagon background fill - * @param {*} strokeColor - Color of octagon outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} radius - Corner radius of octagon (defaults to 2) - * @param {*} rectWidth - Width of octagon (defaults to variable-width) - * @returns a shield definition object - */ -function octagonVerticalShield( - offset, - angle, - fillColor, - strokeColor, - textColor, - radius, - rectWidth -) { - let angleInRadians = (angle * Math.PI) / 180; - textColor = textColor ?? strokeColor; - radius = radius ?? 2; - return { - shapeBlank: { - drawFunc: "octagonVertical", - params: { - offset: offset, - angle: angleInRadians, - fillColor: fillColor, - strokeColor: strokeColor, - radius: radius, - rectWidth: rectWidth, - }, - }, - textLayout: textConstraint("ellipse"), - padding: { - left: 2, - right: 2, - top: 2, - bottom: 2, - }, - textColor: textColor, - }; -} - -/** - * Draws a shield with a pill-shaped background - * - * @param {*} fillColor - Color of pill background fill - * @param {*} strokeColor - Color of pill outline stroke - * @param {*} textColor - Color of text (defaults to strokeColor) - * @param {*} rectWidth - Width of pill (defaults to variable-width) - * @returns a shield definition object - */ -function pillShield(fillColor, strokeColor, textColor, rectWidth) { - textColor = textColor ?? strokeColor; - return { - shapeBlank: { - drawFunc: "roundedRectangle", - params: { - fillColor: fillColor, - strokeColor: strokeColor, - rectWidth: rectWidth, - radius: 10, - }, - }, - textLayout: textConstraint("ellipse"), - padding: { - left: 2, - right: 2, - top: 2, - bottom: 2, - }, - textColor: textColor, - }; -} - -/** - * Draws a circle icon inside a black-outlined white square shield - * - * @param {*} fillColor - Color of circle icon background fill - * @param {*} strokeColor - Color of circle icon outline - * @returns a shield definition object - */ -function paBeltShield(fillColor, strokeColor) { - return { - notext: true, - shapeBlank: { - drawFunc: "paBelt", - params: { - fillColor: fillColor, - strokeColor: strokeColor, - }, - }, - }; -} - -/** - * Draws a rectangle icon inside a white-outlined green square shield - * - * @param {*} fillColor - Color of rectangle icon background fill - * @param {*} strokeColor - Color of rectangle icon outline - * @returns a shield definition object - */ -function bransonRouteShield(fillColor, strokeColor) { - return { - notext: true, - shapeBlank: { - drawFunc: "branson", - params: { - fillColor: fillColor, - strokeColor: strokeColor, - }, - }, - }; -} - -/** - * Adds banner text above a shield - * - * @param {*} baseDef - Shield definition object - * @param {*} banners - Array of strings to be displayed above shield - * @returns a shield definition object - */ -function banneredShield(baseDef, banners) { - return { - banners: banners, - ...baseDef, - }; -} +import { + textConstraint, + homePlateDownShield, + octagonVerticalShield, + hexagonHorizontalShield, + hexagonVerticalShield, + homePlateUpShield, + ovalShield, + circleShield, + roundedRectShield, + pillShield, + trapezoidUpShield, + trapezoidDownShield, + pentagonUpShield, + diamondShield, + triangleDownShield, + escutcheonDownShield, + fishheadDownShield, + banneredShield, + paBeltShield, + bransonRouteShield, +} from "@americana/maplibre-shield-generator"; export function loadShields() { const shields = {};