feat(gps): Яндекс-подложка переведена на JS API 2.1 (v3 без бесплатного тарифа), общий marker-html для обеих подложек

This commit is contained in:
2026-08-25 14:29:47 +03:00
parent 4018ee0f1b
commit c7aac447bc
10 changed files with 277 additions and 482 deletions

View File

@@ -0,0 +1,46 @@
import type { GpsMapMarker } from './GpsMap';
/**
* Общая HTML-разметка кастомных маркеров GPS-модуля.
* Используется обеими реализациями карты: Leaflet (divIcon) и Яндекс.Карты 2.1
* (templateLayoutFactory) — визуально маркеры одинаковые на обеих подложках.
*/
const DEFAULT_COLOR = '#3b82f6';
// Иконки lucide (car / user) в виде SVG-строк: react-компоненты внутрь
// HTML-маркера карты вставить нельзя, разметка собирается вручную
const CAR_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 17h2c.6 0 1-.4 1-1v-3c0-.9-.7-1.7-1.5-1.9C18.7 10.6 16 10 16 10s-1.3-1.4-2.2-2.3c-.5-.4-1.1-.7-1.8-.7H5c-.6 0-1.1.4-1.4.9l-1.4 2.9A3.7 3.7 0 0 0 2 12v4c0 .6.4 1 1 1h2"/><circle cx="7" cy="17" r="2"/><path d="M9 17h6"/><circle cx="17" cy="17" r="2"/></svg>`;
const USER_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
/** Экранирование пользовательского текста для HTML-разметки маркера */
function escapeHtml(text: string): string {
return text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
/**
* HTML маркера: цветной кружок с иконкой (vehicle/person) или маленькая точка
* (dot) + подпись снизу; offline — приглушённый (opacity 0.5).
* Внутренний контейнер центрируется через translate(-50%, -50%) — точка
* привязки карты должна быть в левом верхнем углу элемента.
*/
export function gpsMarkerHtml(marker: GpsMapMarker): string {
const color = marker.color || DEFAULT_COLOR;
const opacity = marker.offline ? 'opacity:0.5;' : '';
const label = marker.label
? `<div style="margin-top:2px;padding:0 4px;border-radius:4px;font-size:10px;line-height:16px;font-weight:500;background:rgba(255,255,255,0.9);border:1px solid #d4d4d8;white-space:nowrap;max-width:120px;overflow:hidden;text-overflow:ellipsis;color:#18181b;">${escapeHtml(marker.label)}</div>`
: '';
if (marker.kind === 'dot') {
return `<div style="display:flex;flex-direction:column;align-items:center;transform:translate(-50%,-50%);user-select:none;${opacity}">
<div style="width:16px;height:16px;border-radius:50%;border:2px solid #fff;background:${color};box-shadow:0 1px 3px rgba(0,0,0,0.3);" title="${escapeHtml(marker.label || '')}"></div>
${label}
</div>`;
}
const icon = marker.kind === 'person' ? USER_SVG : CAR_SVG;
return `<div style="display:flex;flex-direction:column;align-items:center;transform:translate(-50%,-50%);user-select:none;cursor:pointer;${opacity}">
<div style="display:flex;align-items:center;justify-content:center;width:28px;height:28px;border-radius:50%;border:2px solid #fff;background:${color};box-shadow:0 1px 4px rgba(0,0,0,0.4);">${icon}</div>
${label}
</div>`;
}