├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── build-docker.yaml │ └── codeql-analysis.yml ├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── Dockerfile ├── LICENSE ├── README.md ├── cors ├── index.mjs ├── outlook.mjs └── radar.mjs ├── datagenerators ├── chunk.mjs ├── https.mjs ├── output │ ├── regionalcities.json │ ├── stations.json │ └── travelcities.json ├── regionalcities-raw.json ├── regionalcities.mjs ├── stations-states.mjs ├── stations.mjs ├── travelcities-raw.json └── travelcities.mjs ├── dist └── readme.txt ├── gulp ├── publish-frontend.mjs └── update-vendor.mjs ├── gulpfile.mjs ├── index.mjs ├── package-lock.json ├── package.json ├── server ├── fonts │ ├── Star4000 Extended.woff │ ├── Star4000 Large.ttf │ ├── Star4000 Large.woff │ ├── Star4000 Small.woff │ └── Star4000.woff ├── images │ ├── backgrounds │ │ ├── 1-chart.png │ │ ├── 1-wide.png │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4-wide.png │ │ ├── 4.png │ │ ├── 5.png │ │ └── 6.png │ ├── gimp │ │ ├── Background 6.xcf │ │ ├── Radar Basemap.xcf │ │ ├── Radar Basemap2.xcf │ │ ├── Radar Basemap3.xcf │ │ ├── Radar Basemap4.xcf │ │ └── Radar Basemap5.xcf │ ├── icons │ │ ├── current-conditions │ │ │ ├── Blowing-Snow.gif │ │ │ ├── Clear.gif │ │ │ ├── Cloudy.gif │ │ │ ├── Fog.gif │ │ │ ├── Freezing-Rain-Snow.gif │ │ │ ├── Freezing-Rain.gif │ │ │ ├── Heavy-Snow.gif │ │ │ ├── Light-Snow.gif │ │ │ ├── Mostly-Clear.gif │ │ │ ├── Partly-Cloudy.gif │ │ │ ├── Rain-Sleet.gif │ │ │ ├── Rain-Snow.gif │ │ │ ├── Rain.gif │ │ │ ├── Scattered-Thunderstorms-Day.gif │ │ │ ├── Scattered-Thunderstorms-Night.gif │ │ │ ├── Shower.gif │ │ │ ├── Sleet.gif │ │ │ ├── Smoke.gif │ │ │ ├── Snow-Sleet.gif │ │ │ ├── Sunny.gif │ │ │ ├── Thunderstorm.gif │ │ │ └── Windy.gif │ │ ├── moon-phases │ │ │ ├── First-Quarter.gif │ │ │ ├── Full-Moon.gif │ │ │ ├── Last-Quarter.gif │ │ │ └── New-Moon.gif │ │ └── regional-maps │ │ │ ├── Blowing-Snow.gif │ │ │ ├── Clear-1992.gif │ │ │ ├── Clear-Wind-1994.gif │ │ │ ├── Cloudy-Wind.gif │ │ │ ├── Cloudy.gif │ │ │ ├── Cold.gif │ │ │ ├── Fog.gif │ │ │ ├── Freezing-Rain-1992.gif │ │ │ ├── Freezing-Rain-Snow-1994.gif │ │ │ ├── Haze.gif │ │ │ ├── Heavy-Snow-1994.gif │ │ │ ├── Hot.gif │ │ │ ├── Light-Snow.gif │ │ │ ├── Mostly-Cloudy-1994.gif │ │ │ ├── Partly-Clear-1994.gif │ │ │ ├── Partly-Cloudy-Night.gif │ │ │ ├── Partly-Cloudy.gif │ │ │ ├── Rain-1992.gif │ │ │ ├── Rain-Sleet.gif │ │ │ ├── Rain-Snow-1992.gif │ │ │ ├── Scattered-Showers-1994.gif │ │ │ ├── Scattered-Showers-Night-1994.gif │ │ │ ├── Scattered-Tstorms-1994.gif │ │ │ ├── Scattered-Tstorms-Night-1994.gif │ │ │ ├── Sleet.gif │ │ │ ├── Smoke.gif │ │ │ ├── Snow-Sleet.gif │ │ │ ├── Sunny-Wind-1994.gif │ │ │ ├── Sunny.gif │ │ │ ├── ThunderSnow.gif │ │ │ ├── Thunderstorm.gif │ │ │ └── Wind.gif │ ├── logos │ │ ├── logo-corner.png │ │ ├── logo192.png │ │ └── noaa.gif │ ├── maps │ │ ├── basemap.webp │ │ ├── radar-alaska.png │ │ ├── radar-hawaii.png │ │ ├── radar-tiles │ │ │ ├── 00-00.webp │ │ │ ├── 00-01.webp │ │ │ ├── 00-02.webp │ │ │ ├── 00-03.webp │ │ │ ├── 00-04.webp │ │ │ ├── 00-05.webp │ │ │ ├── 00-06.webp │ │ │ ├── 00-07.webp │ │ │ ├── 00-08.webp │ │ │ ├── 00-09.webp │ │ │ ├── 01-00.webp │ │ │ ├── 01-01.webp │ │ │ ├── 01-02.webp │ │ │ ├── 01-03.webp │ │ │ ├── 01-04.webp │ │ │ ├── 01-05.webp │ │ │ ├── 01-06.webp │ │ │ ├── 01-07.webp │ │ │ ├── 01-08.webp │ │ │ ├── 01-09.webp │ │ │ ├── 02-00.webp │ │ │ ├── 02-01.webp │ │ │ ├── 02-02.webp │ │ │ ├── 02-03.webp │ │ │ ├── 02-04.webp │ │ │ ├── 02-05.webp │ │ │ ├── 02-06.webp │ │ │ ├── 02-07.webp │ │ │ ├── 02-08.webp │ │ │ ├── 02-09.webp │ │ │ ├── 03-00.webp │ │ │ ├── 03-01.webp │ │ │ ├── 03-02.webp │ │ │ ├── 03-03.webp │ │ │ ├── 03-04.webp │ │ │ ├── 03-05.webp │ │ │ ├── 03-06.webp │ │ │ ├── 03-07.webp │ │ │ ├── 03-08.webp │ │ │ ├── 03-09.webp │ │ │ ├── 04-00.webp │ │ │ ├── 04-01.webp │ │ │ ├── 04-02.webp │ │ │ ├── 04-03.webp │ │ │ ├── 04-04.webp │ │ │ ├── 04-05.webp │ │ │ ├── 04-06.webp │ │ │ ├── 04-07.webp │ │ │ ├── 04-08.webp │ │ │ ├── 04-09.webp │ │ │ ├── 05-00.webp │ │ │ ├── 05-01.webp │ │ │ ├── 05-02.webp │ │ │ ├── 05-03.webp │ │ │ ├── 05-04.webp │ │ │ ├── 05-05.webp │ │ │ ├── 05-06.webp │ │ │ ├── 05-07.webp │ │ │ ├── 05-08.webp │ │ │ ├── 05-09.webp │ │ │ ├── 06-00.webp │ │ │ ├── 06-01.webp │ │ │ ├── 06-02.webp │ │ │ ├── 06-03.webp │ │ │ ├── 06-04.webp │ │ │ ├── 06-05.webp │ │ │ ├── 06-06.webp │ │ │ ├── 06-07.webp │ │ │ ├── 06-08.webp │ │ │ ├── 06-09.webp │ │ │ ├── 07-00.webp │ │ │ ├── 07-01.webp │ │ │ ├── 07-02.webp │ │ │ ├── 07-03.webp │ │ │ ├── 07-04.webp │ │ │ ├── 07-05.webp │ │ │ ├── 07-06.webp │ │ │ ├── 07-07.webp │ │ │ ├── 07-08.webp │ │ │ ├── 07-09.webp │ │ │ ├── 08-00.webp │ │ │ ├── 08-01.webp │ │ │ ├── 08-02.webp │ │ │ ├── 08-03.webp │ │ │ ├── 08-04.webp │ │ │ ├── 08-05.webp │ │ │ ├── 08-06.webp │ │ │ ├── 08-07.webp │ │ │ ├── 08-08.webp │ │ │ ├── 08-09.webp │ │ │ ├── 09-00.webp │ │ │ ├── 09-01.webp │ │ │ ├── 09-02.webp │ │ │ ├── 09-03.webp │ │ │ ├── 09-04.webp │ │ │ ├── 09-05.webp │ │ │ ├── 09-06.webp │ │ │ ├── 09-07.webp │ │ │ ├── 09-08.webp │ │ │ └── 09-09.webp │ │ └── radar.webp │ ├── nav │ │ ├── ic_fullscreen_exit_white_24dp_2x.png │ │ ├── ic_fullscreen_white_24dp_2x.png │ │ ├── ic_gps_fixed_black_18dp_1x.png │ │ ├── ic_gps_fixed_white_18dp_1x.png │ │ ├── ic_menu_white_24dp_2x.png │ │ ├── ic_pause_white_24dp_2x.png │ │ ├── ic_play_arrow_white_24dp_2x.png │ │ ├── ic_refresh_white_24dp_2x.png │ │ ├── ic_scanlines_off_white_24dp_2x.png │ │ ├── ic_scanlines_on_white_24dp_2x.png │ │ ├── ic_skip_next_white_24dp_2x.png │ │ ├── ic_skip_previous_white_24dp_2x.png │ │ ├── ic_volume_off_white_24dp_2x.png │ │ └── ic_volume_on_white_24dp_2x.png │ └── social │ │ └── 1200x600.png ├── manifest.json ├── music │ ├── default │ │ ├── Catch the Sun.mp3 │ │ ├── Crisp day.mp3 │ │ ├── Rolling Clouds.mp3 │ │ └── Strong Breeze.mp3 │ └── readme.txt ├── robots.txt ├── scripts │ ├── custom.sample.js │ ├── data │ │ ├── regionalcities.js │ │ ├── stations.js │ │ └── travelcities.js │ ├── index.mjs │ ├── modules │ │ ├── almanac.mjs │ │ ├── autocomplete.mjs │ │ ├── currentweather.mjs │ │ ├── currentweatherscroll.mjs │ │ ├── extendedforecast.mjs │ │ ├── hazards.mjs │ │ ├── hourly-graph.mjs │ │ ├── hourly.mjs │ │ ├── icons.mjs │ │ ├── icons │ │ │ ├── icons-hourly.mjs │ │ │ ├── icons-large.mjs │ │ │ └── icons-small.mjs │ │ ├── latestobservations.mjs │ │ ├── localforecast.mjs │ │ ├── media.mjs │ │ ├── navigation.mjs │ │ ├── progress.mjs │ │ ├── radar-utils.mjs │ │ ├── radar-worker.mjs │ │ ├── radar.mjs │ │ ├── regionalforecast-utils.mjs │ │ ├── regionalforecast.mjs │ │ ├── settings.mjs │ │ ├── share.mjs │ │ ├── spc-outlook.mjs │ │ ├── status.mjs │ │ ├── travelforecast.mjs │ │ ├── utils │ │ │ ├── calc.mjs │ │ │ ├── cors.mjs │ │ │ ├── elem.mjs │ │ │ ├── fetch.mjs │ │ │ ├── image.mjs │ │ │ ├── nosleep.mjs │ │ │ ├── polygon.mjs │ │ │ ├── setting.mjs │ │ │ ├── string.mjs │ │ │ ├── units.mjs │ │ │ └── weather.mjs │ │ └── weatherdisplay.mjs │ └── vendor │ │ └── auto │ │ ├── luxon.js.map │ │ ├── luxon.mjs │ │ ├── nosleep.js │ │ ├── suncalc.js │ │ └── swiped-events.js └── styles │ ├── main.css │ ├── main.css.map │ └── scss │ ├── _almanac.scss │ ├── _current-weather.scss │ ├── _extended-forecast.scss │ ├── _hazards.scss │ ├── _hourly-graph.scss │ ├── _hourly.scss │ ├── _latest-observations.scss │ ├── _local-forecast.scss │ ├── _media.scss │ ├── _page.scss │ ├── _progress.scss │ ├── _radar.scss │ ├── _regional-forecast.scss │ ├── _spc-outlook.scss │ ├── _travel.scss │ ├── _weather-display.scss │ ├── main.scss │ └── shared │ ├── _colors.scss │ ├── _scanlines.scss │ └── _utils.scss ├── src ├── overrides.mjs ├── playlist-reader.mjs └── playlist.mjs ├── tests ├── README.md ├── index.mjs ├── locations.json ├── messageformatter.mjs ├── package-lock.json └── package.json ├── views ├── index.ejs └── partials │ ├── almanac.ejs │ ├── current-weather.ejs │ ├── extended-forecast.ejs │ ├── hazards.ejs │ ├── header.ejs │ ├── hourly-graph.ejs │ ├── hourly.ejs │ ├── latest-observations.ejs │ ├── local-forecast.ejs │ ├── progress.ejs │ ├── radar.ejs │ ├── regional-forecast.ejs │ ├── scroll.ejs │ ├── spc-outlook.ejs │ └── travel.ejs └── ws4kp.code-workspace /.dockerignore: -------------------------------------------------------------------------------- 1 | .git/ 2 | Dockerfile 3 | .vscode/ 4 | dist/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.min.* 2 | server/scripts/vendor/* -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "airbnb-base" 9 | ], 10 | "globals": { 11 | "TravelCities": "readonly", 12 | "RegionalCities": "readonly", 13 | "StationInfo": "readonly", 14 | "SunCalc": "readonly", 15 | "NoSleep": "readonly", 16 | "OVERRIDES": "readonly" 17 | }, 18 | "parserOptions": { 19 | "ecmaVersion": "latest", 20 | "sourceType": "module" 21 | }, 22 | "plugins": [], 23 | "rules": { 24 | "indent": [ 25 | "error", 26 | "tab", 27 | { 28 | "SwitchCase": 1 29 | } 30 | ], 31 | "no-tabs": 0, 32 | "no-console": 0, 33 | "max-len": 0, 34 | "no-use-before-define": [ 35 | "error", 36 | { 37 | "variables": false 38 | } 39 | ], 40 | "no-param-reassign": [ 41 | "error", 42 | { 43 | "props": false 44 | } 45 | ], 46 | "no-mixed-operators": [ 47 | "error", 48 | { 49 | "groups": [ 50 | [ 51 | "&", 52 | "|", 53 | "^", 54 | "~", 55 | "<<", 56 | ">>", 57 | ">>>" 58 | ], 59 | [ 60 | "==", 61 | "!=", 62 | "===", 63 | "!==", 64 | ">", 65 | ">=", 66 | "<", 67 | "<=" 68 | ], 69 | [ 70 | "&&", 71 | "||" 72 | ], 73 | [ 74 | "in", 75 | "instanceof" 76 | ] 77 | ], 78 | "allowSamePrecedence": true 79 | } 80 | ], 81 | "import/extensions": [ 82 | "error", 83 | { 84 | "mjs": "always", 85 | "json": "always" 86 | } 87 | ] 88 | }, 89 | "ignorePatterns": [ 90 | "*.min.js" 91 | ] 92 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf 2 | *.mjs text eol=lf 3 | *.ejs text eol=lf 4 | *.html text eol=lf 5 | *.json text eol=lf 6 | *.php text eol=lf -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://buymeacoffee.com/temp.exp'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | Please do not report issues with api.weather.gov being down. It's a new service and not considered fully operational yet. Before reporting an issue or requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser. 11 | 12 | Please include: 13 | * Web browser and OS 14 | * Location for which you are viewing a forecast 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | Before requesting a feature please consider that this is not intended to be a perfect recreation of the WeatherStar 4000, it's a best effort that fits within what's available from the API and within a web browser. 11 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yaml: -------------------------------------------------------------------------------- 1 | name: build-docker 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags: 8 | - 'v*.*.*' 9 | - 'v*.*' 10 | 11 | jobs: 12 | build: 13 | name: Build Image 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | steps: 19 | - name: Checkout 20 | uses: actions/checkout@v4 21 | - name: Docker meta 22 | id: meta 23 | uses: docker/metadata-action@v5 24 | with: 25 | images: | 26 | ghcr.io/netbymatt/ws4kp 27 | flavor: | 28 | latest=false 29 | tags: | 30 | type=raw,priority=1000,value=latest,enable=${{ github.ref == 'refs/heads/main' }} 31 | type=ref,event=branch 32 | type=semver,pattern={{version}} 33 | type=semver,pattern={{major}}.{{minor}} 34 | type=semver,pattern={{major}} 35 | type=sha 36 | - name: Set up QEMU 37 | uses: docker/setup-qemu-action@v3 38 | - name: Set up Buildx 39 | uses: docker/setup-buildx-action@v3 40 | - name: Login to GitHub Container Registry 41 | uses: docker/login-action@v3 42 | with: 43 | registry: ghcr.io 44 | username: ${{ github.actor }} 45 | password: ${{ secrets.GITHUB_TOKEN }} 46 | - name: Build and Push 47 | id: docker_build 48 | uses: docker/build-push-action@v6 49 | with: 50 | context: . 51 | pull: true 52 | push: ${{ github.ref == 'refs/heads/main' }} 53 | platforms: linux/amd64,linux/arm64/v8 54 | tags: ${{ steps.meta.outputs.tags }} 55 | labels: ${{ steps.meta.outputs.labels }} 56 | cache-from: type=gha 57 | cache-to: type=gha,mode=max 58 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 11 * * 6' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v2 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v1 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v1 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v1 72 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/debug.log 3 | server/scripts/custom.js 4 | 5 | #music folder 6 | server/music/* 7 | #except for the readme 8 | !server/music/readme.txt 9 | #and the sample songs 10 | !server/music/default 11 | 12 | #dist folder 13 | dist/* 14 | !dist/readme.txt 15 | 16 | #environment variables 17 | .env -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Frontend", 9 | "request": "launch", 10 | "type": "chrome", 11 | "url": "http://localhost:8080", 12 | "webRoot": "${workspaceFolder}/server", 13 | "skipFiles": [ 14 | "/**", 15 | "**/*.min.js", 16 | "**/vendor/**" 17 | ], 18 | "runtimeArgs": [ 19 | "--autoplay-policy=no-user-gesture-required" 20 | ] 21 | }, 22 | { 23 | "name": "Data:stations", 24 | "program": "${workspaceFolder}/datagenerators/stations.mjs", 25 | "request": "launch", 26 | "skipFiles": [ 27 | "/**" 28 | ], 29 | "type": "node" 30 | }, 31 | { 32 | "name": "Data:regionalcities", 33 | "program": "${workspaceFolder}/datagenerators/regionalcities.mjs", 34 | "request": "launch", 35 | "skipFiles": [ 36 | "/**" 37 | ], 38 | "type": "node" 39 | }, 40 | { 41 | "name": "Data:travelcities", 42 | "program": "${workspaceFolder}/datagenerators/travelcities.mjs", 43 | "request": "launch", 44 | "skipFiles": [ 45 | "/**" 46 | ], 47 | "type": "node" 48 | }, 49 | { 50 | "type": "node", 51 | "request": "launch", 52 | "name": "Server", 53 | "skipFiles": [ 54 | "/**", 55 | ], 56 | "program": "${workspaceFolder}/index.mjs", 57 | }, 58 | { 59 | "type": "node", 60 | "request": "launch", 61 | "name": "Server-dist", 62 | "skipFiles": [ 63 | "/**", 64 | ], 65 | "program": "${workspaceFolder}/index.mjs", 66 | "env": { 67 | "DIST": "1" 68 | } 69 | }, 70 | { 71 | "name": "Test", 72 | "program": "${workspaceFolder}/tests/index.mjs", 73 | "request": "launch", 74 | "skipFiles": [ 75 | "/**" 76 | ], 77 | "type": "node", 78 | "console": "integratedTerminal" 79 | }, 80 | ], 81 | "compounds": [ 82 | { 83 | "name": "Compound", 84 | "configurations": [ 85 | "Frontend", 86 | "Server" 87 | ] 88 | } 89 | ] 90 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "liveSassCompile.settings.formats": [ 3 | { 4 | "format": "compressed", 5 | "extensionName": ".css", 6 | "savePath": "/server/styles", 7 | } 8 | ], 9 | "search.exclude": { 10 | "**/node_modules": true, 11 | "**/bower_components": true, 12 | "**/*.code-search": true, 13 | "**/compiled.css": true, 14 | "**/*.min.js": true, 15 | }, 16 | "editor.formatOnSave": true, 17 | "editor.codeActionsOnSave": { 18 | "source.fixAll.eslint": "explicit" 19 | }, 20 | "eslint.validate": [ 21 | "javascript" 22 | ], 23 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "lint", 7 | "problemMatcher": [ 8 | "$eslint-stylish" 9 | ], 10 | "label": "npm: lint", 11 | "detail": "eslint ./server/scripts/**" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-alpine 2 | WORKDIR /app 3 | 4 | COPY package.json . 5 | COPY package-lock.json . 6 | 7 | RUN npm ci 8 | 9 | COPY . . 10 | CMD ["node", "index.mjs"] 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 Matt Walsh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cors/index.mjs: -------------------------------------------------------------------------------- 1 | // pass through api requests 2 | 3 | // http(s) modules 4 | import https from 'https'; 5 | 6 | // url parsing 7 | import queryString from 'querystring'; 8 | 9 | // return an express router 10 | const cors = (req, res) => { 11 | // add out-going headers 12 | const headers = {}; 13 | headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)'; 14 | headers.accept = req.headers.accept; 15 | 16 | // get query paramaters if the exist 17 | const queryParams = Object.keys(req.query).reduce((acc, key) => { 18 | // skip the paramater 'u' 19 | if (key === 'u') return acc; 20 | // add the paramter to the resulting object 21 | acc[key] = req.query[key]; 22 | return acc; 23 | }, {}); 24 | let query = queryString.encode(queryParams); 25 | if (query.length > 0) query = `?${query}`; 26 | 27 | // get the page 28 | https.get(`https://api.weather.gov${req.path}${query}`, { 29 | headers, 30 | }, (getRes) => { 31 | // pull some info 32 | const { statusCode } = getRes; 33 | // pass the status code through 34 | res.status(statusCode); 35 | 36 | // set headers 37 | res.header('content-type', getRes.headers['content-type']); 38 | // pipe to response 39 | getRes.pipe(res); 40 | }).on('error', (e) => { 41 | console.error(e); 42 | }); 43 | }; 44 | 45 | export default cors; 46 | -------------------------------------------------------------------------------- /cors/outlook.mjs: -------------------------------------------------------------------------------- 1 | // pass through api requests 2 | 3 | // http(s) modules 4 | import https from 'https'; 5 | 6 | // url parsing 7 | import queryString from 'querystring'; 8 | 9 | // return an express router 10 | const outlook = (req, res) => { 11 | // add out-going headers 12 | const headers = {}; 13 | headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)'; 14 | headers.accept = req.headers.accept; 15 | 16 | // get query paramaters if the exist 17 | const queryParams = Object.keys(req.query).reduce((acc, key) => { 18 | // skip the paramater 'u' 19 | if (key === 'u') return acc; 20 | // add the paramter to the resulting object 21 | acc[key] = req.query[key]; 22 | return acc; 23 | }, {}); 24 | let query = queryString.encode(queryParams); 25 | if (query.length > 0) query = `?${query}`; 26 | 27 | // get the page 28 | https.get(`https://www.cpc.ncep.noaa.gov/${req.path}${query}`, { 29 | headers, 30 | }, (getRes) => { 31 | // pull some info 32 | const { statusCode } = getRes; 33 | // pass the status code through 34 | res.status(statusCode); 35 | 36 | // set headers 37 | res.header('content-type', getRes.headers['content-type']); 38 | res.header('last-modified', getRes.headers['last-modified']); 39 | // pipe to response 40 | getRes.pipe(res); 41 | }).on('error', (e) => { 42 | console.error(e); 43 | }); 44 | }; 45 | 46 | export default outlook; 47 | -------------------------------------------------------------------------------- /cors/radar.mjs: -------------------------------------------------------------------------------- 1 | // pass through api requests 2 | 3 | // http(s) modules 4 | import https from 'https'; 5 | 6 | // url parsing 7 | import queryString from 'querystring'; 8 | 9 | // return an express router 10 | const radar = (req, res) => { 11 | // add out-going headers 12 | const headers = {}; 13 | headers['user-agent'] = '(WeatherStar 4000+, ws4000@netbymatt.com)'; 14 | headers.accept = req.headers.accept; 15 | 16 | // get query paramaters if the exist 17 | const queryParams = Object.keys(req.query).reduce((acc, key) => { 18 | // skip the paramater 'u' 19 | if (key === 'u') return acc; 20 | // add the paramter to the resulting object 21 | acc[key] = req.query[key]; 22 | return acc; 23 | }, {}); 24 | let query = queryString.encode(queryParams); 25 | if (query.length > 0) query = `?${query}`; 26 | 27 | // get the page 28 | https.get(`https://radar.weather.gov${req.path}${query}`, { 29 | headers, 30 | }, (getRes) => { 31 | // pull some info 32 | const { statusCode } = getRes; 33 | // pass the status code through 34 | res.status(statusCode); 35 | 36 | // set headers 37 | res.header('content-type', getRes.headers['content-type']); 38 | res.header('last-modified', getRes.headers['last-modified']); 39 | // pipe to response 40 | getRes.pipe(res); 41 | }).on('error', (e) => { 42 | console.error(e); 43 | }); 44 | }; 45 | 46 | export default radar; 47 | -------------------------------------------------------------------------------- /datagenerators/chunk.mjs: -------------------------------------------------------------------------------- 1 | // turn a long array into a set of smaller chunks 2 | 3 | const chunk = (data, chunkSize = 10) => { 4 | const chunks = []; 5 | 6 | let thisChunk = []; 7 | 8 | data.forEach((d) => { 9 | if (thisChunk.length >= chunkSize) { 10 | chunks.push(thisChunk); 11 | thisChunk = []; 12 | } 13 | thisChunk.push(d); 14 | }); 15 | 16 | // final chunk 17 | if (thisChunk.length > 0) chunks.push(thisChunk); 18 | 19 | return chunks; 20 | }; 21 | 22 | export default chunk; 23 | -------------------------------------------------------------------------------- /datagenerators/https.mjs: -------------------------------------------------------------------------------- 1 | // async https wrapper 2 | import https from 'https'; 3 | 4 | const get = (url) => new Promise((resolve, reject) => { 5 | const headers = {}; 6 | headers['user-agent'] = '(WeatherStar 4000+ data generator, ws4000@netbymatt.com)'; 7 | 8 | https.get(url, { 9 | headers, 10 | }, (res) => { 11 | if (res.statusCode === 200) { 12 | const buffers = []; 13 | res.on('data', (data) => buffers.push(data)); 14 | res.on('end', () => resolve(Buffer.concat(buffers).toString())); 15 | } else { 16 | console.log(res); 17 | reject(new Error(`Unable to get: ${url}`)); 18 | } 19 | }).on('error', (e) => { 20 | console.log(e); 21 | reject(e); 22 | }); 23 | }); 24 | 25 | export default get; 26 | -------------------------------------------------------------------------------- /datagenerators/output/travelcities.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "Atlanta", 4 | "Latitude": 33.749, 5 | "Longitude": -84.388, 6 | "point": { 7 | "x": 51, 8 | "y": 87, 9 | "wfo": "FFC" 10 | } 11 | }, 12 | { 13 | "Name": "Boston", 14 | "Latitude": 42.3584, 15 | "Longitude": -71.0598, 16 | "point": { 17 | "x": 71, 18 | "y": 90, 19 | "wfo": "BOX" 20 | } 21 | }, 22 | { 23 | "Name": "Chicago", 24 | "Latitude": 41.9796, 25 | "Longitude": -87.9045, 26 | "point": { 27 | "x": 66, 28 | "y": 77, 29 | "wfo": "LOT" 30 | } 31 | }, 32 | { 33 | "Name": "Cleveland", 34 | "Latitude": 41.4995, 35 | "Longitude": -81.6954, 36 | "point": { 37 | "x": 83, 38 | "y": 65, 39 | "wfo": "CLE" 40 | } 41 | }, 42 | { 43 | "Name": "Dallas", 44 | "Latitude": 32.8959, 45 | "Longitude": -97.0372, 46 | "point": { 47 | "x": 80, 48 | "y": 109, 49 | "wfo": "FWD" 50 | } 51 | }, 52 | { 53 | "Name": "Denver", 54 | "Latitude": 39.7391, 55 | "Longitude": -104.9847, 56 | "point": { 57 | "x": 63, 58 | "y": 61, 59 | "wfo": "BOU" 60 | } 61 | }, 62 | { 63 | "Name": "Detroit", 64 | "Latitude": 42.3314, 65 | "Longitude": -83.0457, 66 | "point": { 67 | "x": 66, 68 | "y": 34, 69 | "wfo": "DTX" 70 | } 71 | }, 72 | { 73 | "Name": "Hartford", 74 | "Latitude": 41.7637, 75 | "Longitude": -72.6851, 76 | "point": { 77 | "x": 21, 78 | "y": 54, 79 | "wfo": "BOX" 80 | } 81 | }, 82 | { 83 | "Name": "Houston", 84 | "Latitude": 29.7633, 85 | "Longitude": -95.3633, 86 | "point": { 87 | "x": 65, 88 | "y": 97, 89 | "wfo": "HGX" 90 | } 91 | }, 92 | { 93 | "Name": "Indianapolis", 94 | "Latitude": 39.7684, 95 | "Longitude": -86.158, 96 | "point": { 97 | "x": 58, 98 | "y": 69, 99 | "wfo": "IND" 100 | } 101 | }, 102 | { 103 | "Name": "Los Angeles", 104 | "Latitude": 34.0522, 105 | "Longitude": -118.2437, 106 | "point": { 107 | "x": 155, 108 | "y": 45, 109 | "wfo": "LOX" 110 | } 111 | }, 112 | { 113 | "Name": "Miami", 114 | "Latitude": 25.7743, 115 | "Longitude": -80.1937, 116 | "point": { 117 | "x": 110, 118 | "y": 51, 119 | "wfo": "MFL" 120 | } 121 | }, 122 | { 123 | "Name": "Minneapolis", 124 | "Latitude": 44.98, 125 | "Longitude": -93.2638, 126 | "point": { 127 | "x": 108, 128 | "y": 72, 129 | "wfo": "MPX" 130 | } 131 | }, 132 | { 133 | "Name": "New York", 134 | "Latitude": 40.7142, 135 | "Longitude": -74.0059, 136 | "point": { 137 | "x": 33, 138 | "y": 35, 139 | "wfo": "OKX" 140 | } 141 | }, 142 | { 143 | "Name": "Norfolk", 144 | "Latitude": 36.8468, 145 | "Longitude": -76.2852, 146 | "point": { 147 | "x": 90, 148 | "y": 52, 149 | "wfo": "AKQ" 150 | } 151 | }, 152 | { 153 | "Name": "Orlando", 154 | "Latitude": 28.5383, 155 | "Longitude": -81.3792, 156 | "point": { 157 | "x": 26, 158 | "y": 68, 159 | "wfo": "MLB" 160 | } 161 | }, 162 | { 163 | "Name": "Philadelphia", 164 | "Latitude": 39.9523, 165 | "Longitude": -75.1638, 166 | "point": { 167 | "x": 50, 168 | "y": 76, 169 | "wfo": "PHI" 170 | } 171 | }, 172 | { 173 | "Name": "Pittsburgh", 174 | "Latitude": 40.4406, 175 | "Longitude": -79.9959, 176 | "point": { 177 | "x": 78, 178 | "y": 66, 179 | "wfo": "PBZ" 180 | } 181 | }, 182 | { 183 | "Name": "St. Louis", 184 | "Latitude": 38.6273, 185 | "Longitude": -90.1979, 186 | "point": { 187 | "x": 95, 188 | "y": 74, 189 | "wfo": "LSX" 190 | } 191 | }, 192 | { 193 | "Name": "San Francisco", 194 | "Latitude": 37.7749, 195 | "Longitude": -122.4194, 196 | "point": { 197 | "x": 85, 198 | "y": 105, 199 | "wfo": "MTR" 200 | } 201 | }, 202 | { 203 | "Name": "Seattle", 204 | "Latitude": 47.6062, 205 | "Longitude": -122.3321, 206 | "point": { 207 | "x": 125, 208 | "y": 68, 209 | "wfo": "SEW" 210 | } 211 | }, 212 | { 213 | "Name": "Syracuse", 214 | "Latitude": 43.0481, 215 | "Longitude": -76.1474, 216 | "point": { 217 | "x": 52, 218 | "y": 99, 219 | "wfo": "BGM" 220 | } 221 | }, 222 | { 223 | "Name": "Tampa", 224 | "Latitude": 27.9475, 225 | "Longitude": -82.4584, 226 | "point": { 227 | "x": 71, 228 | "y": 97, 229 | "wfo": "TBW" 230 | } 231 | }, 232 | { 233 | "Name": "Washington DC", 234 | "Latitude": 38.8951, 235 | "Longitude": -77.0364, 236 | "point": { 237 | "x": 97, 238 | "y": 71, 239 | "wfo": "LWX" 240 | } 241 | } 242 | ] -------------------------------------------------------------------------------- /datagenerators/regionalcities.mjs: -------------------------------------------------------------------------------- 1 | // look up points for each regional city 2 | import fs from 'fs/promises'; 3 | 4 | import chunk from './chunk.mjs'; 5 | import https from './https.mjs'; 6 | 7 | // source data 8 | const regionalCities = JSON.parse(await fs.readFile('./datagenerators/regionalcities-raw.json')); 9 | 10 | const result = []; 11 | const dataChunks = chunk(regionalCities, 5); 12 | 13 | // for loop intentional for use of await 14 | // this keeps the api from getting overwhelmed 15 | for (let i = 0; i < dataChunks.length; i += 1) { 16 | const cityChunk = dataChunks[i]; 17 | 18 | // eslint-disable-next-line no-await-in-loop 19 | const chunkResult = await Promise.all(cityChunk.map(async (city) => { 20 | try { 21 | const data = await https(`https://api.weather.gov/points/${city.lat},${city.lon}`); 22 | const point = JSON.parse(data); 23 | return { 24 | ...city, 25 | point: { 26 | x: point.properties.gridX, 27 | y: point.properties.gridY, 28 | wfo: point.properties.gridId, 29 | }, 30 | }; 31 | } catch (e) { 32 | console.error(e); 33 | return city; 34 | } 35 | })); 36 | 37 | result.push(...chunkResult); 38 | } 39 | 40 | await fs.writeFile('./datagenerators/output/regionalcities.json', JSON.stringify(result, null, ' ')); 41 | -------------------------------------------------------------------------------- /datagenerators/stations-states.mjs: -------------------------------------------------------------------------------- 1 | export default [ 2 | 'AZ', 3 | 'AL', 4 | 'AK', 5 | 'AR', 6 | 'CA', 7 | 'CO', 8 | 'CT', 9 | 'DE', 10 | 'FL', 11 | 'GA', 12 | 'HI', 13 | 'ID', 14 | 'IL', 15 | 'IN', 16 | 'IA', 17 | 'KS', 18 | 'KY', 19 | 'LA', 20 | 'ME', 21 | 'MD', 22 | 'MA', 23 | 'MI', 24 | 'MN', 25 | 'MS', 26 | 'MO', 27 | 'MT', 28 | 'NE', 29 | 'NV', 30 | 'NH', 31 | 'NJ', 32 | 'NM', 33 | 'NY', 34 | 'NC', 35 | 'ND', 36 | 'OH', 37 | 'OK', 38 | 'OR', 39 | 'PA', 40 | 'RI', 41 | 'SC', 42 | 'SD', 43 | 'TN', 44 | 'TX', 45 | 'UT', 46 | 'VT', 47 | 'VA', 48 | 'WA', 49 | 'WV', 50 | 'WI', 51 | 'WY', 52 | 'PR', 53 | ]; 54 | -------------------------------------------------------------------------------- /datagenerators/stations.mjs: -------------------------------------------------------------------------------- 1 | // list all stations in a single file 2 | // only find stations with 4 letter codes 3 | 4 | import { writeFileSync } from 'fs'; 5 | import https from './https.mjs'; 6 | import states from './stations-states.mjs'; 7 | import chunk from './chunk.mjs'; 8 | 9 | // skip stations starting with these letters 10 | const skipStations = ['U', 'C', 'H', 'W', 'Y', 'T', 'S', 'M', 'O', 'L', 'A', 'F', 'B', 'N', 'V', 'R', 'D', 'E', 'I', 'G', 'J']; 11 | 12 | // chunk the list of states 13 | const chunkStates = chunk(states, 1); 14 | 15 | // store output 16 | const output = {}; 17 | 18 | // process all chunks 19 | for (let i = 0; i < chunkStates.length; i += 1) { 20 | const stateChunk = chunkStates[i]; 21 | // loop through states 22 | 23 | // eslint-disable-next-line no-await-in-loop 24 | await Promise.allSettled(stateChunk.map(async (state) => { 25 | try { 26 | let stations; 27 | let next = `https://api.weather.gov/stations?state=${state}`; 28 | let round = 0; 29 | do { 30 | console.log(`Getting: ${state}-${round}`); 31 | // get list and parse the JSON 32 | // eslint-disable-next-line no-await-in-loop 33 | const stationsRaw = await https(next); 34 | stations = JSON.parse(stationsRaw); 35 | // filter stations for 4 letter identifiers 36 | const stationsFiltered4 = stations.features.filter((station) => station.properties.stationIdentifier.match(/^[A-Z]{4}$/)); 37 | // filter against starting letter 38 | const stationsFiltered = stationsFiltered4.filter((station) => !skipStations.includes(station.properties.stationIdentifier.slice(0, 1))); 39 | // add each resulting station to the output 40 | stationsFiltered.forEach((station) => { 41 | const id = station.properties.stationIdentifier; 42 | if (output[id]) { 43 | console.log(`Duplicate station: ${state}-${id}`); 44 | return; 45 | } 46 | output[id] = { 47 | id, 48 | city: station.properties.name, 49 | state, 50 | lat: station.geometry.coordinates[1], 51 | lon: station.geometry.coordinates[0], 52 | }; 53 | }); 54 | next = stations?.pagination?.next; 55 | round += 1; 56 | // write the output 57 | writeFileSync('./datagenerators/output/stations.json', JSON.stringify(output, null, 2)); 58 | } 59 | while (next && stations.features.length > 0); 60 | console.log(`Complete: ${state}`); 61 | return true; 62 | } catch (e) { 63 | console.error(`Unable to get state: ${state}`); 64 | return false; 65 | } 66 | })); 67 | } 68 | -------------------------------------------------------------------------------- /datagenerators/travelcities-raw.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Name": "Atlanta", 4 | "Latitude": 33.749, 5 | "Longitude": -84.388 6 | }, 7 | { 8 | "Name": "Boston", 9 | "Latitude": 42.3584, 10 | "Longitude": -71.0598 11 | }, 12 | { 13 | "Name": "Chicago", 14 | "Latitude": 41.9796, 15 | "Longitude": -87.9045 16 | }, 17 | { 18 | "Name": "Cleveland", 19 | "Latitude": 41.4995, 20 | "Longitude": -81.6954 21 | }, 22 | { 23 | "Name": "Dallas", 24 | "Latitude": 32.8959, 25 | "Longitude": -97.0372 26 | }, 27 | { 28 | "Name": "Denver", 29 | "Latitude": 39.7391, 30 | "Longitude": -104.9847 31 | }, 32 | { 33 | "Name": "Detroit", 34 | "Latitude": 42.3314, 35 | "Longitude": -83.0457 36 | }, 37 | { 38 | "Name": "Hartford", 39 | "Latitude": 41.7637, 40 | "Longitude": -72.6851 41 | }, 42 | { 43 | "Name": "Houston", 44 | "Latitude": 29.7633, 45 | "Longitude": -95.3633 46 | }, 47 | { 48 | "Name": "Indianapolis", 49 | "Latitude": 39.7684, 50 | "Longitude": -86.158 51 | }, 52 | { 53 | "Name": "Los Angeles", 54 | "Latitude": 34.0522, 55 | "Longitude": -118.2437 56 | }, 57 | { 58 | "Name": "Miami", 59 | "Latitude": 25.7743, 60 | "Longitude": -80.1937 61 | }, 62 | { 63 | "Name": "Minneapolis", 64 | "Latitude": 44.98, 65 | "Longitude": -93.2638 66 | }, 67 | { 68 | "Name": "New York", 69 | "Latitude": 40.7142, 70 | "Longitude": -74.0059 71 | }, 72 | { 73 | "Name": "Norfolk", 74 | "Latitude": 36.8468, 75 | "Longitude": -76.2852 76 | }, 77 | { 78 | "Name": "Orlando", 79 | "Latitude": 28.5383, 80 | "Longitude": -81.3792 81 | }, 82 | { 83 | "Name": "Philadelphia", 84 | "Latitude": 39.9523, 85 | "Longitude": -75.1638 86 | }, 87 | { 88 | "Name": "Pittsburgh", 89 | "Latitude": 40.4406, 90 | "Longitude": -79.9959 91 | }, 92 | { 93 | "Name": "St. Louis", 94 | "Latitude": 38.6273, 95 | "Longitude": -90.1979 96 | }, 97 | { 98 | "Name": "San Francisco", 99 | "Latitude": 37.7749, 100 | "Longitude": -122.4194 101 | }, 102 | { 103 | "Name": "Seattle", 104 | "Latitude": 47.6062, 105 | "Longitude": -122.3321 106 | }, 107 | { 108 | "Name": "Syracuse", 109 | "Latitude": 43.0481, 110 | "Longitude": -76.1474 111 | }, 112 | { 113 | "Name": "Tampa", 114 | "Latitude": 27.9475, 115 | "Longitude": -82.4584 116 | }, 117 | { 118 | "Name": "Washington DC", 119 | "Latitude": 38.8951, 120 | "Longitude": -77.0364 121 | } 122 | ] -------------------------------------------------------------------------------- /datagenerators/travelcities.mjs: -------------------------------------------------------------------------------- 1 | // look up points for each travel city 2 | import { readFile, writeFile } from 'fs/promises'; 3 | import chunk from './chunk.mjs'; 4 | import https from './https.mjs'; 5 | 6 | // source data 7 | const travelCities = JSON.parse(await readFile('./datagenerators/travelcities-raw.json')); 8 | 9 | const result = []; 10 | const dataChunks = chunk(travelCities, 5); 11 | 12 | // for loop intentional for use of await 13 | // this keeps the api from getting overwhelmed 14 | for (let i = 0; i < dataChunks.length; i += 1) { 15 | const cityChunk = dataChunks[i]; 16 | 17 | // eslint-disable-next-line no-await-in-loop 18 | const chunkResult = await Promise.all(cityChunk.map(async (city) => { 19 | try { 20 | const data = await https(`https://api.weather.gov/points/${city.Latitude},${city.Longitude}`); 21 | const point = JSON.parse(data); 22 | return { 23 | ...city, 24 | point: { 25 | x: point.properties.gridX, 26 | y: point.properties.gridY, 27 | wfo: point.properties.gridId, 28 | }, 29 | }; 30 | } catch (e) { 31 | console.error(e); 32 | return city; 33 | } 34 | })); 35 | 36 | result.push(...chunkResult); 37 | } 38 | 39 | await writeFile('./datagenerators/output/travelcities.json', JSON.stringify(result, null, ' ')); 40 | -------------------------------------------------------------------------------- /dist/readme.txt: -------------------------------------------------------------------------------- 1 | This folder is a placeholder for static files generated by the gulp task buildDist -------------------------------------------------------------------------------- /gulp/update-vendor.mjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { src, series, dest } from 'gulp'; 3 | import { deleteAsync } from 'del'; 4 | import rename from 'gulp-rename'; 5 | 6 | const clean = () => deleteAsync(['./server/scripts/vendor/auto/**']); 7 | 8 | const vendorFiles = [ 9 | './node_modules/luxon/build/es6/luxon.js', 10 | './node_modules/luxon/build/es6/luxon.js.map', 11 | './node_modules/nosleep.js/dist/NoSleep.js', 12 | './node_modules/suncalc/suncalc.js', 13 | './node_modules/swiped-events/src/swiped-events.js', 14 | ]; 15 | 16 | const copy = () => src(vendorFiles) 17 | .pipe(rename((path) => { 18 | path.dirname = path.dirname.toLowerCase(); 19 | path.basename = path.basename.toLowerCase(); 20 | path.extname = path.extname.toLowerCase(); 21 | if (path.basename === 'luxon') path.extname = '.mjs'; 22 | })) 23 | .pipe(dest('./server/scripts/vendor/auto')); 24 | 25 | const updateVendor = series(clean, copy); 26 | 27 | export default updateVendor; 28 | -------------------------------------------------------------------------------- /gulpfile.mjs: -------------------------------------------------------------------------------- 1 | import updateVendor from './gulp/update-vendor.mjs'; 2 | import publishFrontend, { buildDist, invalidate } from './gulp/publish-frontend.mjs'; 3 | 4 | export { 5 | updateVendor, 6 | publishFrontend, 7 | buildDist, 8 | invalidate, 9 | }; 10 | -------------------------------------------------------------------------------- /index.mjs: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import express from 'express'; 3 | import fs from 'fs'; 4 | import corsPassThru from './cors/index.mjs'; 5 | import radarPassThru from './cors/radar.mjs'; 6 | import outlookPassThru from './cors/outlook.mjs'; 7 | import playlist from './src/playlist.mjs'; 8 | import OVERRIDES from './src/overrides.mjs'; 9 | 10 | const app = express(); 11 | const port = process.env.WS4KP_PORT ?? 8080; 12 | 13 | // template engine 14 | app.set('view engine', 'ejs'); 15 | 16 | // cors pass-thru to api.weather.gov 17 | app.get('/stations/*station', corsPassThru); 18 | app.get('/Conus/*radar', radarPassThru); 19 | app.get('/products/*product', outlookPassThru); 20 | app.get('/playlist.json', playlist); 21 | 22 | // version 23 | const { version } = JSON.parse(fs.readFileSync('package.json')); 24 | 25 | // read and parse environment variables to append to the query string 26 | // use the permalink (share) button on the web app to generate a starting point for your configuration 27 | // then take each key/value in the querystring and append WSQS_ to the beginning, and then replace any 28 | // hyphens with underscores in the key name 29 | // environment variables are read from the command line and .env file via the dotenv package 30 | 31 | const qsVars = {}; 32 | 33 | Object.entries(process.env).forEach(([key, value]) => { 34 | // test for key matching pattern described above 35 | if (key.match(/^WSQS_[A-Za-z0-9_]+$/)) { 36 | // convert the key to a querystring formatted key 37 | const formattedKey = key.replace(/^WSQS_/, '').replaceAll('_', '-'); 38 | qsVars[formattedKey] = value; 39 | } 40 | }); 41 | 42 | // single flag to determine if environment variables are present 43 | const hasQsVars = Object.entries(qsVars).length > 0; 44 | 45 | // turn the environment query string into search params 46 | const defaultSearchParams = (new URLSearchParams(qsVars)).toString(); 47 | 48 | const index = (req, res) => { 49 | // test for no query string in request and if environment query string values were provided 50 | if (hasQsVars && Object.keys(req.query).length === 0) { 51 | // redirect the user to the query-string appended url 52 | const url = new URL(`${req.protocol}://${req.host}${req.url}`); 53 | url.search = defaultSearchParams; 54 | res.redirect(307, url.toString()); 55 | return; 56 | } 57 | // return the standard page 58 | res.render('index', { 59 | production: false, 60 | version, 61 | OVERRIDES, 62 | }); 63 | }; 64 | 65 | const geoip = (req, res) => { 66 | res.set({ 67 | 'x-geoip-city': 'Orlando', 68 | 'x-geoip-country': 'US', 69 | 'x-geoip-country-name': 'United States', 70 | 'x-geoip-country-region': 'FL', 71 | 'x-geoip-country-region-name': 'Florida', 72 | 'x-geoip-latitude': '28.52135', 73 | 'x-geoip-longitude': '-81.41079', 74 | 'x-geoip-postal-code': '32789', 75 | 'x-geoip-time-zone': 'America/New_York', 76 | 'content-type': 'application/json', 77 | }); 78 | res.json({}); 79 | }; 80 | 81 | // debugging 82 | if (process.env?.DIST === '1') { 83 | // distribution 84 | app.use('/images', express.static('./server/images')); 85 | app.use('/fonts', express.static('./server/fonts')); 86 | app.use('/scripts', express.static('./server/scripts')); 87 | app.use('/geoip', geoip); 88 | app.use('/', express.static('./dist')); 89 | } else { 90 | // debugging 91 | app.get('/index.html', index); 92 | app.use('/geoip', geoip); 93 | app.get('/', index); 94 | app.get('*name', express.static('./server')); 95 | } 96 | 97 | const server = app.listen(port, () => { 98 | console.log(`Server listening on port ${port}`); 99 | }); 100 | 101 | // graceful shutdown 102 | const gracefulShutdown = () => { 103 | server.close(() => { 104 | console.log('Server closed'); 105 | }); 106 | }; 107 | 108 | process.on('SIGINT', gracefulShutdown); 109 | process.on('SIGTERM', gracefulShutdown); 110 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ws4kp", 3 | "version": "5.23.3", 4 | "description": "Welcome to the WeatherStar 4000+ project page!", 5 | "main": "index.mjs", 6 | "type": "module", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "build:css": "sass --style=compressed ./server/styles/scss/main.scss ./server/styles/main.css", 10 | "build": "gulp buildDist", 11 | "lint": "eslint ./server/scripts/**/*.mjs", 12 | "lint:fix": "eslint --fix ./server/scripts/**/*.mjs" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/netbymatt/ws4kp.git" 17 | }, 18 | "author": "Matt Walsh", 19 | "license": "MIT", 20 | "bugs": { 21 | "url": "https://github.com/netbymatt/ws4kp/issues" 22 | }, 23 | "homepage": "https://github.com/netbymatt/ws4kp#readme", 24 | "devDependencies": { 25 | "@aws-sdk/client-cloudfront": "^3.609.0", 26 | "del": "^8.0.0", 27 | "eslint": "^8.2.0", 28 | "eslint-config-airbnb-base": "^15.0.0", 29 | "eslint-plugin-import": "^2.10.0", 30 | "gulp": "^5.0.0", 31 | "gulp-awspublish": "^8.0.0", 32 | "gulp-concat": "^2.6.1", 33 | "gulp-ejs": "^5.1.0", 34 | "gulp-file": "^0.4.0", 35 | "gulp-rename": "^2.0.0", 36 | "gulp-s3-uploader": "^1.0.6", 37 | "gulp-sass": "^6.0.0", 38 | "gulp-terser": "^2.0.0", 39 | "luxon": "^3.0.0", 40 | "nosleep.js": "^0.12.0", 41 | "sass": "^1.54.0", 42 | "suncalc": "^1.8.0", 43 | "swiped-events": "^1.1.4", 44 | "terser-webpack-plugin": "^5.3.6", 45 | "webpack-stream": "^7.0.0", 46 | "gulp-html-minifier-terser": "^7.1.0" 47 | }, 48 | "dependencies": { 49 | "dotenv": "^16.5.0", 50 | "ejs": "^3.1.5", 51 | "express": "^5.1.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /server/fonts/Star4000 Extended.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/fonts/Star4000 Extended.woff -------------------------------------------------------------------------------- /server/fonts/Star4000 Large.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/fonts/Star4000 Large.ttf -------------------------------------------------------------------------------- /server/fonts/Star4000 Large.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/fonts/Star4000 Large.woff -------------------------------------------------------------------------------- /server/fonts/Star4000 Small.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/fonts/Star4000 Small.woff -------------------------------------------------------------------------------- /server/fonts/Star4000.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/fonts/Star4000.woff -------------------------------------------------------------------------------- /server/images/backgrounds/1-chart.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/1-chart.png -------------------------------------------------------------------------------- /server/images/backgrounds/1-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/1-wide.png -------------------------------------------------------------------------------- /server/images/backgrounds/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/1.png -------------------------------------------------------------------------------- /server/images/backgrounds/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/2.png -------------------------------------------------------------------------------- /server/images/backgrounds/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/3.png -------------------------------------------------------------------------------- /server/images/backgrounds/4-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/4-wide.png -------------------------------------------------------------------------------- /server/images/backgrounds/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/4.png -------------------------------------------------------------------------------- /server/images/backgrounds/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/5.png -------------------------------------------------------------------------------- /server/images/backgrounds/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/backgrounds/6.png -------------------------------------------------------------------------------- /server/images/gimp/Background 6.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/gimp/Background 6.xcf -------------------------------------------------------------------------------- /server/images/gimp/Radar Basemap.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/gimp/Radar Basemap.xcf -------------------------------------------------------------------------------- /server/images/gimp/Radar Basemap2.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/gimp/Radar Basemap2.xcf -------------------------------------------------------------------------------- /server/images/gimp/Radar Basemap3.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/gimp/Radar Basemap3.xcf -------------------------------------------------------------------------------- /server/images/gimp/Radar Basemap4.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/gimp/Radar Basemap4.xcf -------------------------------------------------------------------------------- /server/images/gimp/Radar Basemap5.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/gimp/Radar Basemap5.xcf -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Blowing-Snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Blowing-Snow.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Clear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Clear.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Cloudy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Cloudy.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Fog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Fog.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Freezing-Rain-Snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Freezing-Rain-Snow.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Freezing-Rain.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Freezing-Rain.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Heavy-Snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Heavy-Snow.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Light-Snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Light-Snow.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Mostly-Clear.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Mostly-Clear.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Partly-Cloudy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Partly-Cloudy.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Rain-Sleet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Rain-Sleet.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Rain-Snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Rain-Snow.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Rain.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Rain.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Scattered-Thunderstorms-Day.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Scattered-Thunderstorms-Day.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Scattered-Thunderstorms-Night.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Scattered-Thunderstorms-Night.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Shower.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Shower.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Sleet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Sleet.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Smoke.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Smoke.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Snow-Sleet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Snow-Sleet.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Sunny.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Sunny.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Thunderstorm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Thunderstorm.gif -------------------------------------------------------------------------------- /server/images/icons/current-conditions/Windy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/current-conditions/Windy.gif -------------------------------------------------------------------------------- /server/images/icons/moon-phases/First-Quarter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/moon-phases/First-Quarter.gif -------------------------------------------------------------------------------- /server/images/icons/moon-phases/Full-Moon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/moon-phases/Full-Moon.gif -------------------------------------------------------------------------------- /server/images/icons/moon-phases/Last-Quarter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/moon-phases/Last-Quarter.gif -------------------------------------------------------------------------------- /server/images/icons/moon-phases/New-Moon.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/moon-phases/New-Moon.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Blowing-Snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Blowing-Snow.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Clear-1992.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Clear-1992.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Clear-Wind-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Clear-Wind-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Cloudy-Wind.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Cloudy-Wind.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Cloudy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Cloudy.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Cold.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Cold.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Fog.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Fog.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Freezing-Rain-1992.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Freezing-Rain-1992.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Freezing-Rain-Snow-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Freezing-Rain-Snow-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Haze.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Haze.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Heavy-Snow-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Heavy-Snow-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Hot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Hot.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Light-Snow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Light-Snow.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Mostly-Cloudy-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Mostly-Cloudy-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Partly-Clear-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Partly-Clear-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Partly-Cloudy-Night.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Partly-Cloudy-Night.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Partly-Cloudy.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Partly-Cloudy.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Rain-1992.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Rain-1992.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Rain-Sleet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Rain-Sleet.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Rain-Snow-1992.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Rain-Snow-1992.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Scattered-Showers-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Scattered-Showers-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Scattered-Showers-Night-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Scattered-Showers-Night-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Scattered-Tstorms-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Scattered-Tstorms-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Scattered-Tstorms-Night-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Scattered-Tstorms-Night-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Sleet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Sleet.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Smoke.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Smoke.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Snow-Sleet.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Snow-Sleet.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Sunny-Wind-1994.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Sunny-Wind-1994.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Sunny.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Sunny.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/ThunderSnow.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/ThunderSnow.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Thunderstorm.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Thunderstorm.gif -------------------------------------------------------------------------------- /server/images/icons/regional-maps/Wind.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/icons/regional-maps/Wind.gif -------------------------------------------------------------------------------- /server/images/logos/logo-corner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/logos/logo-corner.png -------------------------------------------------------------------------------- /server/images/logos/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/logos/logo192.png -------------------------------------------------------------------------------- /server/images/logos/noaa.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/logos/noaa.gif -------------------------------------------------------------------------------- /server/images/maps/basemap.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/basemap.webp -------------------------------------------------------------------------------- /server/images/maps/radar-alaska.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-alaska.png -------------------------------------------------------------------------------- /server/images/maps/radar-hawaii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-hawaii.png -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/00-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/00-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/01-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/01-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/02-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/02-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/03-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/03-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/04-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/04-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/05-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/05-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/06-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/06-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/07-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/07-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/08-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/08-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-00.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-00.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-01.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-01.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-02.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-02.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-03.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-03.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-04.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-04.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-05.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-05.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-06.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-06.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-07.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-07.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-08.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-08.webp -------------------------------------------------------------------------------- /server/images/maps/radar-tiles/09-09.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar-tiles/09-09.webp -------------------------------------------------------------------------------- /server/images/maps/radar.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/maps/radar.webp -------------------------------------------------------------------------------- /server/images/nav/ic_fullscreen_exit_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_fullscreen_exit_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_fullscreen_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_fullscreen_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_gps_fixed_black_18dp_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_gps_fixed_black_18dp_1x.png -------------------------------------------------------------------------------- /server/images/nav/ic_gps_fixed_white_18dp_1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_gps_fixed_white_18dp_1x.png -------------------------------------------------------------------------------- /server/images/nav/ic_menu_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_menu_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_pause_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_pause_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_play_arrow_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_play_arrow_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_refresh_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_refresh_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_scanlines_off_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_scanlines_off_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_scanlines_on_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_scanlines_on_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_skip_next_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_skip_next_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_skip_previous_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_skip_previous_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_volume_off_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_volume_off_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/nav/ic_volume_on_white_24dp_2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/nav/ic_volume_on_white_24dp_2x.png -------------------------------------------------------------------------------- /server/images/social/1200x600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/images/social/1200x600.png -------------------------------------------------------------------------------- /server/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "WeatherStar 4000+", 3 | "icons": [ 4 | { 5 | "src": "/images/logos/logo192.png", 6 | "sizes": "192x192", 7 | "type": "images/png" 8 | } 9 | ], 10 | "start_url": "/", 11 | "display": "standalone" 12 | } -------------------------------------------------------------------------------- /server/music/default/Catch the Sun.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/music/default/Catch the Sun.mp3 -------------------------------------------------------------------------------- /server/music/default/Crisp day.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/music/default/Crisp day.mp3 -------------------------------------------------------------------------------- /server/music/default/Rolling Clouds.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/music/default/Rolling Clouds.mp3 -------------------------------------------------------------------------------- /server/music/default/Strong Breeze.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/music/default/Strong Breeze.mp3 -------------------------------------------------------------------------------- /server/music/readme.txt: -------------------------------------------------------------------------------- 1 | .mp3 files placed in this folder will be available via the un-mute button in the application. 2 | No subdirectories will be scanned, and music will be played in a random order. 3 | The default folder will be used only if no .mp3 files are found in this /server/music folder -------------------------------------------------------------------------------- /server/robots.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/netbymatt/ws4kp/1b49e02cd8f6d3a326dab9544da53b7a6ba2b49d/server/robots.txt -------------------------------------------------------------------------------- /server/scripts/custom.sample.js: -------------------------------------------------------------------------------- 1 | // this file is loaded by the main html page (when renamed to custom.js) 2 | // it is intended to allow for customizations that do not get published back to the git repo 3 | // for example, changing the logo 4 | 5 | const customTask = () => { 6 | // get all of the logo images 7 | const logos = document.querySelectorAll('.logo img'); 8 | // loop through each logo 9 | logos.forEach((elem) => { 10 | // change the source 11 | elem.src = 'my-custom-logo.gif'; 12 | }); 13 | }; 14 | 15 | // start running after all content is loaded, or immediately if page content is already loaded 16 | if (document.readyState === 'loading') { 17 | // Loading hasn't finished yet 18 | document.addEventListener('DOMContentLoaded', customTask); 19 | } else { 20 | // `DOMContentLoaded` has already fired 21 | customTask(); 22 | } 23 | document.addEventListener('DOMContentLoaded', () => { 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /server/scripts/data/travelcities.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | const TravelCities = [ 3 | { 4 | Name: 'Atlanta', 5 | Latitude: 33.749, 6 | Longitude: -84.388, 7 | point: { 8 | x: 51, 9 | y: 87, 10 | wfo: 'FFC', 11 | }, 12 | }, 13 | { 14 | Name: 'Boston', 15 | Latitude: 42.3584, 16 | Longitude: -71.0598, 17 | point: { 18 | x: 71, 19 | y: 90, 20 | wfo: 'BOX', 21 | }, 22 | }, 23 | { 24 | Name: 'Chicago', 25 | Latitude: 41.9796, 26 | Longitude: -87.9045, 27 | point: { 28 | x: 66, 29 | y: 77, 30 | wfo: 'LOT', 31 | }, 32 | }, 33 | { 34 | Name: 'Cleveland', 35 | Latitude: 41.4995, 36 | Longitude: -81.6954, 37 | point: { 38 | x: 83, 39 | y: 65, 40 | wfo: 'CLE', 41 | }, 42 | }, 43 | { 44 | Name: 'Dallas', 45 | Latitude: 32.8959, 46 | Longitude: -97.0372, 47 | point: { 48 | x: 80, 49 | y: 109, 50 | wfo: 'FWD', 51 | }, 52 | }, 53 | { 54 | Name: 'Denver', 55 | Latitude: 39.7391, 56 | Longitude: -104.9847, 57 | point: { 58 | x: 63, 59 | y: 61, 60 | wfo: 'BOU', 61 | }, 62 | }, 63 | { 64 | Name: 'Detroit', 65 | Latitude: 42.3314, 66 | Longitude: -83.0457, 67 | point: { 68 | x: 66, 69 | y: 34, 70 | wfo: 'DTX', 71 | }, 72 | }, 73 | { 74 | Name: 'Hartford', 75 | Latitude: 41.7637, 76 | Longitude: -72.6851, 77 | point: { 78 | x: 21, 79 | y: 54, 80 | wfo: 'BOX', 81 | }, 82 | }, 83 | { 84 | Name: 'Houston', 85 | Latitude: 29.7633, 86 | Longitude: -95.3633, 87 | point: { 88 | x: 65, 89 | y: 97, 90 | wfo: 'HGX', 91 | }, 92 | }, 93 | { 94 | Name: 'Indianapolis', 95 | Latitude: 39.7684, 96 | Longitude: -86.158, 97 | point: { 98 | x: 58, 99 | y: 69, 100 | wfo: 'IND', 101 | }, 102 | }, 103 | { 104 | Name: 'Los Angeles', 105 | Latitude: 34.0522, 106 | Longitude: -118.2437, 107 | point: { 108 | x: 155, 109 | y: 45, 110 | wfo: 'LOX', 111 | }, 112 | }, 113 | { 114 | Name: 'Miami', 115 | Latitude: 25.7743, 116 | Longitude: -80.1937, 117 | point: { 118 | x: 110, 119 | y: 51, 120 | wfo: 'MFL', 121 | }, 122 | }, 123 | { 124 | Name: 'Minneapolis', 125 | Latitude: 44.98, 126 | Longitude: -93.2638, 127 | point: { 128 | x: 108, 129 | y: 72, 130 | wfo: 'MPX', 131 | }, 132 | }, 133 | { 134 | Name: 'New York', 135 | Latitude: 40.7142, 136 | Longitude: -74.0059, 137 | point: { 138 | x: 33, 139 | y: 35, 140 | wfo: 'OKX', 141 | }, 142 | }, 143 | { 144 | Name: 'Norfolk', 145 | Latitude: 36.8468, 146 | Longitude: -76.2852, 147 | point: { 148 | x: 90, 149 | y: 52, 150 | wfo: 'AKQ', 151 | }, 152 | }, 153 | { 154 | Name: 'Orlando', 155 | Latitude: 28.5383, 156 | Longitude: -81.3792, 157 | point: { 158 | x: 26, 159 | y: 68, 160 | wfo: 'MLB', 161 | }, 162 | }, 163 | { 164 | Name: 'Philadelphia', 165 | Latitude: 39.9523, 166 | Longitude: -75.1638, 167 | point: { 168 | x: 50, 169 | y: 76, 170 | wfo: 'PHI', 171 | }, 172 | }, 173 | { 174 | Name: 'Pittsburgh', 175 | Latitude: 40.4406, 176 | Longitude: -79.9959, 177 | point: { 178 | x: 78, 179 | y: 66, 180 | wfo: 'PBZ', 181 | }, 182 | }, 183 | { 184 | Name: 'St. Louis', 185 | Latitude: 38.6273, 186 | Longitude: -90.1979, 187 | point: { 188 | x: 95, 189 | y: 74, 190 | wfo: 'LSX', 191 | }, 192 | }, 193 | { 194 | Name: 'San Francisco', 195 | Latitude: 37.7749, 196 | Longitude: -122.4194, 197 | point: { 198 | x: 85, 199 | y: 105, 200 | wfo: 'MTR', 201 | }, 202 | }, 203 | { 204 | Name: 'Seattle', 205 | Latitude: 47.6062, 206 | Longitude: -122.3321, 207 | point: { 208 | x: 125, 209 | y: 68, 210 | wfo: 'SEW', 211 | }, 212 | }, 213 | { 214 | Name: 'Syracuse', 215 | Latitude: 43.0481, 216 | Longitude: -76.1474, 217 | point: { 218 | x: 52, 219 | y: 99, 220 | wfo: 'BGM', 221 | }, 222 | }, 223 | { 224 | Name: 'Tampa', 225 | Latitude: 27.9475, 226 | Longitude: -82.4584, 227 | point: { 228 | x: 71, 229 | y: 97, 230 | wfo: 'TBW', 231 | }, 232 | }, 233 | { 234 | Name: 'Washington DC', 235 | Latitude: 38.8951, 236 | Longitude: -77.0364, 237 | point: { 238 | x: 97, 239 | y: 71, 240 | wfo: 'LWX', 241 | }, 242 | }, 243 | ]; 244 | -------------------------------------------------------------------------------- /server/scripts/modules/extendedforecast.mjs: -------------------------------------------------------------------------------- 1 | // display extended forecast graphically 2 | // technically uses the same data as the local forecast, we'll let the browser do the caching of that 3 | 4 | import STATUS from './status.mjs'; 5 | import { json } from './utils/fetch.mjs'; 6 | import { DateTime } from '../vendor/auto/luxon.mjs'; 7 | import { getLargeIcon } from './icons.mjs'; 8 | import { preloadImg } from './utils/image.mjs'; 9 | import WeatherDisplay from './weatherdisplay.mjs'; 10 | import { registerDisplay } from './navigation.mjs'; 11 | import settings from './settings.mjs'; 12 | 13 | class ExtendedForecast extends WeatherDisplay { 14 | constructor(navId, elemId) { 15 | super(navId, elemId, 'Extended Forecast', true); 16 | 17 | // set timings 18 | this.timing.totalScreens = 2; 19 | } 20 | 21 | async getData(weatherParameters, refresh) { 22 | if (!super.getData(weatherParameters, refresh)) return; 23 | 24 | // request us or si units 25 | try { 26 | this.data = await json(this.weatherParameters.forecast, { 27 | data: { 28 | units: settings.units.value, 29 | }, 30 | retryCount: 3, 31 | stillWaiting: () => this.stillWaiting(), 32 | }); 33 | } catch (error) { 34 | console.error('Unable to get extended forecast'); 35 | console.error(error.status, error.responseJSON); 36 | // if there's no previous data, fail 37 | if (!this.data) { 38 | this.setStatus(STATUS.failed); 39 | return; 40 | } 41 | } 42 | // we only get here if there was no error above 43 | this.screenIndex = 0; 44 | this.setStatus(STATUS.loaded); 45 | } 46 | 47 | async drawCanvas() { 48 | super.drawCanvas(); 49 | 50 | // determine bounds 51 | // grab the first three or second set of three array elements 52 | const forecast = parse(this.data.properties.periods).slice(0 + 3 * this.screenIndex, 3 + this.screenIndex * 3); 53 | 54 | // create each day template 55 | const days = forecast.map((Day) => { 56 | const fill = { 57 | icon: { type: 'img', src: Day.icon }, 58 | condition: Day.text, 59 | date: Day.dayName, 60 | }; 61 | 62 | const { low, high } = Day; 63 | if (low !== undefined) { 64 | fill['value-lo'] = Math.round(low); 65 | } 66 | fill['value-hi'] = Math.round(high); 67 | 68 | // return the filled template 69 | return this.fillTemplate('day', fill); 70 | }); 71 | 72 | // empty and update the container 73 | const dayContainer = this.elem.querySelector('.day-container'); 74 | dayContainer.innerHTML = ''; 75 | dayContainer.append(...days); 76 | this.finishDraw(); 77 | } 78 | } 79 | 80 | // the api provides the forecast in 12 hour increments, flatten to day increments with high and low temperatures 81 | const parse = (fullForecast) => { 82 | // create a list of days starting with today 83 | const Days = [0, 1, 2, 3, 4, 5, 6]; 84 | 85 | const dates = Days.map((shift) => { 86 | const date = DateTime.local().startOf('day').plus({ days: shift }); 87 | return date.toLocaleString({ weekday: 'short' }); 88 | }); 89 | 90 | // track the destination forecast index 91 | let destIndex = 0; 92 | const forecast = []; 93 | fullForecast.forEach((period) => { 94 | // create the destination object if necessary 95 | if (!forecast[destIndex]) { 96 | forecast.push({ 97 | dayName: '', low: undefined, high: undefined, text: undefined, icon: undefined, 98 | }); 99 | } 100 | // get the object to modify/populate 101 | const fDay = forecast[destIndex]; 102 | // high temperature will always be last in the source array so it will overwrite the low values assigned below 103 | fDay.icon = getLargeIcon(period.icon); 104 | fDay.text = shortenExtendedForecastText(period.shortForecast); 105 | fDay.dayName = dates[destIndex]; 106 | 107 | // preload the icon 108 | preloadImg(fDay.icon); 109 | 110 | if (period.isDaytime) { 111 | // day time is the high temperature 112 | fDay.high = period.temperature; 113 | destIndex += 1; 114 | } else { 115 | // low temperature 116 | fDay.low = period.temperature; 117 | } 118 | }); 119 | 120 | return forecast; 121 | }; 122 | 123 | const regexList = [ 124 | [/ and /gi, ' '], 125 | [/slight /gi, ''], 126 | [/chance /gi, ''], 127 | [/very /gi, ''], 128 | [/patchy /gi, ''], 129 | [/Areas Of /gi, ''], 130 | [/areas /gi, ''], 131 | [/dense /gi, ''], 132 | [/Thunderstorm/g, 'T\'Storm'], 133 | ]; 134 | const shortenExtendedForecastText = (long) => { 135 | // run all regexes 136 | const short = regexList.reduce((working, [regex, replace]) => working.replace(regex, replace), long); 137 | 138 | let conditions = short.split(' '); 139 | if (short.indexOf('then') !== -1) { 140 | conditions = short.split(' then '); 141 | conditions = conditions[1].split(' '); 142 | } 143 | 144 | let short1 = conditions[0].substr(0, 10); 145 | let short2 = ''; 146 | if (conditions[1]) { 147 | if (short1.endsWith('.')) { 148 | short1 = short1.replace(/\./, ''); 149 | } else { 150 | short2 = conditions[1].substr(0, 10); 151 | } 152 | 153 | if (short2 === 'Blowing') { 154 | short2 = ''; 155 | } 156 | } 157 | let result = short1; 158 | if (short2 !== '') { 159 | result += ` ${short2}`; 160 | } 161 | 162 | return result; 163 | }; 164 | 165 | // register display 166 | registerDisplay(new ExtendedForecast(8, 'extended-forecast')); 167 | -------------------------------------------------------------------------------- /server/scripts/modules/hourly-graph.mjs: -------------------------------------------------------------------------------- 1 | // hourly forecast list 2 | 3 | import STATUS from './status.mjs'; 4 | import getHourlyData from './hourly.mjs'; 5 | import WeatherDisplay from './weatherdisplay.mjs'; 6 | import { registerDisplay, timeZone } from './navigation.mjs'; 7 | import { DateTime } from '../vendor/auto/luxon.mjs'; 8 | 9 | // get available space 10 | const availableWidth = 532; 11 | const availableHeight = 285; 12 | 13 | class HourlyGraph extends WeatherDisplay { 14 | constructor(navId, elemId, defaultActive) { 15 | super(navId, elemId, 'Hourly Graph', defaultActive); 16 | 17 | // move the top right data into the correct location on load 18 | document.addEventListener('DOMContentLoaded', () => { 19 | this.moveHeader(); 20 | }); 21 | } 22 | 23 | moveHeader() { 24 | // get the header 25 | const header = this.fillTemplate('top-right', {}); 26 | // place the header 27 | this.elem.querySelector('.header .right').append(header); 28 | } 29 | 30 | async getData(weatherParameters, refresh) { 31 | if (!super.getData(undefined, refresh)) return; 32 | 33 | const data = await getHourlyData(() => this.stillWaiting()); 34 | if (data === undefined) { 35 | this.setStatus(STATUS.failed); 36 | return; 37 | } 38 | 39 | // get interesting data 40 | const temperature = data.map((d) => d.temperature); 41 | const probabilityOfPrecipitation = data.map((d) => d.probabilityOfPrecipitation); 42 | const skyCover = data.map((d) => d.skyCover); 43 | 44 | this.data = { 45 | skyCover, temperature, probabilityOfPrecipitation, temperatureUnit: data[0].temperatureUnit, 46 | }; 47 | 48 | this.setStatus(STATUS.loaded); 49 | } 50 | 51 | drawCanvas() { 52 | if (!this.image) this.image = this.elem.querySelector('.chart img'); 53 | 54 | this.image.width = availableWidth; 55 | this.image.height = availableHeight; 56 | 57 | // get context 58 | const canvas = document.createElement('canvas'); 59 | canvas.width = availableWidth; 60 | canvas.height = availableHeight; 61 | const ctx = canvas.getContext('2d'); 62 | ctx.imageSmoothingEnabled = false; 63 | 64 | // calculate time scale 65 | const timeScale = calcScale(0, 5, this.data.temperature.length - 1, availableWidth); 66 | const startTime = DateTime.now().startOf('hour'); 67 | document.querySelector('.x-axis .l-1').innerHTML = formatTime(startTime); 68 | document.querySelector('.x-axis .l-2').innerHTML = formatTime(startTime.plus({ hour: 6 })); 69 | document.querySelector('.x-axis .l-3').innerHTML = formatTime(startTime.plus({ hour: 12 })); 70 | document.querySelector('.x-axis .l-4').innerHTML = formatTime(startTime.plus({ hour: 18 })); 71 | document.querySelector('.x-axis .l-5').innerHTML = formatTime(startTime.plus({ hour: 24 })); 72 | 73 | // order is important last line drawn is on top 74 | // clouds 75 | const percentScale = calcScale(0, availableHeight - 10, 100, 10); 76 | const cloud = createPath(this.data.skyCover, timeScale, percentScale); 77 | drawPath(cloud, ctx, { 78 | strokeStyle: 'lightgrey', 79 | lineWidth: 3, 80 | }); 81 | 82 | // precip 83 | const precip = createPath(this.data.probabilityOfPrecipitation, timeScale, percentScale); 84 | drawPath(precip, ctx, { 85 | strokeStyle: 'aqua', 86 | lineWidth: 3, 87 | }); 88 | 89 | // temperature 90 | const minTemp = Math.min(...this.data.temperature); 91 | const maxTemp = Math.max(...this.data.temperature); 92 | const midTemp = Math.round((minTemp + maxTemp) / 2); 93 | const tempScale = calcScale(minTemp, availableHeight - 10, maxTemp, 10); 94 | const tempPath = createPath(this.data.temperature, timeScale, tempScale); 95 | drawPath(tempPath, ctx, { 96 | strokeStyle: 'red', 97 | lineWidth: 3, 98 | }); 99 | 100 | // temperature axis labels 101 | // limited to 3 characters, sacraficing degree character 102 | const degree = String.fromCharCode(176); 103 | this.elem.querySelector('.y-axis .l-1').innerHTML = (maxTemp + degree).substring(0, 3); 104 | this.elem.querySelector('.y-axis .l-2').innerHTML = (midTemp + degree).substring(0, 3); 105 | this.elem.querySelector('.y-axis .l-3').innerHTML = (minTemp + degree).substring(0, 3); 106 | 107 | // set the image source 108 | this.image.src = canvas.toDataURL(); 109 | 110 | // change the units in the header 111 | this.elem.querySelector('.temperature').innerHTML = `Temperature ${String.fromCharCode(176)}${this.data.temperatureUnit}`; 112 | 113 | super.drawCanvas(); 114 | this.finishDraw(); 115 | } 116 | } 117 | 118 | // create a scaling function from two points 119 | const calcScale = (x1, y1, x2, y2) => { 120 | const m = (y2 - y1) / (x2 - x1); 121 | const b = y1 - m * x1; 122 | return (x) => m * x + b; 123 | }; 124 | 125 | // create a path as an array of [x,y] 126 | const createPath = (data, xScale, yScale) => data.map((d, i) => [xScale(i), yScale(d)]); 127 | 128 | // draw a path with shadow 129 | const drawPath = (path, ctx, options) => { 130 | // first shadow 131 | ctx.beginPath(); 132 | ctx.strokeStyle = 'black'; 133 | ctx.lineWidth = (options?.lineWidth ?? 2) + 2; 134 | ctx.moveTo(path[0][0], path[0][1]); 135 | path.slice(1).forEach((point) => ctx.lineTo(point[0], point[1] + 2)); 136 | ctx.stroke(); 137 | 138 | // then colored line 139 | ctx.beginPath(); 140 | ctx.strokeStyle = options?.strokeStyle ?? 'red'; 141 | ctx.lineWidth = (options?.lineWidth ?? 2); 142 | ctx.moveTo(path[0][0], path[0][1]); 143 | path.slice(1).forEach((point) => ctx.lineTo(point[0], point[1])); 144 | ctx.stroke(); 145 | }; 146 | 147 | // format as 1p, 12a, etc. 148 | const formatTime = (time) => time.setZone(timeZone()).toFormat('ha').slice(0, -1); 149 | 150 | // register display 151 | registerDisplay(new HourlyGraph(4, 'hourly-graph')); 152 | -------------------------------------------------------------------------------- /server/scripts/modules/icons.mjs: -------------------------------------------------------------------------------- 1 | import largeIcon from './icons/icons-large.mjs'; 2 | import smallIcon from './icons/icons-small.mjs'; 3 | import hourlyIcon from './icons/icons-hourly.mjs'; 4 | 5 | export { 6 | largeIcon as getLargeIcon, 7 | smallIcon as getSmallIcon, 8 | hourlyIcon as getHourlyIcon, 9 | }; 10 | -------------------------------------------------------------------------------- /server/scripts/modules/icons/icons-hourly.mjs: -------------------------------------------------------------------------------- 1 | // internal function to add path to returned icon 2 | const addPath = (icon) => `images/icons/regional-maps/${icon}`; 3 | 4 | const hourlyIcon = (skyCover, weather, iceAccumulation, probabilityOfPrecipitation, snowfallAmount, windSpeed, isNight = false) => { 5 | // possible phenomenon 6 | let thunder = false; 7 | let snow = false; 8 | let ice = false; 9 | let fog = false; 10 | let wind = false; 11 | 12 | // test the phenomenon for various value if it is provided. 13 | weather.forEach((phenomenon) => { 14 | if (!phenomenon.weather) return; 15 | if (phenomenon.weather.toLowerCase().includes('thunder')) thunder = true; 16 | if (phenomenon.weather.toLowerCase().includes('snow')) snow = true; 17 | if (phenomenon.weather.toLowerCase().includes('ice')) ice = true; 18 | if (phenomenon.weather.toLowerCase().includes('fog')) fog = true; 19 | if (phenomenon.weather.toLowerCase().includes('wind')) wind = true; 20 | }); 21 | 22 | // first item in list is highest priority, units are metric where applicable 23 | if (iceAccumulation > 0 || ice) return addPath('Freezing-Rain-1992.gif'); 24 | if (snowfallAmount > 10) { 25 | if (windSpeed > 30 || wind) return addPath('Blowing-Snow.gif'); 26 | return addPath('Heavy-Snow-1994.gif'); 27 | } 28 | if ((snowfallAmount > 0 || snow) && thunder) return addPath('ThunderSnow.gif'); 29 | if (snowfallAmount > 0 || snow) return addPath('Light-Snow.gif'); 30 | if (thunder) return (addPath('Thunderstorm.gif')); 31 | if (probabilityOfPrecipitation > 70) return addPath('Rain-1992.gif'); 32 | if (probabilityOfPrecipitation > 30) { 33 | if (!isNight) return addPath('Scattered-Showers-1994.gif'); 34 | return addPath('Scattered-Showers-Night-1994.gif'); 35 | } 36 | if (fog) return addPath('Fog.gif'); 37 | if (skyCover > 70) return addPath('Cloudy.gif'); 38 | if (skyCover > 50) { 39 | if (!isNight) return addPath('Mostly-Cloudy-1994.gif'); 40 | return addPath('Partly-Clear-1994.gif'); 41 | } 42 | if (skyCover > 30) { 43 | if (!isNight) return addPath('Partly-Cloudy.gif'); 44 | return addPath('Partly-Cloudy-Night.gif'); 45 | } 46 | if (isNight) return addPath('Clear-1992.gif'); 47 | return addPath('Sunny.gif'); 48 | }; 49 | 50 | export default hourlyIcon; 51 | -------------------------------------------------------------------------------- /server/scripts/modules/icons/icons-large.mjs: -------------------------------------------------------------------------------- 1 | /* spell-checker: disable */ 2 | // internal function to add path to returned icon 3 | const addPath = (icon) => `images/icons/current-conditions/${icon}`; 4 | 5 | const largeIcon = (link, _isNightTime) => { 6 | if (!link) return false; 7 | 8 | // extract day or night if not provided 9 | const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0; 10 | 11 | // grab everything after the last slash ending at any of these: ?&, 12 | const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0]; 13 | let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1]; 14 | // using probability as a crude heavy/light indication where possible 15 | const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1]; 16 | 17 | // if a 'DualImage' is captured, adjust to just the j parameter 18 | if (conditionName === 'dualimage') { 19 | const match = link.match(/&j=(.*)&/); 20 | [, conditionName] = match; 21 | } 22 | 23 | // find the icon 24 | switch (conditionName + (isNightTime ? '-n' : '')) { 25 | case 'skc': 26 | case 'hot': 27 | case 'haze': 28 | case 'cold': 29 | return addPath('Sunny.gif'); 30 | 31 | case 'skc-n': 32 | case 'nskc': 33 | case 'nskc-n': 34 | case 'cold-n': 35 | return addPath('Clear.gif'); 36 | 37 | case 'sct': 38 | case 'few': 39 | case 'bkn': 40 | return addPath('Partly-Cloudy.gif'); 41 | 42 | case 'bkn-n': 43 | case 'few-n': 44 | case 'nfew-n': 45 | case 'nfew': 46 | case 'sct-n': 47 | case 'nsct': 48 | case 'nsct-n': 49 | return addPath('Mostly-Clear.gif'); 50 | 51 | case 'ovc': 52 | case 'novc': 53 | case 'ovc-n': 54 | return addPath('Cloudy.gif'); 55 | 56 | case 'fog': 57 | case 'fog-n': 58 | return addPath('Fog.gif'); 59 | 60 | case 'rain_sleet': 61 | case 'rain_sleet-n': 62 | return addPath('Rain-Sleet.gif'); 63 | 64 | case 'sleet': 65 | case 'sleet-n': 66 | return addPath('Sleet.gif'); 67 | 68 | case 'smoke': 69 | case 'smoke-n': 70 | return addPath('Smoke.gif'); 71 | 72 | case 'rain_showers': 73 | case 'rain_showers_high': 74 | case 'rain_showers-n': 75 | case 'rain_showers_high-n': 76 | return addPath('Shower.gif'); 77 | 78 | case 'rain': 79 | case 'rain-n': 80 | return addPath('Rain.gif'); 81 | 82 | case 'snow': 83 | case 'snow-n': 84 | if (value > 50) return addPath('Heavy-Snow.gif'); 85 | return addPath('Light-Snow.gif'); 86 | 87 | case 'rain_snow': 88 | return addPath('Rain-Snow.gif'); 89 | 90 | case 'snow_fzra': 91 | case 'snow_fzra-n': 92 | return addPath('Freezing-Rain-Snow.gif'); 93 | 94 | case 'fzra': 95 | case 'fzra-n': 96 | case 'rain_fzra': 97 | case 'rain_fzra-n': 98 | return addPath('Freezing-Rain.gif'); 99 | 100 | case 'snow_sleet': 101 | return addPath('Snow-Sleet.gif'); 102 | 103 | case 'tsra_sct': 104 | case 'tsra': 105 | return addPath('Scattered-Thunderstorms-Day.gif'); 106 | 107 | case 'tsra_sct-n': 108 | case 'tsra-n': 109 | return addPath('Scattered-Thunderstorms-Night.gif'); 110 | 111 | case 'tsra_hi': 112 | case 'tsra_hi-n': 113 | case 'hurricane': 114 | case 'tropical_storm': 115 | case 'hurricane-n': 116 | case 'tropical_storm-n': 117 | return addPath('Thunderstorm.gif'); 118 | 119 | case 'wind_few': 120 | case 'wind_sct': 121 | case 'wind_bkn': 122 | case 'wind_ovc': 123 | case 'wind_skc': 124 | case 'wind_few-n': 125 | case 'wind_bkn-n': 126 | case 'wind_ovc-n': 127 | case 'wind_skc-n': 128 | case 'wind_sct-n': 129 | return addPath('Windy.gif'); 130 | 131 | case 'blizzard': 132 | case 'blizzard-n': 133 | return addPath('Blowing-Snow.gif'); 134 | 135 | default: 136 | console.log(`Unable to locate icon for ${conditionName} ${link} ${isNightTime}`); 137 | return false; 138 | } 139 | }; 140 | 141 | export default largeIcon; 142 | -------------------------------------------------------------------------------- /server/scripts/modules/icons/icons-small.mjs: -------------------------------------------------------------------------------- 1 | // internal function to add path to returned icon 2 | const addPath = (icon) => `images/icons/regional-maps/${icon}`; 3 | 4 | const smallIcon = (link, _isNightTime) => { 5 | // extract day or night if not provided 6 | const isNightTime = _isNightTime ?? link.indexOf('/night/') >= 0; 7 | 8 | // grab everything after the last slash ending at any of these: ?&, 9 | const afterLastSlash = link.toLowerCase().match(/[^/]+$/)[0]; 10 | let conditionName = afterLastSlash.match(/(.*?)[&,.?]/)[1]; 11 | // using probability as a crude heavy/light indication where possible 12 | const value = +(link.match(/,(\d{2,3})/) ?? [0, 100])[1]; 13 | 14 | // if a 'DualImage' is captured, adjust to just the j parameter 15 | if (conditionName === 'dualimage') { 16 | const match = link.match(/&j=(.*)&/); 17 | [, conditionName] = match; 18 | } 19 | 20 | // find the icon 21 | switch (conditionName + (isNightTime ? '-n' : '')) { 22 | case 'skc': 23 | return addPath('Sunny.gif'); 24 | 25 | case 'skc-n': 26 | case 'nskc': 27 | case 'nskc-n': 28 | case 'cold-n': 29 | return addPath('Clear-1992.gif'); 30 | 31 | case 'bkn': 32 | return addPath('Mostly-Cloudy-1994.gif'); 33 | 34 | case 'bkn-n': 35 | case 'few-n': 36 | case 'nfew-n': 37 | case 'nfew': 38 | return addPath('Partly-Clear-1994.gif'); 39 | 40 | case 'sct': 41 | case 'few': 42 | return addPath('Partly-Cloudy.gif'); 43 | 44 | case 'sct-n': 45 | case 'nsct': 46 | case 'nsct-n': 47 | case 'haze-n': 48 | return addPath('Partly-Cloudy-Night.gif'); 49 | 50 | case 'ovc': 51 | case 'ovc-n': 52 | return addPath('Cloudy.gif'); 53 | 54 | case 'fog': 55 | case 'fog-n': 56 | return addPath('Fog.gif'); 57 | 58 | case 'rain_sleet': 59 | return addPath('Rain-Sleet.gif'); 60 | 61 | case 'rain_showers': 62 | case 'rain_showers_high': 63 | return addPath('Scattered-Showers-1994.gif'); 64 | 65 | case 'rain_showers-n': 66 | case 'rain_showers_high-n': 67 | return addPath('Scattered-Showers-Night-1994.gif'); 68 | 69 | case 'rain': 70 | case 'rain-n': 71 | return addPath('Rain-1992.gif'); 72 | 73 | case 'snow': 74 | case 'snow-n': 75 | if (value > 50) return addPath('Heavy-Snow-1994.gif'); 76 | return addPath('Light-Snow.gif'); 77 | 78 | case 'rain_snow': 79 | case 'rain_snow-n': 80 | return addPath('Rain-Snow-1992.gif'); 81 | 82 | case 'snow_fzra': 83 | case 'snow_fzra-n': 84 | return addPath('Freezing-Rain-Snow-1994.gif'); 85 | 86 | case 'fzra': 87 | case 'fzra-n': 88 | case 'rain_fzra': 89 | case 'rain_fzra-n': 90 | return addPath('Freezing-Rain-1992.gif'); 91 | 92 | case 'snow_sleet': 93 | case 'snow_sleet-n': 94 | return addPath('Snow-Sleet.gif'); 95 | 96 | case 'sleet': 97 | case 'sleet-n': 98 | return addPath('Sleet.gif'); 99 | 100 | case 'tsra_sct': 101 | case 'tsra': 102 | return addPath('Scattered-Tstorms-1994.gif'); 103 | 104 | case 'tsra_sct-n': 105 | case 'tsra-n': 106 | return addPath('Scattered-Tstorms-Night-1994.gif'); 107 | 108 | case 'tsra_hi': 109 | case 'tsra_hi-n': 110 | case 'hurricane': 111 | case 'tropical_storm': 112 | case 'hurricane-n': 113 | case 'tropical_storm-n': 114 | return addPath('Thunderstorm.gif'); 115 | 116 | case 'wind': 117 | case 'wind_': 118 | case 'wind_few': 119 | case 'wind_sct': 120 | case 'wind-n': 121 | case 'wind_-n': 122 | case 'wind_few-n': 123 | return addPath('Wind.gif'); 124 | 125 | case 'wind_bkn': 126 | case 'wind_ovc': 127 | case 'wind_bkn-n': 128 | case 'wind_ovc-n': 129 | return addPath('Cloudy-Wind.gif'); 130 | 131 | case 'wind_skc': 132 | return addPath('Sunny-Wind-1994.gif'); 133 | 134 | case 'wind_skc-n': 135 | case 'wind_sct-n': 136 | return addPath('Clear-Wind-1994.gif'); 137 | 138 | case 'blizzard': 139 | case 'blizzard-n': 140 | return addPath('Blowing Snow.gif'); 141 | 142 | case 'cold': 143 | return addPath('Cold.gif'); 144 | 145 | case 'smoke': 146 | case 'smoke-n': 147 | return addPath('Smoke.gif'); 148 | 149 | case 'hot': 150 | return addPath('Hot.gif'); 151 | 152 | case 'haze': 153 | return addPath('Haze.gif'); 154 | 155 | default: 156 | console.log(`Unable to locate regional icon for ${conditionName} ${link} ${isNightTime}`); 157 | return false; 158 | } 159 | }; 160 | 161 | export default smallIcon; 162 | -------------------------------------------------------------------------------- /server/scripts/modules/localforecast.mjs: -------------------------------------------------------------------------------- 1 | // display text based local forecast 2 | 3 | import STATUS from './status.mjs'; 4 | import { json } from './utils/fetch.mjs'; 5 | import WeatherDisplay from './weatherdisplay.mjs'; 6 | import { registerDisplay } from './navigation.mjs'; 7 | import settings from './settings.mjs'; 8 | 9 | class LocalForecast extends WeatherDisplay { 10 | constructor(navId, elemId) { 11 | super(navId, elemId, 'Local Forecast', true); 12 | 13 | // set timings 14 | this.timing.baseDelay = 5000; 15 | } 16 | 17 | async getData(weatherParameters, refresh) { 18 | if (!super.getData(weatherParameters, refresh)) return; 19 | 20 | // get raw data 21 | const rawData = await this.getRawData(this.weatherParameters); 22 | // check for data, or if there's old data available 23 | if (!rawData && !this.data) { 24 | // fail for no old or new data 25 | this.setStatus(STATUS.failed); 26 | return; 27 | } 28 | // store the data 29 | this.data = rawData || this.data; 30 | // parse raw data 31 | const conditions = parse(this.data); 32 | 33 | // read each text 34 | this.screenTexts = conditions.map((condition) => { 35 | // process the text 36 | let text = `${condition.DayName.toUpperCase()}...`; 37 | const conditionText = condition.Text; 38 | text += conditionText.toUpperCase().replace('...', ' '); 39 | 40 | return text; 41 | }); 42 | 43 | // fill the forecast texts 44 | const templates = this.screenTexts.map((text) => this.fillTemplate('forecast', { text })); 45 | const forecastsElem = this.elem.querySelector('.forecasts'); 46 | forecastsElem.innerHTML = ''; 47 | forecastsElem.append(...templates); 48 | 49 | // increase each forecast height to a multiple of container height 50 | this.pageHeight = forecastsElem.parentNode.offsetHeight; 51 | templates.forEach((forecast) => { 52 | const newHeight = Math.ceil(forecast.scrollHeight / this.pageHeight) * this.pageHeight; 53 | forecast.style.height = `${newHeight}px`; 54 | }); 55 | 56 | this.timing.totalScreens = forecastsElem.scrollHeight / this.pageHeight; 57 | this.calcNavTiming(); 58 | this.setStatus(STATUS.loaded); 59 | } 60 | 61 | // get the unformatted data (also used by extended forecast) 62 | async getRawData(weatherParameters) { 63 | // request us or si units 64 | try { 65 | return await json(weatherParameters.forecast, { 66 | data: { 67 | units: settings.units.value, 68 | }, 69 | retryCount: 3, 70 | stillWaiting: () => this.stillWaiting(), 71 | }); 72 | } catch (error) { 73 | console.error(`GetWeatherForecast failed: ${weatherParameters.forecast}`); 74 | console.error(error.status, error.responseJSON); 75 | return false; 76 | } 77 | } 78 | 79 | async drawCanvas() { 80 | super.drawCanvas(); 81 | 82 | const top = -this.screenIndex * this.pageHeight; 83 | this.elem.querySelector('.forecasts').style.top = `${top}px`; 84 | 85 | this.finishDraw(); 86 | } 87 | } 88 | 89 | // format the forecast 90 | // only use the first 6 lines 91 | const parse = (forecast) => forecast.properties.periods.slice(0, 6).map((text) => ({ 92 | // format day and text 93 | DayName: text.name.toUpperCase(), 94 | Text: text.detailedForecast, 95 | })); 96 | // register display 97 | registerDisplay(new LocalForecast(7, 'local-forecast')); 98 | -------------------------------------------------------------------------------- /server/scripts/modules/media.mjs: -------------------------------------------------------------------------------- 1 | import { json } from './utils/fetch.mjs'; 2 | import Setting from './utils/setting.mjs'; 3 | 4 | let playlist; 5 | let currentTrack = 0; 6 | let player; 7 | 8 | const mediaPlaying = new Setting('mediaPlaying', { 9 | name: 'Media Playing', 10 | type: 'boolean', 11 | defaultValue: false, 12 | sticky: true, 13 | }); 14 | 15 | document.addEventListener('DOMContentLoaded', () => { 16 | // add the event handler to the page 17 | document.getElementById('ToggleMedia').addEventListener('click', toggleMedia); 18 | // get the playlist 19 | getMedia(); 20 | }); 21 | 22 | const getMedia = async () => { 23 | try { 24 | // fetch the playlist 25 | const rawPlaylist = await json('playlist.json'); 26 | // store the playlist 27 | playlist = rawPlaylist; 28 | // enable the media player 29 | enableMediaPlayer(); 30 | } catch (e) { 31 | console.error("Couldn't get playlist"); 32 | console.error(e); 33 | } 34 | }; 35 | 36 | const enableMediaPlayer = () => { 37 | // see if files are available 38 | if (playlist?.availableFiles?.length > 0) { 39 | // randomize the list 40 | randomizePlaylist(); 41 | // enable the icon 42 | const icon = document.getElementById('ToggleMedia'); 43 | icon.classList.add('available'); 44 | // set the button type 45 | setIcon(); 46 | // if we're already playing (sticky option) then try to start playing 47 | if (mediaPlaying.value === true) { 48 | startMedia(); 49 | } 50 | } 51 | }; 52 | 53 | const setIcon = () => { 54 | // get the icon 55 | const icon = document.getElementById('ToggleMedia'); 56 | if (mediaPlaying.value === true) { 57 | icon.classList.add('playing'); 58 | } else { 59 | icon.classList.remove('playing'); 60 | } 61 | }; 62 | 63 | const toggleMedia = (forcedState) => { 64 | // handle forcing 65 | if (typeof forcedState === 'boolean') { 66 | mediaPlaying.value = forcedState; 67 | } else { 68 | // toggle the state 69 | mediaPlaying.value = !mediaPlaying.value; 70 | } 71 | // handle the state change 72 | stateChanged(); 73 | }; 74 | 75 | const startMedia = async () => { 76 | // if there's not media player yet, enable it 77 | if (!player) { 78 | initializePlayer(); 79 | } else { 80 | try { 81 | await player.play(); 82 | setTrackName(playlist.availableFiles[currentTrack]); 83 | } catch (e) { 84 | // report the error 85 | console.error('Couldn\'t play music'); 86 | console.error(e); 87 | // set state back to not playing for good UI experience 88 | mediaPlaying.value = false; 89 | stateChanged(); 90 | setTrackName('Not playing'); 91 | } 92 | } 93 | }; 94 | 95 | const stopMedia = () => { 96 | if (!player) return; 97 | player.pause(); 98 | setTrackName('Not playing'); 99 | }; 100 | 101 | const stateChanged = () => { 102 | // update the icon 103 | setIcon(); 104 | // react to the new state 105 | if (mediaPlaying.value) { 106 | startMedia(); 107 | } else { 108 | stopMedia(); 109 | } 110 | }; 111 | 112 | const randomizePlaylist = () => { 113 | let availableFiles = [...playlist.availableFiles]; 114 | const randomPlaylist = []; 115 | while (availableFiles.length > 0) { 116 | // get a randon item from the available files 117 | const i = Math.floor(Math.random() * availableFiles.length); 118 | // add it to the final list 119 | randomPlaylist.push(availableFiles[i]); 120 | // remove the file from the available files 121 | availableFiles = availableFiles.filter((file, index) => index !== i); 122 | } 123 | playlist.availableFiles = randomPlaylist; 124 | }; 125 | 126 | const initializePlayer = () => { 127 | // basic sanity checks 128 | if (!playlist.availableFiles || playlist?.availableFiles.length === 0) { 129 | throw new Error('No playlist available'); 130 | } 131 | if (player) { 132 | return; 133 | } 134 | // create the player 135 | player = new Audio(); 136 | 137 | // reset the playlist index 138 | currentTrack = 0; 139 | 140 | // add event handlers 141 | player.addEventListener('canplay', playerCanPlay); 142 | player.addEventListener('ended', playerEnded); 143 | 144 | // get the first file 145 | player.src = `music/${playlist.availableFiles[currentTrack]}`; 146 | setTrackName(playlist.availableFiles[currentTrack]); 147 | player.type = 'audio/mpeg'; 148 | }; 149 | 150 | const playerCanPlay = async () => { 151 | // check to make sure they user still wants music (protect against slow loading music) 152 | if (!mediaPlaying.value) return; 153 | // start playing 154 | startMedia(); 155 | }; 156 | 157 | const playerEnded = () => { 158 | // next track 159 | currentTrack += 1; 160 | // roll over and re-randomize the tracks 161 | if (currentTrack >= playlist.availableFiles.length) { 162 | randomizePlaylist(); 163 | currentTrack = 0; 164 | } 165 | // update the player source 166 | player.src = `music/${playlist.availableFiles[currentTrack]}`; 167 | setTrackName(playlist.availableFiles[currentTrack]); 168 | }; 169 | 170 | const setTrackName = (fileName) => { 171 | const trackName = fileName.replace(/\.mp3/gi, '').replace(/(_-)/gi, ''); 172 | document.getElementById('musicTrack').innerHTML = trackName; 173 | }; 174 | 175 | export { 176 | // eslint-disable-next-line import/prefer-default-export 177 | toggleMedia, 178 | }; 179 | -------------------------------------------------------------------------------- /server/scripts/modules/progress.mjs: -------------------------------------------------------------------------------- 1 | // regional forecast and observations 2 | import STATUS, { calcStatusClass, statusClasses } from './status.mjs'; 3 | import WeatherDisplay from './weatherdisplay.mjs'; 4 | import { 5 | registerProgress, message, getDisplay, msg, 6 | } from './navigation.mjs'; 7 | 8 | class Progress extends WeatherDisplay { 9 | constructor(navId, elemId) { 10 | super(navId, elemId, '', false); 11 | 12 | // disable any navigation timing 13 | this.timing = false; 14 | 15 | // setup event listener for dom-required initialization 16 | document.addEventListener('DOMContentLoaded', () => { 17 | this.version = document.querySelector('#version').innerHTML; 18 | this.elem.querySelector('.container').addEventListener('click', this.lineClick.bind(this)); 19 | }); 20 | 21 | this.okToDrawCurrentConditions = false; 22 | } 23 | 24 | async drawCanvas(displays, loadedCount) { 25 | if (!this.elem) return; 26 | super.drawCanvas(); 27 | 28 | // get the progress bar cover (makes percentage) 29 | if (!this.progressCover) this.progressCover = this.elem.querySelector('.scroll .cover'); 30 | 31 | // if no displays provided just draw the backgrounds (above) 32 | if (!displays) return; 33 | const lines = displays.map((display, index) => { 34 | if (display.showOnProgress === false) return false; 35 | const fill = { 36 | name: display.name, 37 | }; 38 | 39 | const statusClass = calcStatusClass(display.status); 40 | 41 | // make the line 42 | const line = this.fillTemplate('item', fill); 43 | // because of timing, this might get called before the template is loaded 44 | if (!line) return false; 45 | 46 | // update the status 47 | const links = line.querySelector('.links'); 48 | links.classList.remove(...statusClasses); 49 | links.classList.add(statusClass); 50 | links.dataset.index = index; 51 | return line; 52 | }).filter((d) => d); 53 | 54 | // get the container and update 55 | const container = this.elem.querySelector('.container'); 56 | container.innerHTML = ''; 57 | container.append(...lines); 58 | 59 | this.finishDraw(); 60 | 61 | // calculate loaded percent 62 | const loadedPercent = (loadedCount / displays.length); 63 | 64 | this.progressCover.style.width = `${(1.0 - loadedPercent) * 100}%`; 65 | if (loadedPercent < 1.0) { 66 | // show the progress bar and set width 67 | this.progressCover.parentNode.classList.add('show'); 68 | } else { 69 | // hide the progressbar after 1 second (lines up with with width transition animation) 70 | setTimeout(() => this.progressCover.parentNode.classList.remove('show'), 1000); 71 | } 72 | } 73 | 74 | lineClick(e) { 75 | // get index 76 | const indexRaw = e.target?.parentNode?.dataset?.index; 77 | if (indexRaw === undefined) return; 78 | const index = +indexRaw; 79 | 80 | // stop playing 81 | message('navButton'); 82 | // use the y value to determine an index 83 | const display = getDisplay(index); 84 | if (display && display.status === STATUS.loaded) { 85 | display.showCanvas(msg.command.firstFrame); 86 | this.elem.classList.remove('show'); 87 | } 88 | } 89 | } 90 | 91 | // register our own display 92 | const progress = new Progress(-1, 'progress'); 93 | registerProgress(progress); 94 | -------------------------------------------------------------------------------- /server/scripts/modules/settings.mjs: -------------------------------------------------------------------------------- 1 | import Setting from './utils/setting.mjs'; 2 | 3 | document.addEventListener('DOMContentLoaded', () => { 4 | init(); 5 | }); 6 | 7 | // default speed 8 | const settings = { speed: { value: 1.0 } }; 9 | 10 | const init = () => { 11 | // create settings see setting.mjs for defaults 12 | settings.wide = new Setting('wide', { 13 | name: 'Widescreen', 14 | defaultValue: false, 15 | changeAction: wideScreenChange, 16 | sticky: true, 17 | }); 18 | settings.kiosk = new Setting('kiosk', { 19 | name: 'Kiosk', 20 | defaultValue: false, 21 | changeAction: kioskChange, 22 | sticky: false, 23 | }); 24 | settings.speed = new Setting('speed', { 25 | name: 'Speed', 26 | type: 'select', 27 | defaultValue: 1.0, 28 | values: [ 29 | [0.5, 'Very Fast'], 30 | [0.75, 'Fast'], 31 | [1.0, 'Normal'], 32 | [1.25, 'Slow'], 33 | [1.5, 'Very Slow'], 34 | ], 35 | }); 36 | settings.scanLines = new Setting('scanLines', { 37 | name: 'Scan Lines', 38 | defaultValue: false, 39 | changeAction: scanLineChange, 40 | sticky: true, 41 | }); 42 | settings.units = new Setting('units', { 43 | name: 'Units', 44 | type: 'select', 45 | defaultValue: 'us', 46 | changeAction: unitChange, 47 | values: [ 48 | ['us', 'US'], 49 | ['si', 'Metric'], 50 | ], 51 | }); 52 | settings.refreshTime = new Setting('refreshTime', { 53 | type: 'select', 54 | defaultValue: 600_000, 55 | sticky: false, 56 | values: [ 57 | [30_000, 'TESTING'], 58 | [300_000, '5 minutes'], 59 | [600_000, '10 minutes'], 60 | [900_000, '15 minutes'], 61 | [1_800_000, '30 minutes'], 62 | ], 63 | visible: false, 64 | }); 65 | 66 | // generate html objects 67 | const settingHtml = Object.values(settings).map((d) => d.generate()); 68 | 69 | // write to page 70 | const settingsSection = document.querySelector('#settings'); 71 | settingsSection.innerHTML = ''; 72 | settingsSection.append(...settingHtml); 73 | }; 74 | 75 | const wideScreenChange = (value) => { 76 | const container = document.querySelector('#divTwc'); 77 | if (value) { 78 | container.classList.add('wide'); 79 | } else { 80 | container.classList.remove('wide'); 81 | } 82 | }; 83 | 84 | const kioskChange = (value) => { 85 | const body = document.querySelector('body'); 86 | if (value) { 87 | body.classList.add('kiosk'); 88 | window.dispatchEvent(new Event('resize')); 89 | } else { 90 | body.classList.remove('kiosk'); 91 | } 92 | }; 93 | 94 | const scanLineChange = (value) => { 95 | const container = document.getElementById('container'); 96 | const navIcons = document.getElementById('ToggleScanlines'); 97 | if (value) { 98 | container.classList.add('scanlines'); 99 | navIcons.classList.add('on'); 100 | } else { 101 | container.classList.remove('scanlines'); 102 | navIcons.classList.remove('on'); 103 | } 104 | }; 105 | 106 | const unitChange = () => { 107 | // reload the data at the top level to refresh units 108 | // after the initial load 109 | if (unitChange.firstRunDone) { 110 | window.location.reload(); 111 | } 112 | unitChange.firstRunDone = true; 113 | }; 114 | 115 | export default settings; 116 | -------------------------------------------------------------------------------- /server/scripts/modules/share.mjs: -------------------------------------------------------------------------------- 1 | import { elemForEach } from './utils/elem.mjs'; 2 | 3 | document.addEventListener('DOMContentLoaded', () => init()); 4 | 5 | // shorthand mappings for frequently used values 6 | const specialMappings = { 7 | kiosk: 'settings-kiosk-checkbox', 8 | }; 9 | 10 | const init = () => { 11 | // add action to existing link 12 | const shareLink = document.querySelector('#share-link'); 13 | shareLink.addEventListener('click', createLink); 14 | 15 | // if navigator.clipboard does not exist, change text 16 | if (!navigator?.clipboard) { 17 | shareLink.textContent = 'Get Permalink'; 18 | } 19 | }; 20 | 21 | const createLink = async (e) => { 22 | // cancel default event (click on hyperlink) 23 | e.preventDefault(); 24 | 25 | // list to receive checkbox statuses 26 | const queryStringElements = {}; 27 | 28 | elemForEach('input[type=checkbox]', (elem) => { 29 | if (elem?.id) { 30 | queryStringElements[elem.id] = elem?.checked ?? false; 31 | } 32 | }); 33 | 34 | // get all select boxes 35 | elemForEach('select', (elem) => { 36 | if (elem?.id) { 37 | queryStringElements[elem.id] = elem?.value ?? 0; 38 | } 39 | }); 40 | 41 | // add the location string 42 | queryStringElements.latLonQuery = localStorage.getItem('latLonQuery'); 43 | queryStringElements.latLon = localStorage.getItem('latLon'); 44 | 45 | const queryString = (new URLSearchParams(queryStringElements)).toString(); 46 | 47 | const url = new URL(`?${queryString}`, document.location.href); 48 | 49 | // send to proper function based on availability of clipboard 50 | if (navigator?.clipboard) { 51 | copyToClipboard(url); 52 | } else { 53 | writeLinkToPage(url); 54 | } 55 | }; 56 | 57 | const copyToClipboard = async (url) => { 58 | try { 59 | // write to clipboard 60 | await navigator.clipboard.writeText(url.toString()); 61 | // alert user 62 | const confirmSpan = document.querySelector('#share-link-copied'); 63 | confirmSpan.style.display = 'inline'; 64 | 65 | // hide confirm text after 5 seconds 66 | setTimeout(() => { 67 | confirmSpan.style.display = 'none'; 68 | }, 5000); 69 | } catch (error) { 70 | console.error(error); 71 | } 72 | }; 73 | 74 | const writeLinkToPage = (url) => { 75 | // get elements 76 | const shareLinkInstructions = document.querySelector('#share-link-instructions'); 77 | const shareLinkUrl = shareLinkInstructions.querySelector('#share-link-url'); 78 | // populate url and display 79 | shareLinkUrl.value = url; 80 | shareLinkInstructions.style.display = 'inline'; 81 | // highlight for convenience 82 | shareLinkUrl.focus(); 83 | shareLinkUrl.select(); 84 | }; 85 | 86 | const parseQueryString = () => { 87 | // return memoized result 88 | if (parseQueryString.params) return parseQueryString.params; 89 | const urlSearchParams = new URLSearchParams(window.location.search); 90 | 91 | // turn into an array of key-value pairs 92 | const paramsArray = [...urlSearchParams]; 93 | 94 | // add additional expanded keys 95 | paramsArray.forEach((paramPair) => { 96 | const expandedKey = specialMappings[paramPair[0]]; 97 | if (expandedKey) { 98 | paramsArray.push([expandedKey, paramPair[1]]); 99 | } 100 | }); 101 | 102 | // memoize result 103 | parseQueryString.params = Object.fromEntries(paramsArray); 104 | 105 | return parseQueryString.params; 106 | }; 107 | 108 | export { 109 | createLink, 110 | parseQueryString, 111 | }; 112 | -------------------------------------------------------------------------------- /server/scripts/modules/spc-outlook.mjs: -------------------------------------------------------------------------------- 1 | // display spc outlook in a bar graph 2 | 3 | import STATUS from './status.mjs'; 4 | import { json } from './utils/fetch.mjs'; 5 | import { DateTime } from '../vendor/auto/luxon.mjs'; 6 | import WeatherDisplay from './weatherdisplay.mjs'; 7 | import { registerDisplay } from './navigation.mjs'; 8 | import testPolygon from './utils/polygon.mjs'; 9 | 10 | // list of interesting files ordered [0] = today, [1] = tomorrow... 11 | const urlPattern = (day) => `https://www.spc.noaa.gov/products/outlook/day${day}otlk_cat.nolyr.geojson`; 12 | 13 | const testAllPoints = (point, data) => { 14 | // returns all points where the data matches as an array of days and then matches of the properties of the data 15 | 16 | const result = []; 17 | // start with a loop of days 18 | data.forEach((day, index) => { 19 | // initialize the result 20 | result[index] = false; 21 | // loop through each category 22 | day.features.forEach((feature) => { 23 | if (!feature.geometry.coordinates) return; 24 | const inPolygon = testPolygon(point, feature.geometry); 25 | if (inPolygon) result[index] = feature.properties; 26 | }); 27 | }); 28 | 29 | return result; 30 | }; 31 | 32 | const barSizes = { 33 | TSTM: 60, 34 | MRGL: 150, 35 | SLGT: 210, 36 | ENH: 270, 37 | MDT: 330, 38 | HIGH: 390, 39 | }; 40 | 41 | class SpcOutlook extends WeatherDisplay { 42 | constructor(navId, elemId) { 43 | super(navId, elemId, 'SPC Outlook', true); 44 | // don't display on progress/navigation screen 45 | this.showOnProgress = false; 46 | 47 | // calculate file names 48 | this.files = [null, null, null].map((v, i) => urlPattern(i + 1)); 49 | 50 | // set timings 51 | this.timing.totalScreens = 1; 52 | } 53 | 54 | async getData(weatherParameters, refresh) { 55 | if (!super.getData(weatherParameters, refresh)) return; 56 | 57 | // initial data does not need to be reloaded on a location change, only during silent refresh 58 | if (!this.initialData || refresh) { 59 | try { 60 | // get the three categorical files to get started 61 | const filePromises = await Promise.allSettled(this.files.map((file) => json(file))); 62 | // store the data, promise will always be fulfilled 63 | this.initialData = filePromises.map((outlookDay) => outlookDay.value); 64 | } catch (error) { 65 | console.error('Unable to get spc outlook'); 66 | console.error(error.status, error.responseJSON); 67 | // if there's no previous data, fail 68 | if (!this.initialData) { 69 | this.setStatus(STATUS.failed); 70 | return; 71 | } 72 | } 73 | } 74 | // do the initial parsing of the data 75 | this.data = testAllPoints([weatherParameters.longitude, weatherParameters.latitude], this.initialData); 76 | 77 | // if all the data returns false the there's nothing to do, skip this screen 78 | if (this.data.reduce((prev, cur) => prev || !!cur, false)) { 79 | this.timing.totalScreens = 1; 80 | } else { 81 | this.timing.totalScreens = 0; 82 | } 83 | this.calcNavTiming(); 84 | 85 | // we only get here if there was no error above 86 | this.screenIndex = 0; 87 | this.setStatus(STATUS.loaded); 88 | } 89 | 90 | async drawCanvas() { 91 | super.drawCanvas(); 92 | 93 | // analyze each day 94 | const days = this.data.map((day, index) => { 95 | // get the day name 96 | const dayName = DateTime.now().plus({ days: index }).toLocaleString({ weekday: 'long' }); 97 | 98 | // fill the name 99 | const fill = {}; 100 | fill['day-name'] = dayName; 101 | 102 | // create the element 103 | const elem = this.fillTemplate('day', fill); 104 | 105 | // update the bar length 106 | const bar = elem.querySelector('.risk-bar'); 107 | if (day.LABEL) { 108 | bar.style.width = `${barSizes[day.LABEL]}px`; 109 | } else { 110 | bar.style.display = 'none'; 111 | } 112 | 113 | return elem; 114 | }); 115 | 116 | // add the days to the display 117 | const dayContainer = this.elem.querySelector('.days'); 118 | dayContainer.innerHTML = ''; 119 | dayContainer.append(...days); 120 | 121 | // finish drawing 122 | this.finishDraw(); 123 | } 124 | } 125 | 126 | // register display 127 | registerDisplay(new SpcOutlook(10, 'spc-outlook')); 128 | -------------------------------------------------------------------------------- /server/scripts/modules/status.mjs: -------------------------------------------------------------------------------- 1 | const STATUS = { 2 | loading: Symbol('loading'), 3 | loaded: Symbol('loaded'), 4 | failed: Symbol('failed'), 5 | noData: Symbol('noData'), 6 | disabled: Symbol('disabled'), 7 | retrying: Symbol('retrying'), 8 | }; 9 | 10 | const calcStatusClass = (statusCode) => { 11 | switch (statusCode) { 12 | case STATUS.loading: 13 | return 'loading'; 14 | case STATUS.loaded: 15 | return 'press-here'; 16 | case STATUS.failed: 17 | return 'failed'; 18 | case STATUS.noData: 19 | return 'no-data'; 20 | case STATUS.disabled: 21 | return 'disabled'; 22 | case STATUS.retrying: 23 | return 'retrying'; 24 | default: 25 | return ''; 26 | } 27 | }; 28 | 29 | const statusClasses = ['loading', 'press-here', 'failed', 'no-data', 'disabled', 'retrying']; 30 | 31 | export default STATUS; 32 | export { 33 | calcStatusClass, 34 | statusClasses, 35 | }; 36 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/calc.mjs: -------------------------------------------------------------------------------- 1 | // wind direction 2 | const directionToNSEW = (Direction) => { 3 | const val = Math.floor((Direction / 22.5) + 0.5); 4 | const arr = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']; 5 | return arr[(val % 16)]; 6 | }; 7 | 8 | const distance = (x1, y1, x2, y2) => Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); 9 | 10 | // wrap a number to 0-m 11 | const wrap = (x, m) => ((x % m) + m) % m; 12 | 13 | export { 14 | directionToNSEW, 15 | distance, 16 | wrap, 17 | }; 18 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/cors.mjs: -------------------------------------------------------------------------------- 1 | // rewrite some urls for local server 2 | const rewriteUrl = (_url) => { 3 | let url = _url; 4 | url = url.replace('https://api.weather.gov/', `${window.location.protocol}//${window.location.host}/`); 5 | url = url.replace('https://www.cpc.ncep.noaa.gov/', `${window.location.protocol}//${window.location.host}/`); 6 | return url; 7 | }; 8 | 9 | export { 10 | // eslint-disable-next-line import/prefer-default-export 11 | rewriteUrl, 12 | }; 13 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/elem.mjs: -------------------------------------------------------------------------------- 1 | const elemForEach = (selector, callback) => { 2 | [...document.querySelectorAll(selector)].forEach(callback); 3 | }; 4 | 5 | export { 6 | // eslint-disable-next-line import/prefer-default-export 7 | elemForEach, 8 | }; 9 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/fetch.mjs: -------------------------------------------------------------------------------- 1 | import { rewriteUrl } from './cors.mjs'; 2 | 3 | const json = (url, params) => fetchAsync(url, 'json', params); 4 | const text = (url, params) => fetchAsync(url, 'text', params); 5 | const blob = (url, params) => fetchAsync(url, 'blob', params); 6 | 7 | const fetchAsync = async (_url, responseType, _params = {}) => { 8 | // add user agent header to json request at api.weather.gov 9 | const headers = {}; 10 | if (_url.toString().match(/api\.weather\.gov/)) { 11 | headers['user-agent'] = 'Weatherstar 4000+; weatherstar@netbymatt.com'; 12 | } 13 | // combine default and provided parameters 14 | const params = { 15 | method: 'GET', 16 | mode: 'cors', 17 | type: 'GET', 18 | retryCount: 0, 19 | ..._params, 20 | headers, 21 | }; 22 | // store original number of retries 23 | params.originalRetries = params.retryCount; 24 | 25 | // build a url, including the rewrite for cors if necessary 26 | let corsUrl = _url; 27 | if (params.cors === true) corsUrl = rewriteUrl(_url); 28 | const url = new URL(corsUrl, `${window.location.origin}/`); 29 | // match the security protocol when not on localhost 30 | // url.protocol = window.location.hostname === 'localhost' ? url.protocol : window.location.protocol; 31 | // add parameters if necessary 32 | if (params.data) { 33 | Object.keys(params.data).forEach((key) => { 34 | // get the value 35 | const value = params.data[key]; 36 | // add to the url 37 | url.searchParams.append(key, value); 38 | }); 39 | } 40 | 41 | // make the request 42 | const response = await doFetch(url, params); 43 | 44 | // check for ok response 45 | if (!response.ok) throw new Error(`Fetch error ${response.status} ${response.statusText} while fetching ${response.url}`); 46 | // return the requested response 47 | switch (responseType) { 48 | case 'json': 49 | return response.json(); 50 | case 'text': 51 | return response.text(); 52 | case 'blob': 53 | return response.blob(); 54 | default: 55 | return response; 56 | } 57 | }; 58 | 59 | // fetch with retry and back-off 60 | const doFetch = (url, params) => new Promise((resolve, reject) => { 61 | fetch(url, params).then((response) => { 62 | if (params.retryCount > 0) { 63 | // 500 status codes should be retried after a short backoff 64 | if (response.status >= 500 && response.status <= 599 && params.retryCount > 0) { 65 | // call the "still waiting" function 66 | if (typeof params.stillWaiting === 'function' && params.retryCount === params.originalRetries) { 67 | params.stillWaiting(); 68 | } 69 | // decrement and retry 70 | const newParams = { 71 | ...params, 72 | retryCount: params.retryCount - 1, 73 | }; 74 | return resolve(delay(retryDelay(params.originalRetries - newParams.retryCount), doFetch, url, newParams)); 75 | } 76 | // not 500 status 77 | return resolve(response); 78 | } 79 | // out of retries 80 | return resolve(response); 81 | }) 82 | .catch(reject); 83 | }); 84 | 85 | const delay = (time, func, ...args) => new Promise((resolve) => { 86 | setTimeout(() => { 87 | resolve(func(...args)); 88 | }, time); 89 | }); 90 | 91 | const retryDelay = (retryNumber) => { 92 | switch (retryNumber) { 93 | case 1: return 1000; 94 | case 2: return 2000; 95 | case 3: return 5000; 96 | case 4: return 10_000; 97 | default: return 30_000; 98 | } 99 | }; 100 | 101 | export { 102 | json, 103 | text, 104 | blob, 105 | }; 106 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/image.mjs: -------------------------------------------------------------------------------- 1 | import { blob } from './fetch.mjs'; 2 | 3 | // preload an image 4 | // the goal is to get it in the browser's cache so it is available more quickly when the browser needs it 5 | // a list of cached icons is used to avoid hitting the cache multiple times 6 | const cachedImages = []; 7 | const preloadImg = (src) => { 8 | if (cachedImages.includes(src)) return false; 9 | blob(src); 10 | cachedImages.push(src); 11 | return true; 12 | }; 13 | 14 | export { 15 | // eslint-disable-next-line import/prefer-default-export 16 | preloadImg, 17 | }; 18 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/nosleep.mjs: -------------------------------------------------------------------------------- 1 | // track state of nosleep locally to avoid a null case error 2 | // when nosleep.disable is called without first calling .enable 3 | 4 | let wakeLock = false; 5 | 6 | const noSleep = (enable = false) => { 7 | // get a nosleep controller 8 | if (!noSleep.controller) noSleep.controller = new NoSleep(); 9 | // don't call anything if the states match 10 | if (wakeLock === enable) return false; 11 | // store the value 12 | wakeLock = enable; 13 | // call the function 14 | if (enable) return noSleep.controller.enable(); 15 | return noSleep.controller.disable(); 16 | }; 17 | 18 | export default noSleep; 19 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/polygon.mjs: -------------------------------------------------------------------------------- 1 | // handle multi-polygon and holes 2 | const testPolygon = (point, _polygons) => { 3 | // turn everything into a multi polygon for ease of processing 4 | let polygons = [[..._polygons.coordinates]]; 5 | if (_polygons.type === 'MultiPolygon') polygons = [..._polygons.coordinates]; 6 | 7 | let inArea = false; 8 | 9 | polygons.forEach((_polygon) => { 10 | // copy the polygon 11 | const polygon = [..._polygon]; 12 | // if a match has been found don't do anything more 13 | if (inArea) return; 14 | 15 | // polygons are defined as [[area], [optional hole 1], [optional hole 2], ...] 16 | const area = polygon.shift(); 17 | // test if inside the initial area 18 | inArea = pointInPolygon(point, area); 19 | 20 | // if not in the area return false 21 | if (!inArea) return; 22 | 23 | // test the holes, if in any hole return false 24 | polygon.forEach((hole) => { 25 | if (pointInPolygon(point, hole)) { 26 | inArea = false; 27 | } 28 | }); 29 | }); 30 | return inArea; 31 | }; 32 | 33 | const pointInPolygon = (point, polygon) => { 34 | // ray casting method from https://github.com/substack/point-in-polygon 35 | const x = point[0]; 36 | const y = point[1]; 37 | let inside = false; 38 | // eslint-disable-next-line no-plusplus 39 | for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { 40 | const xi = polygon[i][0]; 41 | const yi = polygon[i][1]; 42 | const xj = polygon[j][0]; 43 | const yj = polygon[j][1]; 44 | const intersect = ((yi > y) !== (yj > y)) 45 | && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi); 46 | if (intersect) inside = !inside; 47 | } 48 | return inside; 49 | }; 50 | 51 | export default testPolygon; 52 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/string.mjs: -------------------------------------------------------------------------------- 1 | const locationCleanup = (input) => { 2 | // regexes to run 3 | const regexes = [ 4 | // "Chicago / West Chicago", removes before slash 5 | /^[ A-Za-z]+ \/ /, 6 | // "Chicago/Waukegan" removes before slash 7 | /^[ A-Za-z]+\//, 8 | // "Chicago, Chicago O'hare" removes before comma 9 | /^[ A-Za-z]+, /, 10 | ]; 11 | 12 | // run all regexes 13 | return regexes.reduce((value, regex) => value.replace(regex, ''), input); 14 | }; 15 | 16 | export { 17 | // eslint-disable-next-line import/prefer-default-export 18 | locationCleanup, 19 | }; 20 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/units.mjs: -------------------------------------------------------------------------------- 1 | // get the settings for units 2 | import settings from '../settings.mjs'; 3 | // *********************************** unit conversions *********************** 4 | 5 | // round 2 provided for lat/lon formatting 6 | const round2 = (value, decimals) => Math.trunc(value * 10 ** decimals) / 10 ** decimals; 7 | 8 | const kphToMph = (Kph) => Math.round(Kph / 1.609_34); 9 | const celsiusToFahrenheit = (Celsius) => Math.round((Celsius * 9) / 5 + 32); 10 | const fahrenheitToCelsius = (Fahrenheit) => Math.round((Fahrenheit - 32) * 5 / 9); 11 | const kilometersToMiles = (Kilometers) => Math.round(Kilometers / 1.609_34); 12 | const metersToFeet = (Meters) => Math.round(Meters / 0.3048); 13 | const pascalToInHg = (Pascal) => round2(Pascal * 0.000_295_3, 2); 14 | 15 | // each module/page/slide creates it's own unit converter as needed by providing the base units available 16 | // the factory function then returns an appropriate converter or pass-thru function for use on the page 17 | 18 | const windSpeed = (defaultUnit = 'si') => { 19 | // default to passthru 20 | let converter = (passthru) => Math.round(passthru); 21 | // change the converter if there is a mismatch 22 | if (defaultUnit !== settings.units.value) { 23 | converter = kphToMph; 24 | } 25 | // append units 26 | if (settings.units.value === 'si') { 27 | converter.units = 'kph'; 28 | } else { 29 | converter.units = 'MPH'; 30 | } 31 | return converter; 32 | }; 33 | 34 | const temperature = (defaultUnit = 'si') => { 35 | // default to passthru 36 | let converter = (passthru) => Math.round(passthru); 37 | // change the converter if there is a mismatch 38 | if (defaultUnit !== settings.units.value) { 39 | if (defaultUnit === 'us') { 40 | converter = fahrenheitToCelsius; 41 | } else { 42 | converter = celsiusToFahrenheit; 43 | } 44 | } 45 | // append units 46 | if (settings.units.value === 'si') { 47 | converter.units = 'C'; 48 | } else { 49 | converter.units = 'F'; 50 | } 51 | return converter; 52 | }; 53 | 54 | const distanceMeters = (defaultUnit = 'si') => { 55 | // default to passthru 56 | let converter = (passthru) => Math.round(passthru); 57 | // change the converter if there is a mismatch 58 | if (defaultUnit !== settings.units.value) { 59 | // rounded to the nearest 100 (ceiling) 60 | converter = (value) => Math.round(metersToFeet(value) / 100) * 100; 61 | } 62 | // append units 63 | if (settings.units.value === 'si') { 64 | converter.units = 'm.'; 65 | } else { 66 | converter.units = 'ft.'; 67 | } 68 | return converter; 69 | }; 70 | 71 | const distanceKilometers = (defaultUnit = 'si') => { 72 | // default to passthru 73 | let converter = (passthru) => Math.round(passthru / 1000); 74 | // change the converter if there is a mismatch 75 | if (defaultUnit !== settings.units.value) { 76 | converter = (value) => Math.round(kilometersToMiles(value) / 1000); 77 | } 78 | // append units 79 | if (settings.units.value === 'si') { 80 | converter.units = ' km.'; 81 | } else { 82 | converter.units = ' mi.'; 83 | } 84 | return converter; 85 | }; 86 | 87 | const pressure = (defaultUnit = 'si') => { 88 | // default to passthru (millibar) 89 | let converter = (passthru) => Math.round(passthru / 100); 90 | // change the converter if there is a mismatch 91 | if (defaultUnit !== settings.units.value) { 92 | converter = (value) => pascalToInHg(value).toFixed(2); 93 | } 94 | // append units 95 | if (settings.units.value === 'si') { 96 | converter.units = ' mbar'; 97 | } else { 98 | converter.units = ' in.hg'; 99 | } 100 | return converter; 101 | }; 102 | 103 | export { 104 | // unit conversions 105 | windSpeed, 106 | temperature, 107 | distanceMeters, 108 | distanceKilometers, 109 | pressure, 110 | 111 | // formatter 112 | round2, 113 | }; 114 | -------------------------------------------------------------------------------- /server/scripts/modules/utils/weather.mjs: -------------------------------------------------------------------------------- 1 | import { json } from './fetch.mjs'; 2 | 3 | const getPoint = async (lat, lon) => { 4 | try { 5 | return await json(`https://api.weather.gov/points/${lat.toFixed(4)},${lon.toFixed(4)}`); 6 | } catch (error) { 7 | console.log(`Unable to get point ${lat}, ${lon}`); 8 | console.error(error); 9 | return false; 10 | } 11 | }; 12 | 13 | export { 14 | // eslint-disable-next-line import/prefer-default-export 15 | getPoint, 16 | }; 17 | -------------------------------------------------------------------------------- /server/styles/scss/_almanac.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | #almanac-html.weather-display { 5 | background-image: url('../images/backgrounds/3.png'); 6 | } 7 | 8 | .weather-display .main.almanac { 9 | font-family: 'Star4000'; 10 | font-size: 24pt; 11 | @include u.text-shadow(); 12 | 13 | .sun { 14 | display: table; 15 | margin-left: 50px; 16 | height: 100px; 17 | 18 | 19 | &>div { 20 | display: table-row; 21 | position: relative; 22 | 23 | &>div { 24 | display: table-cell; 25 | } 26 | } 27 | 28 | .days { 29 | color: c.$column-header-text; 30 | text-align: right; 31 | top: -5px; 32 | 33 | .day { 34 | padding-right: 10px; 35 | } 36 | 37 | } 38 | 39 | .times { 40 | text-align: right; 41 | 42 | .sun-time { 43 | width: 200px; 44 | } 45 | 46 | &.times-1 { 47 | top: -10px; 48 | } 49 | 50 | &.times-2 { 51 | top: -15px; 52 | } 53 | } 54 | } 55 | 56 | .moon { 57 | position: relative; 58 | top: -10px; 59 | 60 | padding: 0px 60px; 61 | 62 | .title { 63 | color: c.$column-header-text; 64 | } 65 | 66 | .day { 67 | display: inline-block; 68 | text-align: center; 69 | width: 130px; 70 | 71 | .icon { 72 | // shadow in image make it look off center 73 | padding-left: 10px; 74 | } 75 | 76 | .date { 77 | position: relative; 78 | top: -10px; 79 | } 80 | } 81 | } 82 | 83 | 84 | 85 | } -------------------------------------------------------------------------------- /server/styles/scss/_current-weather.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display .main.current-weather { 5 | &.main { 6 | 7 | .col { 8 | height: 50px; 9 | width: 255px; 10 | display: inline-block; 11 | margin-top: 10px; 12 | padding-top: 10px; 13 | position: absolute; 14 | 15 | @include u.text-shadow(); 16 | 17 | &.left { 18 | font-family: 'Star4000 Extended'; 19 | font-size: 24pt; 20 | 21 | } 22 | 23 | &.right { 24 | right: 0px; 25 | font-family: "Star4000 Large"; 26 | font-size: 20px; 27 | font-weight: bold; 28 | line-height: 24px; 29 | 30 | .row { 31 | margin-bottom: 12px; 32 | 33 | .label, 34 | .value { 35 | display: inline-block; 36 | } 37 | 38 | .label { 39 | margin-left: 20px; 40 | } 41 | 42 | .value { 43 | float: right; 44 | margin-right: 10px; 45 | } 46 | 47 | } 48 | 49 | } 50 | } 51 | 52 | .center { 53 | text-align: center; 54 | } 55 | 56 | .temp { 57 | font-family: 'Star4000 Large'; 58 | font-size: 24pt; 59 | } 60 | 61 | .condition {} 62 | 63 | .icon { 64 | height: 100px; 65 | 66 | img { 67 | max-width: 126px; 68 | } 69 | } 70 | 71 | .wind-container { 72 | margin-bottom: 10px; 73 | 74 | &>div { 75 | width: 45%; 76 | display: inline-block; 77 | margin: 0px; 78 | } 79 | 80 | .wind-label { 81 | margin-left: 5px; 82 | } 83 | 84 | .wind { 85 | text-align: right; 86 | } 87 | } 88 | 89 | .wind-gusts { 90 | margin-left: 5px; 91 | } 92 | 93 | .location { 94 | color: c.$title-color; 95 | max-height: 32px; 96 | margin-bottom: 10px; 97 | padding-top: 4px; 98 | overflow: hidden; 99 | text-wrap: nowrap; 100 | } 101 | } 102 | } -------------------------------------------------------------------------------- /server/styles/scss/_extended-forecast.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | #extended-forecast-html.weather-display { 5 | background-image: url('../images/backgrounds/2.png'); 6 | } 7 | 8 | .weather-display .main.extended-forecast { 9 | .day-container { 10 | margin-top: 16px; 11 | margin-left: 27px; 12 | } 13 | 14 | .day { 15 | @include u.text-shadow(); 16 | padding: 5px; 17 | height: 285px; 18 | width: 155px; 19 | display: inline-block; 20 | margin: 0px 15px; 21 | font-family: 'Star4000'; 22 | font-size: 24pt; 23 | 24 | .date { 25 | text-transform: uppercase; 26 | text-align: center; 27 | color: c.$title-color; 28 | } 29 | 30 | .condition { 31 | text-align: center; 32 | height: 74px; 33 | margin-top: 10px; 34 | } 35 | 36 | .icon { 37 | text-align: center; 38 | height: 75px; 39 | 40 | img { 41 | max-height: 75px; 42 | } 43 | } 44 | 45 | .temperatures { 46 | width: 100%; 47 | margin-top: 5px; 48 | 49 | .temperature-block { 50 | display: inline-block; 51 | width: 44%; 52 | vertical-align: top; 53 | 54 | >div { 55 | text-align: center; 56 | } 57 | 58 | .value { 59 | font-family: 'Star4000 Large'; 60 | margin-top: 4px; 61 | } 62 | 63 | &.lo .label { 64 | color: c.$extended-low; 65 | } 66 | 67 | &.hi .label { 68 | color: c.$title-color; 69 | } 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /server/styles/scss/_hazards.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display .main.hazards { 5 | &.main { 6 | overflow-y: hidden; 7 | height: 480px; 8 | 9 | .hazard-lines { 10 | min-height: 400px; 11 | padding-top: 10px; 12 | 13 | background-color: rgb(112, 35, 35); 14 | 15 | .hazard { 16 | font-family: 'Star4000'; 17 | font-size: 24pt; 18 | color: white; 19 | @include u.text-shadow(0px); 20 | position: relative; 21 | text-transform: uppercase; 22 | margin-top: 10px; 23 | margin-left: 80px; 24 | margin-right: 80px; 25 | padding-bottom: 10px; 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /server/styles/scss/_hourly-graph.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | #hourly-graph-html { 5 | background-image: url(../images/backgrounds/1-chart.png); 6 | 7 | .header { 8 | .right { 9 | position: absolute; 10 | top: 35px; 11 | right: 60px; 12 | width: 360px; 13 | font-family: 'Star4000 Small'; 14 | font-size: 32px; 15 | @include u.text-shadow(); 16 | text-align: right; 17 | 18 | div { 19 | margin-top: -18px; 20 | } 21 | 22 | .temperature { 23 | color: red; 24 | } 25 | 26 | .cloud { 27 | color: lightgrey; 28 | } 29 | 30 | .rain { 31 | color: aqua; 32 | } 33 | } 34 | } 35 | } 36 | 37 | .weather-display .main.hourly-graph { 38 | 39 | &.main { 40 | >div { 41 | position: absolute; 42 | } 43 | 44 | .label { 45 | font-family: 'Star4000 Small'; 46 | font-size: 24pt; 47 | color: c.$column-header-text; 48 | @include u.text-shadow(); 49 | margin-top: -15px; 50 | position: absolute; 51 | } 52 | 53 | .x-axis { 54 | bottom: 0px; 55 | left: 0px; 56 | width: 640px; 57 | height: 20px; 58 | 59 | .label { 60 | text-align: center; 61 | width: 50px; 62 | 63 | &.l-1 { 64 | left: 25px; 65 | } 66 | 67 | &.l-2 { 68 | left: 158px; 69 | } 70 | 71 | &.l-3 { 72 | left: 291px; 73 | } 74 | 75 | &.l-4 { 76 | left: 424px; 77 | } 78 | 79 | &.l-5 { 80 | left: 557px; 81 | } 82 | } 83 | 84 | 85 | 86 | } 87 | 88 | .chart { 89 | top: 0px; 90 | left: 50px; 91 | 92 | img { 93 | width: 532px; 94 | height: 285px; 95 | } 96 | } 97 | 98 | .y-axis { 99 | top: 0px; 100 | left: 0px; 101 | width: 50px; 102 | height: 285px; 103 | 104 | .label { 105 | text-align: right; 106 | right: 0px; 107 | 108 | &.l-1 { 109 | top: 0px; 110 | } 111 | 112 | &.l-2 { 113 | top: 140px; 114 | } 115 | 116 | &.l-3 { 117 | bottom: 0px; 118 | } 119 | } 120 | } 121 | 122 | .column-headers { 123 | background-color: c.$column-header; 124 | height: 20px; 125 | position: absolute; 126 | width: 100%; 127 | } 128 | 129 | .column-headers { 130 | position: sticky; 131 | top: 0px; 132 | z-index: 5; 133 | 134 | 135 | .temp { 136 | left: 355px; 137 | } 138 | 139 | .like { 140 | left: 435px; 141 | } 142 | 143 | .wind { 144 | left: 535px; 145 | } 146 | } 147 | 148 | 149 | } 150 | } -------------------------------------------------------------------------------- /server/styles/scss/_hourly.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display .main.hourly { 5 | &.main { 6 | overflow-y: hidden; 7 | 8 | .column-headers { 9 | background-color: c.$column-header; 10 | height: 20px; 11 | position: absolute; 12 | width: 100%; 13 | } 14 | 15 | .column-headers { 16 | position: sticky; 17 | top: 0px; 18 | z-index: 5; 19 | 20 | div { 21 | display: inline-block; 22 | font-family: 'Star4000 Small'; 23 | font-size: 24pt; 24 | color: c.$column-header-text; 25 | position: absolute; 26 | top: -14px; 27 | z-index: 5; 28 | @include u.text-shadow(); 29 | } 30 | 31 | .temp { 32 | left: 355px; 33 | } 34 | 35 | .like { 36 | left: 435px; 37 | } 38 | 39 | .wind { 40 | left: 535px; 41 | } 42 | } 43 | 44 | .hourly-lines { 45 | min-height: 338px; 46 | padding-top: 10px; 47 | 48 | background: repeating-linear-gradient(0deg, c.$gradient-main-background-2 0px, 49 | c.$gradient-main-background-1 136px, 50 | c.$gradient-main-background-1 202px, 51 | c.$gradient-main-background-2 338px, 52 | ); 53 | 54 | .hourly-row { 55 | font-family: 'Star4000 Large'; 56 | font-size: 24pt; 57 | height: 72px; 58 | color: c.$title-color; 59 | @include u.text-shadow(); 60 | position: relative; 61 | 62 | >div { 63 | position: absolute; 64 | white-space: pre; 65 | top: 8px; 66 | } 67 | 68 | .hour { 69 | left: 25px; 70 | } 71 | 72 | .icon { 73 | left: 255px; 74 | width: 70px; 75 | text-align: center; 76 | top: unset; 77 | } 78 | 79 | .temp { 80 | left: 355px; 81 | } 82 | 83 | .like { 84 | left: 425px; 85 | 86 | &.heat-index { 87 | color: #e00; 88 | } 89 | 90 | &.wind-chill { 91 | color: c.$extended-low; 92 | } 93 | } 94 | 95 | .wind { 96 | left: 505px; 97 | width: 100px; 98 | text-align: right; 99 | } 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /server/styles/scss/_latest-observations.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display .latest-observations { 5 | 6 | &.main { 7 | overflow-y: hidden; 8 | 9 | .column-headers { 10 | height: 20px; 11 | position: absolute; 12 | width: 100%; 13 | } 14 | 15 | .column-headers { 16 | top: 0px; 17 | 18 | div { 19 | display: inline-block; 20 | font-family: 'Star4000 Small'; 21 | font-size: 24pt; 22 | position: absolute; 23 | top: -14px; 24 | @include u.text-shadow(); 25 | } 26 | 27 | .temp { 28 | // hidden initially for english/metric switching 29 | display: none; 30 | 31 | &.show { 32 | display: inline-block; 33 | } 34 | } 35 | } 36 | 37 | .temp { 38 | left: 230px; 39 | } 40 | 41 | .weather { 42 | left: 280px; 43 | } 44 | 45 | .wind { 46 | left: 430px; 47 | } 48 | 49 | .observation-lines { 50 | min-height: 338px; 51 | padding-top: 10px; 52 | 53 | .observation-row { 54 | font-family: 'Star4000'; 55 | font-size: 24pt; 56 | @include u.text-shadow(); 57 | position: relative; 58 | height: 40px; 59 | 60 | >div { 61 | position: absolute; 62 | top: 8px; 63 | } 64 | 65 | .wind { 66 | white-space: pre; 67 | text-align: right; 68 | } 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /server/styles/scss/_local-forecast.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display .local-forecast { 5 | .container { 6 | position: relative; 7 | top: 15px; 8 | margin: 0px 10px; 9 | box-sizing: border-box; 10 | height: 280px; 11 | overflow: hidden; 12 | } 13 | 14 | .forecasts { 15 | position: relative; 16 | } 17 | 18 | .forecast { 19 | font-family: 'Star4000'; 20 | font-size: 24pt; 21 | text-transform: uppercase; 22 | @include u.text-shadow(); 23 | min-height: 280px; 24 | line-height: 40px; 25 | } 26 | } -------------------------------------------------------------------------------- /server/styles/scss/_media.scss: -------------------------------------------------------------------------------- 1 | .media { 2 | display: none; 3 | } 4 | 5 | #ToggleMedia { 6 | display: none; 7 | 8 | &.available { 9 | display: inline-block; 10 | 11 | img.on { 12 | display: none; 13 | } 14 | 15 | img.off { 16 | display: block; 17 | } 18 | 19 | // icon switch is handled by adding/removing the .playing class 20 | &.playing { 21 | img.on { 22 | display: block; 23 | } 24 | 25 | img.off { 26 | display: none; 27 | } 28 | 29 | } 30 | 31 | 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /server/styles/scss/_progress.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display .progress { 5 | @include u.text-shadow(); 6 | font-family: 'Star4000 Extended'; 7 | font-size: 19pt; 8 | 9 | .container { 10 | position: relative; 11 | top: 15px; 12 | margin: 0px 10px; 13 | box-sizing: border-box; 14 | height: 310px; 15 | overflow: hidden; 16 | 17 | .item { 18 | position: relative; 19 | 20 | .name { 21 | white-space: nowrap; 22 | 23 | &::after { 24 | content: '........................................................................'; 25 | } 26 | } 27 | 28 | .links { 29 | position: absolute; 30 | text-align: right; 31 | right: 0px; 32 | top: 0px; 33 | 34 | >div { 35 | background-color: c.$blue-box; 36 | display: none; 37 | padding-left: 4px; 38 | } 39 | 40 | @include u.status-colors(); 41 | 42 | &.loading .loading, 43 | &.press-here .press-here, 44 | &.failed .failed, 45 | &.no-data .no-data, 46 | &.disabled .disabled, 47 | &.retrying .retrying { 48 | display: block; 49 | } 50 | 51 | } 52 | } 53 | } 54 | 55 | 56 | } 57 | 58 | #progress-html.weather-display .scroll { 59 | 60 | @keyframes progress-scroll { 61 | 0% { 62 | background-position: -40px 0; 63 | } 64 | 65 | 100% { 66 | background-position: 40px 0; 67 | } 68 | } 69 | 70 | .progress-bar-container { 71 | border: 2px solid black; 72 | background-color: white; 73 | margin: 20px auto; 74 | width: 524px; 75 | position: relative; 76 | display: none; 77 | 78 | &.show { 79 | display: block; 80 | } 81 | 82 | .progress-bar { 83 | height: 20px; 84 | margin: 2px; 85 | width: 520px; 86 | background: repeating-linear-gradient(90deg, 87 | c.$gradient-loading-1 0px, 88 | c.$gradient-loading-1 5px, 89 | c.$gradient-loading-2 5px, 90 | c.$gradient-loading-2 10px, 91 | c.$gradient-loading-3 10px, 92 | c.$gradient-loading-3 15px, 93 | c.$gradient-loading-4 15px, 94 | c.$gradient-loading-4 20px, 95 | c.$gradient-loading-3 20px, 96 | c.$gradient-loading-3 25px, 97 | c.$gradient-loading-2 25px, 98 | c.$gradient-loading-2 30px, 99 | c.$gradient-loading-1 30px, 100 | c.$gradient-loading-1 40px, 101 | ); 102 | // animation 103 | animation-duration: 2s; 104 | animation-fill-mode: forwards; 105 | animation-iteration-count: infinite; 106 | animation-name: progress-scroll; 107 | animation-timing-function: steps(8, end); 108 | } 109 | 110 | .cover { 111 | position: absolute; 112 | top: 0px; 113 | right: 0px; 114 | background-color: white; 115 | width: 100%; 116 | height: 24px; 117 | transition: width 1s steps(6); 118 | } 119 | } 120 | } -------------------------------------------------------------------------------- /server/styles/scss/_radar.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | #radar-html.weather-display { 5 | background-image: url('../images/backgrounds/4.png'); 6 | 7 | .header { 8 | height: 83px; 9 | 10 | .title.dual { 11 | color: white; 12 | font-family: 'Arial', sans-serif; 13 | font-weight: bold; 14 | font-size: 28pt; 15 | left: 155px; 16 | 17 | .top { 18 | top: -4px; 19 | } 20 | 21 | .bottom { 22 | top: 31px; 23 | } 24 | } 25 | 26 | .right { 27 | position: absolute; 28 | right: 0px; 29 | width: 360px; 30 | margin-top: 2px; 31 | font-family: 'Star4000'; 32 | font-size: 18pt; 33 | font-weight: bold; 34 | @include u.text-shadow(); 35 | text-align: center; 36 | 37 | .scale>div { 38 | display: inline-block; 39 | } 40 | 41 | .scale-table { 42 | display: table-row; 43 | border-collapse: collapse; 44 | 45 | .box { 46 | display: table-cell; 47 | border: 2px solid black; 48 | width: 17px; 49 | height: 24px; 50 | padding: 0 51 | } 52 | 53 | .box-1 { 54 | background-color: rgb(49, 210, 22); 55 | } 56 | 57 | .box-2 { 58 | background-color: rgb(28, 138, 18); 59 | } 60 | 61 | .box-3 { 62 | background-color: rgb(20, 90, 15); 63 | } 64 | 65 | .box-4 { 66 | background-color: rgb(10, 40, 10); 67 | } 68 | 69 | .box-5 { 70 | background-color: rgb(196, 179, 70); 71 | } 72 | 73 | .box-6 { 74 | background-color: rgb(190, 72, 19); 75 | } 76 | 77 | .box-7 { 78 | background-color: rgb(171, 14, 14); 79 | } 80 | 81 | .box-8 { 82 | background-color: rgb(115, 31, 4); 83 | } 84 | } 85 | 86 | .scale { 87 | .text { 88 | position: relative; 89 | top: -5px; 90 | } 91 | } 92 | 93 | .time { 94 | position: relative; 95 | font-weight: normal; 96 | top: -14px; 97 | font-family: 'Star4000 Small'; 98 | font-size: 24pt; 99 | } 100 | } 101 | } 102 | } 103 | 104 | .weather-display .main.radar { 105 | overflow: hidden; 106 | height: 367px; 107 | 108 | .container { 109 | 110 | .scroll-area { 111 | position: relative; 112 | } 113 | } 114 | } 115 | 116 | .wide.radar #container { 117 | background: url(../images/backgrounds/4-wide.png); 118 | } -------------------------------------------------------------------------------- /server/styles/scss/_regional-forecast.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | #regional-forecast-html.weather-display { 5 | background-image: url('../images/backgrounds/5.png'); 6 | } 7 | 8 | .weather-display .main.regional-forecast { 9 | 10 | 11 | position: relative; 12 | 13 | .map { 14 | position: absolute; 15 | transform-origin: 0 0; 16 | } 17 | 18 | .location { 19 | position: absolute; 20 | width: 140px; 21 | margin-left: -40px; 22 | margin-top: -35px; 23 | 24 | >div { 25 | position: absolute; 26 | @include u.text-shadow(); 27 | } 28 | 29 | .icon { 30 | top: 26px; 31 | left: 44px; 32 | 33 | img { 34 | max-height: 32px; 35 | } 36 | } 37 | 38 | .temp { 39 | font-family: 'Star4000 Large'; 40 | font-size: 28px; 41 | padding-top: 2px; 42 | color: c.$title-color; 43 | top: 28px; 44 | text-align: right; 45 | width: 40px; 46 | } 47 | 48 | .city { 49 | font-family: Star4000; 50 | font-size: 20px; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /server/styles/scss/_spc-outlook.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | #spc-outlook-html.weather-display { 5 | background-image: url('../images/backgrounds/6.png'); 6 | } 7 | 8 | .weather-display .spc-outlook { 9 | 10 | .container { 11 | position: relative; 12 | top: 0px; 13 | margin: 0px 10px; 14 | box-sizing: border-box; 15 | height: 300px; 16 | overflow: hidden; 17 | } 18 | 19 | .risk-levels { 20 | position: absolute; 21 | left: 206px; 22 | font-family: 'Star4000 Small'; 23 | font-size: 32px; 24 | @include u.text-shadow(); 25 | 26 | 27 | .risk-level { 28 | position: relative; 29 | top: -14px; 30 | height: 20px; 31 | 32 | &:nth-child(1) { 33 | left: calc(20px * 5); 34 | } 35 | 36 | &:nth-child(2) { 37 | left: calc(20px * 4); 38 | } 39 | 40 | &:nth-child(3) { 41 | left: calc(20px * 3); 42 | } 43 | 44 | &:nth-child(4) { 45 | left: calc(20px * 2); 46 | } 47 | 48 | &:nth-child(5) { 49 | left: calc(20px * 1); 50 | } 51 | 52 | &:nth-child(6) { 53 | left: calc(20px * 0); 54 | } 55 | } 56 | } 57 | 58 | .days { 59 | position: absolute; 60 | top: 120px; 61 | 62 | .day { 63 | height: 60px; 64 | 65 | .day-name { 66 | position: absolute; 67 | font-family: 'Star4000'; 68 | font-size: 24pt; 69 | width: 200px; 70 | text-align: right; 71 | @include u.text-shadow(); 72 | padding-top: 20px; 73 | } 74 | 75 | .risk-bar { 76 | position: absolute; 77 | width: 150px; 78 | height: 40px; 79 | left: 210px; 80 | margin-top: 20px; 81 | border: 3px outset hsl(0, 0%, 70%); 82 | background: linear-gradient(0deg, hsl(0, 0%, 40%) 0%, hsl(0, 0%, 60%) 50%, hsl(0, 0%, 40%) 100%); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /server/styles/scss/_travel.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display .main.travel { 5 | &.main { 6 | overflow-y: hidden; 7 | 8 | .column-headers { 9 | background-color: c.$column-header; 10 | height: 20px; 11 | position: absolute; 12 | width: 100%; 13 | } 14 | 15 | .column-headers { 16 | position: sticky; 17 | top: 0px; 18 | z-index: 5; 19 | 20 | div { 21 | display: inline-block; 22 | font-family: 'Star4000 Small'; 23 | font-size: 24pt; 24 | color: c.$column-header-text; 25 | position: absolute; 26 | top: -14px; 27 | z-index: 5; 28 | @include u.text-shadow(); 29 | } 30 | 31 | .temp { 32 | width: 50px; 33 | text-align: center; 34 | 35 | &.low { 36 | left: 455px; 37 | 38 | } 39 | 40 | &.high { 41 | left: 510px; 42 | width: 60px; 43 | } 44 | } 45 | } 46 | 47 | .travel-lines { 48 | min-height: 338px; 49 | padding-top: 10px; 50 | 51 | background: repeating-linear-gradient(0deg, c.$gradient-main-background-2 0px, 52 | c.$gradient-main-background-1 136px, 53 | c.$gradient-main-background-1 202px, 54 | c.$gradient-main-background-2 338px, 55 | ); 56 | 57 | .travel-row { 58 | font-family: 'Star4000 Large'; 59 | font-size: 24pt; 60 | height: 72px; 61 | color: c.$title-color; 62 | @include u.text-shadow(); 63 | position: relative; 64 | 65 | >div { 66 | position: absolute; 67 | white-space: pre; 68 | top: 8px; 69 | } 70 | 71 | .city { 72 | left: 80px; 73 | } 74 | 75 | .icon { 76 | left: 330px; 77 | width: 70px; 78 | text-align: center; 79 | top: unset; 80 | 81 | img { 82 | max-width: 47px; 83 | } 84 | } 85 | 86 | .temp { 87 | width: 50px; 88 | text-align: center; 89 | 90 | &.low { 91 | left: 455px; 92 | } 93 | 94 | &.high { 95 | left: 510px; 96 | width: 60px; 97 | } 98 | } 99 | 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /server/styles/scss/_weather-display.scss: -------------------------------------------------------------------------------- 1 | @use 'shared/_colors'as c; 2 | @use 'shared/_utils'as u; 3 | 4 | .weather-display { 5 | width: 640px; 6 | height: 480px; 7 | overflow: hidden; 8 | position: relative; 9 | background-image: url(../images/backgrounds/1.png); 10 | 11 | /* this method is required to hide blocks so they can be measured while off screen */ 12 | height: 0px; 13 | 14 | &.show { 15 | height: 480px; 16 | } 17 | 18 | .template { 19 | display: none; 20 | } 21 | 22 | .header { 23 | width: 640px; 24 | height: 60px; 25 | padding-top: 30px; 26 | 27 | .title { 28 | color: c.$title-color; 29 | @include u.text-shadow(3px, 1.5px); 30 | font-family: 'Star4000'; 31 | font-size: 24pt; 32 | position: absolute; 33 | width: 250px; 34 | 35 | &.single { 36 | left: 170px; 37 | top: 25px; 38 | } 39 | 40 | &.dual { 41 | left: 170px; 42 | 43 | &>div { 44 | position: absolute; 45 | } 46 | 47 | .top { 48 | top: -3px; 49 | } 50 | 51 | .bottom { 52 | top: 26px; 53 | } 54 | } 55 | 56 | } 57 | 58 | .logo { 59 | top: 30px; 60 | left: 50px; 61 | position: absolute; 62 | z-index: 10; 63 | } 64 | 65 | .noaa-logo { 66 | position: absolute; 67 | top: 39px; 68 | left: 356px; 69 | } 70 | 71 | .title.single { 72 | top: 40px; 73 | } 74 | 75 | .date-time { 76 | white-space: pre; 77 | color: c.$date-time; 78 | font-family: 'Star4000 Small'; 79 | font-size: 24pt; 80 | @include u.text-shadow(3px, 1.5px); 81 | left: 415px; 82 | width: 170px; 83 | text-align: right; 84 | position: absolute; 85 | 86 | &.date { 87 | padding-top: 22px; 88 | } 89 | } 90 | } 91 | 92 | .main { 93 | position: relative; 94 | 95 | &.has-scroll { 96 | width: 640px; 97 | height: 310px; 98 | overflow: hidden; 99 | 100 | &.no-header { 101 | height: 400px; 102 | } 103 | } 104 | 105 | &.has-box { 106 | margin-left: 64px; 107 | margin-right: 64px; 108 | width: calc(100% - 128px); 109 | } 110 | 111 | } 112 | 113 | 114 | .scroll { 115 | @include u.text-shadow(3px, 1.5px); 116 | width: 640px; 117 | height: 70px; 118 | overflow: hidden; 119 | margin-top: 3px; 120 | 121 | &.hazard { 122 | background-color: rgb(112, 35, 35); 123 | } 124 | 125 | .fixed, 126 | .scroll-header { 127 | margin-left: 55px; 128 | margin-right: 55px; 129 | overflow: hidden; 130 | } 131 | 132 | .scroll-header { 133 | height: 26px; 134 | font-family: "Star4000 Small"; 135 | font-size: 20pt; 136 | margin-top: -10px; 137 | } 138 | 139 | .fixed { 140 | font-family: 'Star4000'; 141 | font-size: 24pt; 142 | 143 | .scroll-area { 144 | text-wrap: nowrap; 145 | position: relative; 146 | // the following added by js code as it is dependent on the content of the element 147 | // transition: left (x)s; 148 | // left: calc((elem width) - 640px); 149 | } 150 | } 151 | 152 | } 153 | } -------------------------------------------------------------------------------- /server/styles/scss/main.scss: -------------------------------------------------------------------------------- 1 | @use 'page'; 2 | @use 'weather-display'; 3 | @use 'current-weather'; 4 | @use 'extended-forecast'; 5 | @use 'hourly'; 6 | @use 'hourly-graph'; 7 | @use 'travel'; 8 | @use 'latest-observations'; 9 | @use 'local-forecast'; 10 | @use 'progress'; 11 | @use 'radar'; 12 | @use 'regional-forecast'; 13 | @use 'almanac'; 14 | @use 'hazards'; 15 | @use 'media'; 16 | @use 'spc-outlook'; 17 | @use 'shared/scanlines'; -------------------------------------------------------------------------------- /server/styles/scss/shared/_colors.scss: -------------------------------------------------------------------------------- 1 | $title-color: yellow; 2 | $date-time: white; 3 | $text-shadow: black; 4 | $column-header-text: yellow; 5 | $column-header: rgb(32, 0, 87); 6 | 7 | $gradient-main-background-1: #102080; 8 | $gradient-main-background-2: #001040; 9 | 10 | $gradient-loading-1: #09246f; 11 | $gradient-loading-2: #364ac0; 12 | $gradient-loading-3: #4f99f9; 13 | $gradient-loading-4: #8ffdfa; 14 | 15 | $extended-low: #8080FF; 16 | 17 | $blue-box: #26235a; -------------------------------------------------------------------------------- /server/styles/scss/shared/_scanlines.scss: -------------------------------------------------------------------------------- 1 | /* REGULAR SCANLINES SETTINGS */ 2 | 3 | // width of 1 scanline (min.: 1px) 4 | $scan-width: 1px; 5 | 6 | // emulates a damage-your-eyes bad pre-2000 CRT screen ♥ (true, false) 7 | $scan-crt: false; 8 | 9 | // frames-per-second (should be > 1), only applies if $scan-crt: true; 10 | $scan-fps: 20; 11 | 12 | // scanline-color (rgba) 13 | $scan-color: rgba(#000, .3); 14 | 15 | // set z-index on 8, like in ♥ 8-bits ♥, or… 16 | // set z-index on 2147483648 or more to enable scanlines on Chrome fullscreen (doesn't work in Firefox or IE); 17 | $scan-z-index: 2147483648; 18 | 19 | /* MOVING SCANLINE SETTINGS */ 20 | 21 | // moving scanline (true, false) 22 | $scan-moving-line: true; 23 | 24 | // opacity of the moving scanline 25 | $scan-opacity: .75; 26 | 27 | /* MIXINS */ 28 | 29 | // apply CRT animation: @include scan-crt($scan-crt); 30 | @mixin scan-crt($scan-crt) { 31 | @if $scan-crt==true { 32 | animation: scanlines 1s steps($scan-fps) infinite; 33 | } 34 | 35 | @else { 36 | animation: none; 37 | } 38 | } 39 | 40 | // apply CRT animation: @include scan-crt($scan-crt); 41 | @mixin scan-moving($scan-moving-line) { 42 | @if $scan-moving-line==true { 43 | animation: scanline 6s linear infinite; 44 | } 45 | 46 | @else { 47 | animation: none; 48 | } 49 | } 50 | 51 | /* CSS .scanlines CLASS */ 52 | 53 | .scanlines { 54 | position: relative; 55 | overflow: hidden; // only to animate the unique scanline 56 | 57 | &:before, 58 | &:after { 59 | display: block; 60 | pointer-events: none; 61 | content: ''; 62 | position: absolute; 63 | } 64 | 65 | // unique scanline travelling on the screen 66 | &:before { 67 | // position: absolute; 68 | // bottom: 100%; 69 | width: 100%; 70 | height: $scan-width * 1; 71 | z-index: $scan-z-index + 1; 72 | background: $scan-color; 73 | opacity: $scan-opacity; 74 | // animation: scanline 6s linear infinite; 75 | @include scan-moving($scan-moving-line); 76 | } 77 | 78 | // the scanlines, so! 79 | &:after { 80 | top: 0; 81 | right: 0; 82 | bottom: 0; 83 | left: 0; 84 | z-index: $scan-z-index; 85 | background: linear-gradient(to bottom, 86 | transparent 50%, 87 | $scan-color 51%); 88 | background-size: 100% $scan-width*2; 89 | @include scan-crt($scan-crt); 90 | } 91 | } 92 | 93 | /* ANIMATE UNIQUE SCANLINE */ 94 | @keyframes scanline { 95 | 0% { 96 | transform: translate3d(0, 200000%, 0); 97 | // bottom: 0%; // to have a continuous scanline move, use this line (here in 0% step) instead of transform and write, in &:before, { position: absolute; bottom: 100%; } 98 | } 99 | } 100 | 101 | @keyframes scanlines { 102 | 0% { 103 | background-position: 0 50%; 104 | // bottom: 0%; // to have a continuous scanline move, use this line (here in 0% step) instead of transform and write, in &:before, { position: absolute; bottom: 100%; } 105 | } 106 | } -------------------------------------------------------------------------------- /server/styles/scss/shared/_utils.scss: -------------------------------------------------------------------------------- 1 | @use 'colors'as c; 2 | 3 | @mixin text-shadow($offset: 3px, $outline: 1.5px) { 4 | /* eventually, when chrome supports paint-order for html elements */ 5 | /* -webkit-text-stroke: 2px black; */ 6 | /* paint-order: stroke fill; */ 7 | text-shadow: 8 | $offset $offset 0 c.$text-shadow, 9 | (-$outline) (-$outline) 0 c.$text-shadow, 10 | 0 (-$outline) 0 c.$text-shadow, 11 | $outline (-$outline) 0 c.$text-shadow, 12 | $outline 0 0 c.$text-shadow, 13 | $outline $outline 0 c.$text-shadow, 14 | 0 $outline 0 c.$text-shadow, 15 | (-$outline) $outline 0 c.$text-shadow, 16 | (-$outline) 0 0 c.$text-shadow; 17 | } 18 | 19 | @mixin status-colors() { 20 | 21 | .loading, 22 | .retrying { 23 | color: #ffff00; 24 | } 25 | 26 | .press-here { 27 | color: #00ff00; 28 | cursor: pointer; 29 | } 30 | 31 | .failed { 32 | color: #ff0000; 33 | } 34 | 35 | .no-data { 36 | color: #C0C0C0; 37 | } 38 | 39 | .disabled { 40 | color: #C0C0C0; 41 | } 42 | } -------------------------------------------------------------------------------- /src/overrides.mjs: -------------------------------------------------------------------------------- 1 | // read overrides from environment variables 2 | 3 | const OVERRIDES = {}; 4 | Object.entries(process.env).forEach(([key, value]) => { 5 | if (key.match(/^OVERRIDE_/)) { 6 | OVERRIDES[key.replace('OVERRIDE_', '')] = value; 7 | } 8 | }); 9 | 10 | export default OVERRIDES; 11 | -------------------------------------------------------------------------------- /src/playlist-reader.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises'; 2 | 3 | const mp3Filter = (file) => file.match(/\.mp3$/); 4 | 5 | const reader = async () => { 6 | // get the listing of files in the folder 7 | const rawFiles = await fs.readdir('./server/music'); 8 | // filter for mp3 files 9 | const files = rawFiles.filter(mp3Filter); 10 | // if files were found return them 11 | if (files.length > 0) { 12 | return files; 13 | } 14 | 15 | // fall back to the default folder 16 | const defaultFiles = await fs.readdir('./server/music/default'); 17 | return defaultFiles.map((file) => `default/${file}`).filter(mp3Filter); 18 | }; 19 | 20 | export default reader; 21 | -------------------------------------------------------------------------------- /src/playlist.mjs: -------------------------------------------------------------------------------- 1 | import reader from './playlist-reader.mjs'; 2 | 3 | const playlistGenerator = async (req, res) => { 4 | try { 5 | const availableFiles = await reader(); 6 | res.json({ 7 | availableFiles, 8 | }); 9 | } catch (e) { 10 | console.error(e); 11 | res.json({ 12 | availableFiles: [], 13 | }); 14 | } 15 | }; 16 | 17 | export default playlistGenerator; 18 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations. -------------------------------------------------------------------------------- /tests/index.mjs: -------------------------------------------------------------------------------- 1 | import puppeteer from 'puppeteer'; 2 | import { setTimeout } from 'node:timers/promises'; 3 | import { readFile } from 'fs/promises'; 4 | import messageFormatter from './messageformatter.mjs'; 5 | 6 | const browser = await puppeteer.launch({ 7 | // headless: false, 8 | slowMo: 10, 9 | timeout: 10_000, 10 | dumpio: true, 11 | }); 12 | 13 | // get the list of locations 14 | const LOCATIONS = JSON.parse(await readFile('./tests/locations.json')); 15 | 16 | // get the page 17 | const page = (await browser.pages())[0]; 18 | await page.goto('http://localhost:8080'); 19 | 20 | page.on('console', messageFormatter); 21 | 22 | const tester = async (location, testPage) => { 23 | // Set the address 24 | await testPage.type('#txtAddress', location); 25 | await setTimeout(500); 26 | // get the page 27 | await testPage.click('#btnGetLatLng'); 28 | // wait for errors 29 | await setTimeout(5000); 30 | }; 31 | 32 | // run all the locations 33 | for (let i = 0; i < LOCATIONS.length; i += 1) { 34 | const location = LOCATIONS[i]; 35 | console.log(location); 36 | // eslint-disable-next-line no-await-in-loop 37 | await tester(location, page); 38 | } 39 | 40 | browser.close(); 41 | -------------------------------------------------------------------------------- /tests/locations.json: -------------------------------------------------------------------------------- 1 | [ 2 | "New York, New York", 3 | "Los Angeles, California", 4 | "Chicago, Illinois", 5 | "Houston, Texas", 6 | "Phoenix, Arizona", 7 | "Philadelphia, Pennsylvania", 8 | "San Antonio, Texas", 9 | "San Diego, California", 10 | "Dallas, Texas", 11 | "San Jose, California", 12 | "Austin, Texas", 13 | "Jacksonville, Florida", 14 | "Fort Worth, Texas", 15 | "Columbus, Ohio", 16 | "Charlotte, North Carolina", 17 | "Indianapolis, Indiana", 18 | "San Francisco, California", 19 | "Seattle, Washington", 20 | "Denver, Colorado", 21 | "Nashville, Tennessee", 22 | "Washington, District of Columbia", 23 | "Oklahoma City, Oklahoma", 24 | "Boston, Massachusetts", 25 | "El Paso, Texas", 26 | "Portland, Oregon", 27 | "Las Vegas, Nevada", 28 | "Memphis, Tennessee", 29 | "Detroit, Michigan", 30 | "Baltimore, Maryland", 31 | "Milwaukee, Wisconsin", 32 | "Albuquerque, New Mexico", 33 | "Fresno, California", 34 | "Tucson, Arizona", 35 | "Sacramento, California", 36 | "Mesa, Arizona", 37 | "Kansas City, Missouri", 38 | "Atlanta, Georgia", 39 | "Omaha, Nebraska", 40 | "Colorado Springs, Colorado", 41 | "Raleigh, North Carolina", 42 | "Long Beach, California", 43 | "Virginia Beach, Virginia", 44 | "Oakland, California", 45 | "Miami, Florida", 46 | "Minneapolis, Minnesota", 47 | "Bakersfield, California", 48 | "Tulsa, Oklahoma", 49 | "Aurora, Colorado", 50 | "Arlington, Texas", 51 | "Wichita, Kansas" 52 | ] -------------------------------------------------------------------------------- /tests/messageformatter.mjs: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | const describe = (jsHandle) => jsHandle.evaluate( 4 | // serialize |obj| however you want 5 | (obj) => `OBJ: ${typeof obj}, ${obj}`, 6 | jsHandle, 7 | ); 8 | 9 | const colors = { 10 | LOG: chalk.grey, 11 | ERR: chalk.red, 12 | WAR: chalk.yellow, 13 | INF: chalk.cyan, 14 | }; 15 | 16 | const formatter = async (message) => { 17 | const args = await Promise.all(message.args().map((arg) => describe(arg))); 18 | // make ability to paint different console[types] 19 | const type = message.type().substr(0, 3).toUpperCase(); 20 | const color = colors[type] || chalk.blue; 21 | let text = ''; 22 | for (let i = 0; i < args.length; i += 1) { 23 | text += `[${i}] ${args[i]} `; 24 | } 25 | text += message?.stackTrace()?.[0]?.url ?? ''; 26 | console.log(color(`CONSOLE.${type}: ${message.text()}\n${text} `)); 27 | }; 28 | 29 | export default formatter; 30 | -------------------------------------------------------------------------------- /tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ws4kp-tests", 3 | "version": "1.0.0", 4 | "description": "Currently, tests take a different approach from typical unit testing. The test methodology loads several forecasts for different locations and logs them all to one logger so errors can be found such as missing icons, locations that do not have all of the necessary data or other changes that may occur between geographical locations.", 5 | "main": "index.mjs", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "MIT", 11 | "dependencies": { 12 | "chalk": "^5.4.1", 13 | "puppeteer": "^24.8.2" 14 | }, 15 | "type": "module" 16 | } -------------------------------------------------------------------------------- /views/partials/almanac.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {title:'Almanac', hasTime: true}) %> 2 |
3 |
4 |
5 |
6 |
Monday
7 |
Tuesday
8 |
9 |
10 |
Sunrise:
11 |
6:24 am
12 |
6:25 am
13 |
14 |
15 |
Sunset:
16 |
6:24 am
17 |
6:25 am
18 |
19 |
20 |
21 |
Moon Data:
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/current-weather.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual:{ top: 'Current' , bottom: 'Conditions' }, noaaLogo: true, hasTime: true}) %> 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
Wind:
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
Humidity:
18 |
19 |
20 |
21 |
Dewpoint:
22 |
23 |
24 |
25 |
Ceiling:
26 |
27 |
28 |
29 |
Visibility:
30 |
31 |
32 |
33 |
Pressure:
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/extended-forecast.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual:{ top: 'Extended' , bottom: 'Forecast' }, hasTime: true }) %> 2 |
3 |
4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |
12 |
Lo
13 |
14 |
15 |
16 |
Hi
17 |
18 |
19 |
20 |
21 |
22 |
23 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/hazards.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
-------------------------------------------------------------------------------- /views/partials/header.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 | <% if (locals?.titleDual) { %> 4 |
5 |
6 | <%-titleDual.top %> 7 |
8 |
9 | <%-titleDual.bottom %> 10 |
11 |
12 | <% } else { %> 13 |
14 | <%-title %> 15 |
16 | <% } %> 17 | <% if (locals?.hasTime) { %> 18 |
19 |
20 | <% } else if (!locals?.noaaLogo) { %> 21 |
22 | <% } %> 23 | <% if (locals?.noaaLogo) { %> 24 | 27 | <%}%> 28 |
-------------------------------------------------------------------------------- /views/partials/hourly-graph.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {title: 'Hourly Graph' , hasTime: false }) %> 2 |
3 |
4 |
Temperature
5 |
Cloud %
6 |
Precip %
7 |
8 |
9 |
75
10 |
65
11 |
55
12 |
13 |
14 | 15 |
16 |
17 |
12a
18 |
6a
19 |
12p
20 |
6p
21 |
12a
22 |
23 |
24 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/hourly.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {title: 'Hourly Forecast' , hasTime: true }) %> 2 |
3 |
4 |
TEMP
5 | 6 |
WIND
7 |
8 |
9 |
10 |
11 |
12 |
13 | 14 |
15 |
16 |
17 |
18 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/latest-observations.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual:{ top: 'Latest' , bottom: 'Observations' }, noaaLogo: true, hasTime: true }) %> 2 |
3 |
4 |
5 |
°F
6 |
°C
7 |
Weather
8 |
Wind
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/local-forecast.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual:{ top: 'Local' , bottom: 'Forecast' }, hasTime: true, noaaLogo: true}) %> 2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/progress.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual:{ top: 'WeatherStar' , bottom: '4000+ v' + version }, hasTime: true}) %> 2 |
3 |
4 |
5 |
Current Conditions
6 | 14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
-------------------------------------------------------------------------------- /views/partials/radar.ejs: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | Local 6 |
7 |
8 | Radar 9 |
10 |
11 |
12 |
13 |
PRECIP
14 |
15 |
Light
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
Heavy
27 |
28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
-------------------------------------------------------------------------------- /views/partials/regional-forecast.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual:{ top: 'Regional' , bottom: 'Observations' }, hasTime: true }) %> 2 |
3 |
4 |
5 |
6 |
7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/scroll.ejs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
-------------------------------------------------------------------------------- /views/partials/spc-outlook.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual:{ top: 'Storm Prediction' , bottom: 'Center Outlook' }, hasTime: true}) %> 2 |
3 |
4 |
5 |
High
6 |
Moderate
7 |
Enhanced
8 |
Slight
9 |
Marginal
10 |
T'Storm
11 |
12 |
13 |
14 |
Monday
15 |
16 |
17 |
18 |
19 |
20 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /views/partials/travel.ejs: -------------------------------------------------------------------------------- 1 | <%- include('header.ejs', {titleDual: {top: 'Travel Forecast', bottom: 'For '} , hasTime: true }) %> 2 |
3 |
4 |
LOW
5 |
HIGH
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | <%- include('scroll.ejs') %> -------------------------------------------------------------------------------- /ws4kp.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "search.exclude": { 9 | "**/*.code-search": true, 10 | "**/*.css": true, 11 | "**/*.min.js": true, 12 | "**/bower_components": true, 13 | "**/node_modules": true, 14 | "**/vendor": true, 15 | "dist/**": true 16 | }, 17 | "cSpell.enabledFileTypes": { 18 | "markdown": true, 19 | "JavaScript": true 20 | }, 21 | "cSpell.enabled": true, 22 | "cSpell.ignoreWords": [ 23 | "'storm", 24 | "arcgis", 25 | "Battaglia", 26 | "devbridge", 27 | "gifs", 28 | "ltrim", 29 | "mbar", 30 | "Noaa", 31 | "nosleep", 32 | "Pngs", 33 | "PRECIP", 34 | "rtrim", 35 | "sonarjs", 36 | "T", 37 | "T'storm", 38 | "uscomp", 39 | "Visib", 40 | "Waukegan", 41 | "WSQS", 42 | "Tucsan", 43 | "Malek", 44 | "mwood", 45 | "unmuted", 46 | "dumpio" 47 | ], 48 | "cSpell.ignorePaths": [ 49 | "**/package-lock.json", 50 | "**/node_modules/**", 51 | "**/vscode-extension/**", 52 | "**/.git/objects/**", 53 | ".vscode", 54 | ".vscode-insiders", 55 | "**/vendor/auto/**", 56 | ], 57 | "editor.tabSize": 2, 58 | "emmet.includeLanguages": { 59 | "ejs": "html", 60 | }, 61 | "[html]": { 62 | "editor.defaultFormatter": "j69.ejs-beautify" 63 | }, 64 | "files.exclude": {}, 65 | "files.eol": "\n", 66 | "editor.formatOnSave": true, 67 | "editor.codeActionsOnSave": { 68 | "source.fixAll.eslint": "explicit" 69 | }, 70 | }, 71 | } --------------------------------------------------------------------------------