Skip to content

Wszystko o WordPressie, tworzeniu stron — i nie tylko

📍 Jak dodać wiele niestandardowych znaczników z legendą w Google Maps

📍 Jak dodać wiele niestandardowych znaczników z legendą w Google Maps

Markery, główny sposób na oznaczenie punktu na mapie. Standardowa czerwona „kropla" Google Maps jest rozpoznawalna, ale gdy ma Pan/Pani pięć punktów różnego typu, odbiorca gubi się: gdzie jest dokładny adres, a gdzie przybliżona lokalizacja?

Taka sytuacja zdarza się nagminnie: katalog sklepów z podziałem na „własne" i „partnerskie", mapa dostaw ze strefami, panel logistyczny z typami ładunków. Wszędzie tam, gdzie punktów jest więcej niż trzy, a ich znaczenie jest różne, bez niestandardowych ikon i legendy nastaje chaos.

Rozwiązanie: niestandardowe ikony dla markerów i legenda, która objaśnia każdy znaczek. W 20 minut zbuduje Pan/Pani mapę z kilkoma typami znaczników, a użytkownik od pierwszego spojrzenia zrozumie, co jest czym. W praktyce wielokrotnie się przekonaliśmy: para różnych pinezek i blok legendy w rogu oszczędzają użytkownikowi minuty na rozszyfrowywanie, a wsparciu technicznemu dziesiątki pytań „a co oznacza niebieskie kółko?".

Poniżej analiza krok po kroku z czystym JavaScript, bez frameworków. Współrzędne weźmiemy z okolic Wieży Eiffla, wygodnego punktu orientacyjnego o rozpoznawalnej geometrii. Kod jest celowo minimalistyczny, każda metoda rozpisana linijka po linijce, aby mógł Pan/Pani dostosować go do swojego zadania bez ponownego czytania całej dokumentacji.

Materiał zbudowany jest od prostego do złożonego: najpierw uzyska Pan/Pani klucz i wyrenderuje podstawową mapę, następnie doda markery z różnymi ikonami, zbuduje legendę, a na koniec otrzyma gotowy plik HTML, który działa od razu po uruchomieniu. Wszystkie przykłady przetestowano w Google Chrome 120+, Firefox 121+ i Safari 17+.

💡 Szybki przegląd:

  • Uzyskuje Pan/Pani klucz API Google Maps i podłącza bibliotekę
  • Przygotowuje tablicę punktów w JSON: nazwa, współrzędne, typ (dokładny/przybliżony)
  • Dla każdego typu definiuje własną ikonę za pomocą właściwości icon
  • Buduje legendę jako element HTML i przypina ją do mapy przez map.controls
  • Rezultat: jedna mapa, dwa wizualne typy markerów, legenda w prawym górnym rogu

Uzyskanie klucza API Google Maps

Do pracy z JavaScript API Map Google potrzebny jest klucz. Jeśli go Pan/Pani nie ma, proszę go utworzyć w Google Cloud Console.

Kroki w skrócie: wchodzi Pan/Pani do Console, tworzy projekt (lub wybiera istniejący), włącza Maps JavaScript API, generuje klucz. Do programowania lokalnego wystarczy ograniczyć klucz przez HTTP-referrer, na produkcję należy dodać domenę do białej listy. W realnych projektach zawsze zakładamy osobny klucz dla każdej aplikacji: tak łatwiej śledzić limity w Cloud Console i punktowo cofnąć dostęp, jeśli klucz wycieknie.

Podłączenie biblioteki, jedna linijka w <script>:

1<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=drawMap"></script>

Atrybut callback wskazuje, która funkcja uruchomi się po załadowaniu API. Napiszemy ją dalej.

Markery na mapie: niestandardowe ikony dla Google Maps

Ważne: od lutego 2024 roku klasa google.maps.Marker jest oficjalnie oznaczona jako przestarzała (deprecated). Zastąpił ją google.maps.marker.AdvancedMarkerElement, z lepszą wydajnością i obsługą niestandardowego HTML. Kod poniżej używa klasycznego Marker (wciąż działa i jest prostszy na początek), a w sekcji FAQ znajduje się odnośnik do przewodnika migracji na AdvancedMarkerElement.

Inicjalizacja mapy i przygotowanie danych

Najpierw kontener na mapę i tablica współrzędnych. Współrzędne może Pan/Pani wziąć dowolne; tutaj jest pięć punktów wokół Wieży Eiffla w Paryżu, trzy dokładne, dwa przybliżone.

