Restore Regional Observations map

This commit is contained in:
mrkmntal 2026-04-07 18:08:38 -04:00
commit 1faa580b18
15 changed files with 1006 additions and 302 deletions

View file

@ -3,14 +3,17 @@ import { DateTime } from '../vendor/auto/luxon.mjs';
import { safeJson } from './utils/fetch.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import {
createMap,
addBaseLayers,
setPrimaryLocationMarker,
loadNearbyObservationMarkers,
clearMarkers,
} from './utils/leaflet-weather-map.mjs';
class Radar extends WeatherDisplay {
static metadataUrl = 'https://api.rainviewer.com/public/weather-maps.json';
static baseMapUrl = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}';
static boundaryMapUrl = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
constructor(navId, elemId) {
super(navId, elemId, 'Local Radar');
@ -21,6 +24,7 @@ class Radar extends WeatherDisplay {
this.baseLayer = null;
this.boundaryLayer = null;
this.locationMarker = null;
this.nearbyMarkers = [];
this.radarLayers = [];
this.mapFrames = [];
this.radarHost = '';
@ -42,6 +46,7 @@ class Radar extends WeatherDisplay {
this.map.invalidateSize();
this.map.setView([this.weatherParameters.latitude, this.weatherParameters.longitude], 7);
this.updateLocationMarker();
await this.updateNearbyMarkers();
const radarMetadata = await safeJson(Radar.metadataUrl, {
retryCount: 2,
@ -67,6 +72,7 @@ class Radar extends WeatherDisplay {
} catch (error) {
console.error(`Failed to initialize radar: ${error.message}`);
this.clearRadarLayers();
this.clearNearbyMarkers();
this.timing.totalScreens = 0;
if (this.isEnabled) this.setStatus(STATUS.failed);
}
@ -80,37 +86,8 @@ class Radar extends WeatherDisplay {
throw new Error('Radar map container not found');
}
this.map = window.L.map(mapElement, {
zoomControl: false,
dragging: false,
touchZoom: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
keyboard: false,
tap: false,
attributionControl: false,
preferCanvas: true,
});
this.baseLayer = window.L.tileLayer(Radar.baseMapUrl, {
maxZoom: 10,
minZoom: 1,
crossOrigin: true,
className: 'radar-base-layer',
});
this.baseLayer.addTo(this.map);
this.boundaryLayer = window.L.tileLayer(Radar.boundaryMapUrl, {
maxZoom: 10,
minZoom: 1,
opacity: 0.6,
crossOrigin: true,
className: 'radar-boundary-layer',
});
this.boundaryLayer.addTo(this.map);
this.map = createMap(mapElement);
({ baseLayer: this.baseLayer, boundaryLayer: this.boundaryLayer } = addBaseLayers(this.map));
}
resetRadarLayers() {
@ -162,23 +139,27 @@ class Radar extends WeatherDisplay {
updateLocationMarker() {
if (!this.map) return;
if (this.locationMarker && this.map.hasLayer(this.locationMarker)) {
this.map.removeLayer(this.locationMarker);
}
this.locationMarker = window.L.circleMarker([
this.locationMarker = setPrimaryLocationMarker(
this.map,
this.locationMarker,
this.weatherParameters.latitude,
this.weatherParameters.longitude,
], {
radius: 5,
color: '#000',
weight: 2,
fillColor: '#ff0',
fillOpacity: 1,
interactive: false,
className: 'location-marker',
}).addTo(this.map);
);
}
clearNearbyMarkers() {
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
}
async updateNearbyMarkers() {
if (!this.map) return;
this.clearNearbyMarkers();
this.nearbyMarkers = await loadNearbyObservationMarkers(this.map, {
latitude: this.weatherParameters.latitude,
longitude: this.weatherParameters.longitude,
});
this.nearbyMarkers.forEach((marker) => marker.addTo(this.map));
}
showFrame(screenIndex) {

View file

@ -1,245 +1,115 @@
// regional forecast and observations
// type 0 = observations, 1 = first forecast, 2 = second forecast
// regional observations display
import STATUS from './status.mjs';
import { distance as calcDistance } from './utils/calc.mjs';
import { safeJson, safePromiseAll } from './utils/fetch.mjs';
import { temperature as temperatureUnit } from './utils/units.mjs';
import { getSmallIcon } from './icons.mjs';
import { preloadImg } from './utils/image.mjs';
import { DateTime } from '../vendor/auto/luxon.mjs';
import WeatherDisplay from './weatherdisplay.mjs';
import { registerDisplay } from './navigation.mjs';
import * as utils from './regionalforecast-utils.mjs';
import { getPoint } from './utils/weather.mjs';
import { debugFlag } from './utils/debug.mjs';
import filterExpiredPeriods from './utils/forecast-utils.mjs';
// map offset
const mapOffsetXY = {
x: 240,
y: 117,
};
import {
createMap,
addBaseLayers,
setPrimaryLocationMarker,
loadNearbyObservationMarkers,
clearMarkers,
} from './utils/leaflet-weather-map.mjs';
class RegionalForecast extends WeatherDisplay {
constructor(navId, elemId) {
super(navId, elemId, 'Regional Forecast', true);
// timings
this.timing.totalScreens = 3;
super(navId, elemId, 'Regional Observations', true);
this.timing.totalScreens = 1;
this.map = null;
this.baseLayer = null;
this.boundaryLayer = null;
this.locationMarker = null;
this.nearbyMarkers = [];
this.nearbyMarkersKey = '';
}
async getData(weatherParameters, refresh) {
if (!super.getData(weatherParameters, refresh)) return;
if (!this.weatherParameters?.supportsNoaaDisplays) {
this.data = [];
this.timing.totalScreens = 0;
try {
if (!window.L) {
throw new Error('Leaflet is not available');
}
await this.ensureMap();
this.map.invalidateSize();
this.map.setView([this.weatherParameters.latitude, this.weatherParameters.longitude], 6);
this.locationMarker = setPrimaryLocationMarker(
this.map,
this.locationMarker,
this.weatherParameters.latitude,
this.weatherParameters.longitude,
);
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
this.nearbyMarkersKey = '';
this.timing.totalScreens = 1;
this.setStatus(STATUS.loaded);
return;
} catch (error) {
console.error(`Failed to initialize regional observations: ${error.message}`);
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
this.timing.totalScreens = 0;
if (this.isEnabled) this.setStatus(STATUS.failed);
}
this.timing.totalScreens = 3;
// regional forecast implements a silent reload
// but it will not fall back to previously loaded data if data can not be loaded
// there are enough other cities available to populate the map sufficiently even if some do not load
}
// pre-load the base map
let baseMap = 'images/maps/basemap.webp';
if (weatherParameters.state === 'HI') {
baseMap = 'images/maps/radar-hawaii.png';
} else if (weatherParameters.state === 'AK') {
baseMap = 'images/maps/radar-alaska.png';
}
this.elem.querySelector('.map img').src = baseMap;
async refreshNearbyMarkers() {
if (!this.map || !this.active) return;
// get user's location in x/y
const sourceXY = utils.getXYFromLatitudeLongitude(this.weatherParameters.latitude, this.weatherParameters.longitude, mapOffsetXY.x, mapOffsetXY.y, weatherParameters.state);
this.map.invalidateSize(false);
this.map.setView([this.weatherParameters.latitude, this.weatherParameters.longitude], 6);
// get latitude and longitude limits
const minMaxLatLon = utils.getMinMaxLatitudeLongitude(sourceXY.x, sourceXY.y, mapOffsetXY.x, mapOffsetXY.y, this.weatherParameters.state);
const bounds = this.map.getBounds();
const markerKey = [
this.weatherParameters.latitude.toFixed(2),
this.weatherParameters.longitude.toFixed(2),
bounds.getSouth().toFixed(2),
bounds.getWest().toFixed(2),
bounds.getNorth().toFixed(2),
bounds.getEast().toFixed(2),
].join(':');
// get a target distance
let targetDistance = 2.4;
if (this.weatherParameters.state === 'HI') targetDistance = 1;
if (this.nearbyMarkers.length > 0 && this.nearbyMarkersKey === markerKey) return;
// make station info into an array
const stationInfoArray = Object.values(StationInfo).map((station) => ({ ...station, targetDistance }));
// combine regional cities with station info for additional stations
// stations are intentionally after cities to allow cities priority when drawing the map
const combinedCities = [...RegionalCities, ...stationInfoArray];
// Determine which cities are within the max/min latitude/longitude.
const regionalCities = [];
combinedCities.forEach((city) => {
if (city.lat > minMaxLatLon.minLat && city.lat < minMaxLatLon.maxLat
&& city.lon > minMaxLatLon.minLon && city.lon < minMaxLatLon.maxLon - 1) {
// default to 1 for cities loaded from RegionalCities, use value calculate above for remaining stations
const targetDist = city.targetDistance || 1;
// Only add the city as long as it isn't within set distance degree of any other city already in the array.
const okToAddCity = regionalCities.reduce((acc, testCity) => {
const distance = calcDistance(city.lon, city.lat, testCity.lon, testCity.lat);
return acc && distance >= targetDist;
}, true);
if (okToAddCity) regionalCities.push(city);
}
this.nearbyMarkers = clearMarkers(this.map, this.nearbyMarkers);
this.nearbyMarkers = await loadNearbyObservationMarkers(this.map, {
latitude: this.weatherParameters.latitude,
longitude: this.weatherParameters.longitude,
});
this.nearbyMarkers.forEach((marker) => marker.addTo(this.map));
this.nearbyMarkersKey = markerKey;
}
// get a unit converter
const temperatureConverter = temperatureUnit();
async ensureMap() {
if (this.map) return;
// get regional forecasts and observations using centralized safe Promise handling
const regionalDataAll = await safePromiseAll(regionalCities.map(async (city) => {
try {
const point = city?.point ?? (await getAndFormatPoint(city.lat, city.lon));
if (!point) {
if (debugFlag('verbose-failures')) {
console.warn(`Unable to get Points for '${city.Name ?? city.city}'`);
}
return false;
}
// start off the observation task
const observationPromise = utils.getRegionalObservation(point, city);
const forecast = await safeJson(`https://api.weather.gov/gridpoints/${point.wfo}/${point.x},${point.y}/forecast`);
if (!forecast) {
if (debugFlag('verbose-failures')) {
console.warn(`Regional Forecast request for ${city.Name ?? city.city} failed`);
}
return false;
}
// get XY on map for city
const cityXY = utils.getXYForCity(city, minMaxLatLon.maxLat, minMaxLatLon.minLon, this.weatherParameters.state);
// wait for the regional observation if it's not done yet
const observation = await observationPromise;
if (!observation) return false;
// format the observation the same as the forecast
const regionalObservation = {
daytime: !!/\/day\//.test(observation.icon),
temperature: temperatureConverter(observation.temperature.value),
name: utils.formatCity(city.city),
icon: observation.icon,
x: cityXY.x,
y: cityXY.y,
};
// preload the icon
preloadImg(getSmallIcon(regionalObservation.icon, !regionalObservation.daytime));
// filter out expired periods first, then use the next two periods for forecast
const activePeriods = filterExpiredPeriods(forecast.properties.periods);
// ensure we have enough periods for forecast
if (activePeriods.length < 3) {
console.warn(`Insufficient active periods for ${city.Name ?? city.city}: only ${activePeriods.length} periods available`);
return false;
}
// group together the current observation and next two periods
return [
regionalObservation,
utils.buildForecast(activePeriods[1], city, cityXY),
utils.buildForecast(activePeriods[2], city, cityXY),
];
} catch (error) {
console.error(`Unexpected error getting Regional Forecast data for '${city.name ?? city.city}': ${error.message}`);
return false;
}
}));
// filter out any false (unavailable data)
const regionalData = regionalDataAll.filter((data) => data);
// test for data present
if (regionalData.length === 0) {
this.setStatus(STATUS.noData);
return;
const mapElement = this.elem.querySelector('.leaflet-map');
if (!mapElement) {
throw new Error('Regional observations map container not found');
}
// return the weather data and offsets
this.data = {
regionalData,
mapOffsetXY,
sourceXY,
};
this.setStatus(STATUS.loaded);
this.map = createMap(mapElement);
({ baseLayer: this.baseLayer, boundaryLayer: this.boundaryLayer } = addBaseLayers(this.map));
}
drawCanvas() {
super.drawCanvas();
// break up data into useful values
const { regionalData: data, sourceXY } = this.data;
// draw the header graphics
// draw the appropriate title
const titleTop = this.elem.querySelector('.title.dual .top');
const titleBottom = this.elem.querySelector('.title.dual .bottom');
if (this.screenIndex === 0) {
titleTop.innerHTML = 'Regional';
titleBottom.innerHTML = 'Observations';
} else {
const forecastDate = DateTime.fromISO(data[0][this.screenIndex].time);
titleTop.innerHTML = 'Regional';
titleBottom.innerHTML = 'Observations';
// get the name of the day
const dayName = forecastDate.toLocaleString({ weekday: 'long' });
titleTop.innerHTML = 'Forecast for';
// draw the title
titleBottom.innerHTML = data[0][this.screenIndex].daytime
? dayName
: `${dayName} Night`;
if (this.map) {
this.map.invalidateSize(false);
}
// draw the map
const scale = 640 / (mapOffsetXY.x * 2);
const map = this.elem.querySelector('.map');
map.style.transform = `scale(${scale}) translate(-${sourceXY.x}px, -${sourceXY.y}px)`;
const cities = data.map((city) => {
const fill = {};
const period = city[this.screenIndex];
fill.icon = { type: 'img', src: getSmallIcon(period.icon, !period.daytime) };
fill.city = period.name;
const { temperature } = period;
fill.temp = temperature;
const { x, y } = period;
const elem = this.fillTemplate('location', fill);
elem.style.left = `${x}px`;
elem.style.top = `${y}px`;
return elem;
});
const locationContainer = this.elem.querySelector('.location-container');
locationContainer.innerHTML = '';
locationContainer.append(...cities);
this.finishDraw();
}
async showCanvas(navCmd) {
super.showCanvas(navCmd);
await this.refreshNearbyMarkers();
}
}
const getAndFormatPoint = async (lat, lon) => {
try {
const point = await getPoint(lat, lon);
if (!point) {
return null;
}
return {
x: point.properties.gridX,
y: point.properties.gridY,
wfo: point.properties.gridId,
};
} catch (error) {
throw new Error(`Unexpected error getting point for ${lat},${lon}: ${error.message}`);
}
};
// register display
registerDisplay(new RegionalForecast(6, 'regional-forecast'));

View file

@ -0,0 +1,155 @@
import { safePromiseAll } from './fetch.mjs';
import { loadData } from './data-loader.mjs';
import { getSmallIconFromWmoCode } from '../icons.mjs';
import { getOpenMeteoObservationSnapshot } from './weather.mjs';
const BASE_MAP_URL = 'https://server.arcgisonline.com/ArcGIS/rest/services/Canvas/World_Light_Gray_Base/MapServer/tile/{z}/{y}/{x}';
const BOUNDARY_MAP_URL = 'https://services.arcgisonline.com/ArcGIS/rest/services/Reference/World_Boundaries_and_Places/MapServer/tile/{z}/{y}/{x}';
const DEFAULT_MAX_NEARBY_MARKERS = 7;
const MIN_CITY_DISTANCE_METERS = 25000;
const MIN_MARKER_PIXEL_DISTANCE = 85;
let radarCitiesCache = null;
const createMap = (mapElement) => window.L.map(mapElement, {
zoomControl: false,
dragging: false,
touchZoom: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
keyboard: false,
tap: false,
attributionControl: false,
preferCanvas: true,
});
const addBaseLayers = (map) => {
const baseLayer = window.L.tileLayer(BASE_MAP_URL, {
maxZoom: 10,
minZoom: 1,
crossOrigin: true,
className: 'radar-base-layer',
}).addTo(map);
const boundaryLayer = window.L.tileLayer(BOUNDARY_MAP_URL, {
maxZoom: 10,
minZoom: 1,
opacity: 0.6,
crossOrigin: true,
className: 'radar-boundary-layer',
}).addTo(map);
return { baseLayer, boundaryLayer };
};
const setPrimaryLocationMarker = (map, existingMarker, latitude, longitude) => {
if (existingMarker && map.hasLayer(existingMarker)) {
map.removeLayer(existingMarker);
}
return window.L.circleMarker([latitude, longitude], {
radius: 5,
color: '#000',
weight: 2,
fillColor: '#ff0',
fillOpacity: 1,
interactive: false,
className: 'location-marker',
}).addTo(map);
};
const loadRadarCities = async () => {
if (!radarCitiesCache) {
radarCitiesCache = await loadData('radarcities');
}
return radarCitiesCache ?? [];
};
const selectNearbyCities = (map, sourceLocation, cities, options = {}) => {
const {
maxMarkers = DEFAULT_MAX_NEARBY_MARKERS,
minCityDistanceMeters = MIN_CITY_DISTANCE_METERS,
minMarkerPixelDistance = MIN_MARKER_PIXEL_DISTANCE,
} = options;
const bounds = map.getBounds();
const currentLatLng = window.L.latLng(sourceLocation.latitude, sourceLocation.longitude);
const visibleCities = cities
.filter((city) => bounds.contains([city.lat, city.lon]))
.filter((city) => currentLatLng.distanceTo([city.lat, city.lon]) > minCityDistanceMeters)
.map((city) => ({
...city,
distance: currentLatLng.distanceTo([city.lat, city.lon]),
point: map.latLngToContainerPoint([city.lat, city.lon]),
}))
.sort((a, b) => a.distance - b.distance);
const selected = [];
visibleCities.forEach((city) => {
if (selected.length >= maxMarkers) return;
const overlaps = selected.some((existingCity) => existingCity.point.distanceTo(city.point) < minMarkerPixelDistance);
if (!overlaps) selected.push(city);
});
if (selected.length === 0 && visibleCities.length > 0) {
selected.push(visibleCities[0]);
}
return selected;
};
const buildNearbyWeatherMarker = (city, observation) => {
const icon = getSmallIconFromWmoCode(observation.weatherCode, observation.isDay);
const markerHtml = `
<div class="nearby-weather-marker-inner">
<div class="city">${city.name}</div>
<div class="details">
<div class="temp">${Math.round(observation.temperature)}</div>
<img src="${icon}" alt="${city.name} weather" />
</div>
</div>`;
return window.L.marker([city.lat, city.lon], {
icon: window.L.divIcon({
html: markerHtml,
className: 'nearby-weather-marker',
iconSize: [108, 52],
iconAnchor: [54, 26],
}),
interactive: false,
zIndexOffset: 500,
});
};
const clearMarkers = (map, markers) => {
if (!map || !markers?.length) return [];
markers.forEach((marker) => {
if (map.hasLayer(marker)) map.removeLayer(marker);
});
return [];
};
const loadNearbyObservationMarkers = async (map, sourceLocation, options = {}) => {
const radarCities = await loadRadarCities();
const nearbyCities = selectNearbyCities(map, sourceLocation, radarCities, options);
if (!nearbyCities.length) return [];
const nearbyObservations = await safePromiseAll(nearbyCities.map(async (city) => {
const observation = await getOpenMeteoObservationSnapshot(city.lat, city.lon);
if (!observation || observation.temperature === null) return null;
return { city, observation };
}));
return nearbyObservations
.filter((entry) => entry)
.map(({ city, observation }) => buildNearbyWeatherMarker(city, observation));
};
export {
createMap,
addBaseLayers,
setPrimaryLocationMarker,
loadNearbyObservationMarkers,
clearMarkers,
};

View file

@ -8,6 +8,13 @@ const OPEN_METEO_FORECAST_PARAMETERS = [
'models=best_match',
].join('&');
const OPEN_METEO_RADAR_OBSERVATION_PARAMETERS = [
'hourly=temperature_2m,weather_code,is_day',
'forecast_days=1',
'timezone=auto',
'models=best_match',
].join('&');
const getPoint = async (lat, lon) => {
const point = await safeJson(`https://api.weather.gov/points/${lat.toFixed(4)},${lon.toFixed(4)}`);
if (!point) {
@ -48,6 +55,36 @@ const getAggregatedOpenMeteoForecast = async (lat, lon) => {
};
};
const getOpenMeteoObservationSnapshot = async (lat, lon) => {
const forecast = await safeJson(`https://api.open-meteo.com/v1/forecast?latitude=${lat}&longitude=${lon}&${OPEN_METEO_RADAR_OBSERVATION_PARAMETERS}`);
if (!forecast?.hourly?.time?.length) {
if (debugFlag('verbose-failures')) {
console.warn(`Unable to get Open-Meteo radar observation snapshot for ${lat},${lon}`);
}
return false;
}
const now = Date.now();
let nearestIndex = 0;
let nearestDelta = Number.POSITIVE_INFINITY;
forecast.hourly.time.forEach((time, index) => {
const delta = Math.abs(new Date(time).getTime() - now);
if (delta < nearestDelta) {
nearestDelta = delta;
nearestIndex = index;
}
});
return {
time: forecast.hourly.time[nearestIndex],
temperature: forecast.hourly.temperature_2m?.[nearestIndex] ?? null,
weatherCode: forecast.hourly.weather_code?.[nearestIndex] ?? 0,
isDay: Boolean(forecast.hourly.is_day?.[nearestIndex] ?? 1),
timezone: forecast.timezone,
};
};
const weatherConditions = [
{ codes: [0], text: ['Clear sky'] },
{ codes: [1, 2, 3], text: ['Mainly clear', 'Partly cloudy', 'Overcast'] },
@ -143,6 +180,7 @@ export {
getPoint,
getOpenMeteoForecast,
getAggregatedOpenMeteoForecast,
getOpenMeteoObservationSnapshot,
aggregateWeatherForecastData,
getConditionText,
};

View file

@ -157,6 +157,52 @@
border: 2px solid #000;
border-radius: 50%;
}
.nearby-weather-marker {
background: transparent;
border: 0;
.nearby-weather-marker-inner {
display: flex;
flex-direction: column;
align-items: center;
min-width: 72px;
padding: 2px 4px;
background: rgba(18, 34, 61, 0.88);
border: 1px solid #000;
box-shadow: 1px 1px 0 #000;
color: #fff;
text-align: center;
}
.city {
font-family: 'Star4000 Small';
font-size: 11pt;
line-height: 1;
white-space: nowrap;
margin-bottom: 1px;
text-shadow: 1px 1px 0 #000;
}
.details {
display: flex;
align-items: center;
gap: 2px;
}
.temp {
font-family: 'Star4000';
font-size: 18pt;
line-height: 1;
color: #ff0;
text-shadow: 1px 1px 0 #000;
}
img {
width: auto;
height: 20px;
}
}
}
}

View file

@ -6,48 +6,91 @@
}
.weather-display .main.regional-forecast {
position: relative;
overflow: hidden;
z-index: 0;
.map {
position: absolute;
transform-origin: 0 0;
inset: 0;
}
.location {
position: absolute;
width: 140px;
margin-left: -40px;
margin-top: -35px;
.leaflet-map {
height: 100%;
width: 100%;
background: #061f3e;
}
>div {
position: absolute;
@include u.text-shadow();
}
.leaflet-container {
background: #061f3e;
font-family: inherit;
}
.icon {
top: 26px;
left: 44px;
.radar-base-layer,
.radar-base-layer .leaflet-tile {
filter: grayscale(0.35) brightness(0.58) contrast(1.1) saturate(0.2);
}
img {
max-height: 32px;
}
}
.radar-boundary-layer,
.radar-boundary-layer .leaflet-tile {
filter: grayscale(0.8) brightness(0.7) contrast(1.3) saturate(0.1);
}
.temp {
font-family: 'Star4000 Large';
font-size: 28px;
padding-top: 2px;
color: c.$title-color;
top: 28px;
text-align: right;
width: 40px;
.leaflet-control-container,
.leaflet-control-attribution,
.leaflet-control-zoom {
display: none;
}
.location-marker {
background: #ff0;
border: 2px solid #000;
border-radius: 50%;
}
.nearby-weather-marker {
background: transparent;
border: 0;
.nearby-weather-marker-inner {
display: flex;
flex-direction: column;
align-items: center;
min-width: 72px;
padding: 2px 4px;
background: rgba(18, 34, 61, 0.88);
border: 1px solid #000;
box-shadow: 1px 1px 0 #000;
color: #fff;
text-align: center;
}
.city {
font-family: Star4000;
font-size: 20px;
font-family: 'Star4000 Small';
font-size: 11pt;
line-height: 1;
white-space: nowrap;
margin-bottom: 1px;
@include u.text-shadow();
}
.details {
display: flex;
align-items: center;
gap: 2px;
}
.temp {
font-family: 'Star4000';
font-size: 18pt;
line-height: 1;
color: c.$title-color;
@include u.text-shadow();
}
img {
width: auto;
height: 20px;
}
}
}
}

View file

@ -23,6 +23,8 @@
width: 640px;
height: 60px;
padding-top: 30px;
position: relative;
z-index: 20;
.title {
color: c.$title-color;
@ -172,4 +174,4 @@
.scroll-container {
margin-left: 107px;
}
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long