Mapy.cz API v JavaScriptu – brnkačka nebo tragédie?

Není to tak dávno, co Google zpoplatnil využívání svých map. Od té doby prudce stoupl podíl webů, které využívají API od mapy.cz. Když jsem poprvé viděl dokumentaci, přiznám se, že mě to dost vyděsilo – české komentáře, občas na první pohled nesmyslné metody… Pokusím se v tomto článku přiblížit, jak jsem s SMap API pracoval já. Probereme inicializaci mapy, vykreslení základních ovládacích prvků, práci s markery a clusterování. S tímto tématem úzce souvisí i geolokace – ta si zasluhuje vlastní článek, který (snad) vyjde brzy.

Na první pohled mě vyděsilo to, že vývojáři nejdou moc naproti moderním JS trendům. Nikde jsem pro tyto mapy nenašel NPM balíček, natož nějaké TypeScriptové knihovny, které by pomohly s typováním. Vzhledem k tomu, že jsem mapu chtěl implementovat do React ekosystému, bylo potřeba pár kliček, nicméně i tak se mi podařila vytvořit plnohodnotná mapa, která splňuje vše, co se od ní v moderním webu očekává.

Načtení a vytvoření mapy

Mapu načteme vložením scriptu do stránky následujícím způsobem:

<script type="text/javascript" src="https://api.mapy.cz/loader.js"></script>

Tento script nám do globálního scope přidá proměnou Loader, pomocí které můžeme knihovnu načíst. To můžeme udělat buď synchronně (např. vkládáme-li mapu přímo do stránky – např. do sekce kontakty) nebo asynchronně. To se nám může hodit v případě, kdy mapu chceme použít třeba v modálovém okně a chceme ji načíst až tehdy, kdy ji skutečně potřebujeme.

K synchronnímu načtení slouží metoda .load(), kterou můžeme implementovat v nejnaivnější podobě např. takto:

<script type="text/javascript">Loader.load();</script>

Pokud programujeme např. v Reactu, mapa se dá vyrenderovat v komponentě, která je obalená do MapLoaderu a nemusíme tím strukturu HTML jakkoli měnit. Instanci mapy jsem si v tomhle případě uložil jako instanční proměnnou tak, abych k ní mohl přistupovat v dalších metodách. Níže uvedená ukázka obsahuje implementaci loaderu a inicializaci mapy s výchozí základní vrstvou.

class MapSample extends React.PureComponent {
   state = {
      isMapLoaded: false,
   }

   componentDidMount() {

      // Pokud ještě nebyl script tag vložen
      if (document.getElementById("map-loader-script")) {
         // Vytvoříme script tag s odkazem na SMap API
         const script = document.createElement("script");
         script.src = "https://api.mapy.cz/loader.js";
         script.id = "map-loader-script";
      
         // Po načtení scriptu nastavíme callback
         script.onLoad = () => {
            // Nastavíme asynchronní zpracování
            window.Loader.async = true;
            // Po načtení map nastavíme state a vykreslíme mapu
            window.Loader.load(
               null,
               null,
               this.onMapApiLoaded
            );
         };
         // Script tag přidáme do hlavičky
         document.head.append(scriptTag);
      }
   }
  
   onMapApiLoaded() {
      this.setState({isMapLoaded: true});
   }
  
   render() {
      return (
         <div id="wrapper">
            {this.state.isMapLoaded 
       	       ? <p>Loading...</p>
               : <Map />
            }
         </div>
      )
   }
}

class Map extends React.PureComponent {
   map = null;
   
   componentDidMount() {
      // Inicializujeme mapu a další kód přidáváme zde
      this.map = new window.SMap(
         JAK.gel("map"),
         // Vysvětlení metody fromWGS84 je níže v sekci "Práce se souřadnicemi"
         SMap.Coords.fromWGS84(50, 14),
         9,
      );

      // Přidáme výchozí vrstvu a zapneme jí
      this.map.addDefaultLayer(window.SMap.DEF_BASE).enable();
   }
  
   render() {
      return(
         <div id="map"/>
      );
   }
}

ReactDOM.render(<MapSample />, document.querySelector("#app"))

Mapou si inicializujeme následujícím kódem. Pro umístění mapy musíme využít seznamovskou knihovnu JAK, přičemž doplníme ID html prvku, ke kterému se má mapa navázat. Nastavíme výchozí souřadnice a přiblížení mapy. Mapu si uložíme do nějaké proměnné, abychom později mohli volat další funkce:

const map = new SMap(
   JAK.gel(ID_HTML_PRVKU),
   SMap.Coords.fromWGS84(LAT, LNG),
   DEFAULT_ZOOM
);

Dále je nutné do mapy přidat nějaký podklad. Pokud bychom ho totiž nepřidali, mapu by tvořilo jen bílé místo. Podkladem v tomto případě myslíme vrstvu, která víceméně reprezentuje druh mapy (základní, turistická atd.). Tu přidáme následujícím kódem:

map.addDefaultLayer(SMap.DEF_BASE).enable();

V kódu jsem použil základní mapu. Je to ta samá, jako když navštívíte mapy.cz a nevyberete žádný druh mapy. Je reprezentována konstantou. Existují ale i další, např.:

  • DEF_BASE – již zmíněná základní mapa
  • DEF_TURIST – turistická mapa
  • DEF_OPHOTO – letecká mapa
  • DEF_HYBRID – hybridní (zobrazuje jen minimum informací)
  • DEF_HISTORIC – historická
  • DEF_RELIEF – asi jen hrubý tvar zemského povrchu (? 😀 )
  • DEF_TURIST_WINTER – ukazuje lyžařské sjezdovky vč. barev
  • DEF_GEOGRAPHY

Ovládání mapy

Pokud budeme chtít mapu aplikačně příbližit nebo změnit její vycentrování, uděláme to následujícím způsobem:

map.setCenter(SMap.Coords.fromWGS84(lat, lng));
map.setZoom(zoomLevel);

// Případně obojí najednou
map.setCenterZoom(
   SMap.Coords.fromWGS84(lat, lng),
   zoomLevel,
);

Práce se souřadnicemi