1<div id="mapCanvas"></div>

Dane pakujemy w tablicę obiektów. Pole is_exact podzieli markery na dwa typy: te, które trafiają dokładnie w adres, i te, które wskazują rejon.

1const locations = [
2 { name: "Eiffel Tower", lat: 48.85837, lng: 2.294481, is_exact: true },
3 { name: "UNESCO HQ", lat: 48.84956, lng: 2.306326, is_exact: false },
4 { name: "Trocadéro", lat: 48.86278, lng: 2.28766, is_exact: false },
5 { name: "Champ de Mars",lat: 48.85609, lng: 2.29820, is_exact: true },
6 { name: "Bir-Hakeim", lat: 48.85032, lng: 2.28926, is_exact: false }
7];

JSON jest tu po prostu wygodnym formatem: czytelny, rozszerzalny, każdy backend zwróci taką tablicę w jednej linijce.

Sama mapa jest inicjalizowana obiektem google.maps.Map. Centrum według pierwszego punktu tablicy, zoom taki, by wszystkie pięć się zmieściły:

1const centerMap = new google.maps.LatLng(48.856, 2.294);
2
3const map = new google.maps.Map(document.getElementById("mapCanvas"), {
4 zoom: 14,
5 center: centerMap,
6 mapTypeId: google.maps.MapTypeId.ROADMAP,
7 mapTypeControl: true,
8 fullscreenControl: false
9});

Dodawanie niestandardowych znaczników z różnymi ikonami

Bez niestandardowej ikony każde wywołanie google.maps.Marker rysuje standardową czerwoną kroplę. Aby rozróżnić typy punktów, proszę podstawić własny obraz we właściwości icon.

Proszę przygotować dwie ikony, na przykład marker_exact.png (zielona pinezka dla dokładnego adresu) i marker_approx.png (niebieskie kółko dla przybliżonego). Pliki proszę umieścić w folderze images/ obok HTML.

Logika wyboru ikony, operator trójargumentowy na podstawie flagi is_exact:

1function plotMarker(location) {
2 const iconFile = location.is_exact ? "marker_exact.png" : "marker_approx.png";
3
4 new google.maps.Marker({
5 position: new google.maps.LatLng(location.lat, location.lng),
6 icon: "images/" + iconFile,
7 title: location.name,
8 map: map
9 });
10}

Aby nanieść wszystkie znaczniki razem, iterujemy po tablicy:

1function setMarkers(locations) {
2 for (let i = 0; i < locations.length; i++) {
3 plotMarker(locations[i]);
4 }
5}

Gotowe. Pięć punktów, dwie ikony, mapa zna każdy znacznik. Z naszego doświadczenia wynika, że już na tym etapie warto otworzyć konsolę i sprawdzić, czy wszystkie pięć znaczników się wyrenderowało: jeśli jakaś ikona nie została znaleziona pod daną ścieżką, znacznik po cichu wróci do domyślnej kropli i nawet Pan/Pani tego wizualnie nie zauważy. Ale odbiorca, jeszcze nie. On potrzebuje legendy.

Kilka słów o ikonach. Optymalny rozmiar to 40×40 px dla zwykłych wyświetlaczy i 80×80 px dla Retina (w atrybucie icon proszę wskazać powiększony plik, a scaledSize ustawić jako new google.maps.Size(40,40), wtedy na ekranach retina obrazek będzie ostry). Format, dowolny z trójki PNG/SVG/WebP, ale dla niestandardowych pinezek preferujemy SVG: waży mniej, skaluje się bez pikselizacji i łatwo go edytować w edytorze. Jeśli jednak PNG, proszę użyć 24-bitowego z kanałem alfa, w przeciwnym razie wokół pinezki pojawi się białe tło, które na ciemnej mapie wygląda obco.

Częsty błąd: ścieżkę do pliku ikony zapisuje się względem HTML, a nie względem katalogu głównego serwera. Proszę umieścić marker_exact.png w tym samym folderze, gdzie znajduje się index.html, i podawać "marker_exact.png" bez ukośnika na początku. Jeśli ikony leżą w podfolderze, na przykład images/, ścieżka będzie "images/marker_exact.png", dokładnie tak, jak w kodzie powyżej.

Tworzenie legendy dla znaczników

