21 Paź 2019

Czym zastąpić google maps – Open Street Map oraz Leaflet

Inspirację do napisania tego wpisu było zebranie całej wiedzy w jednym miejscu. Nigdzie nie spotkałem się z całościowym artykułem, wszystko jest porozrzucane po internecie i trzeba nieźle się naszukać.

Wszystkie przykłady będą opierały się o OSM[ Open Street Map ] jest darmowa i dostępna dla wszystkich. Oczywiście jeżeli chcesz ją wykorzystać w większym zakresie to lepiej samemu postawić serwer, który będzie serwować mapy do tego również warto dodać serwer z routingiem bo OSM ich nie posiadają.

Aby mapa w ogóle miała sens należy użyć jakieś biblioteki do obsługi tejże mapy. W naszym przypadku użyjemy [ Leaflet ].

Do każdego punktu postaram się dodać przykłady zrobione w https://jsfiddle.net, najlepiej kliknąć w Edit in JSFiddle aby zobaczyć wynik bo nie zawsze poprawnie na tej stronie się wyświetlają przykłady.

Niestety jedną wielką bolączką OSM jest to, że nie ma w nich zaszytego routingu tak jak w google maps. W związku z tym nie jesteśmy w stanie wyznaczyć trasy a punktu A do punktu B. Jest oczywiście inny serwis który to wspiera i można to z powodzeniem połączyć z OSM.
Więcej informacji za i przeciw mapom OSM w podsumowaniu.

Wpis został podzielony na kilka głównych działów:

  1. Prosta mapa
  2. Pobieramy współrzędne kliknięcia
  3. Pobieramy współrzędne widocznej mapy
  4. Zamiast mapy obrazek
  5. Jak pokazać dużo markerów na mapie
  6. Kontrolowanie różnych grup markerów
  7. Dopasowanie wszystkich markerów do widoku strony
  8. Jak sterować mapą spoza mapy
  9. Podsumowanie

1. PROSTA MAPA

Poniżej znajduje się html. który zawsze jest praktycznie taki sam, czasami coś będzie dodawane ale wtedy napiszę co należy zmodyfikować.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Map</title>
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.5.1/leaflet.css" />
  <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.5.1/leaflet.js"></script>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  <div id="map"></div>
  <script src="script.js"></script>
</body>

</html>

W head dodajemy styl jak i js odpowiadający za leaflet. Mamy też div w którym będzie pojawiać się nasza mapa, oraz plik z naszymi stylami – style.css

html,
body {
  height: 100%;
  margin: 0;
}

#map {
  width: 100%;
  height: 100%;
}

Teraz najważniejsze czyli js.

let zoom = 18;
let lat = 52.2297700;
let lon = 21.0117800;

let map = L.map('map').setView([lat, lon], zoom);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

lat/lon są to współrzędne warszawy oraz zoom czyli powiększenie w jakim pojawi się mapa.

Dobrą praktyką jest dodanie minZoom oraz maxZoom żeby nie pokazywać „całego świata” bo to niepotrzebnie obciąża serwery. Możemy też za pomocą maxBounds ograniczyć przesuwanie mapy do odpowiednich współrzędnych, bo np. po co nam cała mapa jak wystarczy nam widok czy to miasta czy danego kraju.

Poniżej przykład jak ograniczyć mapę za pomocą minZoom/maxZoom

let map = L.map('map', {
  minZoom: 15,
  maxZoom: 18
}).setView([lat, lon], zoom);

Oczywiście mapa bez pina to nie mapa, aby dodać pinezkę wystarczy zadeklarować ją tak jak poniżej.

L.marker([lat, lon]).addTo(map).bindPopup('Środek Warszawy');

Możemy również otworzyć pinezkę od razu po załadowaniu mapy przez dodanie .openPopup();

L.marker([lat, lon]).addTo(map)
  .bindPopup('Środek Warszawy')
  .openPopup();

W binPopup możemy także umieścić html nie tylko tekst, ale także linki, zdjęcia i to co nam w danej chwili jest potrzebne.

L.marker([lat, lon]).addTo(map)
  .bindPopup('<a href="http://kody.wig.pl" target="_blank">Otwórz</a>');

2. POBIERAMY WSPÓŁRZĘDNE KLIKNIĘCIA

