NAV
javascript html

Overview

Il presente documento illustra il funzionamento minimo relativo alla mappa interattiva del progetto Paesaggi Culturali Alpini su Wikipedia.

Lo scopo è quello di permettere un facile riutilizzo dello strumento, con nuove e diverse fonti dati, da parte di utenti mediamente esperti. È pertanto richiesta una conoscenza di base di HTML e Javascript.

L'applicativo è interamente basato sulla libreria di web-mapping MapboxJS. Fare riferimento a questa pagina per ulteriori informazioni sulle API di base.


1. Impostazioni di base

Questa sezione descrive il settaggio delle impostazioni basilari (quali la creazione di variabili e la costruzione di helper functions..) relative agli elementi della mappa e dell'interfaccia utente (menù a scomparsa presente lateralmente a Dx sulla mappa).

Variabili basilari per funzionamento mappa

Inizializzazione delle variabili iniziali di MapboxJS


/**
 * Coordinate centro mappa
 * @type {float}
 */
var lat = 46.023,
    lon = 9.278,
/**
 * Coordinate Bounding Box della comunità montana
 * @type {float}
 */
    maxSouthWest = L.latLng(45.735, 8.7),
    maxNorthEast = L.latLng(46.8, 9.705),
    maxBounds = L.latLngBounds(maxSouthWest, maxNorthEast);

Sostituire di seguito il proprio Access Token, come fornito da account mapbox. Per ulteriori informazioni fare riferimento alla pagina di spiegazione su Access Token

/**
 * mapbox accessToken
 * @type {String}
 */
L.mapbox.accessToken = 'TOKEN-DI-MAPBOX';

Sostituire di seguito l'ID della mappa per cambiare sfondo cartografico (Base Layer)

/**
 * Main Map Object
 * @type {object}
 */
var map = L.mapbox.map('map', 'ID-MAPPA',
  /**
   * @namespace map
   * @property {object} center - the default initial map center
   * @property {object} maxBounds - the max map extension boundaries
   * @property {number} zoom - zoom level range
   * @property {number} maxZoom - max zoom level
   * @property {number} minZoom - min zoom level
   * @property {bool} attributionControl  - display map attribution
   * @property {object} editInOSMControlOptions - display position of map controls
   */
  {
    center: [lat, lon],
    maxBounds: maxBounds,
    zoom: 12,
    maxZoom: 18,
    minZoom: 10,
    attributionControl: true,
    editInOSMControlOptions: {
      position: 'topleft'
    }
  }
  /**
   * Geocoder address search toolbar
   * @type {object}
   */
).addControl(L.mapbox.geocoderControl('mapbox.places', {
  autocomplete: true
}));

Qui di seguito sono riportati alcuni metodi che aggiungono ulteriori elementi opzionali, quali ad esempio i controlli di geolocalizzazione e la scala cartografica

/** Disable manual scroll to avoid being stuck on mobile  */
map.scrollWheelZoom.disable();

/** Add scale and location tools  */
L.control.scale().addTo(map);
L.control.locate().addTo(map);

// spinner inizializzato nell'elemento mappa (si centra di default)
/**
 * map element
 * @type {HTMLElement}
 */
var target = document.getElementById('map');
/**
 * Spinner object
 * @type {Spinner}
 */
var spinner = new Spinner();

Variabili per recupero dati OpenstreetMap

I dati richiesti ad i server OpenStreetMap sono richiesti tramite le API del motore OverpassTurbo. Il formato di risposta (modificabile ) è json.


// Var intialization
var marker,
 markers = [],
 valsa,
 wiki,
 wikiData,
 osmLayerSentieri,
 osmLayerCappellette,
 osmLayerCippi,
 osmData;

// Var di query per chiamata overpass per layer "Sentieri"
var querySentieri = 'http://overpass-api.de/api/interpreter?data=[out:json][timeout:25];relation["route"="hiking"](' +
  maxBounds._southWest.lat + ',' +
  maxBounds._southWest.lng + ',' +
  maxBounds._northEast.lat + ',' +
  maxBounds._northEast.lng +
  ');out body;>;out skel qt;';

