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-pr-checks.yml b/.github/workflows/deploy-pr-checks.yml index 57babd8e2..19e11ef17 100644 --- a/.github/workflows/deploy-pr-checks.yml +++ b/.github/workflows/deploy-pr-checks.yml @@ -112,7 +112,7 @@ jobs: " > pr_preview.md - uses: tibdex/github-app-token@v1 id: checks_token - with: + with: app_id: 396440 #osm-americana checks app private_key: ${{ secrets.CHECKS_WRITER_SECRET }} - name: Print Preview Links to GitHub Checks 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/.github/workflows/test-build-mac.yml b/.github/workflows/test-build-mac.yml index dbb8781e2..48b473012 100644 --- a/.github/workflows/test-build-mac.yml +++ b/.github/workflows/test-build-mac.yml @@ -10,9 +10,9 @@ jobs: steps: - name: Checkout 🛎️ uses: actions/checkout@v3 -# Node v18.17.0, introduced July 18, 2023, introduces an error in unicode processing that breaks test cases on Ubuntu. -# See PR #905 and #908 for more details. -# If this bug is resolved in node, these lines can revert to 18.x rather than 18.16.0. + # Node v18.17.0, introduced July 18, 2023, introduces an error in unicode processing that breaks test cases on Ubuntu. + # See PR #905 and #908 for more details. + # If this bug is resolved in node, these lines can revert to 18.x rather than 18.16.0. - name: Use Node.js 18.16.1 uses: actions/setup-node@v3 with: diff --git a/.github/workflows/test-build-ubuntu.yml b/.github/workflows/test-build-ubuntu.yml index c0515a215..90656f40b 100644 --- a/.github/workflows/test-build-ubuntu.yml +++ b/.github/workflows/test-build-ubuntu.yml @@ -10,9 +10,9 @@ jobs: steps: - name: Checkout 🛎️ uses: actions/checkout@v3 -# Node v18.17.0, introduced July 18, 2023, introduces an error in unicode processing that breaks test cases on Ubuntu. -# See PR #905 and #908 for more details. -# If this bug is resolved in node, these lines can revert to 18.x rather than 18.16.1. + # Node v18.17.0, introduced July 18, 2023, introduces an error in unicode processing that breaks test cases on Ubuntu. + # See PR #905 and #908 for more details. + # If this bug is resolved in node, these lines can revert to 18.x rather than 18.16.1. - name: Use Node.js 18.16.1 uses: actions/setup-node@v3 with: diff --git a/.gitignore b/.gitignore index 8448264f5..7fc8e9cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ config.js dist download local.config.js +shieldlib/docs +samples/ diff --git a/dev/README.md b/dev/README.md index 2ec39e467..1b7c23781 100644 --- a/dev/README.md +++ b/dev/README.md @@ -11,3 +11,12 @@ The highway shield [color palette](americana.gpl) can be imported into Inkscape 3. Import [americana.gpl](americana.gpl). 4. Restart Inkscape. 5. Click the ◀ button to the right of the color palette strip at the bottom of the window (or in the top-right corner of the Color Palette panel), then choose americana.gpl from the menu. + +## Map sample images + +Map sample images can be generated with a script. See [sample_locations.json](test/sample_locations.json) for the format. + +1. Create a JSON file with the map location and clipping rectangles +2. Start the server in the background with `npm start &` +3. Configure playwright: either `npx playwright install chromium` or `export CHROME_BIN=/usr/bin/chromium` +4. Run the generate_samples script and pass the JSON file location: `npm run generate_samples -- test/sample_locations.json`` diff --git a/package-lock.json b/package-lock.json index 793f57612..aab615aa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@basemaps/sprites": "^6.41.0", "@mapbox/vector-tile": "^1.3.1", "@maplibre/maplibre-gl-style-spec": "^17.0.1", + "@playwright/test": "^1.38.1", "@types/chai": "^4.3.4", "@types/color-namer": "^1.3.0", "@types/mocha": "^10.0.1", @@ -569,6 +570,21 @@ "gl-style-validate": "bin/gl-style-validate.js" } }, + "node_modules/@playwright/test": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.38.1.tgz", + "integrity": "sha512-NqRp8XMwj3AK+zKLbZShl0r/9wKgzqI/527bkptKXomtuo+dOjU9NdMASQ8DNC9z9zLOMbG53T4eihYr3XR+BQ==", + "dev": true, + "dependencies": { + "playwright": "1.38.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -730,6 +746,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 +1997,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 +2697,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 +2792,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 +2865,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", @@ -3759,6 +3805,36 @@ "integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg==", "dev": true }, + "node_modules/playwright": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.38.1.tgz", + "integrity": "sha512-oRMSJmZrOu1FP5iu3UrCx8JEFRIMxLDM0c/3o4bpzU5Tz97BypefWf7TuTNPWeCe279TPal5RtPPZ+9lW/Qkow==", + "dev": true, + "dependencies": { + "playwright-core": "1.38.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.38.1.tgz", + "integrity": "sha512-tQqNFUKa3OfMf4b2jQ7aGLB8o9bS3bOY0yMEtldtC2+spf8QXG9zvXLTXUeRsoNuxEYMgLYR+NXfAa1rjKRcrg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/potpack": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", @@ -4349,6 +4425,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 +5124,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 +5225,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 +5427,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 +5435,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..a38ab372e 100644 --- a/package.json +++ b/package.json @@ -11,19 +11,20 @@ "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", "code_format:prettier": "prettier --write --list-different .", "code_format:svgo": "svgo -q -f icons/", "extract_layer": "node scripts/extract_layer", + "generate_samples": "ts-node scripts/generate_samples.ts", "presprites": "shx rm -rf dist/sprites", "serve": "ts-node scripts/serve", "shields": "node scripts/generate_shield_defs.js -o dist/shields.json", @@ -50,6 +51,7 @@ "@basemaps/sprites": "^6.41.0", "@mapbox/vector-tile": "^1.3.1", "@maplibre/maplibre-gl-style-spec": "^17.0.1", + "@playwright/test": "^1.38.1", "@types/chai": "^4.3.4", "@types/color-namer": "^1.3.0", "@types/mocha": "^10.0.1", diff --git a/scripts/generate_samples.ts b/scripts/generate_samples.ts new file mode 100644 index 000000000..07a059276 --- /dev/null +++ b/scripts/generate_samples.ts @@ -0,0 +1,89 @@ +import fs from "node:fs"; +import { chromium } from "@playwright/test"; + +// Declare a global augmentation for the Window interface +declare global { + interface Window { + map?: { + loaded: () => boolean; + }; + } +} + +type LocationClip = { + x: number; + y: number; + width: number; + height: number; +}; + +type SampleSpecification = { + location: string; + name: string; + viewport: { + width: number; + height: number; + }; + clip: LocationClip; +}; + +// Load list of locations to take map screenshots +const loadSampleLocations = (filePath: string): SampleSpecification[] => { + const rawData = fs.readFileSync(filePath, "utf8"); + return JSON.parse(rawData); +}; + +const sampleFolder = "./samples"; + +const jsonSampleLocations = process.argv[2] ?? "test/sample_locations.json"; + +console.log(`Loading sample locations from ${jsonSampleLocations}`); + +const screenshots: SampleSpecification[] = + loadSampleLocations(jsonSampleLocations); + +fs.mkdirSync(sampleFolder, { recursive: true }); + +const browser = await chromium.launch({ + headless: true, + executablePath: process.env.CHROME_BIN, +}); +const context = await browser.newContext(); + +const page = await context.newPage(); + +for (const screenshot of screenshots) { + await page.setViewportSize(screenshot.viewport); + await createImage(screenshot); +} + +async function createImage(screenshot: SampleSpecification) { + await page.goto(`http://localhost:1776/#map=${screenshot.location}`); + + // Wait for map to load, then wait two more seconds for images, etc. to load. + try { + await page.waitForFunction(() => window.map?.loaded(), { + timeout: 3000, + }); + + // Wait for 1.5 seconds on 3D model examples, since this takes longer to load. + const waitTime = 1500; + console.log(`waiting for ${waitTime} ms`); + await page.waitForTimeout(waitTime); + } catch (e) { + console.log(`Timed out waiting for map load`); + } + + try { + await page.screenshot({ + path: `${sampleFolder}/${screenshot.name}.png`, + type: "png", + clip: screenshot.clip, + }); + console.log(`Created ${sampleFolder}/${screenshot.name}.png`); + } catch (err) { + console.error(err); + } +} + +await browser.close(); diff --git a/shieldlib/README.md b/shieldlib/README.md index bd41b6c26..988dcd3db 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, @@ -136,6 +136,8 @@ You should create one definition entry for each network. The entry key must matc } }, "banners": ["ALT"], + "bannerTextColor": "#000", + "bannerTextHaloColor": "#FFF", "textLayout": { "constraintFunc": "roundedRect", "options": { @@ -173,6 +175,8 @@ You should create one definition entry for each network. The entry key must matc ![Bannered routes near Downington, PA](https://wiki.openstreetmap.org/w/images/f/f8/Downington_bannered_routes_Americana.png) +- **`bannerTextColor`**: specify the color of the banner text. +- **`bannerTextHaloColor`**: specify the color of the banner knockout halo. - **`textLayout`**: specify how text should be inscribed within the padded bounds of the shield. The text will be drawn at the maximum size allowed by this constraint. See the [text layout functions](#text-layout-functions) section for text layout options. - **`colorLighten`**: specify that the shield artwork should be lightened (multiplied) by the specified color. This means that black areas will be recolor with this color and white areas will remain the same. Alpha values will remain unmodified. - **`colorDarken`**: specify that the shield artwork should be darkened by the specified color. This means that white areas will be recolor with this color and black areas will remain the same. Alpha values will remain unmodified. @@ -299,9 +303,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 +317,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 76% rename from shieldlib/src/custom_shields.mjs rename to shieldlib/src/custom_shields.ts index c390e60a6..dcf7da765 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 +): number { ShieldDraw.roundedRectangle(r, ctx, { fillColor: "white", strokeColor: "black", @@ -32,11 +38,15 @@ export function paBelt(r, ctx, params) { ctx.lineWidth = lineWidth; ctx.stroke(); - return ctx; + return 20; } // Special case for Branson color-coded routes -export function bransonRoute(r, ctx, params) { +export function bransonRoute( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams +): number { ShieldDraw.roundedRectangle(r, ctx, { fillColor: "#006747", strokeColor: "white", @@ -63,7 +73,7 @@ export function bransonRoute(r, ctx, params) { ctx.lineWidth = lineWidth; ctx.stroke(); - return ctx; + return 20; } export function loadCustomShields() { 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..dd15c53d0 100644 --- a/shieldlib/src/shield.js +++ b/shieldlib/src/shield.js @@ -1,20 +1,13 @@ "use strict"; -import * as ShieldText from "./shield_text.mjs"; -import * as ShieldDraw from "./shield_canvas_draw.mjs"; +import * as ShieldText from "./shield_text.js"; +import * as ShieldDraw from "./shield_canvas_draw.js"; import * as Gfx from "./screen_gfx.js"; - -function drawBannerPart(r, ctx, shieldDef, drawFunc) { - if (shieldDef == null || typeof shieldDef.banners == "undefined") { - return ctx; //Unadorned shield - } - - for (var i = 0; i < shieldDef.banners.length; i++) { - drawFunc(r, ctx, shieldDef.banners[i], i); - } - - return ctx; -} +import { + drawBanners, + drawBannerHalos, + getBannerCount, +} from "./shield_banner.js"; function compoundShieldSize(r, dimension, bannerCount) { return { @@ -29,19 +22,6 @@ export function isValidRef(ref) { return ref !== null && ref.length !== 0 && ref.length <= 6; } -/** - * Get the number of banner placards associated with this shield - * - * @param {*} shield - Shield definition - * @returns the number of banner placards that need to be drawn - */ -function getBannerCount(shield) { - if (shield == null || typeof shield.banners == "undefined") { - return 0; //Unadorned shield - } - return shield.banners.length; -} - /** * Retrieve the shield blank that goes with a particular route. If there are * multiple shields for a route (different widths), it picks the best shield. @@ -186,14 +166,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) { @@ -359,7 +339,7 @@ export function generateShieldCtx(r, routeDef) { } // Add the halo around modifier plaque text - drawBannerPart(r, ctx, shieldDef, ShieldText.drawBannerHaloText); + drawBannerHalos(r, ctx, shieldDef); if (sourceSprite == null) { drawShield(r, ctx, shieldDef, routeDef); @@ -378,7 +358,7 @@ export function generateShieldCtx(r, routeDef) { drawShieldText(r, ctx, shieldDef, routeDef); // Add modifier plaque text - drawBannerPart(r, ctx, shieldDef, ShieldText.drawBannerText); + drawBanners(r, ctx, shieldDef); return ctx; } diff --git a/shieldlib/src/shield_banner.ts b/shieldlib/src/shield_banner.ts new file mode 100644 index 000000000..6961865b5 --- /dev/null +++ b/shieldlib/src/shield_banner.ts @@ -0,0 +1,179 @@ +import { shieldFont } from "./screen_gfx"; +import { ShieldRenderingContext } from "./shield_renderer"; +import { TextPlacement, layoutShieldText } from "./shield_text"; +import { Dimension, ShieldDefinition, TextLayout } from "./types"; + +let bannerLayout: TextLayout = { + constraintFunc: "rect", +}; + +/** + * Add modifier plaque text + * + * @param r - Shield rendering context + * @param ctx - Canvas drawing context + * @param shieldDef - Shield definition + */ +export function drawBanners( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + shieldDef: ShieldDefinition +) { + if (shieldDef.bannerTextColor) { + ctx.fillStyle = shieldDef.bannerTextColor; + } else { + ctx.fillStyle = r.options.bannerTextColor; + } + drawBannerPart(r, ctx, shieldDef, drawBannerText); +} + +/** + * Add the halo around modifier plaque text + * + * @param r - Shield rendering context + * @param ctx - Canvas drawing context + * @param shieldDef - Shield definition + */ +export function drawBannerHalos( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + shieldDef: ShieldDefinition +) { + if (shieldDef.bannerTextHaloColor) { + ctx.strokeStyle = ctx.shadowColor = shieldDef.bannerTextHaloColor; + } else { + ctx.strokeStyle = ctx.shadowColor = r.options.bannerTextHaloColor; + } + drawBannerPart(r, ctx, shieldDef, drawBannerHaloText); +} + +type BannerDrawComponentFunction = ( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + bannerIndex: number +) => void; + +function drawBannerPart( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + shieldDef: ShieldDefinition, + drawFunc: BannerDrawComponentFunction +): void { + if (shieldDef == null || typeof shieldDef.banners == "undefined") { + return; //Unadorned shield + } + + for (var i = 0; i < shieldDef.banners.length; i++) { + drawFunc(r, ctx, shieldDef.banners[i], i); + } +} + +/** + * Get the number of banner placards associated with this shield + * + * @param shield - Shield definition + * @returns the number of banner placards that need to be drawn + */ +export function getBannerCount(shield: ShieldDefinition): number { + if (shield == null || typeof shield.banners == "undefined") { + return 0; //Unadorned shield + } + return shield.banners.length; +} + +/** + * 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 = shieldFont(textLayout.fontPx, r.options.shieldFont); + ctx.textBaseline = "top"; + ctx.textAlign = "center"; + + if (textComponent) { + ctx.fillText( + text, + textLayout.xBaseline, + textLayout.yBaseline + + bannerIndex * r.px(r.options.bannerHeight - r.options.bannerPadding) + ); + } else { + 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; + } +} diff --git a/shieldlib/src/shield_canvas_draw.mjs b/shieldlib/src/shield_canvas_draw.ts similarity index 82% rename from shieldlib/src/shield_canvas_draw.mjs rename to shieldlib/src/shield_canvas_draw.ts index 5b8236d6c..d79160fc5 100644 --- a/shieldlib/src/shield_canvas_draw.mjs +++ b/shieldlib/src/shield_canvas_draw.ts @@ -4,21 +4,28 @@ * Shield blanks which are drawn rather built from raster shield blanks */ -import * as ShieldText from "./shield_text.mjs"; -import { loadCustomShields } from "./custom_shields.mjs"; +import * as ShieldText from "./shield_text"; +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 +): number { 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 +): number { 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 +): number { + 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 +): number { let pointUp = params.pointUp == undefined ? false : params.pointUp; let fill = params.fillColor == undefined ? "white" : params.fillColor; let outline = params.strokeColor == undefined ? "black" : params.strokeColor; @@ -242,9 +270,16 @@ function fishhead(r, ctx, params, ref) { ctx.strokeStyle = outline; ctx.stroke(); } + + return width; } -function triangle(r, ctx, params, ref) { +function triangle( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + params: ShapeBlankParams, + ref: string +): number { 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 +340,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 +): number { 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 +402,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 +): number { 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 +471,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 +): number { 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 +487,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 +497,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 +512,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 +544,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 +): number { + 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 +561,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,17 +598,22 @@ 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 +): number { + 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 halfComplementTangent = Math.tan(Math.PI / 4 - angle / 2); + let sine = Math.sin(sideAngle); + let cosine = Math.cos(sideAngle); + let tangent = Math.tan(sideAngle); + let halfComplementTangent = Math.tan(Math.PI / 4 - sideAngle / 2); let width = computeWidth(r, params, ref, "hexagonHorizontal"); @@ -604,24 +664,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 +): number { + 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 +709,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 +751,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 +760,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 +): number { + return drawFunctions[name](r, ctx, params, ref); } //Register draw functions @@ -710,7 +783,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..177bf9a22 --- /dev/null +++ b/shieldlib/src/shield_helper.d.ts @@ -0,0 +1,146 @@ +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[], + bannerColor?: string +): ShieldDefinition; diff --git a/shieldlib/src/shield_helper.ts b/shieldlib/src/shield_helper.ts new file mode 100644 index 000000000..f70c8b0b4 --- /dev/null +++ b/shieldlib/src/shield_helper.ts @@ -0,0 +1,759 @@ +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[], + bannerColor?: string +): ShieldDefinition { + return { + banners, + bannerTextColor: bannerColor, + ...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..9194feb32 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 +) => number; + 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.mjs b/shieldlib/src/shield_text.ts similarity index 66% rename from shieldlib/src/shield_text.mjs rename to shieldlib/src/shield_text.ts index 964146c2d..350f7f02c 100644 --- a/shieldlib/src/shield_text.mjs +++ b/shieldlib/src/shield_text.ts @@ -1,14 +1,49 @@ "use strict"; import * as Gfx from "./screen_gfx.js"; +import { ShieldRenderingContext } from "./shield_renderer.js"; +import { + BoxPadding, + Dimension, + ShieldDefinition, + TextLayout, + TextLayoutParameters, +} from "./types.js"; const VerticalAlignment = { Middle: "middle", Top: "top", Bottom: "bottom", +} as const; + +type VerticalAlignmentType = + (typeof VerticalAlignment)[keyof typeof VerticalAlignment]; + +type TextLayoutScaler = ( + availSize: Dimension, + textSize: Dimension, + options?: TextLayoutParameters +) => TextTransform; + +interface TextTransform { + scale: number; + valign: VerticalAlignmentType; +} + +export interface TextPlacement { + xBaseline: number; + yBaseline: number; + fontPx: number; +} + +let noPadding: BoxPadding = { + top: 0, + bottom: 0, + left: 0, + right: 0, }; -function ellipseScale(spaceBounds, textBounds) { +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; @@ -19,14 +54,20 @@ function ellipseScale(spaceBounds, textBounds) { return (a * b) / Math.sqrt(a * a * y0 * y0 + b * b * x0 * x0); } -function ellipseTextConstraint(spaceBounds, textBounds) { +function ellipseTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { return { scale: ellipseScale(spaceBounds, textBounds), valign: VerticalAlignment.Middle, }; } -function southHalfEllipseTextConstraint(spaceBounds, textBounds) { +function southHalfEllipseTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { return { scale: ellipseScale(spaceBounds, { //Turn ellipse 90 degrees @@ -37,7 +78,10 @@ function southHalfEllipseTextConstraint(spaceBounds, textBounds) { }; } -function rectTextConstraint(spaceBounds, textBounds) { +function rectTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { var scaleHeight = spaceBounds.height / textBounds.height; var scaleWidth = spaceBounds.width / textBounds.width; @@ -47,7 +91,11 @@ function rectTextConstraint(spaceBounds, textBounds) { }; } -function roundedRectTextConstraint(spaceBounds, textBounds, options) { +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) { @@ -63,7 +111,10 @@ function roundedRectTextConstraint(spaceBounds, textBounds, options) { ); } -function diamondTextConstraint(spaceBounds, textBounds) { +function diamondTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { let a = spaceBounds.width; let b = spaceBounds.height; @@ -76,7 +127,10 @@ function diamondTextConstraint(spaceBounds, textBounds) { }; } -function triangleDownTextConstraint(spaceBounds, textBounds) { +function triangleDownTextConstraint( + spaceBounds: Dimension, + textBounds: Dimension +): TextTransform { return { scale: diamondTextConstraint(spaceBounds, textBounds).scale, valign: VerticalAlignment.Top, @@ -95,14 +149,14 @@ function triangleDownTextConstraint(spaceBounds, textBounds) { * @param {*} maxFontSize - maximum font size * @returns JOSN object containing (X,Y) draw position and font size */ -function layoutShieldText( - r, - text, - padding, - bounds, - textLayoutDef, - maxFontSize -) { +export 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; @@ -147,7 +201,7 @@ function layoutShieldText( metrics = ctx.measureText(text); textHeight = metrics.actualBoundingBoxDescent; - var yBaseline; + let yBaseline: number; switch (textConstraint.valign) { case VerticalAlignment.Top: @@ -163,19 +217,26 @@ function layoutShieldText( } return { - xBaseline: xBaseline, - yBaseline: yBaseline, + xBaseline, + yBaseline, fontPx: fontSize, }; } -const defaultDefForLayout = { +const defaultDefForLayout: ShieldDefinition = { padding: { top: 0, bottom: 0, left: 0, right: 0, }, + shapeBlank: { + drawFunc: "rect", + params: { + fillColor: "white", + strokeColor: "black", + }, + }, }; /** @@ -188,13 +249,18 @@ const defaultDefForLayout = { * @param {*} bounds - size of the overall graphics area * @returns JOSN object containing (X,Y) draw position and font size */ -export function layoutShieldTextFromDef(r, text, def, bounds) { +export function layoutShieldTextFromDef( + r: ShieldRenderingContext, + text: string, + def: ShieldDefinition, + bounds: Dimension +): TextPlacement { //FIX if (def == null) { def = defaultDefForLayout; } - var padding = def.padding || {}; + var padding = def.padding || noPadding; var textLayoutDef = { constraintFunc: "rect", @@ -221,7 +287,12 @@ export function layoutShieldTextFromDef(r, text, def, bounds) { * @param {*} text - text to draw * @param {*} textLayout - location to draw text */ -export function renderShieldText(r, ctx, text, textLayout) { +export function renderShieldText( + r: ShieldRenderingContext, + ctx: CanvasRenderingContext2D, + text: string, + textLayout: TextPlacement +): void { //Text color is set by fillStyle configureShieldText(r, ctx, textLayout); @@ -236,11 +307,16 @@ export function renderShieldText(r, ctx, text, textLayout) { * @param {*} text - text to draw * @param {*} textLayout - location to draw text */ -export function drawShieldHaloText(r, ctx, text, textLayout) { +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; + ctx.shadowColor = ctx.strokeStyle.toString(); ctx.shadowBlur = 0; ctx.lineWidth = r.px(2); @@ -249,97 +325,26 @@ export function drawShieldHaloText(r, ctx, text, textLayout) { ctx.shadowBlur = null; } -function configureShieldText(r, ctx, textLayout) { +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, ctx, text, bannerIndex) { - 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, ctx, text, bannerIndex) { - 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, ctx, text, bannerIndex, textComponent) { - const bannerPadding = { - padding: { - top: r.options.bannerPadding, - bottom: 0, - left: 0, - right: 0, - }, - }; - var textLayout = layoutShieldTextFromDef(r, text, bannerPadding, { - width: ctx.canvas.width, - height: r.px(r.options.bannerHeight - r.options.bannerPadding), - }); - - 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, text, fontSize) { +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); } -export function drawText(name, options, ref) { - return drawTextFunctions[name](options, ref); -} - //Register text draw functions const drawTextFunctions = {}; @@ -349,7 +354,7 @@ const drawTextFunctions = {}; * @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, fxn) { +function registerDrawTextFunction(name: string, fxn: TextLayoutScaler): void { drawTextFunctions[name] = fxn; } diff --git a/shieldlib/src/types.d.ts b/shieldlib/src/types.d.ts index eb2a19e7d..a81c725ad 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[]; @@ -57,3 +56,8 @@ export interface GraphicsFactory { */ pixelRatio(): number; } + +export interface Dimension { + width: number; + height: number; +} diff --git a/shieldlib/src/types.ts b/shieldlib/src/types.ts index 170eef2bc..63ae4ff1d 100644 --- a/shieldlib/src/types.ts +++ b/shieldlib/src/types.ts @@ -1,73 +1,218 @@ 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; -} - -export interface ShieldDefinition { - spriteBlank: string[]; - textColor: string; - padding: { - left: number; - right: number; - top: number; - bottom: number; - }; + /** If set, only match routes with this ref value */ + ref?: string; + /** If set, only match routes with this name value */ + name?: string; +} + +/** 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; + /** Color of banner text */ + bannerTextColor?: string; + /** Color of banner text halo */ + bannerTextHaloColor?: 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; /** @@ -77,3 +222,8 @@ export interface GraphicsFactory { */ pixelRatio(): number; } + +export interface Dimension { + width: number; + height: number; +} diff --git a/shieldlib/tsconfig.json b/shieldlib/tsconfig.json index db5212aa2..1204f7fe6 100644 --- a/shieldlib/tsconfig.json +++ b/shieldlib/tsconfig.json @@ -11,5 +11,5 @@ "experimentalSpecifierResolution": "node" }, "exclude": ["node_modules", "**/*.json"], - "include": ["src/**/*.ts", "scripts/**.ts"] + "include": ["src/**/*.ts", "scripts/**.ts", "src/shield.js", "src/shield.js"] } diff --git a/src/js/shield_defs.js b/src/js/shield_defs.js index e4534a85b..c1085579c 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 = {}; @@ -1198,7 +509,8 @@ export function loadShields() { textColor: Color.shields.brown, colorLighten: Color.shields.brown, }, - ["HIST"] + ["HIST"], + Color.shields.brown ); // Federal Agencies @@ -1242,7 +554,11 @@ export function loadShields() { notext: true, }; - shields["GLCT:Loop"] = banneredShield(shields["GLCT"], ["LOOP"]); + shields["GLCT:Loop"] = banneredShield( + shields["GLCT"], + ["LOOP"], + Color.shields.brown + ); // Alaska shields["US:AK"] = { @@ -1454,7 +770,11 @@ export function loadShields() { bottom: 4, }, }; - shields["US:CA:Business"] = banneredShield(shields["US:CA"], ["BUS"]); + shields["US:CA:Business"] = banneredShield( + shields["US:CA"], + ["BUS"], + Color.shields.green + ); shields["US:CA:CR"] = pentagonUpShield( 3, 15, @@ -1859,7 +1179,8 @@ export function loadShields() { textColor: Color.shields.green, colorLighten: Color.shields.green, }, - ["BUS"] + ["BUS"], + Color.shields.green ); // Maine @@ -2259,9 +1580,11 @@ export function loadShields() { spriteBlank: "shield_us_nj_ace_noref", notext: true, }; - shields["US:NJ:ACE:Connector"] = banneredShield(shields["US:NJ:ACE"], [ - "CONN", - ]); + shields["US:NJ:ACE:Connector"] = banneredShield( + shields["US:NJ:ACE"], + ["CONN"], + Color.shields.blue + ); shields["US:NJ:GSP"] = { spriteBlank: "shield_us_nj_gsp_noref", notext: true, @@ -2306,8 +1629,16 @@ export function loadShields() { Color.shields.white, Color.shields.black ); - shields["US:NJ:CR:Spur"] = banneredShield(shields["US:NJ:CR"], ["SPUR"]); - shields["US:NJ:CR:Truck"] = banneredShield(shields["US:NJ:CR"], ["TRK"]); + shields["US:NJ:CR:Spur"] = banneredShield( + shields["US:NJ:CR"], + ["SPUR"], + Color.shields.blue + ); + shields["US:NJ:CR:Truck"] = banneredShield( + shields["US:NJ:CR"], + ["TRK"], + Color.shields.blue + ); // New Mexico shields["US:NM"] = pillShield( @@ -2815,9 +2146,21 @@ export function loadShields() { bottom: 3, }, }; - shields["US:SC:Truck"] = banneredShield(shields["US:SC"], ["TRK"]); - shields["US:SC:Business"] = banneredShield(shields["US:SC"], ["BUS"]); - shields["US:SC:Alternate"] = banneredShield(shields["US:SC"], ["ALT"]); + shields["US:SC:Truck"] = banneredShield( + shields["US:SC"], + ["TRK"], + Color.shields.blue + ); + shields["US:SC:Business"] = banneredShield( + shields["US:SC"], + ["BUS"], + Color.shields.blue + ); + shields["US:SC:Alternate"] = banneredShield( + shields["US:SC"], + ["ALT"], + Color.shields.blue + ); // South Dakota shields["US:SD"] = { @@ -2954,7 +2297,8 @@ export function loadShields() { textColor: Color.shields.brown, colorLighten: Color.shields.brown, }, - ["R"] + ["R"], + Color.shields.brown ); shields["US:TX:NASA"] = banneredShield(shields["US:TX"], ["NASA"]); @@ -2963,22 +2307,31 @@ export function loadShields() { Color.shields.blue, Color.shields.white ); - shields["US:TX:Express:Toll"] = banneredShield(shields["US:TX:Toll"], [ - "EXPR", - ]); - shields["US:TX:Loop:Toll"] = banneredShield(shields["US:TX:Toll"], ["LOOP"]); - shields["US:TX:Loop:Express:Toll"] = banneredShield(shields["US:TX:Toll"], [ - "EXPR", - "LOOP", - ]); + shields["US:TX:Express:Toll"] = banneredShield( + shields["US:TX:Toll"], + ["EXPR"], + Color.shields.blue + ); + shields["US:TX:Loop:Toll"] = banneredShield( + shields["US:TX:Toll"], + ["LOOP"], + Color.shields.blue + ); + shields["US:TX:Loop:Express:Toll"] = banneredShield( + shields["US:TX:Toll"], + ["EXPR", "LOOP"], + Color.shields.blue + ); shields["US:TX:CTRMA"] = roundedRectShield( Color.shields.blue, Color.shields.yellow, Color.shields.white ); - shields["US:TX:CTRMA:Express"] = banneredShield(shields["US:TX:CTRMA"], [ - "EXPR", - ]); + shields["US:TX:CTRMA:Express"] = banneredShield( + shields["US:TX:CTRMA"], + ["EXPR"], + Color.shields.blue + ); shields["US:TX:Montgomery:MCTRA"] = homePlateDownShield( 5, Color.shields.blue, @@ -3063,11 +2416,13 @@ export function loadShields() { ); shields["US:TX:Jackson"] = banneredShield( roundedRectShield(Color.shields.blue, Color.shields.white), - ["CR"] + ["CR"], + Color.shields.blue ); shields["US:TX:Andrews:Andrews:Loop"] = banneredShield( roundedRectShield(Color.shields.white, Color.shields.blue), - ["LOOP"] + ["LOOP"], + Color.shields.blue ); // Utah @@ -3117,7 +2472,12 @@ export function loadShields() { bottom: 2, }, }; - shields["US:VT:Alternate"] = banneredShield(shields["US:VT"], ["ALT"]); + shields["US:VT:Alternate"] = banneredShield( + shields["US:VT"], + ["ALT"], + Color.shields.green + ); + // Vermont routes town maintained sections - black and white ovals shields["US:VT:Town"] = ovalShield(Color.shields.white, Color.shields.black); @@ -4190,7 +3550,8 @@ export function loadShields() { ); shields[`AU:${state_or_territory}:ALT`] = banneredShield( roundedRectShield(Color.shields.green, Color.shields.yellow), - ["ALT"] + ["ALT"], + Color.shields.green ); shields[`AU:${state_or_territory}:ALT_NR`] = banneredShield( homePlateDownShield(5, Color.shields.white, Color.shields.black), @@ -4198,7 +3559,8 @@ export function loadShields() { ); shields[`AU:${state_or_territory}:ALT_S`] = banneredShield( fishheadDownShield(Color.shields.blue, Color.shields.white), - ["ALT"] + ["ALT"], + Color.shields.blue ); } ); diff --git a/test/sample_locations.json b/test/sample_locations.json new file mode 100644 index 000000000..81272a54e --- /dev/null +++ b/test/sample_locations.json @@ -0,0 +1,44 @@ +[ + { + "location": "4/40.5/-94", + "name": "full_window", + "viewport": { + "width": 600, + "height": 250 + }, + "clip": { + "x": 0, + "y": 0, + "width": 600, + "height": 250 + } + }, + { + "location": "13/40.01264/-75.70446", + "name": "downingtown_alt_trk_bus", + "viewport": { + "width": 600, + "height": 600 + }, + "clip": { + "x": 100, + "y": 100, + "width": 400, + "height": 400 + } + }, + { + "location": "13/35.22439/-99.99495", + "name": "historic_us66_oklahoma", + "viewport": { + "width": 600, + "height": 600 + }, + "clip": { + "x": 100, + "y": 100, + "width": 400, + "height": 400 + } + } +]