Mamy mapę i chcemy dodać marker. Skąd pobrać współrzędne latlng Możemy utworzyć funkcję na potrzeby pozyskania tych danych. Wystarczy że do działającej mapy dodamy poniższą funkcję. Teraz klikasz w miejsce którego chcesz poznać współrzędne i w console.log oraz alert się pojawiają.

map.on('click', function (e) {
  console.log(e.latlng)
  alert(e.latlng)
})

3. POBIERAMY WSPÓŁRZĘDNE WIDOCZNEJ MAPY

Przyda nam się do ograniczenia przesuwania mapy. Możemy pobrać getSouthWest/getNorthEast i ustawić ograniczenie na maxBounds.

let southWest = map.getBounds().getSouthWest().toString();
let northEast = map.getBounds().getNorthEast().toString();

Ale jeszcze lepszym rozwiązaniem jest utworzenie funkcji na zdarzenie dragend pobierze nam te dane.

let sn = [];
map.on('dragend', function onDragEnd() {
  Object.entries(map.getBounds()).forEach(item => {
    console.log(item);
    const array = [item[1].lat, item[1].lng];
    sn.push(array);
  });
  console.table(sn);
});

Teraz aby wykorzystać te dane możemy zrobić tak jak w poniższym kodzie.
Jak to sprawdzić, spróbuj przesunąć mapę jak najdalej w w każdym kierunku. Zaobserwujesz że mapa jest ograniczona przez pewien obszar i dochodzimy do „ściany” tak jakby krawędzi mapy.

let southWest = L.latLng(52.22860444057678, 21.008021235466007);
let northEast = L.latLng(52.23142354438319, 21.015842556953434);
let bounds = L.latLngBounds(southWest, northEast);

let map = L.map('map', {
  maxBounds: bounds,
  minZoom: 5,
  maxZoom: 18
}).setView([lat, lon], zoom);

4. ZAMIAST MAPY OBRAZEK

Do czego może się to przydać, np. do zbudowania poziomów sklepu, tutaj jest taki przykład.

Poniżej prosty przykład dodania zdjęcia.

let bounds = [[0,0], [847,1280]];

let map = L.map('map', {
  crs: L.CRS.Simple,
  maxZoom: 0,
  minZoom: -4,
  maxBounds: bounds
});

let image = L.imageOverlay('http://kody.wig.pl/wp-content/uploads/factory_UML.png', bounds).addTo(map);

map.fitBounds(bounds);

Małe omówienie, let bounds = [[0,0], [847,1280]]; Tablica bounds składa się z dwóch innych tablic, pierwsza odpowiada za padding obrazka po załadowaniu się mapy. Druga tablica są to wymiary zdjęcia w naszym przypadku jest to 847px wysokości i 1280px szerokości.
maxZoom ustawiamy na 0 bo nie chcemy powiększać i tak dużego zdjęcia ale za to minZoom jest na minusie co daje nam możliwość pomniejszania obrazka.
No i najważniejszy element to L.CRS.Simple

5. JAK POKAZAĆ DUŻO MARKERÓW NA MAPIE

Najprościej to użyć pluginu Leaflet.markercluster

Do head naszej strony dodajemy plugin leaflet.markercluster-src.js oraz MarkerCluster.css i MarkerCluster.Default.css

Ten przykład będzie na dość małej próbce markerów, ale zerknijcie na githubie można przetestować sobie nawet 10 i 50 tyś. markerów. I działa to naprawdę sprawnie.
Dobra dodaliśmy niezbędne pliki do head, teraz czas na js.

let addressPoints = [
  [52.22922544734814, 21.008997559547428, 'punk 1'],
  [52.22941930482576, 21.009861230850223, 'punk 2'],
  [52.22966244690615, 21.011084318161014, 'punk 3'],
  [52.22980701724154, 21.01167440414429, 'punk 4'],
  [52.22998444382795, 21.012511253356937, 'punk 5'],
  [52.230188154960125, 21.013487577438358, 'punk 6'],
  [52.230299867119605, 21.01395428180695, 'punk 7'],
  [51.26191485308451, 17.753906250000004, 'okolice 1'],
  [51.23440735163461, 17.578125000000004, 'okolice 2'],
  [50.84757295365389, 17.753906250000004, 'okolice 3'],
  [50.90303283111257, 18.061523437500004, 'okolice 4'],
  [51.04139389812637, 17.446289062500004, 'okolice 5']
];