// Var di query per chiamata overpass per layer "Punti di Veduta (ex-cappellette votive)"
var queryCappellette = 'http://overpass-api.de/api/interpreter?data=[out:json][timeout:25];node["tourism"="viewpoint"](' +
  maxBounds._southWest.lat + ',' +
  maxBounds._southWest.lng + ',' +
  maxBounds._northEast.lat + ',' +
  maxBounds._northEast.lng +
  ');out body;>;out skel qt;';

// Var di query per chiamata overpass per layer "Cippi commemorativi"
var queryCippi = 'http://overpass-api.de/api/interpreter?data=[out:json][timeout:25];node["historic"="memorial"](' +
  maxBounds._southWest.lat + ',' +
  maxBounds._southWest.lng + ',' +
  maxBounds._northEast.lat + ',' +
  maxBounds._northEast.lng +
  ');out body;>;out skel qt;';

Variabili per oggetti su mappa (Feature Layers)

Viene costruito un livello (layer) per ciascuna, diversa, fonte dati. Qui di seguito sono presenti:

Un livello addizionale è rappresentato dal Marker circolare, che agisce come filtro dei markers puntuali contenuti negli altri livelli.


/*---- Feature Layers ------*/
// --------------------------

// 1 - ValsassinaCultura.it
// NOTA BENE: sto caricando un geoJSON
valsa = L.mapbox.featureLayer().loadURL('PERCORSO/PER/FILE.json');

Per caricare un differente dataset, convertirlo in formato GeoJSON e fornirlo al costruttore del livello


// 2 - WIKIPEDIA
wiki = L.mapbox.featureLayer();

// 3 - OpenStreetMap (OSM)
osmLayerSentieri = L.mapbox.featureLayer();
osmLayerCappellette = L.mapbox.featureLayer();
osmLayerCippi = L.mapbox.featureLayer();

// 4 - Marker Radius per lente di ricerca con raggio 500 metri
var RADIUS = 500;
var filterCircle = L.circle(L.latLng(40, -75), RADIUS, {
  opacity: 1,
  weight: 1,
  fillOpacity: 0.4
});

Metodi Menù/UI

Metodi per interagire con i livelli del menù a scomparsa.

Per aggiungere/rimuovere eventuali altri bottoni, ricordarsi di modifcare anche il corrispondente markup HTML presente nel file app.html

Un esempio di tale markup è presente nel tab relativo al codice HTML.

<!-- map menu -->
                <div class="menu-menu-map">
                    <nav class="cbp-spmenu cbp-spmenu-vertical cbp-spmenu-right" id="cbp-spmenu-s2">
                        <h3>Livelli:</h3>
            <!-- Bottone Wikipedia -->
                        <div class="tooltip-wide"><a href="#" id="wiki" class="btn btn-block btn-lg btn-inverse" data-toggle="tooltip" data-placement="left" title="Esplora i punti di interesse mappati su Wikipedia" >Wikipedia</a></div>
            <!-- Bottone ValsassinaCultura.it -->
            <div class="tooltip-wide"><a href="#" id="valsa" class="btn btn-block btn-lg btn-inverse" data-toggle="tooltip" data-placement="left" title="Esplora i punti di interesse del progetto sul paesaggio culturale Valsassina Cultura" >Valsassina Cultura</a></div>
            <!-- Bottone OSM - Sentieri -->
                        <div class="tooltip-wide"><a href="#" id="osmSentieri" class="btn btn-block btn-lg btn-inverse" data-toggle="tooltip" data-placement="left" title="Esplora i sentieri e i percorsi montani mappati su Openstreetmap" >Sentieri</a></div>
            <!-- Bottone OSM - Punti di Veduta -->
            <div class="tooltip-wide"><a href="#" id="osmCappelle" class="btn btn-block btn-lg btn-inverse" data-toggle="tooltip" data-placement="left" title="Esplora i sentieri e i punti di veduta mappati su Openstreetmap" >Punti di Veduta</a></div>
            <!-- Bottone OSM - Cippi commemorativi -->
            <div class="tooltip-wide"><a href="#" id="osmCippi" class="btn btn-block btn-lg btn-inverse" data-toggle="tooltip" data-placement="left" title="Esplora i cippi e i memoriali mappati su Openstreetmap" >Cippi e memoriali</a></div>
            <!-- Bottone Raggio di Ricerca -->
            <div class="tooltip-wide"><a href="#" id="filtro" class="btn btn-block btn-lg btn-inverse" data-toggle="tooltip" data-placement="left" title="Muovi il mouse per visualizzare i punti d'interesse in un raggio di 500 m." >Attorno a te</a></div>
                    </nav>
                </div>