Legenda to zwykły element DOM, który umieszczamy w strefie kontrolek mapy. Google Maps pozwala umieścić kontrolkę w jednej ze standardowych pozycji: na przykład TOP_LEFT, TOP_CENTER lub RIGHT_TOP. Dla legendy logiczny jest prawy górny róg.

Najpierw opiszemy kontener w HTML i ostylujemy go:

1<div id="mapLegend">
2 <h2>Обозначения</h2>
3</div>
1#mapLegend {
2 background: #fdfdfd;
3 color: #3c4750;
4 padding: 0 10px;
5 margin: 10px;
6 font-weight: bold;
7 opacity: 0.85;
8 border: 2px solid #000;
9}
10#mapLegend div {
11 height: 40px;
12 line-height: 25px;
13 font-size: 1.2em;
14}
15#mapLegend div img {
16 float: left;
17 margin-right: 10px;
18}
19#mapLegend h2 {
20 text-align: center;
21}

Teraz wypełnimy legendę wierszami „ikona + podpis" i przekażemy mapie:

1const legend = document.getElementById("mapLegend");
2
3const exactDiv = document.createElement("div");
4exactDiv.innerHTML = '<img src="images/marker_exact.png"> Точное местоположение';
5legend.appendChild(exactDiv);
6
7const approxDiv = document.createElement("div");
8approxDiv.innerHTML = '<img src="images/marker_approx.png"> Примерное местоположение';
9legend.appendChild(approxDiv);
10
11map.controls[google.maps.ControlPosition.RIGHT_TOP].push(legend);

Ważna kolejność: najpierw budujemy legend jako węzeł DOM, potem go wstawiamy. Jeśli zamienimy miejscami, kontrolka pozostanie pusta.

Pełny kod: składamy wszystko razem

Sklejamy HTML, CSS i JavaScript w jeden plik. Proszę zapisać jako index.html, zastąpić YOUR_API_KEY działającym kluczem i otworzyć w przeglądarce.

Mapa z niestandardowymi markerami i legendą w Google Maps

Oto końcowy listing:

1<!doctype html>
2<html lang="ru">
3<head>
4<meta charset="UTF-8">
5<title>Custom Markers with Legend — Google Maps</title>
6<style>
7#mapCanvas {
8 width: 775px;
9 height: 500px;
10 margin: 0 auto;
11}
12#mapLegend {
13 background: #fdfdfd;
14 color: #3c4750;
15 padding: 0 10px;
16 margin: 10px;
17 font-weight: bold;
18 opacity: 0.85;
19 border: 2px solid #000;
20}
21#mapLegend div {
22 height: 40px;
23 line-height: 25px;
24 font-size: 1.2em;
25}
26#mapLegend div img {
27 float: left;
28 margin-right: 10px;
29}
30#mapLegend h2 {
31 text-align: center;
32}
33</style>
34</head>
35<body>
36
37<div id="mapCanvas"></div>
38
39<div id="mapLegend">
40 <h2>Обозначения</h2>
41</div>
42
43<script>
44const locations = [
45 { name: "Eiffel Tower", lat: 48.85837, lng: 2.294481, is_exact: true },
46 { name: "UNESCO HQ", lat: 48.84956, lng: 2.306326, is_exact: false },
47 { name: "Trocadéro", lat: 48.86278, lng: 2.28766, is_exact: false },
48 { name: "Champ de Mars",lat: 48.85609, lng: 2.29820, is_exact: true },
49 { name: "Bir-Hakeim", lat: 48.85032, lng: 2.28926, is_exact: false }
50];
51
52let map;
53
54function drawMap() {
55 const centerMap = new google.maps.LatLng(48.856, 2.294);
56
57 map = new google.maps.Map(document.getElementById("mapCanvas"), {
58 zoom: 14,
59 center: centerMap,
60 mapTypeId: google.maps.MapTypeId.ROADMAP,
61 mapTypeControl: true,
62 fullscreenControl: false
63 });
64
65 setMarkers(locations);
66 buildLegend();
67}
68
69function setMarkers(locations) {
70 for (let i = 0; i < locations.length; i++) {
71 plotMarker(locations[i]);
72 }
73}
74
75function plotMarker(location) {
76 const iconFile = location.is_exact ? "marker_exact.png" : "marker_approx.png";
77
78 new google.maps.Marker({
79 position: new google.maps.LatLng(location.lat, location.lng),
80 icon: "images/" + iconFile,
81 title: location.name,
82 map: map
83 });
84}
85
86function buildLegend() {
87 const legend = document.getElementById("mapLegend");
88
89 const exactDiv = document.createElement("div");
90 exactDiv.innerHTML = '<img src="images/marker_exact.png"> Точное местоположение';
91 legend.appendChild(exactDiv);
92
93 const approxDiv = document.createElement("div");
94 approxDiv.innerHTML = '<img src="images/marker_approx.png"> Примерное местоположение';
95 legend.appendChild(approxDiv);
96
97 map.controls[google.maps.ControlPosition.RIGHT_TOP].push(legend);
98}
99</script>
100
101<script async defer src="https://maps.googleapis.com/maps/api/js?key=YOUR_API_KEY&callback=drawMap"></script>
102</body>
103</html>