let markers = L.markerClusterGroup();

for (let i = 0; i < addressPoints.length; i++) {
  let a = addressPoints[i];
  let title = a[2];
  let marker = L.marker(new L.LatLng(a[0], a[1]), { title });
  marker.bindPopup(title);
  markers.addLayer(marker);
}

map.addLayer(markers);

Najpierw tworzymy tablicę składającą się z tablic ze współrzędnymi, trzeci parametr to treść do chmurki która pojawia się po kliknięciu w marker.
Następnie wywołujemy funkcję – L.markerClusterGroup();, później w pętli przypisujemy do tej funkcji poszczególne makery.
Na koniec do mapy dodajemy warstwę tych markerów.

Plugin ten ma wiele opcji i polecam zapoznać się z nim bo naprawdę się przydaje.

6. KONTROLOWANIE RÓŻNYCH GRUP MARKERÓW

Nasz przykład będzie składał się z dwóch grup markerów, jedna grupa będzie widoczna od razu, zaś druga grupa będzie ukryta. Posłużymy się tutaj L.control.layers usługa ta tworzy nam w prawym górnym roku ikonę po najechaniu pojawią się checkboxy z nazwami miejscowości, dodatkowo checkbox który pokaże wszystkie grupy marków razem.

let zoom = 6;
let lat = 52.2297700;
let lon = 21.0117800;

let citiesA = L.layerGroup();
let citiesB = L.layerGroup();
let allCity = L.layerGroup();

L.marker([52.435920583590125, 21.42333984375]).bindPopup('1').addTo(citiesA),
L.marker([52.1267438596429, 21.42333984375]).bindPopup('1').addTo(citiesA),
L.marker([52.03897658307622, 20.950927734375004]).bindPopup('1').addTo(citiesA),
L.marker([52.328625488430184, 20.753173828125004]).bindPopup('1').addTo(citiesA),
L.marker([52.48947038534306, 20.928955078125004]).bindPopup('1').addTo(citiesA);

L.marker([51.19999983412071, 17.215576171875004]).bindPopup('1').addTo(citiesB),
L.marker([50.98609893339354, 17.468261718750004]).bindPopup('1').addTo(citiesB),
L.marker([50.84063582806037, 17.149658203125004]).bindPopup('1').addTo(citiesB),
L.marker([50.972264889367494, 16.743164062500004]).bindPopup('1').addTo(citiesB),
L.marker([51.337475662965225, 16.907958984375004]).bindPopup('1').addTo(citiesB);


let map = L.map('map', {
  minZoom: 0,
  maxZoom: 18,
  layers: [citiesA]
}).setView([lat, lon], zoom);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

let overlays = {
  Warszawa: citiesA,
  Wrocław: citiesB,
  'Wszystkie miasta': allCity
};

overlays['Wszystkie miasta'].on('add', function() {
  setTimeout(function() {
    for (let overlay in overlays) {
      overlays[overlay].addTo(map);
    }
  }, 0);
});

overlays['Wszystkie miasta'].on('remove', function () {
  setTimeout(function () {
    for (let overlay in overlays) {
      map.removeLayer(overlays[overlay]);
    }
  }, 0);
});

let control = L.control.layers(null, overlays).addTo(map);

Najpierw tworzymy trzy grupy markerów L.layerGroup, będą one odpowiadać grupie o nazwie Warszawa, Wrocław oraz Wszystkie miasta

Następnie do tych grup przypisujemy odpowiednie markery. W taki sposób – L.marker([52.435920583590125, 21.42333984375]).bindPopup(‚1’).addTo(citiesA)

Aby pokazać tylko jedną grupę od razu po załadowaniu mapy musimy ją dodać do mapy – layers: [citiesA]

Następnie tworzymy obiekt do którego przypisujemy wszystkie nasze grupy, kluczem jest nazwa która będzie wyświetlana w kontrolce overlays.

Ostatnia rzecz to obsługa przycisku „Wszystkie miasta”. Po kliknięciu dodajemy wszystkie warstwy to mapy.
Odpowiada za to ten kawałek kodu.