/*----------  Bottoni del menù a scomparsa  ----------*/


// Bottone per livello "ValsassinaCultura"
map.getContainer().querySelector('#valsa').onclick = function () {
// Controllo se il menè è attivo
  if ($('#valsa').hasClass('active')) {
    // Rimuovo il Feature Layer alla mappa
    map.removeLayer(valsa);
    $(this).removeClass('active');
    // Layer caricato. Fermo Spinner
    spinner.stop();
  } else {
    // Layer in caricamento. Attivo Spinner
    spinner.spin(target);
    // Carico i Marker dal dataset
    createValsaMarker(valsa);
    // Aggiungo il Feature Layer alla mappa
    map.addLayer(valsa);
    $(this).addClass('active');
  }
  return false;
};

// Bottone per livello "Wikipedia"
map.getContainer().querySelector('#wiki').onclick = function () {
  if ($('#wiki').hasClass('active')) {
    map.removeLayer(wiki);
    $(this).removeClass('active');
    spinner.stop();
  } else {
    getWiki();
    map.addLayer(wiki);
    $(this).addClass('active');
  }
  return false;
};

// Bottone per livello "OSM - Sentieri"
map.getContainer().querySelector('#osmSentieri').onclick = function (e) {
  if ($('#osmSentieri').hasClass('active')) {
    map.removeLayer(osmLayerSentieri);
    $(this).removeClass('active');
    spinner.stop();
  } else {
    var evento = e;
    getOSM(evento);
    osmLayerSentieri.addTo(map);
    $(this).addClass('active');
  }
  return false;
};

// Bottone per livello "OSM - Viewpoint"
map.getContainer().querySelector('#osmCappelle').onclick = function (e) {
  if ($('#osmCappelle').hasClass('active')) {
    console.log('0dovrei togliere');
    map.removeLayer(osmLayerCappellette);
    $(this).removeClass('active');
    spinner.stop();
  } else {
    var evento = e;
    getOSM(evento);
    osmLayerCappellette.addTo(map);
    $(this).addClass('active');
    $('#osmCappelle').hasClass('active');
  }
  return false;
};

// Bottone per livello "OSM - Cippi Votivi"
map.getContainer().querySelector('#osmCippi').onclick = function (e) {
  if ($('#osmCippi').hasClass('active')) {
    console.log('0dovrei togliere');
    map.removeLayer(osmLayerCippi);
    $(this).removeClass('active');
    spinner.stop();
  } else {
    var evento = e;
    getOSM(evento);
    osmLayerCippi.addTo(map);
    $(this).addClass('active');
    $('#osmCippi').hasClass('active');
  }
  return false;
};

// Bottone per livello "Filtro di Ricerca"
map.getContainer().querySelector('#filtro').onclick = function () {
  if ($('#filtro').hasClass('active')) {
    map.removeLayer(filterCircle);
    map.off('mousemove', impostaFiltro);
    mostra();

    $(this).removeClass('active');
  } else {
    map.on('mousemove', impostaFiltro);
    map.addLayer(filterCircle);
    $(this).addClass('active');
  }
  return false;
};

2. Recupero e gestione dati

In questa sezione sono presenti le funzioni per chiamate API a server remoti (OSM, Wikipedia) e per il caricamento di dataset locali, oltre che per la successiva costruzione dei layers su mappa.