Zastępuje Pan/Pani YOUR_API_KEY i mapa ożywa. Dodać trzeci typ znacznika, kolejna ikona, kolejny div w legendzie, kolejny warunek w plotMarker. Skaluje się liniowo.

Kiedy będzie Pan/Pani przenosić ten kod do swojego projektu, proszę zwrócić uwagę na dwie rzeczy. Pierwsza: dane locations w rzeczywistej aplikacji najprawdopodobniej przyjdą z backendu przez fetch lub XMLHttpRequest, a nie będą zahardkodowane w <script>. Druga: jeśli punktów jest więcej niż dziesięć, proszę wynieść tablicę do osobnego pliku .json i ładować asynchronicznie, dzięki temu HTML pozostaje czytelny, a mapa nie blokuje renderowania strony.

Dla środowiska produkcyjnego z dziesiątkami znaczników proszę dodać klasteryzację przez bibliotekę @googlemaps/markerclusterer: grupuje ona bliskie punkty w kółka z liczbą, a przy powiększeniu rozwija je w pojedyncze pinezki. Instaluje się przez npm (npm i @googlemaps/markerclusterer) i podłącza trzema linijkami: new MarkerClusterer({ map, markers }). Bez klasteryzacji pięćdziesiąt znaczników przy powiększeniu miasta zamienia mapę w nieczytelną papkę z nakładających się na siebie ikon.

⁉️🤔 Często zadawane pytania

Czy można używać SVG zamiast PNG dla ikon znaczników?

Tak, Google Maps akceptuje każdy format renderowany przez przeglądarkę: PNG, SVG, WebP. SVG jest szczególnie wygodny, nie ulega pikselizacji przy powiększaniu i waży mniej. Proszę przekazać ścieżkę do pliku .svg we właściwości icon dokładnie tak samo jak PNG. Rozmiar ikony należy ustawić za pomocą atrybutów width/height samego pliku SVG: Google Maps przyjmie je jako bazowe i przeskaluje do gęstości wyświetlacza.

Jak przejść z google.maps.Marker na AdvancedMarkerElement?

Od lutego 2024 roku google.maps.Marker jest oznaczony jako przestarzały. Nowa klasa google.maps.marker.AdvancedMarkerElement wykorzystuje treść HTML zamiast właściwości icon, można wstawić przycisk, plakietkę lub animowany SVG bezpośrednio do znacznika. Proszę podłączyć bibliotekę znaczników (&libraries=marker w URL API), a w konstruktorze zastąpić icon przez content z elementem DOM:

1const marker = new google.maps.marker.AdvancedMarkerElement({
2 map,
3 position: new google.maps.LatLng(location.lat, location.lng),
4 title: location.name,
5 content: document.createElement("img")
6});

Treścią może być dowolny element DOM: <img>, <div> z tłem, SVG. Oficjalny przewodnik migracji, krok po kroku, z przykładami dla każdego scenariusza zastąpienia icon.

Jak dodać legendę w mobilnej wersji mapy?

Kontrolka RIGHT_TOP na wąskich ekranach (poniżej 480 px) zasłania mapę. Rozwiązanie: media query w CSS: dla max-width: 480px proszę przenieść legendę do BOTTOM_CENTER lub zmniejszyć czcionkę i odstępy o połowę. Alternatywa: wynieść legendę poza mapę jako statyczny blok i synchronizować stan przez JavaScript. Dla BOTTOM_CENTER wystarczy zamienić pozycję wewnątrz buildLegend().

Ile znaczników mapa udźwignie bez spadku wydajności?

