ol-owm
Version:
Weather layer for OpenLayers and Leaflet using OpenWeatherMap
251 lines (250 loc) • 10.3 kB
JavaScript
import L from "leaflet";
import { getSvgImage } from "./hoocks/get.svg.image";
import { layers } from "./layers";
import { makeLegend, removeLegend } from "./hoocks/make.lengnd";
import { WindAnimation } from "./layers/wind";
import { LeafletAdapter } from "./adapters/leaflet";
const defaultProperties = {
lang: "en",
legend: true,
};
function lonLatToTile(lon, lat, zoom) {
const x = Math.floor(((lon + 180) / 360) * Math.pow(2, zoom));
const y = Math.floor(((1 -
Math.log(Math.tan((lat * Math.PI) / 180) + 1 / Math.cos((lat * Math.PI) / 180)) /
Math.PI) /
2) *
Math.pow(2, zoom));
return { x, y };
}
function formatUnixTime(timestamp, timezoneOffset) {
const localTimestamp = (timestamp + timezoneOffset) * 1000;
const date = new Date(localTimestamp);
const hours = date.getUTCHours().toString().padStart(2, "0");
const minutes = date.getUTCMinutes().toString().padStart(2, "0");
return `${hours}:${minutes}`;
}
export class LeafletWeather {
constructor(map, owmKey, properties = defaultProperties) {
this.activeTileLayer = null;
this.windFetchController = null;
this.activeCities = false;
this.activeKey = null;
this.activeWind = false;
this.update = async () => {
const zoom = Math.floor(this.map.getZoom());
const bounds = this.map.getBounds();
const minTile = lonLatToTile(bounds.getWest(), bounds.getNorth(), zoom);
const maxTile = lonLatToTile(bounds.getEast(), bounds.getSouth(), zoom);
const features = [];
const requests = [];
for (let x = minTile.x; x <= maxTile.x; x++) {
for (let y = minTile.y; y <= maxTile.y; y++) {
const tileKey = `${zoom}/${x}/${y}`;
const url = `https://b.maps.owm.io/weather/cities/${tileKey}.geojson?appid=${this.owmKey}`;
requests.push(fetch(url)
.then((r) => r.json())
.then((geojson) => {
features.push(...geojson.features);
})
.catch((err) => {
console.warn(`Ошибка загрузки тайла ${tileKey}:`, err);
}));
}
}
await Promise.all(requests);
if (!this.activeCities)
return;
if (!this.layerGroup || !this.map.hasLayer(this.layerGroup)) {
this.layerGroup = L.layerGroup().addTo(this.map);
}
else {
this.layerGroup.clearLayers();
}
for (const feature of features) {
const { geometry, properties } = feature;
const [lon, lat] = geometry.coordinates;
const { city, wind_speed = 0, temp = 0 } = properties;
const rawSvg = getSvgImage(properties);
const icon = L.divIcon({
className: "", // убираем стандартные стили Leaflet
html: `
<div style="
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
transform: translateY(-10px);
">
<div style="
color: #000;
font-size: 10px;
font-weight: 600;
line-height: 1.2;
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff,
-1px 1px 0 #fff, 1px 1px 0 #fff;
margin-top: 2px;
">
${parseInt(temp)}°C
</div>
<img src="data:image/svg+xml;charset=utf-8,${encodeURIComponent(rawSvg)}" style="width: 32px; height: 32px; object-fit: cover;" alt="weather" />
<div style="
color: #000;
font-size: 10px;
font-weight: 600;
line-height: 1.2;
text-shadow: -1px -1px 0 #fff, 1px -1px 0 #fff,
-1px 1px 0 #fff, 1px 1px 0 #fff;
margin-top: 2px;
">
${city}<br/>${wind_speed.toFixed(1)}м/с
</div>
</div>
`,
});
L.marker([lat, lon], { icon }).addTo(this.layerGroup);
}
};
this.onMapDoubleClick = async (e) => {
const { lat, lng } = e.latlng;
const url = `https://api.openweathermap.org/data/2.5/weather?lat=${lat}&lon=${lng}&units=metric&lang=en&appid=${this.owmKey}`;
try {
const res = await fetch(url);
const data = await res.json();
const { name, weather, main, wind, sys, timezone, clouds, visibility } = data;
const content = `
<div style="font-family: 'Segoe UI', sans-serif; color: #1a1a1a; min-width: 240px;">
<div style="display: flex; align-items: center; gap: 12px; margin-bottom: 10px;">
<img src="https://openweathermap.org/img/wn/${weather === null || weather === void 0 ? void 0 : weather[0].icon}@2x.png" width="48" height="48" style="
background: #404040;
border-radius: 10px;
"/>
<div>
<div style="font-size: 16px; font-weight: 600;">${name || "Unknown"}, ${sys.country}</div>
<div style="font-size: 13px; color: #666;">${weather === null || weather === void 0 ? void 0 : weather[0].description}</div>
</div>
</div>
<div style="font-size: 14px; line-height: 1.7;">
🌡️ <b>Temperature:</b> ${main.temp.toFixed(1)}°C<br />
🤒 <b>Feels like:</b> ${main.feels_like.toFixed(1)}°C<br />
📈 <b>Max/Min:</b> ${main.temp_max.toFixed(1)}°C / ${main.temp_min.toFixed(1)}°C<br />
💧 <b>Humidity:</b> ${main.humidity}%<br />
🧭 <b>Pressure:</b> ${main.pressure} hPa<br />
☁️ <b>Cloudiness:</b> ${clouds.all}%<br />
👁 <b>Visibility:</b> ${(visibility / 1000).toFixed(1)} km<br />
🌬️ <b>Wind:</b> ${wind.speed.toFixed(1)} m/s ${wind.gust ? `(gusts up to ${wind.gust.toFixed(1)} m/s)` : ""}<br />
↗ <b>Direction:</b> ${wind.deg}°<br />
🌅 <b>Sunrise:</b> ${formatUnixTime(sys.sunrise, timezone)}<br />
🌇 <b>Sunset:</b> ${formatUnixTime(sys.sunset, timezone)}
</div>
</div>
`;
this.popup.setLatLng([lat, lng]).setContent(content).openOn(this.map);
}
catch (e) {
console.warn("Ошибка запроса погоды:", e);
}
};
this.map = map;
this.owmKey = owmKey;
this.properties = properties;
this.popup = L.popup();
this.wind = new WindAnimation(new LeafletAdapter(map), properties.windProperties);
}
status() {
return !!this.layerGroup;
}
layers() {
return layers.map((x) => {
return {
name: x.name,
key: x.key,
};
});
}
setLayer(key) {
if (key === this.activeKey)
return;
// Удаляем текущий слой, если он есть
if (this.activeTileLayer) {
removeLegend("-l");
this.map.removeLayer(this.activeTileLayer);
this.activeTileLayer = null;
this.activeKey = null;
}
// Если key не передан, просто выходим
if (!key)
return;
const layerData = layers.find((x) => x.key === key);
if (!layerData) {
console.warn("Layer not found for key:", key);
return;
}
const tileLayer = L.tileLayer(layerData.url + this.owmKey, {
opacity: 0.7,
attribution: "© <a href='https://openweathermap.org/'>OpenWeatherMap</a>",
});
setTimeout(() => {
if (this.properties.legend && this.properties.legendElement) {
makeLegend("-l", this.properties.legendElement, layerData);
}
}, 0);
tileLayer.addTo(this.map);
this.activeTileLayer = tileLayer;
this.activeKey = key;
}
toggleWind() {
if (this.properties.windDataURL) {
if (!this.activeWind) {
this.activeWind = true;
// отмена предыдущего запроса
if (this.windFetchController) {
this.windFetchController.abort();
}
this.windFetchController = new AbortController();
fetch(this.properties.windDataURL, {
signal: this.windFetchController.signal,
})
.then((r) => r.json())
.then((data) => {
if (this.activeWind) {
this.wind.start(data);
}
})
.catch((e) => {
if (e.name !== "AbortError")
console.warn("Ошибка ветра:", e);
});
}
else {
this.activeWind = false;
this.wind.stop();
// отмена fetch, если ещё идёт
if (this.windFetchController) {
this.windFetchController.abort();
this.windFetchController = null;
}
}
}
}
async show() {
this.activeCities = true;
this.map.doubleClickZoom.disable();
this.map.on("moveend", this.update);
this.map.on("dblclick", this.onMapDoubleClick);
this.map.on("click", () => {
this.map.closePopup();
});
await this.update();
}
hide() {
this.activeCities = false;
this.map.doubleClickZoom.enable();
this.map.off("moveend", this.update);
this.map.off("dblclick", this.onMapDoubleClick);
if (this.layerGroup) {
this.map.removeLayer(this.layerGroup);
this.layerGroup = undefined; // <- вот эта строчка
}
}
}