overlays['Wszystkie miasta'].on('add', function() {
  setTimeout(function() {
    for (let overlay in overlays) {
      overlays[overlay].addTo(map);
    }
  }, 0);
});

overlays['Wszystkie miasta'].on('remove', function () {
  setTimeout(function () {
    for (let overlay in overlays) {
      map.removeLayer(overlays[overlay]);
    }
  }, 0);
});

No i oczywiście wszystko trzeba dodać do mapy – let control = L.control.layers(null, overlays).addTo(map);

7. DOPASOWANIE WSZYSTKICH MARKERÓW DO WIDOKU STRONY

W naszym przykładzie mamy dokładnie 4 markery, które chcemy pokazać na mapie a dokładnie chodzi o to że należy je dopasować do widoku strony. Chodź mamy ustawiony środek „Warszawa” i zoom ustawiony to i tak map.fitBounds dopasuje nam widok do wszystkich naszych markerów.
Przechodzimy do „mięska” js 😉

let zoom = 18;
let lat = 52.2297700;
let lon = 21.0117800;

let featureGroups = [
  L.marker([52.22966244690615, 21.011084318161014]).bindPopup('A'),
  L.marker([52.234616998160874, 21.008858084678653]).bindPopup('B'),
  L.marker([52.22998444382795, 21.012511253356937]).bindPopup('C'),
  L.marker([52.22858801170828, 21.00593984127045]).bindPopup('D')
];

let map = L.map('map', {
  minZoom: 0,
  maxZoom: 18
}).setView([lat, lon], zoom);

L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
  attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map);

for (let i = 0; i < featureGroups.length; i++) {
  featureGroups[i].addTo(map);
}

let group = new L.featureGroup(featureGroups);
map.fitBounds(group.getBounds());

Krótki opis co dzieje w powyższym kodzie. Najpierw tak jak zawsze mam zadeklarowane współrzędne centrum warszawy, do tego zoom ustawiony na najwyższe powiększenie.
Później tworzę tablicę markerów wraz z opisem bindPopup
Tak jak zawsze mapa jest dodawana do html a dokładnie do div o id=”map”

Później w pętli dodajemy każdy marker do mapy nic szczególnego, ale za to następne dwie linie kodu są najciekawsze.
Do grupy L.featureGroup dodajemy naszą tablicę z markerami, a następnie wywołujemy map.fitBounds(group.getBounds());, która to usługa zmienia nam tak mapę aby wszystkie zadeklarowane markery znalazły się na środku strony i były widoczne.

Oczywiście ta właściwość ma mnóstwo innych opcji, jak dodanie paddingu, animacja itd. Możemy fitBounds stosować nie tylko na grupie ale także na poszczególnych warstwach, pojedynczych markerach, klikamy np. w marker który jest gdzieś z boku mapy a mapa ładnie z markerem centruje się na środek strony. Centrowanie jest pokazanie w następnym punkcie.

8. JAK STEROWAĆ MAPĄ SPOZA MAPY

Mamy trzy linki poza mapą klikając w któryś z nich otwiera się nam marker na mapie i to w dodatku na środku mapy. Najpierw zmodyfikujmy html.

<div id="map"></div>
<a id="A" class="marker-click" href="#">Marker A</a><br>
<a id="B" class="marker-click" href="#">Marker B</a><br>
<a id="C" class="marker-click" href="#">Marker C</a><br>
<script src="script.js"></script>

Zmieniamy również style które odpowiadają za wysokość mapy tak aby nasze markery były widoczne pod mapą.

#map {
  width: 100%;
  height: 90%;
}

Teraz js, będzie praktycznie ten sam co w punkcie wyżej.

let featureGroups = [
  L.marker([52.22966244690615, 21.011084318161014],{title: 'A'}).bindPopup('A'),
  L.marker([52.22998444382795, 21.012511253356937],{title: 'B'}).bindPopup('B'),
  L.marker([52.22870029595543, 21.00647628307342],{title: 'C'}).bindPopup('C')
];

function markerOpen(id) {
  for(let i in featureGroups) {
    const markerId = featureGroups[i].options.title;
    if(markerId === id) {
      featureGroups[i].openPopup();
      centerMarker(featureGroups[i].getLatLng());
    }
  }
}