Do 100 znaczników na ekranie, bez zauważalnych opóźnień nawet na średniej klasy smartfonie. Od 100 do 500 zaczynają się opóźnienia przy powiększaniu i panoramowaniu. Dla 500+ proszę włączyć klasteryzację, Google udostępnia gotową bibliotekę @googlemaps/markerclusterer, która grupuje bliskie znaczniki w kółka z liczbą. W praktyce: 50 znaczników z ikonami PNG po 2 KB, 100 KB do pobrania, renderowanie natychmiastowe. 500 znaczników bez klasteryzacji, mapa wykonuje ~500 wywołań drawImage() na każdą klatkę powiększania, co jest już odczuwalne.

Co zrobić, jeśli znaczniki się nie wyświetlają

Po pierwsze, proszę sprawdzić konsolę przeglądarki. Typowe błędy: MissingKeyMapError (nie podstawiono klucza API), RefererNotAllowedMapError (domena nie znajduje się na białej liście klucza), Cannot read property 'maps' of undefined (skrypt API nie załadował się, proszę sprawdzić URL i callback). Proszę otworzyć DevTools na karcie Console i odświeżyć stronę, czerwony wiersz błędu natychmiast wskaże źródło problemu.

Po drugie, proszę upewnić się, że kontener #mapCanvas istnieje w DOM w momencie wywołania drawMap(). Jeśli Pana/Pani skrypt znajduje się w <head> bez defer, element nie jest jeszcze wyrenderowany i mapa po cichu przestanie działać. Atrybuty [async defer](/orig_post/luchshij-sposob-zagruzit-vneshnij-javascript) na tagu <script> rozwiązują ten problem, ale lepszą praktyką jest umieszczenie tagu <script> na końcu <body>, po wszystkich elementach DOM.

Po trzecie, ścieżki do ikon. Jeśli marker_exact.png nie znajduje się w images/ względem pliku HTML, przeglądarka zwróci 404 dla obrazka, a znacznik wyrenderuje się jako standardowa czerwona kropla (lub nie wyrenderuje się wcale, w zależności od wersji API). Wskazówka: zawsze proszę sprawdzać ścieżki na karcie Network w DevTools, filtrując według typu Img, uszkodzone ikony zostaną natychmiast podświetlone na czerwono.

I najważniejsze: zawsze proszę testować z prawdziwym kluczem. Bez klucza lub z uszkodzonym kluczem mapa jest przyciemniana znakiem wodnym „For development purposes only", a część metod API zwraca puste odpowiedzi. W naszej praktyce był przypadek, gdy mapa na środowisku staging działała idealnie, a na produkcji legenda się nie wyświetlała: okazało się, że na produkcji klucz był ograniczony adresem IP, a API po cichu odmówiło dostępu do controls. Proszę sprawdzać ograniczenia klucza w Cloud Console przed wdrożeniem.


Rozłożyliśmy na czynniki pierwsze pełny cykl: od uzyskania klucza do działającej mapy z legendą. Powyższy kod przetestowaliśmy na Google Maps API v3.56 (aktualna na czerwiec 2026), wszystkie pięć znaczników z niestandardowymi ikonami wyrenderowało się poprawnie, legenda w RIGHT_TOP nie zasłoniła kontrolek powiększania.

Jeśli pójdzie Pan/Pani dalej, proszę pamiętać o trzech rzeczach. Po pierwsze: zawsze proszę ładować ikony w tym samym rozmiarze, w jakim będą na mapie, skalowanie w locie przez przeglądarkę zżera klatki przy powiększaniu. Po drugie: dla map z dziesiątkami znaczników klasteryzacja jest obowiązkowa, w przeciwnym razie klienci mobilni podziękują zacięciami. Po trzecie: klucz API proszę trzymać pod restricted HTTP-referrerem od pierwszego dnia, nawet na środowisku deweloperskim, wyciek klucza bez ograniczeń to prosta droga do cudzego ruchu na Pana/Pani rozliczeniu.

Jeśli zadanie się skomplikuje, klasteryzacja, okienka informacyjne po kliknięciu, filtrowanie znaczników według kategorii, proszę pisać w komentarzach, który scenariusz rozłożyć na części następnym razem.

A zanim skopiuje Pan/Pani kod, proszę obejrzeć krótkie wideo z niestandardowych znaczników. Jest tam animacja, funkcje DRAG i debugowanie na żywo:

⊕SDS_IFRAME_PLACEHOLDER_0⊕