Adam Verner

displaying horosvaz.cz map using mapy.cz

From time to time i love to discover new climbing locations and the easiest way for me is to look at horosvaz.cz and search for locations nearby. But horosvaz.cz uses google maps, which I don't think is appropriate for Czech climbing website.

"Borrowing" data

Looking at the source code of the page, the whole script for importing way-points into the map is right in the body of the document.

<div id="GoogleMap1" class="google-map"></div>  <script> var GoogleMap1, VizusGoogleMap1, VizusGoogleMap1Area1, VizusGoogleMap1Area2, VizusGoogleMap1Area3, VizusGoogleMap1Area4, VizusGoogleMap1Area5, VizusGoogleMap1Area6, VizusGoogleMap1Area7, VizusGoogleMap1Area8, VizusGoogleMap1Area9, VizusGoogleMap1Area10, VizusGoogleMap1Area11, VizusGoogleMap1Area12, VizusGoogleMap1Area13, VizusGoogleMap1Area14, VizusGoogleMap1Area15
function loadMap() {
if (typeof VizusGoogleMap1Before == 'function')
setTimeout(function(){VizusGoogleMap1Before()}, 200);var GoogleMap1Options = {mapTypeId: google.maps.MapTypeId.TERRAIN,zoom: 7,center: new google.maps.LatLng(49.8037633, 15.4749126)};
GoogleMap1 = new google.maps.Map(document.getElementById('GoogleMap1'), GoogleMap1Options);
VizusGoogleMap1 = new vizus.web.maps.google.Map(GoogleMap1);
VizusGoogleMap1Area1 = VizusGoogleMap1.areas.add();
VizusGoogleMap1Area1.add({"lat":49.672538,"lng":16.04649,"title":"B\u00edl\u00e1 sk\u00e1la (sektor)","events":[{"event":"click","action":"openWindow","url":"\/index.php?cmd=skaly-mapa-info&type=sektor&id=833"}],"icon":{"url":"\/res\/images\/icon\/circle\/medium\/blue-dark.png","width":22,"height":22,"anchor":{"horizontal":"center","vertical":"center"}}});
VizusGoogleMap1Area1.add({"lat":49.3599003,"lng":15.6823078,"title":"B\u00edl\u00e1 sk\u00e1la u Luk nad Jihlavou (sektor)","events":[{"event":"click","action":"openWindow","url":"\/index.php?cmd=skaly-mapa-info&type=sektor&id=982"}],"icon":{"url":"\/res\/images\/icon\/circle\/medium\/blue-dark.png","width":22,"height":22,"anchor":{"horizontal":"center","vertical":"center"}}});
VizusGoogleMap1Area1.add({"lat":49.5983444,"lng":16.455865,"title":"Bohu\u0148ovsk\u00e9 sk\u00e1ly (sektor)","events":[{"event":"click","action":"openWindow","url":"\/index.php?cmd=skaly-mapa-info&type=sektor&id=470"}],"icon":{"url":"\/res\/images\/icon\/circle\/medium\/blue-dark.png","width":22,"height":22,"anchor":{"horizontal":"center","vertical":"center"}}});

To keep our version up to date with new sectors, we'll parse this on the client side.

var xmlhttp = new XMLHttpRequest();  
xmlhttp.onreadystatechange = function() {  
  if (this.readyState == 4 && this.status == 200) {  
    console.log('Loaded horosvaz.cz');  
    console.log(this.responseText.match(/\.add\({.+}\);/g))
  }  
  else if (this.readyState == 4 && this.status != 200) {
    console.log('Failed loading horosvaz.cz');  
  }
};  
xmlhttp.open("GET", 'https://www.horosvaz.cz/index.php?cmd=skaly-mapa&type=regiony', true);  
xmlhttp.send();

This ideally should yield an array of json data, we could parse, but there is an issue with CORS.

Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://www.horosvaz.cz/index.php?cmd=skaly-mapa&type=regiony. (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

This means, we are not allowed to access content from horosvaz.cz through the client-side js. We could move the logic to the backend, but since this page is meant to be served statically (using either gihub.io or gitlab pages) it would be nicer to get around this somehow. Luckily for us, there are bunch of Free CORS Proxies around the internet we can use to access content from anywhere.

We'll use the demo version of CORS anywhere. The "demo" here means, that it's rate-limited, but that's not an issue because no-one will use this site anyway.

Fallback

There is a chance the horosvaz "API" will change in future or the cors-anywhere page will hit limit. So we should have some sort of fallback.

The easiest way to do that is by just downloading the whole page and parsing that in case something goes south.

Loading the map

The API documentation suggests to put the Loader.load call into the html header.

<script type="text/javascript" src="https://api.mapy.cz/loader.js"></script>
<script type="text/javascript">Loader.load();</script>

But i don't like it that way. Instead i have a event listener for DOM load.

document.addEventListener("DOMContentLoaded", function(){
    Loader.async = true;
    Loader.load(null, null, createMap);
});

createMap is my function which created the map and then calls loader for the horosvaz content.

Creating map

Creating the most basic is fairly easy. Create Map, add default layer and you're good to go basically. It's great idea to add a center coordinates. I have them in the geo-center of Czech republic, i think.

var center = SMap.Coords.fromWGS84(14.41790, 50.12655);
var m = new SMap(JAK.gel("m"), center, 9);
m.addDefaultLayer(SMap.DEF_SMART_BASE).enable();
m.addDefaultControls();

// Grow map to the size of it's parent element
var sync = new SMap.Control.Sync();
m.addControl(sync);

// create marker layer we will later fill
var layer = new SMap.Layer.Marker();
m.addLayer(layer);
layer.enable();

Parsing the data

Is as simple as iterating over matches we get from the regex and creating waypoints we'll add to the map layer.

To add a waypoint we need to create marker with coordinates and a card. The content of the card must be dynamically loaded becasue we cant just make 500 requests, due to api limitation and our sanity. For now we'll just store the endpoint which contains the card content into the card body and handle it later.

tag.forEach(function (raw, idx) {
    let innerJson = raw.match(/{.*}/);
    let entry = JSON.parse(innerJson);
    var card = new SMap.Card();
    card.getContainer().style.width = 'auto';
    card.getHeader().innerHTML = '<span class="cardHeader">'+ entry['title'] +'</span>';
    card.getBody().innerText = 'https://cors-anywhere.herokuapp.com/https://www.horosvaz.cz' + entry['events'][0]['url'];
   
    /* create marker with embedded card */
    var options = { title: entry['title'] };
    var coord = SMap.Coords.fromWGS84(entry['lng'], entry['lat']);
    var marker = new SMap.Marker(coord, "rockMarker" + idx, options);
    marker.decorate(SMap.Marker.Feature.Card, card);
    
    /* add marker to map */
    layer.addMarker(marker);
});

card load listener

The most simple way to load the card would be to just use iframe to view the content. But there is a problem we've already encountered, CORS. Unfortunately the cors-anywhere solution does not work anymore as the server needs to have origin header set and we can't do that from the iframe.

We could find different proxy, e.g.: allorigins.win, but then we'll face an issue with link. The links on the horosvaz side are relative, so they would all point to allorigins.win. So we would need to replace them anyway.

We'll use http request the same way we did when requesting the homepage


var listener = function(e) {
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            var sector_link_parser = /(?<=href=").+?(?=")/;
            var loc = this.responseText.match(sector_link_parser);
            e.target._dom.body.innerHTML = this.responseText.replace(sector_link_parser, 'https://www.horosvaz.cz' + loc[0])
            // card.sync();
        }
    };
    
    xmlhttp.open("GET", e.target._dom.body.innerText, true);
    xmlhttp.send();
}

var signals = map.getSignals();
signals.addListener(window, "card-open", listener);

The only issue we can face here is when user tries to open the card for second card the listener will fail as it will try to access some weird url(the html content). But the innerHTML will already be set and we won't even notice.

Conclusion

The page is somewhat usable, but in future innerText with card location should be replaced with some loading icon and the url hidden away. There should be fallback for the cors-anywhere as it has limited API usage.

The whole source code can be checked out at github.com/AdamVerner/rocks.averner.cz/