Come precedentemente accennato, nell'applicativo originario sono presenti tre fonti dati principali, (con tre conseguenti sistemi di chiamata):

Origine Dato Sorgente Formato
Dati locali Valsassina.it GeoJSON
Dati remoti OpenStreetMap JSON
Wikipedia JSON

È inoltre presente un commento alla funzione per gestire il bottone “filtro di ricerca” (collocato in questa sezione in quanto relativa a modifiche/interazioni sui dati).

Caricamento GeoJSON locale

/*----------  // VALSASSINA  ----------*/
// creo layer da
function createValsaMarker(layer) {

  layer.eachLayer(function (marker) {
    var feature = marker.feature;

    var id = feature.id;
    var id_sezione = feature.properties.id_sezione;


    var valsaUrl = '<a href="http://valsassinacultura.it/scheda.php?idcontent=' + id + '&id=' + id_sezione + '" target="_blank">Valsassinacultura.it</a>';

    var titolo = '<b>' + feature.properties.titolo + '</b>';
    var testoLength = 500;
    var testo = feature.properties.descrizione;
    var testoTrimmed = testo.substring(0, testoLength);
    // var image =
    // wiki.eachLayer(function(f){console.log(f.toGeoJSON().properties.image)})
    var popUpContent = titolo + '<br>' + testoTrimmed + '...' + '<br>' + '<b>More info at:  <br/> </b>' + valsaUrl;


    marker.bindPopup(popUpContent).setIcon(L.mapbox.marker.icon({
      'marker-size': 'small',
      'marker-symbol': 'circle',
      'marker-color': '#f8f7ba'
    }));

  })
  spinner.stop();
}

Query API Wikipedia

/*----------  // WIKIPEDIA  ----------*/

centre = map.getCenter();

function getWiki() {

  // change coords to new map center
  var wiki_lng = map.getCenter()['lng'];
  var wiki_lat = map.getCenter()['lat'];
  spinner.spin(target)
    // return  geojson([lon, lat],
  return geojson([wiki_lng, wiki_lat], {
      language: 'it',
      limit: 100,
      radius: 10000,
      images: true,
      summaries: true,
      templates: true
    },
    function (data) {
      wiki.setGeoJSON(data);
      createMarkerFromWikiLayer(wiki);
      spinner.stop();
    });
}

/*
la funzione createMarkerFromWikiLayer deve rimanere nella callback di getWiki
 perchè altrimenti la pagina parte senza avere caricato i dati!
 */
// Wikipedia
// I want to iterate over the markers of an already present (on the map) featurelayer
// in order to avoid to manually create each marker
function createMarkerFromWikiLayer(wiki) {


  wiki.eachLayer(function (marker) {
      var color, help, icon, marker, needsWorkTemplates, pos, summary, template, url, _i, _len, _ref, article, __indexOf = [].indexOf;
      article = marker.feature;
        // nota come inverta le coordinate, perchè rispetto a come sono definite in leaflet qui vengono usa girate (lat/long e non long/lat)
      pos = [article.geometry.coordinates[1], article.geometry.coordinates[0]];
      url = article.id;
      icon = "library";
      color = "#8f9eda";
      help = '';
      needsWorkTemplates = ["Copy edit", "Cleanup-copyedit", "Cleanup-english", "Copy-edit", "Copyediting", "Gcheck", "Grammar", "Copy edit-section", "Copy edit-inline", "messages/Cleanup", "Tone"];
      _ref = article.properties.templates;

      summary = article.properties.summary;
      if (summary && summary.length > 500) {
        summary = summary.slice(0, 501) + " ... ";
      }

      /*inizio for sui marker*/
      for (_i = 0, _len = _ref.length; _i < _len; _i++) {
        template = _ref[_i];

        if (__indexOf.call(needsWorkTemplates, template) >= 0) {
          help = "This article is in need of copy-editing."
          marker.setIcon(L.mapbox.maker.icon({
            'marker-size': 'large',
            'marker-symbol': "hairdresser",
            'marker-color': "#fa0"
          }))
        } else if (template === "Citation needed" || template === "Citation") {
          help = "This article needs one or more citations."
          marker.bindPopup("<div class=\"summary\"><a target=\"_new\" href=\"" + url + "\">" + article.properties.name + "</a> - " + summary + " <div class=\"help\">" + help + "</div></div>")
            .setIcon(L.mapbox.marker.icon({
              'marker-size': 'large',
              'marker-symbol': "entrance",
              'marker-color': "#fa0"
            }))
        } else if (!article.properties.image) {
          help = "Questa voce Wiki necessita di un immagine."
          marker.bindPopup("<div class=\"summary\"><a target=\"_new\" href=\"" + url + "\">" + article.properties.name + "</a> - " + summary + " <div class=\"help\"> <b>" + help + " </b></div></div>")
            .setIcon(L.mapbox.marker.icon({
              'marker-size': 'small',
              'marker-symbol': "camera",
              // 'marker-color':"#e65f5f"
              'marker-color': "#e7857f"
            }))
        } else {
          marker.bindPopup("<div class=\"summary\"><a target=\"_new\" href=\"" + url + "\">" + article.properties.name + "</a> - " + summary + "</div>")
            .setIcon(L.mapbox.marker.icon({
              'marker-size': 'small',
              'marker-symbol': icon,
              'marker-color': color
            }))
        }

      }
      /*fine loop sui marker*/
    })
    /*esco da eachLayer*/

};


