diff --git a/cypress/e2e/weather-bar.cy.ts b/cypress/e2e/weather-bar.cy.ts index d5bd9eab..3e1f7274 100644 --- a/cypress/e2e/weather-bar.cy.ts +++ b/cypress/e2e/weather-bar.cy.ts @@ -292,6 +292,14 @@ describe('Weather bar', () => { 'WNW', 'WNW' ]; + const expectedWindBearings = [ + 253, + 278, + 293, + 359, + 285, + 283 + ]; it('shows wind speed/direction if specified in config', () => { cy.configure({ @@ -332,6 +340,21 @@ describe('Weather bar', () => { }); }); + it('shows wind barbs if specified in config', () => { + cy.configure({ + show_wind: 'barb' + }); + cy.get('weather-bar') + .shadow() + .find('div.axes > div.bar-block div.wind span') + .should('have.length', 6) + .each((el, i) => { + cy.wrap(el).should('have.attr', 'title', `${expectedWindSpeeds[i]} mph ${expectedWindDirections[i]}`) + .find('svg').should('exist') + .and('have.attr', 'style', `transform:rotate(${expectedWindBearings[i]}deg);`); + }); + }); + it('does not show precipitation by default', () => { cy.get('weather-bar') .shadow() diff --git a/rollup.config.js b/rollup.config.js index 45205d56..97d02f4c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -14,6 +14,7 @@ import { ignoreSelectFiles } from './elements/ignore/select'; import { ignoreSwitchFiles } from './elements/ignore/switch'; const dev = process.env.ROLLUP_WATCH; +if (dev) console.log('Development build'); const serveopts = { contentBase: ['./dist'], @@ -42,6 +43,7 @@ const plugins = [ json(), babel({ exclude: 'node_modules/**', + babelHelpers: 'bundled' }), dev && serve(serveopts), !dev && terser(), diff --git a/src/editor.ts b/src/editor.ts index 2aca2882..04ffbb2b 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -157,6 +157,7 @@ export class HourlyWeatherCardEditor extends ScopedRegistryHost(LitElement) impl ${localize('editor.both')} ${localize('editor.speed_only')} ${localize('editor.direction_only')} + ${localize('editor.barb')} new Date(f.datetime).getDate()); const uniqueDates = new Set(dates); diff --git a/src/lib/svg-wind-barbs/LICENSE b/src/lib/svg-wind-barbs/LICENSE new file mode 100644 index 00000000..d6be4690 --- /dev/null +++ b/src/lib/svg-wind-barbs/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2021-present, Qulle +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/lib/svg-wind-barbs/MODIFICATIONS b/src/lib/svg-wind-barbs/MODIFICATIONS new file mode 100644 index 00000000..e38a1fc0 --- /dev/null +++ b/src/lib/svg-wind-barbs/MODIFICATIONS @@ -0,0 +1,7 @@ +This library code is based on the open-source work at https://github.com/qulle/svg-wind-barbs licensed under the BSD +2-clause license. It has been modified to fit the needs of this project. Specifically: + +- The source was refactored into Typescript +- The exported function now returns a bare SVG `path` rather than a fully-formed SVG with `style` tag +- Fills and strokes are fully handled in CSS classes rather than hard-coded +- SVG strings are returned as Lit HTML template results rather than raw strings diff --git a/src/lib/svg-wind-barbs/index.ts b/src/lib/svg-wind-barbs/index.ts new file mode 100644 index 00000000..b99c1c5e --- /dev/null +++ b/src/lib/svg-wind-barbs/index.ts @@ -0,0 +1,91 @@ +import { svg, TemplateResult } from "lit"; + +const WIND_BARB_0 = svg``; +const WIND_BARB_2 = svg``; +const WIND_BARB_5 = svg``; +const WIND_BARB_10 = svg``; +const WIND_BARB_15 = svg``; +const WIND_BARB_20 = svg``; +const WIND_BARB_25 = svg``; +const WIND_BARB_30 = svg``; +const WIND_BARB_35 = svg``; +const WIND_BARB_40 = svg``; +const WIND_BARB_45 = svg``; +const WIND_BARB_50 = svg``; +const WIND_BARB_55 = svg``; +const WIND_BARB_60 = svg``; +const WIND_BARB_65 = svg``; +const WIND_BARB_70 = svg``; +const WIND_BARB_75 = svg``; +const WIND_BARB_80 = svg``; +const WIND_BARB_85 = svg``; +const WIND_BARB_90 = svg``; +const WIND_BARB_95 = svg``; +const WIND_BARB_100 = svg``; +const WIND_BARB_105 = svg``; +const WIND_BARB_110 = svg``; +const WIND_BARB_115 = svg``; +const WIND_BARB_120 = svg``; +const WIND_BARB_125 = svg``; +const WIND_BARB_130 = svg``; +const WIND_BARB_135 = svg``; +const WIND_BARB_140 = svg``; +const WIND_BARB_145 = svg``; +const WIND_BARB_150 = svg``; +const WIND_BARB_155 = svg``; +const WIND_BARB_160 = svg``; +const WIND_BARB_165 = svg``; +const WIND_BARB_170 = svg``; +const WIND_BARB_175 = svg``; +const WIND_BARB_180 = svg``; +const WIND_BARB_185 = svg``; +const WIND_BARB_190 = svg``; + +/** + * Get SVG barb for the specified wind speed + * @param windSpeed Wind speed in meters per second + * @returns Lit template of SVG path for barb + */ +export function getWindBarbSVG(windSpeed: number): TemplateResult<2> { + if (windSpeed >= 0.0 && windSpeed < 1.0) return WIND_BARB_0; + else if (windSpeed >= 1.0 && windSpeed < 2.5) return WIND_BARB_2; + else if (windSpeed >= 2.5 && windSpeed < 5.0) return WIND_BARB_5; + else if (windSpeed >= 5.0 && windSpeed < 7.5) return WIND_BARB_10; + else if (windSpeed >= 7.5 && windSpeed < 10.0) return WIND_BARB_15; + else if (windSpeed >= 10.0 && windSpeed < 12.5) return WIND_BARB_20; + else if (windSpeed >= 12.5 && windSpeed < 15.0) return WIND_BARB_25; + else if (windSpeed >= 15.0 && windSpeed < 17.5) return WIND_BARB_30; + else if (windSpeed >= 17.5 && windSpeed < 20.0) return WIND_BARB_35; + else if (windSpeed >= 20.0 && windSpeed < 22.5) return WIND_BARB_40; + else if (windSpeed >= 22.5 && windSpeed < 25.0) return WIND_BARB_45; + else if (windSpeed >= 25.0 && windSpeed < 27.5) return WIND_BARB_50; + else if (windSpeed >= 27.5 && windSpeed < 30.0) return WIND_BARB_55; + else if (windSpeed >= 30.0 && windSpeed < 32.5) return WIND_BARB_60; + else if (windSpeed >= 32.5 && windSpeed < 35.0) return WIND_BARB_65; + else if (windSpeed >= 35.0 && windSpeed < 37.5) return WIND_BARB_70; + else if (windSpeed >= 37.5 && windSpeed < 40.0) return WIND_BARB_75; + else if (windSpeed >= 40.0 && windSpeed < 42.5) return WIND_BARB_80; + else if (windSpeed >= 42.5 && windSpeed < 45.0) return WIND_BARB_85; + else if (windSpeed >= 45.0 && windSpeed < 47.5) return WIND_BARB_90; + else if (windSpeed >= 47.5 && windSpeed < 50.0) return WIND_BARB_95; + else if (windSpeed >= 50.0 && windSpeed < 52.5) return WIND_BARB_100; + else if (windSpeed >= 52.5 && windSpeed < 55.0) return WIND_BARB_105; + else if (windSpeed >= 55.0 && windSpeed < 57.5) return WIND_BARB_110; + else if (windSpeed >= 57.5 && windSpeed < 60.0) return WIND_BARB_115; + else if (windSpeed >= 60.0 && windSpeed < 62.5) return WIND_BARB_120; + else if (windSpeed >= 62.5 && windSpeed < 65.0) return WIND_BARB_125; + else if (windSpeed >= 65.0 && windSpeed < 67.5) return WIND_BARB_130; + else if (windSpeed >= 67.5 && windSpeed < 70.0) return WIND_BARB_135; + else if (windSpeed >= 70.0 && windSpeed < 72.5) return WIND_BARB_140; + else if (windSpeed >= 72.5 && windSpeed < 75.0) return WIND_BARB_145; + else if (windSpeed >= 75.0 && windSpeed < 77.5) return WIND_BARB_150; + else if (windSpeed >= 77.5 && windSpeed < 80.0) return WIND_BARB_155; + else if (windSpeed >= 80.0 && windSpeed < 82.5) return WIND_BARB_160; + else if (windSpeed >= 82.5 && windSpeed < 85.0) return WIND_BARB_165; + else if (windSpeed >= 85.0 && windSpeed < 87.5) return WIND_BARB_170; + else if (windSpeed >= 87.5 && windSpeed < 90.0) return WIND_BARB_175; + else if (windSpeed >= 90.0 && windSpeed < 92.5) return WIND_BARB_180; + else if (windSpeed >= 92.5 && windSpeed < 95.0) return WIND_BARB_185; + else if (windSpeed >= 95.0 && windSpeed < 97.5) return WIND_BARB_190; + else return WIND_BARB_0; +} diff --git a/src/localize/languages/de.json b/src/localize/languages/de.json index 8639f099..3c9f184c 100644 --- a/src/localize/languages/de.json +++ b/src/localize/languages/de.json @@ -18,7 +18,8 @@ "neither": "Weder", "both": "Beide", "speed_only": "Nur Geschwindigkeit", - "direction_only": "Nur Richtung" + "direction_only": "Nur Richtung", + "barb": "Als Windbarbe" }, "errors": { "missing_entity": "Keine Wetter-Entität festgelegt", diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index ad3851e6..f53ba222 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -18,7 +18,8 @@ "neither": "Neither", "both": "Both", "speed_only": "Speed only", - "direction_only": "Direction only" + "direction_only": "Direction only", + "barb": "As wind barb" }, "errors": { "missing_entity": "entity is missing in configuration", diff --git a/src/localize/languages/es.json b/src/localize/languages/es.json index 1cc3c338..b4651491 100644 --- a/src/localize/languages/es.json +++ b/src/localize/languages/es.json @@ -18,7 +18,8 @@ "neither": "Ninguno de los dos", "both": "Ambas cosas", "speed_only": "Solo velocidad", - "direction_only": "Solo dirección" + "direction_only": "Solo dirección", + "barb": "Como púa de viento" }, "errors": { "missing_entity": "falta la entidad en la configuración", diff --git a/src/localize/languages/fr.json b/src/localize/languages/fr.json index 57d887e8..3eeb4286 100644 --- a/src/localize/languages/fr.json +++ b/src/localize/languages/fr.json @@ -18,7 +18,8 @@ "neither": "Ni", "both": "Tous les deux", "speed_only": "Vitesse uniquement", - "direction_only": "Sens uniquement" + "direction_only": "Sens uniquement", + "barb": "Comme barbillon de vent" }, "errors": { "missing_entity": "Entité manquante dans la configuration", diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json index 61dbca98..f0eca94f 100644 --- a/src/localize/languages/it.json +++ b/src/localize/languages/it.json @@ -18,7 +18,8 @@ "neither": "Né", "both": "Tutti e due", "speed_only": "Solo velocità", - "direction_only": "Solo direzione" + "direction_only": "Solo direzione", + "barb": "Come una punta di vento" }, "errors": { "missing_entity": "entità mancante nella configurazione", @@ -64,4 +65,4 @@ "nw": "NO", "nnw": "NNO" } -} +} \ No newline at end of file diff --git a/src/localize/languages/nb.json b/src/localize/languages/nb.json index 29380e74..b0b6e625 100644 --- a/src/localize/languages/nb.json +++ b/src/localize/languages/nb.json @@ -18,7 +18,8 @@ "neither": "Ingen", "both": "Både", "speed_only": "Kun hastighet", - "direction_only": "Kun retning" + "direction_only": "Kun retning", + "barb": "Som vindmothak" }, "errors": { "missing_entity": "entity mangler i konfigurasjonen", diff --git a/src/localize/languages/nl.json b/src/localize/languages/nl.json index 335fb767..aa721f7d 100644 --- a/src/localize/languages/nl.json +++ b/src/localize/languages/nl.json @@ -18,7 +18,8 @@ "neither": "Geen van beide", "both": "Beide", "speed_only": "Alleen snelheid", - "direction_only": "Alleen richting" + "direction_only": "Alleen richting", + "barb": "Als windhaak" }, "errors": { "missing_entity": "entity ontbreekt in configuratie", @@ -64,4 +65,4 @@ "nw": "NW", "nnw": "NNW" } -} +} \ No newline at end of file diff --git a/src/localize/languages/pl.json b/src/localize/languages/pl.json index 69b4aa06..a63ba923 100644 --- a/src/localize/languages/pl.json +++ b/src/localize/languages/pl.json @@ -18,7 +18,8 @@ "neither": "Żaden", "both": "Obie", "speed_only": "Tylko prędkość", - "direction_only": "Tylko kierunek" + "direction_only": "Tylko kierunek", + "barb": "Jak kolce wiatru" }, "errors": { "missing_entity": "encja nie istnieje", diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json index 06d29084..edc36da6 100644 --- a/src/localize/languages/pt-BR.json +++ b/src/localize/languages/pt-BR.json @@ -18,7 +18,8 @@ "neither": "Nenhum", "both": "Ambos", "speed_only": "Apenas velocidade", - "direction_only": "Apenas direção" + "direction_only": "Apenas direção", + "barb": "Como farpa de vento" }, "errors": { "missing_entity": "entidade está faltando na configuração", diff --git a/src/localize/languages/pt.json b/src/localize/languages/pt.json index b6db91b0..33b8e8cf 100644 --- a/src/localize/languages/pt.json +++ b/src/localize/languages/pt.json @@ -18,7 +18,8 @@ "neither": "Nenhum", "both": "Ambos", "speed_only": "Apenas velocidade", - "direction_only": "Apenas direção" + "direction_only": "Apenas direção", + "barb": "Como farpa de vento" }, "errors": { "missing_entity": "A entidade não existe na configuração", @@ -64,4 +65,4 @@ "nw": "NO", "nnw": "NNO" } -} +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts index e6f082c6..f8e7fc36 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,7 +7,7 @@ declare global { } } -export type WindType = 'true' | 'false' | 'speed' | 'direction'; +export type WindType = 'true' | 'false' | 'speed' | 'direction' | 'barb'; export interface HourlyWeatherCardConfig extends LovelaceCardConfig { type: string; @@ -21,7 +21,7 @@ export interface HourlyWeatherCardConfig extends LovelaceCardConfig { colors?: ColorConfig; hide_hours?: boolean; hide_temperatures?: boolean; - show_wind?: WindType; // 'true' | 'false' | 'speed' | 'direction' + show_wind?: WindType; // 'true' | 'false' | 'speed' | 'direction' | 'barb' show_precipitation_amounts?: boolean; label_spacing?: string; // number test_gui?: boolean; @@ -74,7 +74,9 @@ export interface SegmentTemperature { export interface SegmentWind { hour: string, windSpeed: string, - windDirection: string + windSpeedRawMS: number, + windDirection: string, + windDirectionRaw: number } export interface SegmentPrecipitation { diff --git a/src/weather-bar.ts b/src/weather-bar.ts index 8a19bfc4..7b7cdf6f 100644 --- a/src/weather-bar.ts +++ b/src/weather-bar.ts @@ -3,6 +3,7 @@ import { property } from "lit/decorators.js"; import { StyleInfo, styleMap } from 'lit/directives/style-map.js'; import tippy, { Instance } from 'tippy.js'; import { LABELS, ICONS } from "./conditions"; +import { getWindBarbSVG } from "./lib/svg-wind-barbs"; import type { ColorMap, ConditionSpan, SegmentTemperature, SegmentWind, SegmentPrecipitation, WindType } from "./types"; const tippyStyles: string = process.env.TIPPY_CSS || ''; @@ -71,14 +72,18 @@ export class WeatherBar extends LitElement { const hideTemperature = this.hide_temperatures || skipLabel; const showWindSpeed = (this.show_wind === 'true' || this.show_wind === 'speed') && !skipLabel; const showWindDirection = (this.show_wind === 'true' || this.show_wind === 'direction') && !skipLabel; + const showWindBarb = this.show_wind === 'barb' && !skipLabel; const showPrecipitationAmounts = this.show_precipitation_amounts && !skipLabel; const { hour, temperature } = this.temperatures[i]; - const { windSpeed, windDirection } = this.wind[i]; + const { windSpeed, windSpeedRawMS, windDirection, windDirectionRaw } = this.wind[i]; const wind: TemplateResult[] = []; if (showWindSpeed) wind.push(html`${windSpeed}`); if (showWindSpeed && showWindDirection) wind.push(html`
`); if (showWindDirection) wind.push(html`${windDirection}`); + if (showWindBarb) wind.push(html` + ${this.getWindBarb(windSpeedRawMS, windDirectionRaw)} + `); const { precipitationAmount } = this.precipitation[i]; barBlocks.push(html` @@ -133,6 +138,15 @@ export class WeatherBar extends LitElement { `; } + private getWindBarb(speed: number, direction: number): TemplateResult { + const svgStyles = { + transform: `rotate(${direction}deg)` + }; + return html` + ${getWindBarbSVG(speed)} + `; + } + static styles = [unsafeCSS(tippyStyles), css` .main { --color-clear-night: #111; @@ -275,5 +289,22 @@ export class WeatherBar extends LitElement { line-height: 1.1rem; padding-top: 0.1rem; } + .barb { + transform-box: fill-box; + transform-origin: center; + height: 3rem; + } + .svg-wb, .svg-wb-fill { + fill: var(--primary-text-color, black); + } + .svg-wb, .svg-wb-stroke { + stroke: var(--primary-text-color, black); + } + .svg-wb { + stroke-width: 3; + stroke-linecap: round; + stroke-linejoin: round; + stroke-miterlimit: 10; + } `]; }