function centerMarker(latlng) {
  console.log(latlng);
  const marker = L.marker([latlng.lat, latlng.lng]);
  let group = new L.featureGroup([marker]);
  map.fitBounds(group.getBounds());
}

window.addEventListener('DOMContentLoaded', () => {
  const markersDiv = document.querySelectorAll('.marker-click');

  markersDiv.forEach(marker => {
    marker.addEventListener('click', () => {
      markerOpen(marker.id);
    });
  });
});

Tablica featureGroups lekko została zmodyfikowana, dodałem title po którym będziemy otwierać poszczególne markery.

Teraz funkcja markerOpen do której przekazuję id po którym otwieram marker.
Następnie pobieram wszystkie linki z klasą marker-click. Jeżeli kliknę w któryś link, wywołuję funkcję „markerOpen” do której przekazuję id z klikniętego linka.

Jest jeszcze funkcja która po kliknięciu w link centruje nam mapę z markerem – centerMarker i to tyle.

Oczywiście centrowanie można zrobić nie tylko z jednym markerem ale na grupie markerów czy poligonach.

W poniżej mapie widać tylko 2 markery, jeden jest poza obszarem widocznym. Wystarczy że kliknę w link wtedy mapa oraz marker zostaje przesunięty na środek widoku.

9. PODSUMOWANIE

Mapy OSM moim zdaniem mają bardzo dużą przewagę nad google maps, są o wiele dokładniejsze przynajmniej na terenie Polski, porównywałem 1:1
Zerknijcie na to porównanie, z lewej strony OSM z prawej Google Maps różnica jest bardzo duża. Moim zdaniem na korzyść OSM

Dlaczego własny serwis z mapami jest dobrym pomysłem, oczywiście jeżeli przewidujemy duży ruch, podam kilka najważniejszych cech.

Na plus:

  • mapy mogą mieć wygląd taki jaki my chcemy, można sterować wyglądem, kolorami, czcionkami itd,
  • możemy wyłączyć poszczególne punkty POI, do tego ich liczba jest stanowczo większa niż w google
  • mapy są bardzo aktualne, dokładniejsze niż google maps
  • nie musimy mieć całego świata (dużo GB danych), możemy ograniczyć się do kontynentów, państwa czy nawet województw, co stanowczo zmniejsza koszty wprowadzenia takiej mapy jak i czas ich wdrożenia
  • chyba najważniejsza własność, są darmowe

Na minus:

  • nie przespacerujemy się po mieście tak jak w street view
  • oczywiście pieniądze na serwer a raczej serwery przy dużym ruchu + adminstrator. No chyba że sami będziemy tym się zajmować 😉
  • to chyba nie jest minus ale, dodam aby była większa świadomość. Długi czas generowania się mapy, wskazany serwer oparty o dyski SSD. Oczywiście to generowanie na samym początku jest długie, później każda zmiana może być dodawana automatycznie tylko wtedy gdy coś ulegnie zmianie. Dobrym pomysłem jest wygenerowanie kilku poziomów we własnym zakresie a reszta będzie generowana przez użytkowników poprzez używanie mapy. W innym wypadku zbyt duży ruch „zabije” serwery mapowe.
  • brak routingu „out the box” tak jak w google maps, aby mieć taką funkcjonalność należy skorzystać z zewnętrznego serwisu lub jak mapy postawić następny serwer z routingiem i połączyć z OSM

OSM jest wykorzystywane przez wiele firm i instytucji, najciekawsze projekty które korzystają z niej to:

  • Yanosik
  • jakdojade.pl
  • pkt.pl – sam wdrażałem 😉
  • maps.me aplikacja na androida i iphona – polecam rewelacyjnie sprawdza się na wycieczkach, działa offline

I wiele innych, sami zobaczcie.

Wpis nie zawiera tak podstawowych rzeczy jak sterowanie wyświetlaniem zoom a dokładnie jego pozycji, zmiana kolorów markerów czy ich kształt, dodawania buttonów i wiele innych przydatnych właściwości, jest to temat rzeka. Polecam gorąco zapoznanie się z dokumentacją [ Leaflet ]

Oczywiście jeśli ktoś chce się dowiedzieć jak podłączyć routing w mapach OSM to poproszę o komentarz, a postaram się zrobić wpis na ten temat.