Query API OpenStreetMap (via OverpassTurbo)


/*----------  // OPENSTREETMAP  ----------*/

/* Utility functions usate nella callback di getOSM per mostrare i dati */

// filtro per geometrie
function filtro(features) {
  if (features.geometry.type == 'LineString') {
    return features;
  } else {
    return null;
  }
};
// costruisco layer Viewpoint (ex cappellette votive)
function getOSMCappellette(layer) {
  layer.eachLayer(function (f) {

    var name = f.feature.properties.tags.name || 'Punto panoramico';
    var description = f.feature.properties.tags.description || 'non presente';
    if (f.feature.properties.tags.wheelchair) {
      var wheelchair = '<p>Accesso per disabili <span class="fa fa-wheelchair"></span> </p>';
      var text = "<p><b>" + name + "</b></p> <p> Descrizione: " + description + "</p> " + wheelchair;
    } else {
      var text = "<p><b>" + name + "</b></p> <p> Descrizione: " + description + "</p> ";
    }
    f.setIcon(L.mapbox.marker.icon({
        'marker-size': 'small',
        'marker-symbol': "star-stroked",
        'marker-color': "#c2e5e9"
      }))
      .bindPopup(text);
  });
}
// costruisco layer Cippi commemorativi
function getOSMCippi(layer) {
  layer.eachLayer(function (f) {

    var name = f.feature.properties.tags.name || 'Cippo';
    var description = f.feature.properties.tags.description || 'non presente';
    var inscription = f.feature.properties.tags.inscription || 'non presente';

    f.setIcon(L.mapbox.marker.icon({
        'marker-size': 'small',
        'marker-symbol': "monument",
        // // 'marker-color':"#e65f5f"
        // 'marker-color':"#fdcba7"
        'marker-color': "#f4bdbd"
      }))
      .bindPopup("<p><b>" + name + "</b></p> <p> Descrizione: " + description + "</p> <p><i> Dedica: " + inscription + "</i></p>");
  });
}