Jak je vidět, SMap do žádné ze svých metod neumožňují předávat souřadnice jako číslo nebo nějaký jednoduchý objekt. Je proto potřeba pro souřadnice vytvořit objekt SMap.Coords (např. pomocí metody fromWGS84, která přijímá gps pozici (viz. předchozí kód).

Ovládací prvky

Takto vytvořená mapa nic neumí. Máme jí vykreslenou, vycentrovanou, přiblíženou, ale uživatel s ní prozatím nemůže provádět žádné interakce. S tím nám pomůže API, jelikož nám poskytuje několik předdefinovaných ovládacích prvků. Nejprve základní ovládání myší:

map.addControl(
   new SMap.Control.Mouse(
      SMap.MOUSE_PAN | SMap.MOUSE_WHEEL | SMap.MOUSE_ZOOM
   )
);
  • MOUSE_PAN – posouvání mapy myší
  • MOUSE_WHEEL a MOUSE_ZOOM – obojí souvisí s přibližováním, pro správné fungování je nutné aby tyto flagy byly použity zároveň

Pokud chceme použít základní ovládací prvky (kompas a zoom) a zároveň umožnit uživatelům mapu ovládat myší, nemusíme nutně specifikovat všechny tyto prvky, ale můžeme je přidat pomocí map.addDefaultControls().

Další komponenty

Pokud by nám nevyhovovala pozice nebo chování výchozích komponent, můžeme místo nich vložit komponenty jednotlivě a rovnou si u nich specifikovat chování. Existuje jich více, níže uvádím příklady komponent, které mi přijdou nejužitečnější:

Zoom

Zoom má na svou jednoduchost poměrně dost konfiguračních položek. Ve výchozím stavu se nám zobrazuje tzv. zoomMenu, které nám indikuje stav přiblížení (úrovně např. „stát“, „město“, „kraj“ atd.). Chování zoomMenu si můžeme přizpůsobit a to dost neintuitivním způsobem, ve které se nám míchá konfigurační objekt a argument konstruktoru. Níže uvedený příklad nastaví:

  • to, že chceme toto expanded menu zobrazit
  • výšku jezdce v indikátoru zoomu na 30px
  • výšku úrovně přiblížení na 10px
  • hover label na tlačítkách plus a mínus
  • popis jednotlivých levelů zoomu – výchozímu stavu by mohlo odpovídat např. 12 – stát, 8 – město atp.
  • absolutní pozici prvku, který obsahuje tlačítka – 100px nahoře a 100px vpravo
const options = {
   titles: ["Popis tl. +", "Popis tl. -"],
   step: 10,
   sliderHeight: 30,
   showZoomMenu: true
};
const control = new SMap.Control.Zoom(
   {
      8: "Zoom 8",
      12: "Zoom 12"
   },
   options,
);
m.addControl(control, {top:"100px", right:"10px"});

Scale

Tato komponenta nám umožňuje zobrazit jakési „měřítko“. Tím mám na mysli takové to pravítko, které nám dává představu, kolik cm na mapě odpovídá reálné vzdálenosti. Jako argument přijímá počet „dílků“, které se mají zobrazit.

const component = new SMap.Control.Scale(4);

Markery

SMap API je poměrně přátelské k práci s markery (v řeči smap se používá termín značky). Pro umístění značky si musíme vytvořit novou vrstvu, značky do ní přidat a poté vrstvu zapnout. Je důležité si uvědomit, že po každém přidání značky se mapa překreslí. Z toho vyplývá, že je lepší nejdříve značky vložit do ještě nevykreslené vrstvy (tím se vyhneme mnohonásobnému zbytečnému překreslení) a teprve poté vrstvu zobrazit. Pokud to tak neuděláme, hrozí nám (zejména s vyššími počty markerů) citelné zpomalení mapy, které opravdu nevypadá dobře.

Pokud mapu používáme v Reactu, není vhodné (zejména, máte-li více markerů najednou) se snažit o tzv. „controlled“ mapu. V praxi to znamená, že stav mapy (jako např. center, zoom nebo vrstvy) neukládáme do state, ale např. do instančních proměnných, případně do useRef hooku. Pokud bychom totiž udělali reaktivní mapu, koledujeme si o performance problémy, způsobené nežádoucími překresleními.

V následující ukázce kódu jsem z pole souřadnic vytvořil markery, přidal je do vrstvy a ještě nad vrtstvou nastavil clusterer. Ten je vhodný v případě, že máme hodně markerů a nechceme je všechny vykreslovat. Můžeme místo toho markery „spojit“ do jednoho a zobrazit je tehdy, když budou mít mezi sebou větší vzdálenost (resp. uživatel mapu dostatečně přiblíží).

// Data se souřadnicemi
const coordsArray = [
   {lat: 49.7870347, lng: 13.4828194, id: "home"},
   {lat: 49.6546218, lng: 13.1848109, id: "work"},
   {lat: 49.9124872, lng: 13.9842114, id: "shop"},
];

// Pro data vytvoříme markery
const markers = coordsArray.map((coordsData) => {
   return new SMap.Marker(
      SMap.Coords.fromWGS84(coordsData.lng, coordsData.lat),
      coordsData.id,
      {
         url: "url-k-obrazku.cz/home.jpg",
      }
   );
})

// Vytvoříme vrstvu pro markery s libovolným ID
const markersLayer = new SMap.Layer.Marker("markersLayer");
// Vytvoříme clusterer - clustering distance definuje maximální vzdálenost, do které se značky "spojí" do sebe.
const clusterer = new SMap.Marker.Clusterer(this.map, CLUSTERING_DISTANCE);
markersLayer.setClusterer(clusterer);

// Všechny markery přidáme do vrstvy se značkami
markers.forEach((marker) => {
   markersLayer.addMarker(marker);
});

// Přidáme vrstvu do mapy
map.addLayer(markersLayer);
// Nakonec vrstvu zapneme. Je důležité ji tehdy, když máme všechny značky přidány abychom se vyhnuli zbytečnému překreslení
markersLayer.enable();

Kliknutí na marker

Pro vyvolání nějaké akce po kliknutí na marker nám pomůžou tzv. signály. Ty fungují podobně jako např. DOM eventy, ale dají se použít pouze pro všechny markery najednou. Musíme si tedy (např. podle ID markeru) vyfiltrovat, kterého markeru se signál týká a na základě toho vykonat určitou akci. V následující ukázce si necháme vyskočit alert na náš marker, který ukazuje domov.

map.getSignals().addListener(this, "marker-click", (e) => {
   if (e.target.getId() === "home") {
      alert("This is home!");
   }
})

To by bylo pro ukázku práce s SMap API vše. Rád bych v některém z dalších článků přiblížil práci s geolokátorem. Díky tomu můžeme podle hledaného textu získat konkrétní souřadnice.

Live mapu můžete nalézt na Bonami v nákupním procesu, případně na detailu produktu v sekci osobní odběr.

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *

Přesunout se na začátek