@@ -1209,7 +1273,7 @@ export class Flightradar24CardEditor extends HTMLElement {
Custom Image Marker
Use a PNG image as aircraft marker instead of the default triangle. Image should have a transparent background.
${(() => {
- const marker = radar['aircraft-marker']?.default || {};
+ const marker: AircraftMarkerEntry = radar['aircraft-marker']?.default || ({} as AircraftMarkerEntry);
return `
Image URL:
@@ -1247,24 +1311,39 @@ export class Flightradar24CardEditor extends HTMLElement {
None
System (auto dark/light)
- Black & White (requires API key)
- Light
- Color
- Dark
- Voyager
- Satellite
- Topographic
- Outlines (requires API key)
+ ${this._backgroundMapOptionsHtml(radar.background_map || 'none')}
- ${this._mapTypeRequiresApiKey(radar.background_map) ? `
-
- ` : ''}
+ ${
+ radar.background_map === 'system'
+ ? `
+
Pick one map for light themes and one for dark themes. Each map can have its own API key.
+
+ Light Theme Map:
+
+ ${this._themeMapOptionsHtml(radar.background_map_light || 'color')}
+
+
+ ${this._themeApiKeyRowHtml('light', 'Light Map API Key', radar.background_map_light || 'color', radar.background_map_light_api_key)}
+
+ Dark Theme Map:
+
+ ${this._themeMapOptionsHtml(radar.background_map_dark || 'dark')}
+
+
+ ${this._themeApiKeyRowHtml('dark', 'Dark Map API Key', radar.background_map_dark || 'dark', radar.background_map_dark_api_key)}
+ `
+ : this._mapTypeRequiresApiKey(radar.background_map) ? `
+
+ ` : ''
+ }
Map Opacity:
+
+ Position:
+
+ Below (default)
+ Left
+ Right
+
+ Place the flight list to the side of the radar/map on wide cards. If the card is too narrow to fit the flight list side by side, it automatically falls back to the "below" layout.
+
No Flights Message:
{
+ const radar = this._config.radar || {};
+ this._config = { ...this._config, radar: { ...radar, view: (e.target as HTMLSelectElement).value as 'radar' | 'map' } };
+ this._emitConfigChanged();
+ this._render();
+ });
+ }
+
+ // Radar rings / lines
+ const radarRings = root.getElementById('radar-rings') as HTMLInputElement;
+ if (radarRings) {
+ radarRings.addEventListener('change', (e) => {
+ const radar = this._config.radar || {};
+ this._config = { ...this._config, radar: { ...radar, rings: (e.target as HTMLInputElement).checked } };
+ this._emitConfigChanged();
+ });
+ }
+
// Radar colors (new properties)
['background-color', 'aircraft-color', 'aircraft-selected-color', 'radar-grid-color', 'local-features-color'].forEach(prop => {
// Properties already prefixed with 'radar-' don't need the extra prefix
@@ -2128,6 +2237,34 @@ export class Flightradar24CardEditor extends HTMLElement {
});
}
+ const bindThemeMapSelect = (id: string, field: 'background_map_light' | 'background_map_dark') => {
+ const el = root.getElementById(id) as HTMLSelectElement;
+ if (el) {
+ el.addEventListener('change', (e) => {
+ const radar = this._config.radar || {};
+ this._config = { ...this._config, radar: { ...radar, [field]: (e.target as HTMLSelectElement).value as any } };
+ this._emitConfigChanged();
+ this._render();
+ });
+ }
+ };
+ bindThemeMapSelect('radar-background-map-light', 'background_map_light');
+ bindThemeMapSelect('radar-background-map-dark', 'background_map_dark');
+
+ const bindThemeMapKey = (id: string, field: 'background_map_light_api_key' | 'background_map_dark_api_key') => {
+ const el = root.getElementById(id) as HTMLInputElement;
+ if (el) {
+ el.addEventListener('input', (e) => {
+ const radar = this._config.radar || {};
+ const value = (e.target as HTMLInputElement).value;
+ this._config = { ...this._config, radar: { ...radar, [field]: value || undefined } };
+ this._emitConfigChanged();
+ });
+ }
+ };
+ bindThemeMapKey('radar-background-map-light-api-key', 'background_map_light_api_key');
+ bindThemeMapKey('radar-background-map-dark-api-key', 'background_map_dark_api_key');
+
const radarBackgroundMapApiKey = root.getElementById('radar-background-map-api-key') as HTMLInputElement;
if (radarBackgroundMapApiKey) {
radarBackgroundMapApiKey.addEventListener('input', (e) => {
@@ -2173,6 +2310,19 @@ export class Flightradar24CardEditor extends HTMLElement {
});
}
+ const listPosition = root.getElementById('list-position') as HTMLSelectElement;
+ if (listPosition) {
+ listPosition.addEventListener('change', (e) => {
+ const list = this._config.list || {};
+ const value = (e.target as HTMLSelectElement).value as 'below' | 'left' | 'right';
+ // 'below' is the default, so only persist it when it is not the default
+ const position = value === 'below' ? undefined : value;
+ this._config = { ...this._config, list: { ...list, position } };
+ this._emitConfigChanged();
+ this._render();
+ });
+ }
+
const noFlightsMessage = root.getElementById('no-flights-message') as HTMLInputElement;
if (noFlightsMessage) {
noFlightsMessage.addEventListener('input', (e) => {
@@ -3414,14 +3564,3 @@ export class Flightradar24CardEditor extends HTMLElement {
}
customElements.define('flightradar24-radar-card-editor', Flightradar24CardEditor);
-
-// Backwards-compatible alias for the original editor element. The Flightradar24
-// integration ships its own card under 'flightradar24-card-editor', so only
-// register this alias when that name is not already taken by another card.
-if (!customElements.get('flightradar24-card-editor')) {
- try {
- customElements.define('flightradar24-card-editor', class extends Flightradar24CardEditor {});
- } catch (e) {
- console.error('[FR24Card] Could not register flightradar24-card-editor alias:', e);
- }
-}
diff --git a/flightradar24-card-state.ts b/flightradar24-card-state.ts
index 2c75a11..5d49ca0 100644
--- a/flightradar24-card-state.ts
+++ b/flightradar24-card-state.ts
@@ -8,7 +8,8 @@ import type { Hass } from './types/hass';
import type { CardConfig, RadarConfig, ListConfig, UnitsConfig, SortCriterion } from './types/config';
import type { CardState, Dimensions, FlightsContext, DomRefs, MainCard, LeafletMap } from './types/cardState';
-type BackgroundMapType = 'none' | 'system' | 'bw' | 'color' | 'dark' | 'outlines';
+type BackgroundMapType = 'none' | 'system' | 'bw' | 'light' | 'color' | 'dark' | 'voyager' | 'satellite' | 'topo' | 'outlines';
+type ThemeMapType = Exclude
;
interface Defaults {
flights_entity: string;
@@ -18,9 +19,14 @@ interface Defaults {
units: UnitsConfig;
radar: {
range: number;
+ view: 'radar' | 'map';
background_map: BackgroundMapType;
background_map_opacity: number;
background_map_api_key: string;
+ background_map_light: ThemeMapType;
+ background_map_dark: ThemeMapType;
+ background_map_light_api_key: string;
+ background_map_dark_api_key: string;
};
sort: SortCriterion[];
templates: Record;
@@ -31,13 +37,18 @@ const defaults: Defaults = {
flights_entity: 'sensor.flightradar24_current_in_area',
projection_interval: 5,
no_flights_message: 'No flights are currently visible. Please check back later.',
- list: { hide: false, showListStatus: true },
+ list: { hide: false, showListStatus: true, position: 'below' },
units: unitsConfig,
radar: {
range: unitsConfig.distance === 'km' ? 35 : 25,
+ view: 'radar',
background_map: 'none',
background_map_opacity: 0,
- background_map_api_key: ''
+ background_map_api_key: '',
+ background_map_light: 'color',
+ background_map_dark: 'dark',
+ background_map_light_api_key: '',
+ background_map_dark_api_key: ''
},
sort: sortConfig,
templates: templateConfig,
@@ -59,6 +70,7 @@ export class Flightradar24CardState implements CardState {
selectedFlights: string[];
renderDynamicOnRangeChange: boolean;
_leafletMap: LeafletMap | null;
+ _currentMapConfig?: { type: string; apiKey?: string };
sortFn: (a: Flight, b: Flight) => number;
renderDynamicFn?: () => void;
dom?: DomRefs;
@@ -95,9 +107,14 @@ export class Flightradar24CardState implements CardState {
this.units = { ...defaults.units, ...config.units };
this.radar = {
range: this.units.distance === 'km' ? defaults.radar.range : 25,
+ view: config.radar?.view ?? defaults.radar.view,
background_map: config.radar?.background_map ?? defaults.radar.background_map,
background_map_opacity: config.radar?.background_map_opacity ?? defaults.radar.background_map_opacity,
background_map_api_key: config.radar?.background_map_api_key ?? defaults.radar.background_map_api_key,
+ background_map_light: config.radar?.background_map_light ?? defaults.radar.background_map_light,
+ background_map_dark: config.radar?.background_map_dark ?? defaults.radar.background_map_dark,
+ background_map_light_api_key: config.radar?.background_map_light_api_key ?? defaults.radar.background_map_light_api_key,
+ background_map_dark_api_key: config.radar?.background_map_dark_api_key ?? defaults.radar.background_map_dark_api_key,
...config.radar
};
this.radar.initialRange = this.radar.range;
diff --git a/flightradar24-card.ts b/flightradar24-card.ts
index dfd63f5..f151023 100644
--- a/flightradar24-card.ts
+++ b/flightradar24-card.ts
@@ -38,6 +38,7 @@ class Flightradar24Card extends HTMLElement implements MainCard {
_visibilityChangeHandler: (() => void) | null = null;
cardState: Flightradar24CardState;
shadowRoot!: ShadowRoot;
+ _layoutOptions: Record | null = null;
constructor() {
super();
@@ -90,6 +91,28 @@ class Flightradar24Card extends HTMLElement implements MainCard {
};
}
+ /**
+ * Modern (sections/masonry) view layout support.
+ * Home Assistant calls getGridSize() to decide how many grid cells a card
+ * occupies in the sections view. Without it the card is pinned to 1x1 and
+ * can never grow into a wider section.
+ */
+ getGridSize(): number {
+ return 1;
+ }
+
+ getLayoutOptions(): unknown {
+ return this._layoutOptions ?? { columns: 1, rows: 1 };
+ }
+
+ setLayoutOptions(layout: unknown): void {
+ this._layoutOptions = layout as Record;
+ }
+
+ cardSize(): number {
+ return 2;
+ }
+
set hass(hass: Hass) {
try {
this.cardState.hass = hass;
@@ -605,17 +628,6 @@ class Flightradar24Card extends HTMLElement implements MainCard {
customElements.define('flightradar24-radar-card', Flightradar24Card);
-// Backwards-compatible alias for the original card type. The Flightradar24
-// integration ships its own card under 'flightradar24-card', so only register
-// this alias when that name is not already taken by another card.
-if (!customElements.get('flightradar24-card')) {
- try {
- customElements.define('flightradar24-card', class extends Flightradar24Card {});
- } catch (e) {
- console.error('[FR24Card] Could not register flightradar24-card alias:', e);
- }
-}
-
if (typeof window !== 'undefined') {
(window as any).customCards = (window as any).customCards || [];
(window as any).customCards.push({
diff --git a/home-assistant-flightradar24-card.js b/home-assistant-flightradar24-card.js
index c03cfb1..af3c537 100644
--- a/home-assistant-flightradar24-card.js
+++ b/home-assistant-flightradar24-card.js
@@ -1,19 +1,65 @@
-(function(){var tt=Object.defineProperty,U=(t,e)=>()=>(t&&(e=t(t=0)),e),gt=(t,e)=>{let a={};for(var i in t)tt(a,i,{get:t[i],enumerable:!0});return e||tt(a,Symbol.toStringTag,{value:"Module"}),a};function mt(t,e){const a=e.querySelector("style[data-fr24-style]");a&&a.remove();const i=t.radar,o=i["background-color"]||i["primary-color"]||"var(--dark-primary-color)",r=i["aircraft-color"]||i["accent-color"]||"var(--accent-color)",n=i["aircraft-selected-color"]||i["aircraft-color"]||i["accent-color"]||"var(--accent-color)",d=i["radar-grid-color"]||i["feature-color"]||"var(--secondary-text-color)",s=i["local-features-color"]||i["feature-color"]||i["radar-grid-color"]||"var(--secondary-text-color)",f=i["callsign-label-color"]||"var(--primary-background-color)",b=i["background-opacity"]!==void 0?Math.max(0,Math.min(1,i["background-opacity"])):.05,_=i.radar_size!==void 0?Math.max(30,Math.min(90,i.radar_size)):70,v=(100-_)/2,y=t.config.scale!==void 0?Math.max(.5,Math.min(3,t.config.scale)):1,C=document.createElement("style");C.setAttribute("data-fr24-style","1"),C.textContent=`
+var ft = Object.defineProperty, J = (t, e) => () => (t && (e = t(t = 0)), e), It = (t, e) => {
+ let a = {};
+ for (var i in t)
+ ft(a, i, {
+ get: t[i],
+ enumerable: !0
+ });
+ return e || ft(a, Symbol.toStringTag, { value: "Module" }), a;
+};
+function St(t, e) {
+ const a = e.querySelector("style[data-fr24-style]");
+ a && a.remove();
+ const i = t.radar, o = i["background-color"] || i["primary-color"] || "var(--dark-primary-color)", r = i["aircraft-color"] || i["accent-color"] || "var(--accent-color)", n = i["aircraft-selected-color"] || i["aircraft-color"] || i["accent-color"] || "var(--accent-color)", d = i["radar-grid-color"] || i["feature-color"] || "var(--secondary-text-color)", l = i["local-features-color"] || i["feature-color"] || i["radar-grid-color"] || "var(--secondary-text-color)", u = i["callsign-label-color"] || "var(--primary-background-color)", _ = i["background-opacity"] !== void 0 ? Math.max(0, Math.min(1, i["background-opacity"])) : 0.05, m = i.radar_size !== void 0 ? Math.max(30, Math.min(90, i.radar_size)) : 70, g = (100 - m) / 2, y = t.config.scale !== void 0 ? Math.max(0.5, Math.min(3, t.config.scale)) : 1, k = document.createElement("style");
+ k.setAttribute("data-fr24-style", "1"), k.textContent = `
:host {
--radar-background-color: ${o};
--radar-aircraft-color: ${r};
--radar-aircraft-selected-color: ${n};
--radar-grid-color: ${d};
- --radar-local-features-color: ${s};
- --radar-callsign-label-color: ${f};
+ --radar-local-features-color: ${l};
+ --radar-callsign-label-color: ${u};
}
#flights-card {
padding: 16px;
transform: scale(${y});
transform-origin: top center;
+ container-type: inline-size;
+ }
+ #layout-root {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ }
+ /*
+ Responsive list position: when the list is configured to sit on the
+ left or right of the radar/map, only keep it side by side when the card
+ is wide enough. Narrower cards fall back to the stacked "below" layout.
+ */
+ @container (min-width: 560px) {
+ #layout-root.layout-left,
+ #layout-root.layout-right {
+ flex-direction: row;
+ align-items: stretch;
+ gap: 16px;
+ }
+ #layout-root.layout-left #radar-container,
+ #layout-root.layout-right #radar-container {
+ flex: 1 1 60%;
+ min-width: 0;
+ }
+ #layout-root.layout-left #flights,
+ #layout-root.layout-right #flights {
+ flex: 1 1 40%;
+ min-width: 0;
+ }
+ #layout-root.layout-left #flights {
+ order: -1;
+ }
}
#flights {
padding: 0px;
+ min-width: 0;
}
#flights .flight {
margin-top: 16px;
@@ -57,16 +103,18 @@
#radar-container {
display: flex;
justify-content: space-between;
+ position: relative;
+ min-width: 0;
}
#radar-overlay {
position: absolute;
- width: ${_}%;
- left: ${v}%;
- padding: 0 0 ${_}% 0;
+ width: ${m}%;
+ left: ${g}%;
+ padding: 0 0 ${m}% 0;
margin-bottom: 5%;
z-index: 1;
opacity: 0;
- pointer-events: auto;
+ pointer-events: none;
border-radius: 50%;
overflow: hidden;
}
@@ -86,6 +134,7 @@
font-size: 0.9em;
padding: 0;
margin: 0 15px;
+ z-index: 10;
}
.toggle {
display: flex;
@@ -98,14 +147,21 @@
}
#radar {
position: relative;
- width: ${_}%;
+ width: ${m}%;
height: 0;
- margin: 0 ${v}%;
- padding-bottom: ${_}%;
+ margin: 0 ${g}%;
+ padding-bottom: ${m}%;
margin-bottom: 5%;
border-radius: 50%;
overflow: hidden;
}
+ /* Square map view: full-bleed square map instead of the circular radar screen */
+ #layout-root.view-map #radar {
+ width: 100%;
+ margin: 0;
+ padding-bottom: 100%;
+ border-radius: 0;
+ }
#radar-screen {
position: absolute;
width: 100%;
@@ -120,7 +176,7 @@
margin: 0;
padding: 0%;
background-color: var(--radar-background-color);
- opacity: ${b};
+ opacity: ${_};
}
#tracker {
position: absolute;
@@ -156,6 +212,10 @@
width: 8px;
height: 16px;
}
+ .plane.plane-custom {
+ width: 12px;
+ height: 14px;
+ }
.plane .arrow {
position: absolute;
width: 0;
@@ -184,6 +244,19 @@
.plane.selected .arrow {
border-bottom-color: var(--radar-aircraft-selected-color);
}
+ .custom-marker {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ translate: -50% -50%;
+ }
+ .custom-marker img,
+ .custom-marker canvas {
+ display: block;
+ width: 12px;
+ height: auto;
+ }
+
.callsign-label {
position: absolute;
background-color: var(--radar-callsign-label-color);
@@ -240,9 +313,1411 @@
background-color: var(--radar-local-features-color);
opacity: 0.35;
}
- `,e.appendChild(C)}function _t(t,e){if(!e)return;e.innerHTML="";const a=t.config.toggles||{},i=!!window.customElements&&!!customElements.get("ha-switch");Object.keys(a).forEach(o=>{const r=a[o],n=document.createElement("div");n.className="toggle";const d=document.createElement("label");d.textContent=r.label||o,n.appendChild(d);let s;i?s=document.createElement("ha-switch"):(s=document.createElement("input"),s.type="checkbox"),s.checked=r.default===!0,s.addEventListener("change",()=>{t.setToggleValue&&t.setToggleValue(o,s.checked)}),n.appendChild(s),e.appendChild(n)})}function O(t){return t*(Math.PI/180)}function W(t){return t*(180/Math.PI)}function z(t,e,a,i,o="km"){const n=O(a-t),d=O(i-e),s=Math.sin(n/2)*Math.sin(n/2)+Math.cos(O(t))*Math.cos(O(a))*Math.sin(d/2)*Math.sin(d/2),f=2*Math.atan2(Math.sqrt(s),Math.sqrt(1-s));return o==="km"?6371*f:6371*f/1.60934}function q(t,e,a,i){const o=O(i-e),r=Math.sin(o)*Math.cos(O(a)),n=Math.cos(O(t))*Math.sin(O(a))-Math.sin(O(t))*Math.cos(O(a))*Math.cos(o);return(W(Math.atan2(r,n))+360)%360}function G(t,e,a,i){const r=O(a),n=O(t),d=O(e),s=i/6371,f=Math.asin(Math.sin(n)*Math.cos(s)+Math.cos(n)*Math.sin(s)*Math.cos(r)),b=d+Math.atan2(Math.sin(r)*Math.sin(s)*Math.cos(n),Math.cos(s)-Math.sin(n)*Math.sin(f));return{lat:W(f),lon:W(b)}}function vt(t,e,a,i,o){const r=q(a,i,t,e),n=Math.abs((o-r+360)%360);return G(a,i,o,z(t,e,a,i)*Math.cos(O(n)))}function bt(t){return["N","NE","E","SE","S","SW","W","NW"][Math.round(t/45)%8]}function et(t,e,a=60){const i=Math.abs((t-e+360)%360);return i<=a||i>=360-a}function K(t){if(!t||!t.config)return console.error("Config not set in getLocation"),{latitude:0,longitude:0};const{config:e,hass:a}=t;if(e.location_tracker&&a&&a.states&&e.location_tracker in a.states){const i=a.states[e.location_tracker].attributes;return{latitude:i.latitude,longitude:i.longitude}}else{if(e.location)return{latitude:e.location.lat,longitude:e.location.lon};if(a&&a.config)return{latitude:a.config.latitude,longitude:a.config.longitude}}return{latitude:0,longitude:0}}var yt=new Set(["bw","light","color","dark","voyager","satellite","topo","outlines","system"]);function at(t){const e=t?.radar;return!(!e||e.hide===!0||!e.background_map||!yt.has(e.background_map))}function xt(t,e,a){if(at(t)){if(window.L){a();return}if(!e.querySelector("#leaflet-css-loader")){const i=document.createElement("link");i.id="leaflet-css-loader",i.rel="stylesheet",i.href="https://unpkg.com/leaflet/dist/leaflet.css",e.appendChild(i)}if(e.querySelector("#leaflet-js-loader")){const i=setInterval(()=>{window.L&&(clearInterval(i),a())},50)}else{const i=document.createElement("script");i.id="leaflet-js-loader",i.src="https://unpkg.com/leaflet/dist/leaflet.js",i.async=!0,i.defer=!0,i.onload=a,i.onerror=()=>i.remove(),e.appendChild(i)}}}function wt(t,e){const{config:a,dimensions:i}=t;if(!at(t)){t._leafletMap&&(t._leafletMap.remove(),t._leafletMap=null);const w=e.querySelector("#radar-map-bg");w&&w.remove();return}const o=a?.radar?.background_map,r={bw:["https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png",{api_key:"?api_key=",attribution:"Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap",subdomains:[]}],light:["https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",{attribution:"© CartoDB, © OpenStreetMap contributors",subdomains:["a","b","c","d"]}],color:["https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",{attribution:"© OpenStreetMap contributors",subdomains:["a","b","c"]}],dark:["https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",{attribution:"© CartoDB, © OpenStreetMap contributors",subdomains:["a","b","c","d"]}],voyager:["https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png",{attribution:"© CartoDB, © OpenStreetMap contributors",subdomains:["a","b","c","d"]}],satellite:["https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",{attribution:"© Esri, Maxar, Earthstar Geographics",subdomains:[]}],topo:["https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",{attribution:"© OpenTopoMap, © OpenStreetMap contributors",subdomains:["a","b","c"]}],outlines:["https://tiles.stadiamaps.com/tiles/stamen_toner_lines/{z}/{x}/{y}.png",{api_key:"?api_key=",attribution:"Map tiles by Stamen Design, hosted by Stadia Maps; Data by OpenStreetMap",subdomains:[]}],system:null},n=typeof a?.radar?.background_map_opacity=="number"?Math.max(0,Math.min(1,a.radar.background_map_opacity)):1;let d=e.querySelector("#radar-map-bg");d?d.style.opacity=String(n):(d=document.createElement("div"),d.id="radar-map-bg",d.style.position="absolute",d.style.top="0",d.style.left="0",d.style.width="100%",d.style.height="100%",d.style.zIndex="0",d.style.pointerEvents="none",d.style.opacity=String(n),e.appendChild(d)),d.style.transform="",t._leafletMap&&t._leafletMap.getContainer()!==d&&(t._leafletMap.remove(),t._leafletMap=null);const s=K(t),f=Math.max(i?.range||1,1),b=t.units?.distance==="miles"?f*1.60934:f,_=s?.latitude||0,v=s?.longitude||0,y=Math.PI/180,C=111.13209-.56605*Math.cos(2*_*y)+.0012*Math.cos(4*_*y),M=111.32*Math.cos(_*y)-.094*Math.cos(3*_*y),u=b/C,h=b/M,c=[[_-u,v-h],[_+u,v+h]];let g=o;if(o==="system"){const w=window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches;let $=!1;try{$=!!(window.parent&&window.parent.document&&window.parent.document.body.classList.contains("dark"))}catch{}$||w?g="dark":g="color"}const m=r[g||"bw"]||r.bw;if(!m)return d;let[l,p]=m;const x=p&&"api_key"in p,k=a?.radar?.background_map_api_key&&a.radar.background_map_api_key.trim().length>0;if(x&&!k)return t._leafletMap&&(t._leafletMap.remove(),t._leafletMap=null),d.innerHTML='API key required for this map type. Configure in Background Map settings.
',d;if(t._leafletMap||(d.innerHTML=""),x&&k&&a?.radar?.background_map_api_key&&(l=l+p.api_key+encodeURIComponent(a.radar.background_map_api_key)),window.L){const w={type:g||"bw",apiKey:a?.radar?.background_map_api_key},$=!t._currentMapConfig||t._currentMapConfig.type!==w.type||t._currentMapConfig.apiKey!==w.apiKey;t._leafletMap?$&&(t._leafletMap.eachLayer(A=>{t._leafletMap.removeLayer(A)}),window.L.tileLayer(l,p).addTo(t._leafletMap),t._currentMapConfig=w):(t._leafletMap=window.L.map(d,{attributionControl:!1,zoomControl:!1,dragging:!1,scrollWheelZoom:!1,boxZoom:!1,doubleClickZoom:!1,keyboard:!1,touchZoom:!1,pointerEvents:!1}),window.L.tileLayer(l,p).addTo(t._leafletMap),t._currentMapConfig=w),t._leafletMap.fitBounds(c,{animate:!1,padding:[0,0]}),requestAnimationFrame(()=>{if(!t._leafletMap)return;const A=t._leafletMap.getContainer(),L=A.offsetHeight,S=A.offsetWidth;if(L===0||S===0)return;const F=window.L.point(0,L/2),E=window.L.point(S,L/2),R=t._leafletMap.containerPointToLatLng(F),P=t._leafletMap.containerPointToLatLng(E),T=z(R.lat,R.lng,P.lat,P.lng,"km")/(b*2);Math.abs(T-1)>.01?d.style.transform=`scale(${T})`:d.style.transform=""})}return d}function it(t={},e,a=[]){if(a.includes(e))return console.error("Circular template dependencies detected. "+a.join(" -> ")+" -> "+e),"";if(t["compiled_"+e])return t["compiled_"+e];let i=t[e];if(i===void 0)return console.error("Missing template reference: "+e),"";const o=/tpl\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;let r;const n={};for(;(r=o.exec(i))!==null;){const d=r[1];n[d]||(n[d]=it(t,d,[...a,e])),i=i.replace(`tpl.${d}`,"(`"+n[d]+'`).replace(/^undefined$/, "")')}return t["compiled_"+e]=i,i}function Y(t,e,a,i){const o=t.templates||{},r=t.flightsContext||{},n=t.units||{distance:"km",altitude:"ft",speed:"kts"},d=t.radar||{range:35},s=it(o,e);try{const f=new Function("flights","flight","tpl","units","radar_range","joinList",`return \`${s.replace(/\${(.*?)}/g,(b,_)=>`\${${_}}`)}\``)(r,a,{},n,Math.round(d.range),i);return f!=="undefined"?f:""}catch(f){return console.error("Error when rendering: "+s,f),""}}function J(t,e,a,i){const{defines:o={},config:r={},radar:n={range:35},selectedFlights:d=[]}=t;if(typeof e=="string"&&e.startsWith("${")&&e.endsWith("}")){const s=e.slice(2,-1);if(s==="selectedFlights")return d;if(s==="radar_range")return i&&i(!0),n.range;if(s in o)return o[s];if(r.toggles&&s in r.toggles)return r.toggles[s].default;if(a!==void 0)return a;console.error("Unresolved placeholder: "+s),console.debug("Defines",o)}return e}function X(t){const{units:e,radar:a,dom:i,dimensions:o,hass:r}=t,n=i?.radarInfoDisplay||i&&i.radarContainer?.querySelector("#radar-info");n&&(n.innerHTML=[a?.hide_range!==!0?Y(t,"radar_range",null,void 0):""].filter(m=>m).join(" "));const d=i?.radarScreen||i&&i.radarContainer?.querySelector("#radar-screen")||t.mainCard?.shadowRoot&&t.mainCard.shadowRoot.getElementById("radar-screen");if(!d)return;Array.from(d.childNodes).forEach(m=>{const l=m;l.id!=="radar-map-bg"&&l.id!=="radar-screen-background"&&d.removeChild(m)});let s=d.querySelector("#radar-screen-background");s||(s=document.createElement("div"),s.id="radar-screen-background",d.appendChild(s)),wt(t,d);const{width:f,height:b,range:_,scaleFactor:v,centerX:y,centerY:C}=o||{};if(!f||!b||!_||!v||y==null||C==null)return;const M=_*1.15,u=a?.ring_distance??10,h=Math.floor(_/u);for(let m=1;m<=h;m++){const l=m*u*v,p=document.createElement("div");p.className="ring",p.style.width=p.style.height=l*2+"px",p.style.top=Math.floor(C-l)+"px",p.style.left=Math.floor(y-l)+"px",d.appendChild(p)}for(let m=0;m<360;m+=45){const l=document.createElement("div");l.className="dotted-line",l.style.transform=`rotate(${m-90}deg)`,d.appendChild(l)}const c=K(t),g=a?.local_features;if(g&&r&&c){const m=c.latitude,l=c.longitude;g.forEach(p=>{if(!(p.max_range&&a.range&&p.max_range<=a.range)){if(p.type==="outline"&&p.points&&p.points.length>1)for(let x=0;xv&&(t.radar.range=v),t.mainCard.updateRadarRange(b*2)}function n(f){f.touches.length===2&&(a=o(f.touches),i=t.radar.range)}function d(f){if(f.touches.length===2&&a!==null&&i!==null){f.preventDefault();const b=o(f.touches),_=a/b,v=t.radar.min_range||1,y=t.radar.max_range||Math.max(100,t.radar.initialRange||35);let C=Math.round(i*_);Cy&&(C=y),t.radar.range=C,t.mainCard.updateRadarRange(0)}}function s(){a!==null&&(a=null,i=null,t.config.updateRangeFilterOnTouchEnd&&t.renderDynamicOnRangeChange&&t.mainCard.renderDynamic())}return e&&(e.addEventListener("wheel",r,{passive:!1}),e.addEventListener("touchstart",n,{passive:!0}),e.addEventListener("touchmove",d,{passive:!1}),e.addEventListener("touchend",s,{passive:!0})),()=>{e&&(e.removeEventListener("wheel",r),e.removeEventListener("touchstart",n),e.removeEventListener("touchmove",d),e.removeEventListener("touchend",s))}}function Ct(t,e){e.shadowRoot.innerHTML="";const a=document.createElement("ha-card");if(a.id="flights-card",!t.radar?.hide){const o=document.createElement("div");o.id="radar-container";const r=document.createElement("div");r.id="radar-overlay",o.appendChild(r);const n=document.createElement("div");n.id="radar-info",o.appendChild(n);const d=document.createElement("div");d.id="toggle-container",o.appendChild(d);const s=document.createElement("div");s.id="radar";const f=document.createElement("div");f.id="radar-screen",s.appendChild(f);const b=document.createElement("div");b.id="tracker",s.appendChild(b);const _=document.createElement("div");_.id="planes",s.appendChild(_),o.appendChild(s),a.appendChild(o),requestAnimationFrame(()=>{X(t),e.observeRadarResize(),nt(t,r)}),t.dom=t.dom||{},t.dom.toggleContainer=d,t.dom.planesContainer=_,t.dom.radar=s,t.dom.radarScreen=f,t.dom.radarInfoDisplay=n,t.dom.shadowRoot=e.shadowRoot,t.mainCard=e}const i=document.createElement("div");i.id="flights",t.list&&t.list.hide===!0&&(i.style.display="none"),a.appendChild(i),e.shadowRoot.appendChild(a),mt(t,e.shadowRoot),t.dom?.toggleContainer&&_t(t,t.dom.toggleContainer)}function ot(t,e){return(t.flights||[]).filter(a=>rt(t,a,e))}function rt(t,e,a){return Array.isArray(a)?a.every(i=>B(t,e,i)):B(t,e,a)}function B(t,e,a){let i=!0;if(a.type==="AND"&&a.conditions)i=a.conditions.every(o=>B(t,e,o));else if(a.type==="OR"&&a.conditions)i=a.conditions.some(o=>B(t,e,o));else if(a.type==="NOT"&&a.condition)i=!B(t,e,a.condition);else{const{field:o,defined:r,defaultValue:n,comparator:d}=a,s=J(t,a.value),f=o?e[o]:r?J(t,"${"+r+"}",n):void 0;switch(d){case"eq":i=f===s;break;case"lt":i=Number(f)Number(s);break;case"gte":i=Number(f)>=Number(s);break;case"oneOf":i=(Array.isArray(s)?s:typeof s=="string"?s.split(",").map(b=>b.trim()):[]).includes(f);break;case"containsOneOf":{const b=Array.isArray(s)?s:typeof s=="string"?s.split(",").map(_=>_.trim()):[];i=!!f&&b.some(_=>f.includes(_));break}default:i=!1}}return a.debugIf===i&&console.debug("applyCondition",a,e,i),i}function Z(t){const{flights:e,radar:a,selectedFlights:i,dimensions:o,dom:r}=t;let n;a&&a.filter===!0?n=t.flightsFiltered||e:a&&a.filter&&typeof a.filter=="object"?n=ot(t,a.filter):n=e;const d=r?.planesContainer||t.mainCard?.shadowRoot&&t.mainCard.shadowRoot.getElementById("planes");if(!d)return;d.innerHTML="";const{range:s,scaleFactor:f,centerX:b,centerY:_}=o;if(!s||!f||b===void 0||_===void 0)return;const v=s*1.15;n.slice().reverse().forEach(y=>{const C=y.distance_to_tracker;if(C!==void 0&&C<=v){const M=document.createElement("div");M.className="plane";const u=y.heading_from_tracker??0,h=b+Math.cos((u-90)*Math.PI/180)*C*f,c=_+Math.sin((u-90)*Math.PI/180)*C*f;M.style.top=c+"px",M.style.left=h+"px";const g=document.createElement("div");g.className="arrow",g.style.transform=`rotate(${y.heading}deg)`,M.appendChild(g);const m=document.createElement("div");m.className="callsign-label",m.textContent=y.callsign??y.aircraft_registration??"n/a",d.appendChild(m);const l=m.getBoundingClientRect(),p=l.width+3,x=l.height+6;m.style.top=c-x+"px",m.style.left=h-p+"px",(y.altitude??0)<=0?M.classList.add("plane-small"):M.classList.add("plane-medium");const k=a["aircraft-marker-size"];k&&k!=="normal"&&M.classList.add(`marker-size-${k}`),i&&i.includes(y.id)&&M.classList.add("selected"),M.addEventListener("click",()=>t.toggleSelectedFlight(y)),m.addEventListener("click",()=>t.toggleSelectedFlight(y)),d.appendChild(M)}})}function st(t,e){const a=document.createElement("img");return a.setAttribute("src",`https://flagsapi.com/${t}/shiny/16.png`),a.setAttribute("title",`${e}`),a.style.position="relative",a.style.top="3px",a.style.left="2px",a}function kt(t,e,a){try{let i=e[a];if(t.config.annotate){const o=Object.assign({},e);t.config.annotate.filter(r=>r.field===a).forEach(r=>{rt(t,e,r.conditions)&&(o[a]=r.render.replace(/\$\{([^}]*)\}/g,(n,d)=>String(o[d]||"")))}),i=String(o[a]||"")}return i}catch(i){return console.error(`[FR24Card] flightField error for field '${a}':`,i),""}}function $t(t,e){try{const a=Object.assign({},e);["flight_number","callsign","aircraft_registration","aircraft_model","aircraft_code","airline","airline_short","airline_iata","airline_icao","airport_origin_name","airport_origin_code_iata","airport_origin_code_icao","airport_origin_country_name","airport_origin_country_code","airport_destination_name","airport_destination_code_iata","airport_destination_code_icao","airport_destination_country_name","airport_destination_country_code"].forEach(o=>{a[o]=kt(t,a,o)}),a.origin_flag=a.airport_origin_country_code?st(a.airport_origin_country_code,a.airport_origin_country_name||"").outerHTML:"",a.destination_flag=a.airport_destination_country_code?st(a.airport_destination_country_code,a.airport_destination_country_name||"").outerHTML:"",a.climb_descend_indicator=Math.abs(a.vertical_speed)>100?a.vertical_speed>100?"↑":"↓":"",a.alt_in_unit=a.altitude>=17750?`FL${Math.round(a.altitude/1e3)*10}`:a.altitude>0?t.units.altitude==="m"?`${Math.round(a.altitude*.3048)} m`:`${Math.round(a.altitude)} ft`:void 0,a.spd_in_unit=a.ground_speed>0?t.units.speed==="kmh"?`${Math.round(a.ground_speed*1.852)} km/h`:t.units.speed==="mph"?`${Math.round(a.ground_speed*1.15078)} mph`:`${Math.round(a.ground_speed)} kts`:void 0,a.approach_indicator=a.ground_speed>70?a.is_approaching?"↓":a.is_receding?"↑":"":"",a.dist_in_unit=`${Math.round(a.distance_to_tracker||0)} ${t.units.distance}`,a.direction_info=`${Math.round(a.heading_from_tracker||0)}° ${a.cardinal_direction_from_tracker||""}`;const i=document.createElement("div");return i.style.clear="both",i.className="flight",t.selectedFlights&&t.selectedFlights.includes(a.id)&&(i.className+=" selected"),i.innerHTML=Y(t,"flight_element",a,o=>(...r)=>r?.filter(n=>n).join(o||" ")),i.addEventListener("click",()=>t.toggleSelectedFlight(a)),i}catch(a){console.error("[FR24Card] renderFlight error:",a);const i=document.createElement("div");return i.className="flight error",i.textContent=`Error rendering flight: ${a}`,i}}var lt={altitude:"ft",speed:"kts",distance:"km"},Mt=[{field:"id",comparator:"oneOf",value:"${selectedFlights}",order:"DESC"},{field:"altitude",comparator:"eq",value:0,order:"ASC"},{field:"closest_passing_distance ?? distance_to_tracker",order:"ASC"}],V,dt=U((()=>{V={img_element:'${flight.aircraft_photo_small ? ` ` : ""}',icon:'${flight.altitude > 0 ? (flight.vertical_speed > 100 ? "airplane-takeoff" : flight.vertical_speed < -100 ? "airplane-landing" : "airplane") : "airport"}',icon_element:' ',flight_info:'${joinList(" - ")(flight.airline_short, flight.flight_number, flight.callsign !== flight.flight_number ? flight.callsign : "")}',flight_info_element:'${tpl.flight_info}
',header:"${tpl.img_element}${tpl.icon_element}${tpl.flight_info_element}
",aircraft_info:'${joinList(" - ")(flight.aircraft_registration, flight.aircraft_model)}',aircraft_info_element:'${tpl.aircraft_info ? `${tpl.aircraft_info}
` : ""}',departure_info:'${flight.altitude === 0 && flight.time_scheduled_departure ? ` (${new Date(flight.time_scheduled_departure * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })})` : ""}',origin_info:'${joinList("")(flight.airport_origin_code_iata, tpl.departure_info, flight.origin_flag)}',arrival_info:"",destination_info:'${joinList("")(flight.airport_destination_code_iata, tpl.arrival_info, flight.destination_flag)}',route_info:'${joinList(" -> ")(tpl.origin_info, tpl.destination_info)}',route_element:"${tpl.route_info}
",alt_info:'${flight.alt_in_unit ? "Alt: " + flight.alt_in_unit + flight.climb_descend_indicator : undefined}',spd_info:'${flight.spd_in_unit ? "Spd: " + flight.spd_in_unit : undefined}',hdg_info:'${flight.heading ? "Hdg: " + flight.heading + "°" : undefined}',dist_info:'${flight.dist_in_unit ? "Dist: " + flight.dist_in_unit + flight.approach_indicator : undefined}',flight_status:'${joinList(" - ")(tpl.alt_info, tpl.spd_info, tpl.hdg_info)}
',position_status:'${joinList(" - ")(tpl.dist_info, flight.direction_info)}
',proximity_info:'${flight.is_approaching && flight.ground_speed > 70 && flight.closest_passing_distance < 15 ? `Closest Distance: ${flight.closest_passing_distance} ${units.distance}, ETA: ${flight.eta_to_closest_distance} min` : ""}
',flight_element:"${tpl.header}${tpl.aircraft_info_element}${tpl.route_element}${tpl.flight_status}${tpl.position_status}${tpl.proximity_info}",radar_range:"Range: ${radar_range} ${units.distance}",list_status:"${flights.shown}/${flights.total}"}}));dt();function ct(t,e){return e.split(" ?? ").reduce((a,i)=>a??t[i],void 0)}function At(t,e=a=>a){return function(a,i){for(const o of t){const{field:r,comparator:n,order:d="ASC"}=o,s=e(o.value),f=ct(a,r),b=ct(i,r);let _=0;switch(n){case"eq":f===s&&b!==s?_=1:f!==s&&b===s&&(_=-1);break;case"lt":f=s?_=1:f>=s&&bs?_=1:f>s&&b<=s&&(_=-1);break;case"gt":f>s&&b<=s?_=1:f<=s&&b>s&&(_=-1);break;case"gte":f>=s&&b=s&&(_=-1);break;case"oneOf":if(s!=null&&(Array.isArray(s)||typeof s=="string")){const v=s.includes(f),y=s.includes(b);v&&!y?_=1:!v&&y&&(_=-1)}break;case"containsOneOf":if(Array.isArray(s)&&s.length>0){const v=s.some(C=>(Array.isArray(f)||typeof f=="string")&&f.includes(C)),y=s.some(C=>(Array.isArray(b)||typeof b=="string")&&b.includes(C));v&&!y?_=1:!v&&y&&(_=-1)}break;default:_=f-b;break}if(_!==0)return d.toUpperCase()==="DESC"?-_:_}return 0}}var D={flights_entity:"sensor.flightradar24_current_in_area",projection_interval:5,no_flights_message:"No flights are currently visible. Please check back later.",list:{hide:!1,showListStatus:!0},units:lt,radar:{range:lt.distance==="km"?35:25,background_map:"none",background_map_opacity:0,background_map_api_key:""},sort:Mt,templates:V,defines:{}},pt=class{constructor(){this.hass=null,this.config={},this.radar={range:35},this.list={},this.templates={},this.defines={},this.units={altitude:"ft",speed:"kts",distance:"km"},this.flightsContext={},this.dimensions={},this.flights=[],this.selectedFlights=[],this.renderDynamicOnRangeChange=!1,this._leafletMap=null,this.sortFn=()=>0}setConfig(t){if(!t)throw new Error("Configuration is missing.");this.config={...t},this.config.flights_entity=t.flights_entity??D.flights_entity,this.config.projection_interval=t.projection_interval??D.projection_interval,this.config.no_flights_message=t.no_flights_message??D.no_flights_message,this.list={...D.list,...t.list},this.units={...D.units,...t.units},this.radar={range:this.units.distance==="km"?D.radar.range:25,background_map:t.radar?.background_map??D.radar.background_map,background_map_opacity:t.radar?.background_map_opacity??D.radar.background_map_opacity,background_map_api_key:t.radar?.background_map_api_key??D.radar.background_map_api_key,...t.radar},this.radar.initialRange=this.radar.range,this.defines={...D.defines,...t.defines},this.sortFn=At(t.sort??D.sort,e=>J(this,e,void 0,a=>{this.renderDynamicOnRangeChange=a})),this.templates={...D.templates,...t.templates}}toggleSelectedFlight(t){this.selectedFlights||(this.selectedFlights=[]),this.selectedFlights.includes(t.id)?this.selectedFlights=this.selectedFlights.filter(e=>e!==t.id):this.selectedFlights.push(t.id),typeof this.renderDynamicFn=="function"&&this.renderDynamicFn()}setRenderDynamic(t){this.renderDynamicFn=t}setToggleValue(t,e){this.config&&this.config.toggles&&(this.defines[t]=["true",!0,1].includes(e),typeof this.renderDynamicFn=="function"&&this.renderDynamicFn())}};async function Lt(){if(N)return N;try{const e=await fetch("/local/flightradar24-card/runways.csv");if(e.ok)return N=await e.text(),N}catch{}try{const e=await fetch("data/runways.csv");if(e.ok)return N=await e.text(),N}catch{}const t=await fetch("https://davidmegginson.github.io/ourairports-data/runways.csv");if(!t.ok)throw new Error(`Failed to fetch runway data: ${t.status}`);return N=await t.text(),N}async function Et(){if(j)return j;try{const e=await fetch("/local/flightradar24-card/airports.csv");if(e.ok)return j=await e.text(),j}catch{}try{const e=await fetch("data/airports.csv");if(e.ok)return j=await e.text(),j}catch{}const t=await fetch("https://davidmegginson.github.io/ourairports-data/airports.csv");if(!t.ok)throw new Error(`Failed to fetch airport data: ${t.status}`);return j=await t.text(),j}function H(t){const e=[];let a="",i=!1;for(let o=0;ok.score-x.score).slice(0,10).map(({score:x,...k})=>k)}var N,j,St=U((()=>{N=null,j=null})),Tt=gt({Flightradar24CardEditor:()=>Q}),Q,ut=U((()=>{St(),dt(),Q=class extends HTMLElement{constructor(){super(),this._config={},this._openSections=new Set(["basic-settings"]),this._openConditions=new Set,this._openFeatures=new Set,this._openAnnotations=new Set,this._mapModal=null,this._internalUpdate=!1,this._shadowRoot=this.attachShadow({mode:"open"})}setConfig(t){this._config={...t},this._internalUpdate||this._render(),this._internalUpdate=!1}get availableFlightEntities(){return this.hass?Object.keys(this.hass.states).filter(t=>t.includes("flightradar")).sort():[]}get availableTrackerEntities(){return this.hass?Object.keys(this.hass.states).filter(t=>t.startsWith("device_tracker.")||t.startsWith("person.")||t.startsWith("zone.")).sort():[]}get availableFlightFields(){return[{value:"id",label:"ID",group:"Basic"},{value:"flight_number",label:"Flight Number",group:"Basic"},{value:"callsign",label:"Callsign",group:"Basic"},{value:"aircraft_registration",label:"Aircraft Registration",group:"Aircraft"},{value:"aircraft_model",label:"Aircraft Model",group:"Aircraft"},{value:"aircraft_code",label:"Aircraft Code",group:"Aircraft"},{value:"airline",label:"Airline Name",group:"Airline"},{value:"airline_short",label:"Airline Short",group:"Airline"},{value:"airline_iata",label:"Airline IATA",group:"Airline"},{value:"airline_icao",label:"Airline ICAO",group:"Airline"},{value:"airport_origin_name",label:"Origin Airport",group:"Origin"},{value:"airport_origin_code_iata",label:"Origin IATA",group:"Origin"},{value:"airport_origin_country_name",label:"Origin Country",group:"Origin"},{value:"airport_origin_country_code",label:"Origin Country Code",group:"Origin"},{value:"airport_destination_name",label:"Destination Airport",group:"Destination"},{value:"airport_destination_code_iata",label:"Destination IATA",group:"Destination"},{value:"airport_destination_country_name",label:"Destination Country",group:"Destination"},{value:"airport_destination_country_code",label:"Destination Country Code",group:"Destination"},{value:"latitude",label:"Latitude",group:"Position"},{value:"longitude",label:"Longitude",group:"Position"},{value:"altitude",label:"Altitude",group:"Position"},{value:"vertical_speed",label:"Vertical Speed",group:"Movement"},{value:"ground_speed",label:"Ground Speed",group:"Movement"},{value:"heading",label:"Heading",group:"Movement"},{value:"distance_to_tracker",label:"Distance to Tracker",group:"Tracking"},{value:"heading_from_tracker",label:"Heading from Tracker",group:"Tracking"},{value:"cardinal_direction_from_tracker",label:"Cardinal Direction",group:"Tracking"},{value:"is_approaching",label:"Is Approaching",group:"Tracking"},{value:"is_receding",label:"Is Receding",group:"Tracking"},{value:"closest_passing_distance",label:"Closest Passing Distance",group:"Approach"},{value:"eta_to_closest_distance",label:"ETA to Closest",group:"Approach"},{value:"heading_from_tracker_to_closest_passing",label:"Heading to Closest",group:"Approach"}]}_mapTypeRequiresApiKey(t){return t==="bw"||t==="outlines"}get validFlightFields(){return new Set(this.availableFlightFields.map(t=>t.value))}get allDefineAndToggleKeys(){const t=new Set;return Object.keys(this._config.toggles||{}).forEach(e=>t.add(e)),Object.keys(this._config.defines||{}).forEach(e=>t.add(e)),t}getUsedDefinesAndToggles(){const t=new Set,e=this._config.templates||{},a=this._config.filter,i=this._config.sort||[];Object.values(e).forEach(r=>{const n=r.matchAll(/\$\{(\w+)\}/g);for(const d of n){const s=d[1];this.allDefineAndToggleKeys.has(s)&&t.add(s)}});const o=r=>{r.forEach(n=>{if("type"in n&&(n.type==="AND"||n.type==="OR"))o(n.conditions||[]);else if("type"in n&&n.type==="NOT")o([n.condition]);else{const d=n;d.field&&this.allDefineAndToggleKeys.has(d.field)&&t.add(d.field);const s=d.value;if(typeof s=="string"&&s.startsWith("${")&&s.endsWith("}")){const f=s.slice(2,-1);this.allDefineAndToggleKeys.has(f)&&t.add(f)}}})};return a&&Array.isArray(a)&&o(a),i.forEach(r=>{r.field&&this.allDefineAndToggleKeys.has(r.field)&&t.add(r.field)}),t}getUnusedDefinesAndToggles(){const t=this.getUsedDefinesAndToggles(),e=[],a=[];return Object.keys(this._config.toggles||{}).forEach(i=>{t.has(i)||e.push(i)}),Object.keys(this._config.defines||{}).forEach(i=>{t.has(i)||a.push(i)}),{toggles:e,defines:a}}getUsedTemplateKeys(){const t=new Set,e=this._config.templates||{};return["flight_element","radar_range","list_status"].forEach(a=>{e[a]!==void 0&&t.add(a)}),Object.values(e).forEach(a=>{const i=a.matchAll(/\$\{(\w+)\([\s\S]*?\)\}/g);for(const o of i){const r=o[1];e[r]!==void 0&&t.add(r)}}),t}getUnusedTemplates(){const t=this.getUsedTemplateKeys(),e=this._config.templates||{},a=[];return Object.keys(e).forEach(i=>{t.has(i)||a.push(i)}),a}validateConditionField(t){return this.validFlightFields.has(t)?{valid:!0}:this.allDefineAndToggleKeys.has(t)?{valid:!0}:{valid:!1,error:`Unknown field: "${t}". Not a flight property or define/toggle.`}}hasValidationErrors(){const t=this.getUnusedDefinesAndToggles();if(t.toggles.length>0||t.defines.length>0||this.getUnusedTemplates().length>0)return!0;const e=this._config.filter;if(e&&Array.isArray(e)&&this._checkConditionsForInvalidFields(e))return!0;const a=this._config.sort||[];for(const i of a)if(i.field&&!this.validateConditionField(i.field).valid)return!0;return!1}_checkConditionsForInvalidFields(t){for(const e of t)if("type"in e&&(e.type==="AND"||e.type==="OR")){if(this._checkConditionsForInvalidFields(e.conditions||[]))return!0}else if("type"in e&&e.type==="NOT"){if(this._checkConditionsForInvalidFields([e.condition]))return!0}else{const a=e;if(a.field&&!this.validateConditionField(a.field).valid)return!0}return!1}_render(){this.hass&&(this._saveOpenSections(),this._shadowRoot.innerHTML=`
+ /* Inline critical Leaflet pane CSS — survives Shadow DOM clears and CSP */
+ .leaflet-pane {
+ position: absolute;
+ left: 0;
+ top: 0;
+ }
+ .leaflet-tile {
+ pointer-events: none;
+ }
+ `, e.appendChild(k);
+}
+function Dt(t, e) {
+ if (!e) return;
+ e.innerHTML = "";
+ const a = t.config.toggles || {}, i = !!window.customElements && !!customElements.get("ha-switch");
+ Object.keys(a).forEach((o) => {
+ const r = a[o], n = document.createElement("div");
+ n.className = "toggle";
+ const d = document.createElement("label");
+ d.textContent = r.label || o, n.appendChild(d);
+ let l;
+ i ? l = document.createElement("ha-switch") : (l = document.createElement("input"), l.type = "checkbox"), l.checked = r.default === !0, l.addEventListener("change", () => {
+ t.setToggleValue && t.setToggleValue(o, l.checked);
+ }), n.appendChild(l), e.appendChild(n);
+ });
+}
+function N(t) {
+ return t * (Math.PI / 180);
+}
+function at(t) {
+ return t * (180 / Math.PI);
+}
+function j(t, e, a, i, o = "km") {
+ const n = N(a - t), d = N(i - e), l = Math.sin(n / 2) * Math.sin(n / 2) + Math.cos(N(t)) * Math.cos(N(a)) * Math.sin(d / 2) * Math.sin(d / 2), u = 2 * Math.atan2(Math.sqrt(l), Math.sqrt(1 - l));
+ return o === "km" ? 6371 * u : 6371 * u / 1.60934;
+}
+function V(t, e, a, i) {
+ const o = N(i - e), r = Math.sin(o) * Math.cos(N(a)), n = Math.cos(N(t)) * Math.sin(N(a)) - Math.sin(N(t)) * Math.cos(N(a)) * Math.cos(o);
+ return (at(Math.atan2(r, n)) + 360) % 360;
+}
+function it(t, e, a, i) {
+ const r = N(a), n = N(t), d = N(e), l = i / 6371, u = Math.asin(Math.sin(n) * Math.cos(l) + Math.cos(n) * Math.sin(l) * Math.cos(r)), _ = d + Math.atan2(Math.sin(r) * Math.sin(l) * Math.cos(n), Math.cos(l) - Math.sin(n) * Math.sin(u));
+ return {
+ lat: at(u),
+ lon: at(_)
+ };
+}
+function zt(t, e, a, i, o) {
+ const r = V(a, i, t, e), n = Math.abs((o - r + 360) % 360);
+ return it(a, i, o, j(t, e, a, i) * Math.cos(N(n)));
+}
+function Pt(t) {
+ return [
+ "N",
+ "NE",
+ "E",
+ "SE",
+ "S",
+ "SW",
+ "W",
+ "NW"
+ ][Math.round(t / 45) % 8];
+}
+function ht(t, e, a = 60) {
+ const i = Math.abs((t - e + 360) % 360);
+ return i <= a || i >= 360 - a;
+}
+function K(t) {
+ if (!t || !t.config)
+ return console.error("Config not set in getLocation"), {
+ latitude: 0,
+ longitude: 0
+ };
+ const { config: e, hass: a } = t;
+ if (e.location_tracker && a && a.states && e.location_tracker in a.states) {
+ const i = a.states[e.location_tracker].attributes;
+ return {
+ latitude: i.latitude,
+ longitude: i.longitude
+ };
+ } else {
+ if (e.location) return {
+ latitude: e.location.lat,
+ longitude: e.location.lon
+ };
+ if (a && a.config) return {
+ latitude: a.config.latitude,
+ longitude: a.config.longitude
+ };
+ }
+ return {
+ latitude: 0,
+ longitude: 0
+ };
+}
+function Q(t) {
+ if (t)
+ return Ct.get(t);
+}
+function xt(t) {
+ return !!Q(t)?.apiKeyParam;
+}
+function gt(t) {
+ return Q(t)?.apiKeyHelp || "";
+}
+function Nt(t, e) {
+ const a = Q(t);
+ return a ? a.apiKeyParam && e && e.trim().length > 0 ? a.url + a.apiKeyParam + encodeURIComponent(e.trim()) : a.url : "";
+}
+var X, Ct, Z, $t = J((() => {
+ X = [
+ {
+ id: "color",
+ label: "Color (OpenStreetMap)",
+ group: "keyless",
+ url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
+ attribution: "© OpenStreetMap contributors",
+ subdomains: [
+ "a",
+ "b",
+ "c"
+ ]
+ },
+ {
+ id: "satellite",
+ label: "Satellite",
+ group: "keyless",
+ url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}",
+ attribution: "© Esri, Maxar, Earthstar Geographics",
+ subdomains: []
+ },
+ {
+ id: "topo",
+ label: "Topographic",
+ group: "keyless",
+ url: "https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png",
+ attribution: "© OpenTopoMap, © OpenStreetMap contributors",
+ subdomains: [
+ "a",
+ "b",
+ "c"
+ ]
+ },
+ {
+ id: "light",
+ label: "Light (CARTO)",
+ group: "keyed",
+ url: "https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}.png",
+ apiKeyParam: "?key=",
+ apiKeyHelp: 'Required for Light, Dark and Voyager. Request a free CARTO key .',
+ attribution: "© CartoDB, © OpenStreetMap contributors",
+ subdomains: [
+ "a",
+ "b",
+ "c",
+ "d"
+ ]
+ },
+ {
+ id: "dark",
+ label: "Dark (CARTO)",
+ group: "keyed",
+ url: "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png",
+ apiKeyParam: "?key=",
+ apiKeyHelp: 'Required for Light, Dark and Voyager. Request a free CARTO key .',
+ attribution: "© CartoDB, © OpenStreetMap contributors",
+ subdomains: [
+ "a",
+ "b",
+ "c",
+ "d"
+ ]
+ },
+ {
+ id: "voyager",
+ label: "Voyager (CARTO)",
+ group: "keyed",
+ url: "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}.png",
+ apiKeyParam: "?key=",
+ apiKeyHelp: 'Required for Light, Dark and Voyager. Request a free CARTO key .',
+ attribution: "© CartoDB, © OpenStreetMap contributors",
+ subdomains: [
+ "a",
+ "b",
+ "c",
+ "d"
+ ]
+ },
+ {
+ id: "bw",
+ label: "Black & White (Stadia)",
+ group: "keyed",
+ url: "https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}.png",
+ apiKeyParam: "?api_key=",
+ apiKeyHelp: 'Required for Black & White and Outlines. Get a free Stadia Maps key .',
+ attribution: "Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap",
+ subdomains: []
+ },
+ {
+ id: "outlines",
+ label: "Outlines (Stadia)",
+ group: "keyed",
+ url: "https://tiles.stadiamaps.com/tiles/stamen_toner_lines/{z}/{x}/{y}.png",
+ apiKeyParam: "?api_key=",
+ apiKeyHelp: 'Required for Black & White and Outlines. Get a free Stadia Maps key .',
+ attribution: "Map tiles by Stamen Design, hosted by Stadia Maps; Data by OpenStreetMap",
+ subdomains: []
+ }
+ ], Ct = new Map(X.map((t) => [t.id, t])), Z = new Set(X.map((t) => t.id));
+}));
+$t();
+function Mt(t) {
+ const e = t?.radar;
+ if (!e || e.hide === !0) return !1;
+ const a = t?.config?.radar?.background_map, i = !!a && (Z.has(a) || a === "system");
+ return e.view === "map" ? !a || i : !a || a === "none" ? !1 : i;
+}
+function jt() {
+ const t = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
+ let e = !1;
+ try {
+ e = !!(window.parent && window.parent.document && window.parent.document.body.classList.contains("dark"));
+ } catch {
+ }
+ return e || t;
+}
+function qt(t, e, a) {
+ if (!Mt(t)) {
+ t.radar?.hide !== !0 && a();
+ return;
+ }
+ if (!e.querySelector("#leaflet-css-loader")) {
+ const i = document.createElement("link");
+ i.id = "leaflet-css-loader", i.rel = "stylesheet", i.href = "https://unpkg.com/leaflet/dist/leaflet.css", e.appendChild(i);
+ }
+ if (window.L) {
+ a();
+ return;
+ }
+ if (e.querySelector("#leaflet-js-loader")) {
+ const i = setInterval(() => {
+ window.L && (clearInterval(i), a());
+ }, 50);
+ } else {
+ const i = document.createElement("script");
+ i.id = "leaflet-js-loader", i.src = "https://unpkg.com/leaflet/dist/leaflet.js", i.async = !0, i.onload = a, i.onerror = () => {
+ i.remove(), console.error("[FR24] Leaflet script load failed");
+ }, e.appendChild(i);
+ }
+}
+function Bt(t, e) {
+ const { config: a, dimensions: i } = t;
+ if (!Mt(t)) {
+ t._leafletMap && (t._leafletMap.remove(), t._leafletMap = null);
+ const E = e.querySelector("#radar-map-bg");
+ E && E.remove();
+ return;
+ }
+ const o = a?.radar?.background_map;
+ let r = o;
+ t.radar?.view === "map" && (!o || o === "none" || !Z.has(o)) && (r = "system");
+ const n = t.radar?.view === "map" ? 1 : typeof a?.radar?.background_map_opacity == "number" ? Math.max(0, Math.min(1, a.radar.background_map_opacity)) : 1;
+ let d = e.querySelector("#radar-map-bg");
+ d ? d.style.opacity = String(n) : (d = document.createElement("div"), d.id = "radar-map-bg", d.style.position = "absolute", d.style.top = "0", d.style.left = "0", d.style.width = "100%", d.style.height = "100%", d.style.zIndex = "0", d.style.pointerEvents = "none", d.style.opacity = String(n), e.appendChild(d)), d.style.transform = "", t._leafletMap && t._leafletMap.getContainer() !== d && (t._leafletMap.remove(), t._leafletMap = null);
+ const l = K(t), u = Math.max(i?.range || 1, 1), _ = t.units?.distance === "miles" ? u * 1.60934 : u, m = l?.latitude || 0, g = l?.longitude || 0, y = Math.PI / 180, k = 111.13209 - 0.56605 * Math.cos(2 * m * y) + 12e-4 * Math.cos(4 * m * y), A = 111.32 * Math.cos(m * y) - 0.094 * Math.cos(3 * m * y), R = _ / k, F = _ / A, S = [[m - R, g - F], [m + R, g + F]];
+ let x, $;
+ r === "system" ? jt() ? (x = a?.radar?.background_map_dark || "dark", $ = a?.radar?.background_map_dark_api_key || "") : (x = a?.radar?.background_map_light || "color", $ = a?.radar?.background_map_light_api_key || "") : (x = o && Z.has(o) ? o : "color", $ = a?.radar?.background_map_api_key || "");
+ const C = Q(x);
+ if (!C) return d;
+ const M = Nt(x, $);
+ if (!M) return d;
+ const w = {
+ attribution: C.attribution,
+ subdomains: C.subdomains
+ };
+ if (xt(x) && !($ && $.trim().length > 0))
+ return t._leafletMap && (t._leafletMap.remove(), t._leafletMap = null), d.innerHTML = 'API key required for this map type. Configure in Background Map settings.
', d;
+ if (t._leafletMap || (d.innerHTML = ""), window.L) {
+ const E = {
+ type: x,
+ apiKey: $
+ }, L = !t._currentMapConfig || t._currentMapConfig.type !== E.type || t._currentMapConfig.apiKey !== E.apiKey;
+ t._leafletMap ? L && (t._leafletMap.eachLayer((O) => {
+ t._leafletMap.removeLayer(O);
+ }), window.L.tileLayer(M, w).addTo(t._leafletMap), t._currentMapConfig = E) : (t._leafletMap = window.L.map(d, {
+ attributionControl: !1,
+ zoomControl: !1,
+ dragging: !1,
+ scrollWheelZoom: !1,
+ boxZoom: !1,
+ doubleClickZoom: !1,
+ keyboard: !1,
+ touchZoom: !1,
+ pointerEvents: !1
+ }), window.L.tileLayer(M, w).addTo(t._leafletMap), t._currentMapConfig = E), Lt(t._leafletMap, d, S, _), t.mapCenter = {
+ lat: Math.round(m * 100) / 100,
+ lon: Math.round(g * 100) / 100
+ }, t.mapZoom = Math.round(t._leafletMap.getZoom());
+ }
+ return d;
+}
+function Lt(t, e, a, i, o = 15) {
+ e.offsetHeight;
+ const r = t.getContainer(), n = r.offsetWidth, d = r.offsetHeight;
+ if (n > 0 && d > 0) {
+ t.fitBounds(a, {
+ animate: !1,
+ padding: [0, 0]
+ });
+ const l = window.L.point(0, d / 2), u = window.L.point(n, d / 2), _ = t.containerPointToLatLng(l), m = t.containerPointToLatLng(u), g = j(_.lat, _.lng, m.lat, m.lng, "km") / (i * 2);
+ e.style.transform = `scale(${g})`;
+ } else o > 0 && setTimeout(() => {
+ Lt(t, e, a, i, o - 1);
+ }, 50);
+}
+function Ht(t) {
+ const e = t._leafletMap;
+ if (!e) return;
+ const a = K(t), i = Math.max(t.dimensions?.range || 1, 1), o = t.units?.distance === "miles" ? i * 1.60934 : i, r = a?.latitude || 0, n = a?.longitude || 0, d = Math.PI / 180, l = 111.13209 - 0.56605 * Math.cos(2 * r * d) + 12e-4 * Math.cos(4 * r * d), u = 111.32 * Math.cos(r * d) - 0.094 * Math.cos(3 * r * d), _ = o / l, m = o / u, g = [[r - _, n - m], [r + _, n + m]], y = e.getContainer();
+ y.offsetHeight;
+ const k = y.offsetWidth, A = y.offsetHeight;
+ if (k > 0 && A > 0) {
+ e.fitBounds(g, {
+ animate: !1,
+ padding: [0, 0]
+ });
+ const R = window.L.point(0, A / 2), F = window.L.point(k, A / 2), S = e.containerPointToLatLng(R), x = e.containerPointToLatLng(F), $ = j(S.lat, S.lng, x.lat, x.lng, "km") / (o * 2);
+ y.style.transform = `scale(${$})`;
+ }
+}
+function Et(t = {}, e, a = []) {
+ if (a.includes(e))
+ return console.error("Circular template dependencies detected. " + a.join(" -> ") + " -> " + e), "";
+ if (t["compiled_" + e]) return t["compiled_" + e];
+ let i = t[e];
+ if (i === void 0)
+ return console.error("Missing template reference: " + e), "";
+ const o = /tpl\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
+ let r;
+ const n = {};
+ for (; (r = o.exec(i)) !== null; ) {
+ const d = r[1];
+ n[d] || (n[d] = Et(t, d, [...a, e])), i = i.replace(`tpl.${d}`, "(`" + n[d] + '`).replace(/^undefined$/, "")');
+ }
+ return t["compiled_" + e] = i, i;
+}
+function pt(t, e, a, i) {
+ const o = t.templates || {}, r = t.flightsContext || {}, n = t.units || {
+ distance: "km",
+ altitude: "ft",
+ speed: "kts"
+ }, d = t.radar || { range: 35 }, l = Et(o, e);
+ try {
+ const u = new Function("flights", "flight", "tpl", "units", "radar_range", "joinList", `return \`${l.replace(/\${(.*?)}/g, (_, m) => `\${${m}}`)}\``)(r, a, {}, n, Math.round(d.range), i);
+ return u !== "undefined" ? u : "";
+ } catch (u) {
+ return console.error("Error when rendering: " + l, u), "";
+ }
+}
+function nt(t, e, a, i) {
+ const { defines: o = {}, config: r = {}, radar: n = { range: 35 }, selectedFlights: d = [] } = t;
+ if (typeof e == "string" && e.startsWith("${") && e.endsWith("}")) {
+ const l = e.slice(2, -1);
+ if (l === "selectedFlights") return d;
+ if (l === "radar_range")
+ return i && i(!0), n.range;
+ if (l in o) return o[l];
+ if (r.toggles && l in r.toggles) return r.toggles[l].default;
+ if (a !== void 0) return a;
+ console.error("Unresolved placeholder: " + l), console.debug("Defines", o);
+ }
+ return e;
+}
+function ot(t, e) {
+ if (!t) return "";
+ try {
+ const a = new Function("map_lat", "map_lon", "zoom", "radar_range", "click_lat", "click_lon", "flight", "entity", "return `" + t.replace(/\${(.*?)}/g, "${$1}") + "`")(e.map_lat, e.map_lon, e.zoom, e.radar_range, e.click_lat, e.click_lon, e.flight ?? null, e.entity ?? null);
+ return a !== "undefined" ? a : "";
+ } catch (a) {
+ return console.error("Error rendering URL path:", t, a), t;
+ }
+}
+function rt(t) {
+ const { units: e, radar: a, dom: i, dimensions: o, hass: r } = t, n = i?.radarInfoDisplay || i && i.radarContainer?.querySelector("#radar-info");
+ n && (n.innerHTML = [a?.hide_range !== !0 ? pt(t, "radar_range", null, void 0) : ""].filter((x) => x).join(" "));
+ const d = i?.radarScreen || i && i.radarContainer?.querySelector("#radar-screen") || t.mainCard?.shadowRoot && t.mainCard.shadowRoot.getElementById("radar-screen");
+ if (!d) return;
+ Array.from(d.childNodes).forEach((x) => {
+ const $ = x;
+ $.id !== "radar-map-bg" && $.id !== "radar-screen-background" && d.removeChild(x);
+ });
+ let l = d.querySelector("#radar-screen-background");
+ l || (l = document.createElement("div"), l.id = "radar-screen-background", d.appendChild(l));
+ const u = K(t);
+ t.mapCenter = {
+ lat: Math.round(u.latitude * 100) / 100,
+ lon: Math.round(u.longitude * 100) / 100
+ }, t._leafletMap || (t.mapZoom = 8), Bt(t, d);
+ const { width: _, height: m, range: g, scaleFactor: y, centerX: k, centerY: A } = o || {};
+ if (!_ || !m || !g || !y || k == null || A == null) return;
+ const R = g * 1.15;
+ if (a?.rings ?? a?.view !== "map") {
+ const x = a?.ring_distance ?? 10, $ = Math.floor(g / x);
+ for (let C = 1; C <= $; C++) {
+ const M = C * x * y, w = document.createElement("div");
+ w.className = "ring", w.style.width = w.style.height = M * 2 + "px", w.style.top = Math.floor(A - M) + "px", w.style.left = Math.floor(k - M) + "px", d.appendChild(w);
+ }
+ for (let C = 0; C < 360; C += 45) {
+ const M = document.createElement("div");
+ M.className = "dotted-line", M.style.transform = `rotate(${C - 90}deg)`, d.appendChild(M);
+ }
+ }
+ const F = K(t), S = a?.local_features;
+ if (S && r && F) {
+ const x = F.latitude, $ = F.longitude;
+ S.forEach((C) => {
+ if (!(C.max_range && a.range && C.max_range <= a.range)) {
+ if (C.type === "outline" && C.points && C.points.length > 1) for (let M = 0; M < C.points.length - 1; M++) {
+ const w = C.points[M], E = C.points[M + 1], L = j(x, $, w.lat, w.lon, e.distance), O = j(x, $, E.lat, E.lon, e.distance);
+ if (L <= R || O <= R) {
+ const z = V(x, $, w.lat, w.lon), T = V(x, $, E.lat, E.lon), c = k + Math.cos((z - 90) * Math.PI / 180) * L * y, p = A + Math.sin((z - 90) * Math.PI / 180) * L * y, s = k + Math.cos((T - 90) * Math.PI / 180) * O * y, h = A + Math.sin((T - 90) * Math.PI / 180) * O * y, v = document.createElement("div");
+ v.className = "outline-line", v.style.width = Math.hypot(s - c, h - p) + "px", v.style.height = "1px", v.style.top = p + "px", v.style.left = c + "px", v.style.transformOrigin = "0 0", v.style.transform = `rotate(${Math.atan2(h - p, s - c) * (180 / Math.PI)}deg)`, d.appendChild(v);
+ }
+ }
+ else if ("position" in C && C.position) {
+ const { lat: M, lon: w } = C.position, E = j(x, $, M, w, e.distance);
+ if (E <= R) {
+ const L = V(x, $, M, w), O = k + Math.cos((L - 90) * Math.PI / 180) * E * y, z = A + Math.sin((L - 90) * Math.PI / 180) * E * y;
+ if (C.type === "runway") {
+ const T = C.heading ?? 0, c = C.length ?? 0, p = e.distance === "km" ? c * 3048e-7 : c * 18939e-8, s = document.createElement("div");
+ s.className = "runway", s.style.width = p * y + "px", s.style.height = "1px", s.style.top = z + "px", s.style.left = O + "px", s.style.transformOrigin = "0 50%", s.style.transform = `rotate(${T - 90}deg)`, d.appendChild(s);
+ }
+ if (C.type === "location") {
+ const T = document.createElement("div");
+ T.className = "location-dot";
+ const c = C.label;
+ if (T.title = c ?? "Location", T.style.top = z + "px", T.style.left = O + "px", d.appendChild(T), c) {
+ const p = document.createElement("div");
+ p.className = "location-label", p.textContent = c || "Location", d.appendChild(p);
+ const s = p.getBoundingClientRect(), h = s.width, v = s.height;
+ p.style.top = z - v - 4 + "px", p.style.left = O - h / 2 + "px";
+ }
+ }
+ }
+ }
+ }
+ });
+ }
+}
+function At(t, e) {
+ let a = null, i = null;
+ function o(u) {
+ const _ = u[0], m = u[1], g = _.clientX - m.clientX, y = _.clientY - m.clientY;
+ return Math.sqrt(g * g + y * y);
+ }
+ function r(u) {
+ u.preventDefault();
+ const _ = Math.sign(u.deltaY);
+ t.radar.range += _ * 2;
+ const m = t.radar.min_range || 1, g = t.radar.max_range || Math.max(100, t.radar.initialRange || 35);
+ t.radar.range < m && (t.radar.range = m), t.radar.range > g && (t.radar.range = g), t.mainCard.updateRadarRange(_ * 2);
+ }
+ function n(u) {
+ u.touches.length === 2 && (a = o(u.touches), i = t.radar.range);
+ }
+ function d(u) {
+ if (u.touches.length === 2 && a !== null && i !== null) {
+ u.preventDefault();
+ const _ = o(u.touches), m = a / _, g = t.radar.min_range || 1, y = t.radar.max_range || Math.max(100, t.radar.initialRange || 35);
+ let k = Math.round(i * m);
+ k < g && (k = g), k > y && (k = y), t.radar.range = k, t.mainCard.updateRadarRange(0);
+ }
+ }
+ function l() {
+ a !== null && (a = null, i = null, t.config.updateRangeFilterOnTouchEnd && t.renderDynamicOnRangeChange && t.mainCard.renderDynamic());
+ }
+ return e && (e.addEventListener("wheel", r, { passive: !1 }), e.addEventListener("touchstart", n, { passive: !0 }), e.addEventListener("touchmove", d, { passive: !1 }), e.addEventListener("touchend", l, { passive: !0 })), () => {
+ e && (e.removeEventListener("wheel", r), e.removeEventListener("touchstart", n), e.removeEventListener("touchmove", d), e.removeEventListener("touchend", l));
+ };
+}
+function Vt(t, e, a) {
+ const i = t.config?.tap_action;
+ if (!i) return;
+ const o = t.dom?.radar;
+ if (!o) {
+ dt(ot(i, {
+ map_lat: t.mapCenter?.lat,
+ map_lon: t.mapCenter?.lon,
+ zoom: t.mapZoom,
+ radar_range: t.radar?.range,
+ entity: lt(t)
+ }));
+ return;
+ }
+ const r = o.getBoundingClientRect(), n = e - r.left, d = a - r.top, l = r.width, u = r.height, _ = t.units?.distance === "miles" ? (t.radar?.range || 1) * 1.60934 : t.radar?.range || 1, m = l / 2, g = u / 2, y = n - m, k = d - g, A = Math.sqrt(y * y + k * k) / Math.min(m, g) * _, R = (Math.atan2(y, -k) * (180 / Math.PI) + 360) % 360, F = t.mapCenter?.lat || 0, S = t.mapCenter?.lon || 0, x = R * Math.PI / 180, $ = 111.32, C = 111.32 * Math.cos(F * Math.PI / 180), M = Math.round((F + A / $ * Math.cos(x)) * 100) / 100, w = Math.round((S + A / C * Math.sin(x)) * 100) / 100;
+ dt(ot(i, {
+ map_lat: t.mapCenter?.lat,
+ map_lon: t.mapCenter?.lon,
+ zoom: t.mapZoom,
+ radar_range: t.radar?.range,
+ click_lat: M,
+ click_lon: w,
+ entity: lt(t)
+ }));
+}
+function st(t, e) {
+ const a = (t.config?.flight_tap_action || "toggle").split("|").map((r) => r.trim()), i = a.includes("toggle"), o = a.find((r) => r !== "toggle") || "";
+ i && t.toggleSelectedFlight(e), o && dt(ot(o, {
+ map_lat: t.mapCenter?.lat,
+ map_lon: t.mapCenter?.lon,
+ zoom: t.mapZoom,
+ radar_range: t.radar?.range,
+ flight: e,
+ entity: lt(t)
+ }));
+}
+function lt(t) {
+ const e = t.config?.flights_entity;
+ if (!e || !t.hass?.states) return;
+ const a = t.hass.states[e];
+ if (a)
+ return a;
+}
+function dt(t) {
+ t && window.open(t, "_blank");
+}
+function Ut(t, e) {
+ e.shadowRoot.innerHTML = "";
+ const a = document.createElement("ha-card");
+ a.id = "flights-card";
+ const i = document.createElement("div");
+ i.id = "layout-root";
+ const o = t.list?.position || "below";
+ if (i.classList.add(`layout-${o}`), t.radar?.view === "map" && i.classList.add("view-map"), !t.radar?.hide) {
+ const n = document.createElement("div");
+ n.id = "radar-container";
+ const d = document.createElement("div");
+ d.id = "radar-overlay", n.appendChild(d);
+ const l = document.createElement("div");
+ l.id = "radar-info", n.appendChild(l);
+ const u = document.createElement("div");
+ u.id = "toggle-container";
+ const _ = document.createElement("div");
+ _.id = "radar";
+ const m = document.createElement("div");
+ m.id = "radar-screen", _.appendChild(m);
+ const g = document.createElement("div");
+ g.id = "tracker", _.appendChild(g);
+ const y = document.createElement("div");
+ y.id = "planes", _.appendChild(y), n.appendChild(_), n.appendChild(u), i.appendChild(n), requestAnimationFrame(() => {
+ rt(t), e.observeRadarResize(), At(t, _), _.addEventListener("click", (k) => {
+ k.composedPath().some((A) => A.classList?.contains?.("plane")) || Vt(t, k.clientX, k.clientY);
+ });
+ }), t.dom = t.dom || {}, t.dom.toggleContainer = u, t.dom.planesContainer = y, t.dom.radar = _, t.dom.radarScreen = m, t.dom.radarInfoDisplay = l, t.dom.radarContainer = n, t.dom.shadowRoot = e.shadowRoot, t.mainCard = e;
+ }
+ const r = document.createElement("div");
+ r.id = "flights", t.list && t.list.hide === !0 && (r.style.display = "none"), i.appendChild(r), a.appendChild(i), e.shadowRoot.appendChild(a), St(t, e.shadowRoot), t.dom?.toggleContainer && Dt(t, t.dom.toggleContainer);
+}
+function Rt(t, e) {
+ return (t.flights || []).filter((a) => Ot(t, a, e));
+}
+function Ot(t, e, a) {
+ return Array.isArray(a) ? a.every((i) => U(t, e, i)) : U(t, e, a);
+}
+function U(t, e, a) {
+ let i = !0;
+ if (a.type === "AND" && a.conditions) i = a.conditions.every((o) => U(t, e, o));
+ else if (a.type === "OR" && a.conditions) i = a.conditions.some((o) => U(t, e, o));
+ else if (a.type === "NOT" && a.condition) i = !U(t, e, a.condition);
+ else {
+ const { field: o, defined: r, defaultValue: n, comparator: d } = a, l = nt(t, a.value), u = o ? e[o] : r ? nt(t, "${" + r + "}", n) : void 0;
+ switch (d) {
+ case "eq":
+ i = u === l;
+ break;
+ case "lt":
+ i = Number(u) < Number(l);
+ break;
+ case "lte":
+ i = Number(u) <= Number(l);
+ break;
+ case "gt":
+ i = Number(u) > Number(l);
+ break;
+ case "gte":
+ i = Number(u) >= Number(l);
+ break;
+ case "oneOf":
+ i = (Array.isArray(l) ? l : typeof l == "string" ? l.split(",").map((_) => _.trim()) : []).includes(u);
+ break;
+ case "containsOneOf": {
+ const _ = Array.isArray(l) ? l : typeof l == "string" ? l.split(",").map((m) => m.trim()) : [];
+ i = !!u && _.some((m) => u.includes(m));
+ break;
+ }
+ default:
+ i = !1;
+ }
+ }
+ return a.debugIf === i && console.debug("applyCondition", a, e, i), i;
+}
+var Wt = 12, mt = /* @__PURE__ */ new Map(), _t = /* @__PURE__ */ new Map();
+function Kt(t) {
+ if (!t) return [0, 0];
+ const e = t.split(",").map(Number);
+ return [e[0] || 0, e[1] || 0];
+}
+function Yt(t) {
+ const e = {
+ offsetX: 0,
+ offsetY: 0,
+ blur: 0,
+ color: "rgba(0,0,0,0.5)"
+ };
+ if (!t) return e;
+ const a = t.trim().split(/\s+/);
+ if (a.length < 2) return e;
+ e.offsetX = parseFloat(a[0]) || 0, e.offsetY = parseFloat(a[1]) || 0;
+ let i = 2;
+ return a.length > 2 && /^[\d.]+(?:px|em|rem|pt|cm|mm|in|pc|ex|ch|vw|vh|vmin|vmax)$/i.test(a[2]) && (e.blur = Math.max(0, parseFloat(a[2]) || 0), i = 3), a.length > i && (e.color = a.slice(i).join(" ")), e;
+}
+function Gt(t) {
+ const e = mt.get(t);
+ if (e) return e;
+ const a = new Promise((i, o) => {
+ const r = new Image();
+ r.onload = () => i(r), r.onerror = () => o(/* @__PURE__ */ new Error(`Failed to load marker image: ${t}`)), r.src = t;
+ });
+ return mt.set(t, a), a;
+}
+function Xt(t, e) {
+ const a = t.width, i = t.height, o = a / Wt, r = e["aircraft-marker-color-overlay"], n = e["aircraft-marker-outline-width"] ?? 0, d = e["aircraft-marker-outline-color"] || "#000000", l = e["aircraft-marker-shadow"] || "", u = Yt(l), _ = Math.round(u.offsetX * o), m = Math.round(u.offsetY * o), g = Math.round(u.blur * o), y = Math.ceil(n * o), k = Math.max(1, Math.round(y * 0.4)), A = Math.ceil(Math.max(y + k * 2, Math.abs(_) + g * 2, Math.abs(m) + g * 2)), R = a + 2 * A, F = i + 2 * A, S = document.createElement("canvas");
+ S.width = R, S.height = F;
+ const x = S.getContext("2d"), $ = A, C = A;
+ if (l && (_ !== 0 || m !== 0 || g > 0)) {
+ const M = document.createElement("canvas");
+ M.width = a, M.height = i;
+ const w = M.getContext("2d");
+ w.drawImage(t, 0, 0, a, i), w.globalCompositeOperation = "source-atop", w.fillStyle = u.color, w.fillRect(0, 0, a, i), x.save(), g > 0 && (x.filter = `blur(${g}px)`), x.drawImage(M, $ + _, C + m, a, i), x.restore();
+ }
+ if (y > 0) {
+ const M = document.createElement("canvas");
+ M.width = R, M.height = F;
+ const w = M.getContext("2d"), E = a + 2 * y, L = i + 2 * y;
+ w.save(), w.filter = `blur(${k}px)`, w.drawImage(t, $ - y, C - y, E, L), w.filter = "none", w.globalCompositeOperation = "source-atop", w.fillStyle = d, w.fillRect(0, 0, R, F), w.restore(), x.drawImage(M, 0, 0);
+ }
+ return x.drawImage(t, $, C, a, i), r && (x.globalCompositeOperation = "source-atop", x.fillStyle = r, x.fillRect($, C, a, i)), S;
+}
+function Zt(t) {
+ return `${t["aircraft-marker-url"]}|${t["aircraft-marker-color-overlay"]}|${t["aircraft-marker-outline-width"]}|${t["aircraft-marker-outline-color"]}|${t["aircraft-marker-shadow"]}`;
+}
+function Jt(t) {
+ const e = Zt(t), a = _t.get(e);
+ if (a) return a;
+ const i = Gt(t["aircraft-marker-url"]).then((o) => Xt(o, t));
+ return _t.set(e, i), i;
+}
+function Qt(t, e) {
+ const a = document.createElement("div");
+ a.className = "custom-marker";
+ const i = document.createElement("div");
+ i.className = "custom-marker-transform", a.appendChild(i);
+ const o = t["aircraft-marker-url"], r = t["aircraft-marker-color-overlay"], n = t["aircraft-marker-outline-width"] ?? 0;
+ t["aircraft-marker-outline-color"];
+ const d = t["aircraft-marker-shadow"] || "";
+ if (r || n > 0 || d.length > 0) {
+ const g = document.createElement("canvas");
+ i.appendChild(g), Jt(t).then((y) => {
+ g.width = y.width, g.height = y.height, g.getContext("2d").drawImage(y, 0, 0);
+ }).catch(() => {
+ });
+ } else {
+ const g = document.createElement("img");
+ g.src = o, g.draggable = !1, i.appendChild(g);
+ }
+ const l = t["aircraft-marker-rotation"] ?? 0, u = t["aircraft-marker-scale"] ?? 1, [_, m] = Kt(t["aircraft-marker-center"]);
+ return i.style.transform = `rotate(${e + l}deg) scale(${u})`, i.style.transformOrigin = `calc(50% + ${_}px) calc(50% + ${m}px)`, a;
+}
+function et(t) {
+ const { flights: e, radar: a, selectedFlights: i, dimensions: o, dom: r } = t;
+ let n;
+ a && a.filter === !0 ? n = t.flightsFiltered || e : a && a.filter && typeof a.filter == "object" ? n = Rt(t, a.filter) : n = e;
+ const d = r?.planesContainer || t.mainCard?.shadowRoot && t.mainCard.shadowRoot.getElementById("planes");
+ if (!d) return;
+ d.innerHTML = "";
+ const { range: l, scaleFactor: u, centerX: _, centerY: m } = o;
+ if (!l || !u || _ === void 0 || m === void 0) return;
+ const g = l * 1.15, y = a?.["aircraft-marker"]?.default;
+ n.slice().reverse().forEach((k) => {
+ const A = k.distance_to_tracker;
+ if (A !== void 0 && A <= g) {
+ const R = document.createElement("div");
+ R.className = "plane";
+ const F = k.heading_from_tracker ?? 0, S = _ + Math.cos((F - 90) * Math.PI / 180) * A * u, x = m + Math.sin((F - 90) * Math.PI / 180) * A * u;
+ if (R.style.top = x + "px", R.style.left = S + "px", y?.["aircraft-marker-url"]) {
+ R.classList.add("plane-custom");
+ const L = Qt(y, k.heading ?? 0);
+ R.appendChild(L);
+ } else {
+ const L = document.createElement("div");
+ L.className = "arrow", L.style.transform = `rotate(${k.heading}deg)`, R.appendChild(L), (k.altitude ?? 0) <= 0 ? R.classList.add("plane-small") : R.classList.add("plane-medium");
+ }
+ const $ = document.createElement("div");
+ $.className = "callsign-label", $.textContent = k.callsign ?? k.aircraft_registration ?? "n/a", d.appendChild($);
+ const C = $.getBoundingClientRect(), M = C.width + 3, w = C.height + 6;
+ $.style.top = x - w + "px", $.style.left = S - M + "px";
+ const E = a["aircraft-marker-size"];
+ E && E !== "normal" && R.classList.add(`marker-size-${E}`), i && i.includes(k.id) && R.classList.add("selected"), R.addEventListener("click", (L) => {
+ L.stopPropagation(), st(t, k);
+ }), $.addEventListener("click", (L) => {
+ L.stopPropagation(), st(t, k);
+ }), d.appendChild(R);
+ }
+ });
+}
+function vt(t, e) {
+ const a = document.createElement("img");
+ return a.setAttribute("src", `https://flagsapi.com/${t}/shiny/16.png`), a.setAttribute("title", `${e}`), a.style.position = "relative", a.style.top = "3px", a.style.left = "2px", a;
+}
+function te(t, e, a) {
+ try {
+ let i = e[a];
+ if (t.config.annotate) {
+ const o = Object.assign({}, e);
+ t.config.annotate.filter((r) => r.field === a).forEach((r) => {
+ Ot(t, e, r.conditions) && (o[a] = r.render.replace(/\$\{([^}]*)\}/g, (n, d) => String(o[d] || "")));
+ }), i = String(o[a] || "");
+ }
+ return i;
+ } catch (i) {
+ return console.error(`[FR24Card] flightField error for field '${a}':`, i), "";
+ }
+}
+function ee(t, e) {
+ try {
+ const a = Object.assign({}, e);
+ [
+ "flight_number",
+ "callsign",
+ "aircraft_registration",
+ "aircraft_model",
+ "aircraft_code",
+ "airline",
+ "airline_short",
+ "airline_iata",
+ "airline_icao",
+ "airport_origin_name",
+ "airport_origin_code_iata",
+ "airport_origin_code_icao",
+ "airport_origin_country_name",
+ "airport_origin_country_code",
+ "airport_destination_name",
+ "airport_destination_code_iata",
+ "airport_destination_code_icao",
+ "airport_destination_country_name",
+ "airport_destination_country_code"
+ ].forEach((o) => {
+ a[o] = te(t, a, o);
+ }), a.origin_flag = a.airport_origin_country_code ? vt(a.airport_origin_country_code, a.airport_origin_country_name || "").outerHTML : "", a.destination_flag = a.airport_destination_country_code ? vt(a.airport_destination_country_code, a.airport_destination_country_name || "").outerHTML : "", a.climb_descend_indicator = Math.abs(a.vertical_speed) > 100 ? a.vertical_speed > 100 ? "↑" : "↓" : "", a.alt_in_unit = a.altitude >= 17750 ? `FL${Math.round(a.altitude / 1e3) * 10}` : a.altitude > 0 ? t.units.altitude === "m" ? `${Math.round(a.altitude * 0.3048)} m` : `${Math.round(a.altitude)} ft` : void 0, a.spd_in_unit = a.ground_speed > 0 ? t.units.speed === "kmh" ? `${Math.round(a.ground_speed * 1.852)} km/h` : t.units.speed === "mph" ? `${Math.round(a.ground_speed * 1.15078)} mph` : `${Math.round(a.ground_speed)} kts` : void 0, a.approach_indicator = a.ground_speed > 70 ? a.is_approaching ? "↓" : a.is_receding ? "↑" : "" : "", a.dist_in_unit = `${Math.round(a.distance_to_tracker || 0)} ${t.units.distance}`, a.direction_info = `${Math.round(a.heading_from_tracker || 0)}° ${a.cardinal_direction_from_tracker || ""}`;
+ const i = document.createElement("div");
+ return i.style.clear = "both", i.className = "flight", t.selectedFlights && t.selectedFlights.includes(a.id) && (i.className += " selected"), i.innerHTML = pt(t, "flight_element", a, (o) => (...r) => r?.filter((n) => n).join(o || " ")), i.addEventListener("click", (o) => {
+ o.stopPropagation(), st(t, a);
+ }), i;
+ } catch (a) {
+ console.error("[FR24Card] renderFlight error:", a);
+ const i = document.createElement("div");
+ return i.className = "flight error", i.textContent = `Error rendering flight: ${a}`, i;
+ }
+}
+var bt = {
+ altitude: "ft",
+ speed: "kts",
+ distance: "km"
+}, ae = [
+ {
+ field: "id",
+ comparator: "oneOf",
+ value: "${selectedFlights}",
+ order: "DESC"
+ },
+ {
+ field: "altitude",
+ comparator: "eq",
+ value: 0,
+ order: "ASC"
+ },
+ {
+ field: "closest_passing_distance ?? distance_to_tracker",
+ order: "ASC"
+ }
+], W, Ft = J((() => {
+ W = {
+ img_element: '${flight.aircraft_photo_small ? ` ` : ""}',
+ icon: '${flight.altitude > 0 ? (flight.vertical_speed > 100 ? "airplane-takeoff" : flight.vertical_speed < -100 ? "airplane-landing" : "airplane") : "airport"}',
+ icon_element: ' ',
+ flight_info: '${joinList(" - ")(flight.airline_short, flight.flight_number, flight.callsign !== flight.flight_number ? flight.callsign : "")}',
+ flight_info_element: '${tpl.flight_info}
',
+ header: "${tpl.img_element}${tpl.icon_element}${tpl.flight_info_element}
",
+ aircraft_info: '${joinList(" - ")(flight.aircraft_registration, flight.aircraft_model)}',
+ aircraft_info_element: '${tpl.aircraft_info ? `${tpl.aircraft_info}
` : ""}',
+ departure_info: '${flight.altitude === 0 && flight.time_scheduled_departure ? ` (${new Date(flight.time_scheduled_departure * 1000).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })})` : ""}',
+ origin_info: '${joinList("")(flight.airport_origin_code_iata, tpl.departure_info, flight.origin_flag)}',
+ arrival_info: "",
+ destination_info: '${joinList("")(flight.airport_destination_code_iata, tpl.arrival_info, flight.destination_flag)}',
+ route_info: '${joinList(" -> ")(tpl.origin_info, tpl.destination_info)}',
+ route_element: "${tpl.route_info}
",
+ alt_info: '${flight.alt_in_unit ? "Alt: " + flight.alt_in_unit + flight.climb_descend_indicator : undefined}',
+ spd_info: '${flight.spd_in_unit ? "Spd: " + flight.spd_in_unit : undefined}',
+ hdg_info: '${flight.heading ? "Hdg: " + flight.heading + "°" : undefined}',
+ dist_info: '${flight.dist_in_unit ? "Dist: " + flight.dist_in_unit + flight.approach_indicator : undefined}',
+ flight_status: '${joinList(" - ")(tpl.alt_info, tpl.spd_info, tpl.hdg_info)}
',
+ position_status: '${joinList(" - ")(tpl.dist_info, flight.direction_info)}
',
+ proximity_info: '${flight.is_approaching && flight.ground_speed > 70 && flight.closest_passing_distance < 15 ? `Closest Distance: ${flight.closest_passing_distance} ${units.distance}, ETA: ${flight.eta_to_closest_distance} min` : ""}
',
+ flight_element: "${tpl.header}${tpl.aircraft_info_element}${tpl.route_element}${tpl.flight_status}${tpl.position_status}${tpl.proximity_info}",
+ radar_range: "Range: ${radar_range} ${units.distance}",
+ list_status: "${flights.shown}/${flights.total}"
+ };
+}));
+Ft();
+function yt(t, e) {
+ return e.split(" ?? ").reduce((a, i) => a ?? t[i], void 0);
+}
+function ie(t, e = (a) => a) {
+ return function(a, i) {
+ for (const o of t) {
+ const { field: r, comparator: n, order: d = "ASC" } = o, l = e(o.value), u = yt(a, r), _ = yt(i, r);
+ let m = 0;
+ switch (n) {
+ case "eq":
+ u === l && _ !== l ? m = 1 : u !== l && _ === l && (m = -1);
+ break;
+ case "lt":
+ u < l && _ >= l ? m = 1 : u >= l && _ < l && (m = -1);
+ break;
+ case "lte":
+ u <= l && _ > l ? m = 1 : u > l && _ <= l && (m = -1);
+ break;
+ case "gt":
+ u > l && _ <= l ? m = 1 : u <= l && _ > l && (m = -1);
+ break;
+ case "gte":
+ u >= l && _ < l ? m = 1 : u < l && _ >= l && (m = -1);
+ break;
+ case "oneOf":
+ if (l != null && (Array.isArray(l) || typeof l == "string")) {
+ const g = l.includes(u), y = l.includes(_);
+ g && !y ? m = 1 : !g && y && (m = -1);
+ }
+ break;
+ case "containsOneOf":
+ if (Array.isArray(l) && l.length > 0) {
+ const g = l.some((k) => (Array.isArray(u) || typeof u == "string") && u.includes(k)), y = l.some((k) => (Array.isArray(_) || typeof _ == "string") && _.includes(k));
+ g && !y ? m = 1 : !g && y && (m = -1);
+ }
+ break;
+ default:
+ m = u - _;
+ break;
+ }
+ if (m !== 0) return d.toUpperCase() === "DESC" ? -m : m;
+ }
+ return 0;
+ };
+}
+var P = {
+ flights_entity: "sensor.flightradar24_current_in_area",
+ projection_interval: 5,
+ no_flights_message: "No flights are currently visible. Please check back later.",
+ list: {
+ hide: !1,
+ showListStatus: !0,
+ position: "below"
+ },
+ units: bt,
+ radar: {
+ range: bt.distance === "km" ? 35 : 25,
+ view: "radar",
+ background_map: "none",
+ background_map_opacity: 0,
+ background_map_api_key: "",
+ background_map_light: "color",
+ background_map_dark: "dark",
+ background_map_light_api_key: "",
+ background_map_dark_api_key: ""
+ },
+ sort: ae,
+ templates: W,
+ defines: {}
+}, kt = class {
+ constructor() {
+ this.hass = null, this.config = {}, this.radar = { range: 35 }, this.list = {}, this.templates = {}, this.defines = {}, this.units = {
+ altitude: "ft",
+ speed: "kts",
+ distance: "km"
+ }, this.flightsContext = {}, this.dimensions = {}, this.flights = [], this.selectedFlights = [], this.renderDynamicOnRangeChange = !1, this._leafletMap = null, this.sortFn = () => 0;
+ }
+ setConfig(t) {
+ if (!t) throw new Error("Configuration is missing.");
+ this.config = { ...t }, this.config.flights_entity = t.flights_entity ?? P.flights_entity, this.config.projection_interval = t.projection_interval ?? P.projection_interval, this.config.no_flights_message = t.no_flights_message ?? P.no_flights_message, this.list = {
+ ...P.list,
+ ...t.list
+ }, this.units = {
+ ...P.units,
+ ...t.units
+ }, this.radar = {
+ range: this.units.distance === "km" ? P.radar.range : 25,
+ view: t.radar?.view ?? P.radar.view,
+ background_map: t.radar?.background_map ?? P.radar.background_map,
+ background_map_opacity: t.radar?.background_map_opacity ?? P.radar.background_map_opacity,
+ background_map_api_key: t.radar?.background_map_api_key ?? P.radar.background_map_api_key,
+ background_map_light: t.radar?.background_map_light ?? P.radar.background_map_light,
+ background_map_dark: t.radar?.background_map_dark ?? P.radar.background_map_dark,
+ background_map_light_api_key: t.radar?.background_map_light_api_key ?? P.radar.background_map_light_api_key,
+ background_map_dark_api_key: t.radar?.background_map_dark_api_key ?? P.radar.background_map_dark_api_key,
+ ...t.radar
+ }, this.radar.initialRange = this.radar.range, this.defines = {
+ ...P.defines,
+ ...t.defines
+ }, this.sortFn = ie(t.sort ?? P.sort, (e) => nt(this, e, void 0, (a) => {
+ this.renderDynamicOnRangeChange = a;
+ })), this.templates = {
+ ...P.templates,
+ ...t.templates
+ };
+ }
+ toggleSelectedFlight(t) {
+ this.selectedFlights || (this.selectedFlights = []), this.selectedFlights.includes(t.id) ? this.selectedFlights = this.selectedFlights.filter((e) => e !== t.id) : this.selectedFlights.push(t.id), typeof this.renderDynamicFn == "function" && this.renderDynamicFn();
+ }
+ setRenderDynamic(t) {
+ this.renderDynamicFn = t;
+ }
+ setToggleValue(t, e) {
+ this.config && this.config.toggles && (this.defines[t] = [
+ "true",
+ !0,
+ 1
+ ].includes(e), typeof this.renderDynamicFn == "function" && this.renderDynamicFn());
+ }
+};
+async function ne() {
+ if (q) return q;
+ try {
+ const e = await fetch("/local/flightradar24-card/runways.csv");
+ if (e.ok)
+ return q = await e.text(), q;
+ } catch {
+ }
+ try {
+ const e = await fetch("data/runways.csv");
+ if (e.ok)
+ return q = await e.text(), q;
+ } catch {
+ }
+ const t = await fetch("https://davidmegginson.github.io/ourairports-data/runways.csv");
+ if (!t.ok) throw new Error(`Failed to fetch runway data: ${t.status}`);
+ return q = await t.text(), q;
+}
+async function oe() {
+ if (B) return B;
+ try {
+ const e = await fetch("/local/flightradar24-card/airports.csv");
+ if (e.ok)
+ return B = await e.text(), B;
+ } catch {
+ }
+ try {
+ const e = await fetch("data/airports.csv");
+ if (e.ok)
+ return B = await e.text(), B;
+ } catch {
+ }
+ const t = await fetch("https://davidmegginson.github.io/ourairports-data/airports.csv");
+ if (!t.ok) throw new Error(`Failed to fetch airport data: ${t.status}`);
+ return B = await t.text(), B;
+}
+function G(t) {
+ const e = [];
+ let a = "", i = !1;
+ for (let o = 0; o < t.length; o++) {
+ const r = t[o];
+ r === '"' ? i = !i : r === "," && !i ? (e.push(a), a = "") : a += r;
+ }
+ return e.push(a), e;
+}
+function re(t, e, a, i, o) {
+ let r = 0;
+ a && a === t && (r += 1e3), a && a.startsWith(t) && (r += 500), e === t && (r += 900), e.startsWith(t) && (r += 400), o && `${e}${o}`.includes(t) && (r += 300);
+ const n = i.toUpperCase().split(/[\s,/-]+/);
+ for (const d of n) if (d.startsWith(t)) {
+ r += 250;
+ break;
+ }
+ return i.toUpperCase().includes(t) && (r += 100), r;
+}
+async function se(t) {
+ if (!t || t.length < 2) return [];
+ const e = t.trim().toUpperCase(), a = [], [i, o] = await Promise.all([ne(), oe()]), r = /* @__PURE__ */ new Map(), n = o.split(`
+`), d = G(n[0]), l = d.indexOf("ident"), u = d.indexOf("name"), _ = d.indexOf("iata_code");
+ for (let w = 1; w < n.length; w++) {
+ const E = n[w].trim();
+ if (!E) continue;
+ const L = G(E), O = L[l], z = L[u], T = L[_];
+ O && r.set(O, {
+ name: z || "",
+ iata: T || ""
+ });
+ }
+ const m = i.split(`
+`), g = G(m[0]), y = g.indexOf("airport_ident"), k = g.indexOf("le_ident"), A = g.indexOf("he_ident"), R = g.indexOf("le_latitude_deg"), F = g.indexOf("le_longitude_deg"), S = g.indexOf("he_latitude_deg"), x = g.indexOf("he_longitude_deg"), $ = g.indexOf("le_heading_degT"), C = g.indexOf("he_heading_degT"), M = g.indexOf("length_ft");
+ for (let w = 1; w < m.length; w++) {
+ const E = m[w].trim();
+ if (!E) continue;
+ const L = G(E), O = L[y], z = L[k], T = L[A], c = r.get(O);
+ if (!c) continue;
+ const { name: p, iata: s } = c, h = O.startsWith(e), v = s && s.toUpperCase().startsWith(e), f = p.toUpperCase().includes(e), b = z && `${O}${z}`.includes(e), I = T && `${O}${T}`.includes(e);
+ if (!h && !v && !f && !b && !I) continue;
+ const H = re(e, O, s, p, z || T || "");
+ if (z) {
+ const D = [];
+ s && D.push(s), D.push(O), D.push(`RWY${z}`), p && D.push(`- ${p}`), a.push({
+ displayText: D.join(" "),
+ airportCode: O,
+ airportName: p,
+ iataCode: s,
+ runwayDesignator: z,
+ data: {
+ airportCode: O,
+ runwayDesignator: z,
+ latitude: parseFloat(L[R]),
+ longitude: parseFloat(L[F]),
+ heading: parseFloat(L[$]),
+ length: parseFloat(L[M])
+ },
+ score: H
+ });
+ }
+ if (T) {
+ const D = [];
+ s && D.push(s), D.push(O), D.push(`RWY${T}`), p && D.push(`- ${p}`), a.push({
+ displayText: D.join(" "),
+ airportCode: O,
+ airportName: p,
+ iataCode: s,
+ runwayDesignator: T,
+ data: {
+ airportCode: O,
+ runwayDesignator: T,
+ latitude: parseFloat(L[S]),
+ longitude: parseFloat(L[x]),
+ heading: parseFloat(L[C]),
+ length: parseFloat(L[M])
+ },
+ score: H
+ });
+ }
+ }
+ return a.sort((w, E) => E.score - w.score).slice(0, 10).map(({ score: w, ...E }) => E);
+}
+var q, B, le = J((() => {
+ q = null, B = null;
+})), de = /* @__PURE__ */ It({ Flightradar24CardEditor: () => ct }), ct, Tt = J((() => {
+ le(), Ft(), $t(), ct = class extends HTMLElement {
+ constructor() {
+ super(), this._config = {}, this._openSections = /* @__PURE__ */ new Set(["basic-settings"]), this._openConditions = /* @__PURE__ */ new Set(), this._openFeatures = /* @__PURE__ */ new Set(), this._openAnnotations = /* @__PURE__ */ new Set(), this._mapModal = null, this._internalUpdate = !1, this._shadowRoot = this.attachShadow({ mode: "open" });
+ }
+ setConfig(t) {
+ this._config = { ...t }, this._internalUpdate || this._render(), this._internalUpdate = !1;
+ }
+ get availableFlightEntities() {
+ return this.hass ? Object.keys(this.hass.states).filter((t) => t.includes("flightradar")).sort() : [];
+ }
+ get availableTrackerEntities() {
+ return this.hass ? Object.keys(this.hass.states).filter((t) => t.startsWith("device_tracker.") || t.startsWith("person.") || t.startsWith("zone.")).sort() : [];
+ }
+ get availableFlightFields() {
+ return [
+ {
+ value: "id",
+ label: "ID",
+ group: "Basic"
+ },
+ {
+ value: "flight_number",
+ label: "Flight Number",
+ group: "Basic"
+ },
+ {
+ value: "callsign",
+ label: "Callsign",
+ group: "Basic"
+ },
+ {
+ value: "aircraft_registration",
+ label: "Aircraft Registration",
+ group: "Aircraft"
+ },
+ {
+ value: "aircraft_model",
+ label: "Aircraft Model",
+ group: "Aircraft"
+ },
+ {
+ value: "aircraft_code",
+ label: "Aircraft Code",
+ group: "Aircraft"
+ },
+ {
+ value: "airline",
+ label: "Airline Name",
+ group: "Airline"
+ },
+ {
+ value: "airline_short",
+ label: "Airline Short",
+ group: "Airline"
+ },
+ {
+ value: "airline_iata",
+ label: "Airline IATA",
+ group: "Airline"
+ },
+ {
+ value: "airline_icao",
+ label: "Airline ICAO",
+ group: "Airline"
+ },
+ {
+ value: "airport_origin_name",
+ label: "Origin Airport",
+ group: "Origin"
+ },
+ {
+ value: "airport_origin_code_iata",
+ label: "Origin IATA",
+ group: "Origin"
+ },
+ {
+ value: "airport_origin_country_name",
+ label: "Origin Country",
+ group: "Origin"
+ },
+ {
+ value: "airport_origin_country_code",
+ label: "Origin Country Code",
+ group: "Origin"
+ },
+ {
+ value: "airport_destination_name",
+ label: "Destination Airport",
+ group: "Destination"
+ },
+ {
+ value: "airport_destination_code_iata",
+ label: "Destination IATA",
+ group: "Destination"
+ },
+ {
+ value: "airport_destination_country_name",
+ label: "Destination Country",
+ group: "Destination"
+ },
+ {
+ value: "airport_destination_country_code",
+ label: "Destination Country Code",
+ group: "Destination"
+ },
+ {
+ value: "latitude",
+ label: "Latitude",
+ group: "Position"
+ },
+ {
+ value: "longitude",
+ label: "Longitude",
+ group: "Position"
+ },
+ {
+ value: "altitude",
+ label: "Altitude",
+ group: "Position"
+ },
+ {
+ value: "vertical_speed",
+ label: "Vertical Speed",
+ group: "Movement"
+ },
+ {
+ value: "ground_speed",
+ label: "Ground Speed",
+ group: "Movement"
+ },
+ {
+ value: "heading",
+ label: "Heading",
+ group: "Movement"
+ },
+ {
+ value: "distance_to_tracker",
+ label: "Distance to Tracker",
+ group: "Tracking"
+ },
+ {
+ value: "heading_from_tracker",
+ label: "Heading from Tracker",
+ group: "Tracking"
+ },
+ {
+ value: "cardinal_direction_from_tracker",
+ label: "Cardinal Direction",
+ group: "Tracking"
+ },
+ {
+ value: "is_approaching",
+ label: "Is Approaching",
+ group: "Tracking"
+ },
+ {
+ value: "is_receding",
+ label: "Is Receding",
+ group: "Tracking"
+ },
+ {
+ value: "closest_passing_distance",
+ label: "Closest Passing Distance",
+ group: "Approach"
+ },
+ {
+ value: "eta_to_closest_distance",
+ label: "ETA to Closest",
+ group: "Approach"
+ },
+ {
+ value: "heading_from_tracker_to_closest_passing",
+ label: "Heading to Closest",
+ group: "Approach"
+ }
+ ];
+ }
+ _mapTypeRequiresApiKey(t) {
+ return xt(t);
+ }
+ _backgroundMapOptionsHtml(t) {
+ const e = (a, i) => `${X.filter((o) => o.group === i).map((o) => `${o.label} `).join("")} `;
+ return e("Keyless", "keyless") + e("Requires API key", "keyed");
+ }
+ _themeMapOptionsHtml(t) {
+ return this._backgroundMapOptionsHtml(t);
+ }
+ _themeApiKeyRowHtml(t, e, a, i) {
+ return this._mapTypeRequiresApiKey(a) ? `
+ ` : "";
+ }
+ get validFlightFields() {
+ return new Set(this.availableFlightFields.map((t) => t.value));
+ }
+ get allDefineAndToggleKeys() {
+ const t = /* @__PURE__ */ new Set();
+ return Object.keys(this._config.toggles || {}).forEach((e) => t.add(e)), Object.keys(this._config.defines || {}).forEach((e) => t.add(e)), t;
+ }
+ getUsedDefinesAndToggles() {
+ const t = /* @__PURE__ */ new Set(), e = this._config.templates || {}, a = this._config.filter, i = this._config.sort || [];
+ Object.values(e).forEach((r) => {
+ const n = r.matchAll(/\$\{(\w+)\}/g);
+ for (const d of n) {
+ const l = d[1];
+ this.allDefineAndToggleKeys.has(l) && t.add(l);
+ }
+ });
+ const o = (r) => {
+ r.forEach((n) => {
+ if ("type" in n && (n.type === "AND" || n.type === "OR")) o(n.conditions || []);
+ else if ("type" in n && n.type === "NOT") o([n.condition]);
+ else {
+ const d = n;
+ d.field && this.allDefineAndToggleKeys.has(d.field) && t.add(d.field), d.defined && this.allDefineAndToggleKeys.has(d.defined) && t.add(d.defined);
+ const l = d.value;
+ if (typeof l == "string" && l.startsWith("${") && l.endsWith("}")) {
+ const u = l.slice(2, -1);
+ this.allDefineAndToggleKeys.has(u) && t.add(u);
+ }
+ }
+ });
+ };
+ return a && Array.isArray(a) && o(a), i.forEach((r) => {
+ r.field && this.allDefineAndToggleKeys.has(r.field) && t.add(r.field);
+ }), t;
+ }
+ getUnusedDefinesAndToggles() {
+ const t = this.getUsedDefinesAndToggles(), e = [], a = [];
+ return Object.keys(this._config.toggles || {}).forEach((i) => {
+ t.has(i) || e.push(i);
+ }), Object.keys(this._config.defines || {}).forEach((i) => {
+ t.has(i) || a.push(i);
+ }), {
+ toggles: e,
+ defines: a
+ };
+ }
+ getUsedTemplateKeys() {
+ const t = /* @__PURE__ */ new Set(), e = this._config.templates || {}, a = {
+ ...W,
+ ...e
+ };
+ return [
+ "flight_element",
+ "radar_range",
+ "list_status"
+ ].forEach((i) => {
+ e[i] !== void 0 && t.add(i);
+ }), Object.values(a).forEach((i) => {
+ const o = i.matchAll(/\$\{(\w+)\([\s\S]*?\)\}/g);
+ for (const n of o) {
+ const d = n[1];
+ e[d] !== void 0 && t.add(d);
+ }
+ const r = i.matchAll(/tpl\.(\w+)/g);
+ for (const n of r) {
+ const d = n[1];
+ e[d] !== void 0 && t.add(d);
+ }
+ }), t;
+ }
+ getUnusedTemplates() {
+ const t = this.getUsedTemplateKeys(), e = this._config.templates || {}, a = [];
+ return Object.keys(e).forEach((i) => {
+ t.has(i) || a.push(i);
+ }), a;
+ }
+ validateConditionField(t) {
+ const e = t.split(" ?? ");
+ for (const a of e)
+ if (!this.validFlightFields.has(a) && !this.allDefineAndToggleKeys.has(a))
+ return {
+ valid: !1,
+ error: `Unknown field: "${a}". Not a flight property or define/toggle.`
+ };
+ return { valid: !0 };
+ }
+ _renameConfigKey(t, e, a) {
+ if (e === a || !a.trim()) return;
+ const i = { ...this._config.templates }, o = new RegExp(`\\$\\{${e}\\}`, "g");
+ for (const [d, l] of Object.entries(i))
+ l.includes(`\${${e}}`) && (i[d] = l.replace(o, `\${${a}}`)), t === "template" && (l.includes(`\${tpl.${e}}`) || l.includes(`tpl.${e}`)) && (i[d] = l.replace(new RegExp(`tpl\\.${e}`, "g"), `tpl.${a}`));
+ t === "template" && e in i && (i[a] = i[e], delete i[e]);
+ const r = this._config.filter ? JSON.parse(JSON.stringify(this._config.filter)) : void 0;
+ if (r) {
+ const d = (l) => {
+ l.forEach((u) => {
+ u.type === "AND" || u.type === "OR" ? d(u.conditions || []) : u.type === "NOT" ? d([u.condition]) : (u.field === e && (u.field = a), u.defined === e && (u.defined = a), typeof u.value == "string" && (u.value = u.value.replace(o, `\${${a}}`)));
+ });
+ };
+ d(r);
+ }
+ const n = (this._config.sort || []).map((d) => {
+ if (d.field === e) return {
+ ...d,
+ field: a
+ };
+ if (d.field?.includes(" ?? ")) {
+ const l = d.field.split(" ?? ").map((u) => u === e ? a : u);
+ return {
+ ...d,
+ field: l.join(" ?? ")
+ };
+ }
+ return d;
+ });
+ this._config = {
+ ...this._config,
+ templates: Object.keys(i).length > 0 ? i : void 0,
+ filter: r && r.length > 0 ? r : void 0,
+ sort: n.length > 0 ? n : void 0
+ };
+ }
+ hasValidationErrors() {
+ const t = this.getUnusedDefinesAndToggles();
+ if (t.toggles.length > 0 || t.defines.length > 0 || this.getUnusedTemplates().length > 0) return !0;
+ const e = this._config.filter;
+ if (e && Array.isArray(e) && this._checkConditionsForInvalidFields(e))
+ return !0;
+ const a = this._config.sort || [];
+ for (const i of a) if (i.field && !this.validateConditionField(i.field).valid)
+ return !0;
+ return !1;
+ }
+ _checkConditionsForInvalidFields(t) {
+ for (const e of t) if ("type" in e && (e.type === "AND" || e.type === "OR")) {
+ if (this._checkConditionsForInvalidFields(e.conditions || [])) return !0;
+ } else if ("type" in e && e.type === "NOT") {
+ if (this._checkConditionsForInvalidFields([e.condition])) return !0;
+ } else {
+ const a = e;
+ if (a.field && !this.validateConditionField(a.field).valid || a.defined && !this.allDefineAndToggleKeys.has(a.defined)) return !0;
+ }
+ return !1;
+ }
+ _render() {
+ this.hass && (this._saveOpenSections(), this._shadowRoot.innerHTML = `
@@ -254,7 +1729,34 @@
${this._renderTogglesAndDefinesConfig()}
${this._renderTemplatesConfig()}
- `,this._attachEventListeners(),this._restoreOpenSections())}_saveOpenSections(){this._shadowRoot.querySelectorAll("details").forEach(t=>{const e=t.getAttribute("data-section-id");e&&(t.open?this._openSections.add(e):this._openSections.delete(e));const a=t.getAttribute("data-condition-path");a&&(t.open?this._openConditions.add(a):this._openConditions.delete(a));const i=t.getAttribute("data-feature-id");i&&(t.open?this._openFeatures.add(i):this._openFeatures.delete(i));const o=t.getAttribute("data-annotation-id");o&&(t.open?this._openAnnotations.add(o):this._openAnnotations.delete(o))})}_restoreOpenSections(){this._shadowRoot.querySelectorAll("details").forEach(t=>{const e=t.getAttribute("data-section-id");e&&this._openSections.has(e)&&(t.open=!0);const a=t.getAttribute("data-condition-path");a&&this._openConditions.has(a)&&(t.open=!0);const i=t.getAttribute("data-feature-id");i&&this._openFeatures.has(i)&&(t.open=!0);const o=t.getAttribute("data-annotation-id");o&&this._openAnnotations.has(o)&&(t.open=!0)})}_getStyles(){return`
+ `, this._attachEventListeners(), this._restoreOpenSections());
+ }
+ _saveOpenSections() {
+ this._shadowRoot.querySelectorAll("details").forEach((t) => {
+ const e = t.getAttribute("data-section-id");
+ e && (t.open ? this._openSections.add(e) : this._openSections.delete(e));
+ const a = t.getAttribute("data-condition-path");
+ a && (t.open ? this._openConditions.add(a) : this._openConditions.delete(a));
+ const i = t.getAttribute("data-feature-id");
+ i && (t.open ? this._openFeatures.add(i) : this._openFeatures.delete(i));
+ const o = t.getAttribute("data-annotation-id");
+ o && (t.open ? this._openAnnotations.add(o) : this._openAnnotations.delete(o));
+ });
+ }
+ _restoreOpenSections() {
+ this._shadowRoot.querySelectorAll("details").forEach((t) => {
+ const e = t.getAttribute("data-section-id");
+ e && this._openSections.has(e) && (t.open = !0);
+ const a = t.getAttribute("data-condition-path");
+ a && this._openConditions.has(a) && (t.open = !0);
+ const i = t.getAttribute("data-feature-id");
+ i && this._openFeatures.has(i) && (t.open = !0);
+ const o = t.getAttribute("data-annotation-id");
+ o && this._openAnnotations.has(o) && (t.open = !0);
+ });
+ }
+ _getStyles() {
+ return `
.editor-container {
position: relative;
z-index: 1000;
@@ -370,6 +1872,21 @@
margin: 2px 0;
line-height: 1.3;
}
+ .input-with-help {
+ display: flex;
+ flex-direction: row;
+ align-items: center;
+ gap: 8px;
+ }
+ .input-with-help .full-width {
+ flex: 1;
+ min-width: 0;
+ }
+ .input-with-help .help-text {
+ flex: 0 0 auto;
+ max-width: 60%;
+ margin: 0;
+ }
.item-box {
border: 1px solid var(--divider-color, #ccc);
border-radius: 4px;
@@ -738,7 +2255,10 @@
position: relative;
z-index: 1;
}
- `}_renderBasicSettings(){return`
+ `;
+ }
+ _renderBasicSettings() {
+ return `
Basic
@@ -746,7 +2266,7 @@
Flights Entity:
Select entity...
- ${this.availableFlightEntities.map(t=>`${t} `).join("")}
+ ${this.availableFlightEntities.map((t) => `${t} `).join("")}
@@ -754,20 +2274,20 @@
Location Tracker:
Manual coordinates...
- ${this.availableTrackerEntities.map(t=>`${t} `).join("")}
+ ${this.availableTrackerEntities.map((t) => `${t} `).join("")}
- ${this._config.location_tracker?"":`
+ ${this._config.location_tracker ? "" : `