/* Recupero dati OSM e costruisco layer */
function getOSM(e) {
  console.log(e);
  // sentieri
  if (e.srcElement.id === 'osmSentieri') {
    console.log('sentieri');
    spinner.spin(target);
    $.getJSON(querySentieri, function (data) {
      var osmData = osmtogeojson(data);
      osmLayerSentieri.setGeoJSON(osmData);
      osmLayerSentieri.setFilter(filtro);
      osmLayerSentieri.setStyle({
        color: '#FC4353',
        opacity: 0.8,
        weight: 3
      });
      osmLayerSentieri.eachLayer(function (f) {
        f.bindPopup("<b>" + f.feature.properties.relations[0].reltags.name + "</b>" + "</br>" + f.feature.properties.relations[0].reltags.description);
      });
      spinner.stop();
    });
    // cappellette ex-Viewpoint
  } else if (e.srcElement.id === 'osmCappelle') {
    console.log('cappelle');
    spinner.spin(target);
    $.getJSON(queryCappellette, function (data) {
      var osmData = osmtogeojson(data);
      osmLayerCappellette.setGeoJSON(osmData);
      getOSMCappellette(osmLayerCappellette);
      spinner.stop();
    });
  } else {
    console.log('cippi');
    spinner.spin(target);
    $.getJSON(queryCippi, function (data) {
      var osmData = osmtogeojson(data);
      osmLayerCippi.setGeoJSON(osmData);
      getOSMCippi(osmLayerCippi);
      spinner.stop();
    });
  }
};


Costruzione Filtro di Ricerca


/*----------  //FILTRO   ----------*/

var filtroAttivo;

// attiva filtro AKA disattiva Features Layers
function impostaFiltro(e) {
  filterCircle.setLatLng(e.latlng).addTo(map);
  wiki.setFilter(function showAirport(feature) {
    return e.latlng.distanceTo(L.latLng(
      feature.geometry.coordinates[1],
      feature.geometry.coordinates[0])) < RADIUS;
  });
  valsa.setFilter(function showAirport(feature) {
    return e.latlng.distanceTo(L.latLng(
      feature.geometry.coordinates[1],
      feature.geometry.coordinates[0])) < RADIUS;
  });
  osmLayerCappellette.setFilter(function showAirport(feature) {
    return e.latlng.distanceTo(L.latLng(
      feature.geometry.coordinates[1],
      feature.geometry.coordinates[0])) < RADIUS;
  });
  osmLayerCippi.setFilter(function showAirport(feature) {
    return e.latlng.distanceTo(L.latLng(
      feature.geometry.coordinates[1],
      feature.geometry.coordinates[0])) < RADIUS;
  });
  createValsaMarker(valsa);
  createMarkerFromWikiLayer(wiki);
  getOSMCappellette(osmLayerCappellette);
  getOSMCippi(osmLayerCippi);

};

// ricarica Features Layers sulla mappa
function mostra() {
  wiki.setFilter(function (f) {
    // Returning true for all markers shows everything.
    return true;
  });
  createMarkerFromWikiLayer(wiki);
  valsa.setFilter(function (f) {
    // Returning true for all markers shows everything.
    return true;
  });
  createValsaMarker(valsa);
  osmLayerCappellette.setFilter(function (f) {
    // Returning true for all markers shows everything.
    return true;
  });
  getOSMCappellette(osmLayerCappellette);
  osmLayerCippi.setFilter(function (f) {
    // Returning true for all markers shows everything.
    return true;
  });
  getOSMCippi(osmLayerCippi);
};

3. Caricamento interfaccia utente

Funzione per caricamento menù laterale della webapp.

Funzione lineare per mostrare/nascondere il menù che comanda gli elementi su mappa.


$(document).ready(function () {
  var r = 0,
    dir = true;
  $('#switch-01:checkbox').on('switchChange.bootstrapSwitch', function (event, state) {
    dir = !dir;
    r = dir ? -250 : 0;
    $(".menu-layer").stop().animate({
      right: r + 'px'
    }, 600);
  });

});

4. Embedding in un tag iframe

È possibile embeddare in qualsiasi pagina HTML l'app tramite utilizzo di un tag iframe. Per farlo, passare all'attributo src del tag il percorso relativo alla pagina HTML da embeddare (in questo caso app.html). A margine (nel tab HTML) è riportato un esempio.

Per una descrizione più esaustiva dei vari attributi del tag fare riferimento alla pagina relativa su MDN.

<!DOCTYPE html>
<html>
<head>
<title>La mia pagina</title>
</head>
<body>
<p>Contenuto...</p>
<iframe src="/PERCORSO/PER/app.htm" width="555" height="200">
   Sorry your browser does not support inline frames.
</iframe>
<p>Ulteriore Contenuto...</p>
</body>
</html>