All checks were successful
Deploy Trade-In / changes (push) Successful in 8s
Deploy Trade-In / test (push) Has been skipped
Deploy Trade-In / build-browser (push) Has been skipped
Deploy Trade-In / build-backend (push) Has been skipped
Deploy Trade-In / build-frontend (push) Successful in 1m57s
Deploy Trade-In / deploy (push) Successful in 52s
108 lines
5.6 KiB
TypeScript
108 lines
5.6 KiB
TypeScript
/**
|
||
* Единая точка работы с Leaflet для карт МЕРЫ.
|
||
*
|
||
* До этого модуля loadLeaflet() и константы LEAFLET_* были дословно
|
||
* скопированы в шести компонентах (MapCard, MapPicker, SaleShareMap,
|
||
* v2/HeroBar, v2/ParamsPanel, v2/SourcesMap). Копии разъезжались: правку
|
||
* атрибуции или версии приходилось вносить шесть раз, и любая пропущенная
|
||
* копия молча оставалась со старым поведением. Здесь один источник правды.
|
||
*
|
||
* Leaflet по-прежнему грузится с CDN (unpkg) с SRI-хэшами, а не как npm-дep —
|
||
* это сознательный статус-кво, менять его отдельной задачей.
|
||
*/
|
||
/* eslint-disable @typescript-eslint/no-explicit-any -- интероп с CDN-библиотекой Leaflet (window.L без типов) */
|
||
|
||
export const LEAFLET_VER = "1.9.4";
|
||
export const LEAFLET_CSS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.css`;
|
||
export const LEAFLET_JS = `https://unpkg.com/leaflet@${LEAFLET_VER}/dist/leaflet.js`;
|
||
export const LEAFLET_CSS_SRI = "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=";
|
||
export const LEAFLET_JS_SRI = "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=";
|
||
|
||
/** Тайлы OSM (стандартный слой). */
|
||
export const OSM_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
||
|
||
/** Максимальный зум стандартного слоя OSM. */
|
||
export const OSM_MAX_ZOOM = 19;
|
||
|
||
/**
|
||
* Атрибуция тайлов OSM — обязательна по ODbL и Attribution Guidelines OSMF.
|
||
*
|
||
* Раньше во всех картах стояло просто "© OpenStreetMap": без ссылки на
|
||
* лицензию и без слова contributors, то есть требованиям не соответствовало.
|
||
* Guidelines требуют кредит «© OpenStreetMap contributors» со ссылкой на
|
||
* https://www.openstreetmap.org/copyright; само имя OpenStreetMap переводить
|
||
* нельзя, поэтому строка остаётся в канонической английской форме.
|
||
*
|
||
* Вставляется Leaflet'ом через innerHTML контрола атрибуции, поэтому это
|
||
* намеренно HTML. Строка — константа модуля, пользовательского ввода в ней
|
||
* нет.
|
||
*/
|
||
export const OSM_ATTRIBUTION =
|
||
'© <a href="https://www.openstreetmap.org/copyright" target="_blank"' +
|
||
' rel="noopener noreferrer">OpenStreetMap</a> contributors';
|
||
|
||
/**
|
||
* Подгружает Leaflet с CDN ровно один раз на страницу, резолвит window.L.
|
||
*
|
||
* Повторные вызовы либо сразу отдают уже готовый window.L, либо подписываются
|
||
* на load уже висящего в DOM <script data-leaflet> — параллельные карты на
|
||
* одной странице не тянут библиотеку дважды.
|
||
*/
|
||
export function loadLeaflet(): Promise<any> {
|
||
return new Promise((resolve, reject) => {
|
||
const w = window as any;
|
||
if (w.L) {
|
||
resolve(w.L);
|
||
return;
|
||
}
|
||
if (!document.querySelector(`link[data-leaflet]`)) {
|
||
const link = document.createElement("link");
|
||
link.rel = "stylesheet";
|
||
link.href = LEAFLET_CSS;
|
||
link.integrity = LEAFLET_CSS_SRI;
|
||
link.crossOrigin = "anonymous";
|
||
link.setAttribute("data-leaflet", "1");
|
||
document.head.appendChild(link);
|
||
}
|
||
const existing = document.querySelector<HTMLScriptElement>(`script[data-leaflet]`);
|
||
if (existing) {
|
||
existing.addEventListener("load", () => resolve(w.L));
|
||
existing.addEventListener("error", () => reject(new Error("leaflet load failed")));
|
||
return;
|
||
}
|
||
const script = document.createElement("script");
|
||
script.src = LEAFLET_JS;
|
||
script.integrity = LEAFLET_JS_SRI;
|
||
script.crossOrigin = "anonymous";
|
||
script.setAttribute("data-leaflet", "1");
|
||
script.onload = () => resolve(w.L);
|
||
script.onerror = () => reject(new Error("leaflet load failed"));
|
||
document.body.appendChild(script);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Обёртка над L.map(): создаёт карту и сразу гасит prefix контрола атрибуции.
|
||
*
|
||
* Зачем: Leaflet 1.9.4 по умолчанию подставляет в prefix украинский флаг
|
||
* (leaflet-src.js: `ukrainianFlag` + `prefix: '<a ...>' + flag + 'Leaflet</a>'`).
|
||
* Политического флага в продуктовых картах быть не должно. setPrefix(false)
|
||
* убирает флаг вместе с собственной подписью Leaflet — это законно, BSD-лицензия
|
||
* Leaflet не требует упоминания в UI.
|
||
*
|
||
* Что НЕ делаем: `attributionControl: false`. Это снесло бы весь контрол вместе
|
||
* с атрибуцией тайлов OSM, которая обязательна по лицензии. Контрол остаётся,
|
||
* из него уходит только prefix.
|
||
*
|
||
* Возвращает ту же карту, что и L.map(), поэтому чейнинг `.setView(...)`
|
||
* работает как раньше.
|
||
*/
|
||
export function createMap(
|
||
L: any,
|
||
container: HTMLElement,
|
||
options?: Record<string, unknown>,
|
||
): any {
|
||
const map = L.map(container, options);
|
||
map.attributionControl?.setPrefix(false);
|
||
return map;
|
||
}
|