/**
* Wallonie#Demain Prototype
*
* Requires jQuery
**/
// We don't have working GeoJSON support yet
USE_GEOJSON = false
VERSION = '0.0.1'
// Subset of Dewey Maps' Deck
// See https://lite.framacalc.org/XNQBPjpQuO
const Categories = [
{"id":6, "name":"1. S'alimenter", "subcategories":[
{"name":"Marchés", "id":170},
{"name":"Ateliers cuisine", "id":180},
{"name":"Spots 'plantes aromatiques'", "id":155},
{"name":"Épiceries solidaires", "id":135},
{"name":"Eau potable", "id":115},
{"name":"Repas gratuits", "id":154},
{"name":"Restaurants sociaux", "id":118},
{"name":"Initiatives de récup' alimentaire", "id":28},
{"name":"GASAP (groupes d'achat)", "id":27},
{"name":"GASAP (producteurs)", "id":185}
], "color":"#FF9200"},
{"id":11, "name":"2. Se laver, s'habiller", "subcategories":[
{"name":"Boîtes à dons", "id":210},
{"name":"Vestiaires sociaux", "id":156},
{"name":"Donneries, marchés gratuits", "id":46},
{"name":"Douches publiques", "id":144},
{"name":"Friperies, vêtements de 2ème main", "id":104}
], "color":"#FFEB00"},
{"id":10, "name":"3. Guérir, se soigner", "subcategories":[
{"name":"Maisons médicales", "id":18},
{"name":"Réseaux de santé", "id":145},
{"name":"Services de santé pour personnes précaires", "id":157}
], "color":"#7CFB80"},
{"id":15, "name":"4. Recycler, réparer", "subcategories":[
{"name":"Ateliers de travail du bois", "id":138},
{"name":"Récup' de cartouches d'imprimantes", "id":136},
{"name":"Boîtes à livres", "id":50},
{"name":"Aide au compostage", "id":132},
{"name":"Repair cafés", "id":42},
{"name":"Récup' de matériaux de construction", "id":113},
{"name":"Bulles à vêtements", "id":206},
{"name":"Matériaux informatiques recyclés", "id":47},
{"name":"Recyclage de verre", "id":205},
{"name":"Marchés aux puces", "id":120}
], "color":"#02ACCC"},
{"id":7, "name":"5. respirer, se mettre au vert", "subcategories":[
{"name":"Soutien aux potagistes", "id":128},
{"name":"Composts collectifs ou 'de quartier'", "id":121},
{"name":"Potagers & vergers", "id":116},
{"name":"Réserves naturelles", "id":133},
{"name":"Associations de naturalistes", "id":194},
{"name":"Parcs publics", "id":129},
{"name":"Grainothèques, bourses aux semences", "id":125},
{"name":"Associations apicoles", "id":127}
], "color":"#97C000"},
{"id":3, "name":"6. Se rencontrer, s'entraider", "subcategories":[
{"name":"Comités de quartiers", "id":102},
{"name":"Associations de soutien aux seniors", "id":19},
{"name":"Maisons de Jeunes", "id":17},
{"name":"Soutien à l'enfance et à la famille", "id":14},
{"name":"Maisons de quartiers", "id":10},
{"name":"Monnaies complémentaires", "id":211},
{"name":"Associations de soutien scolaire", "id":119},
{"name":"Associations de Femmes", "id":15},
{"name":"Accueil des réfugiés", "id":188},
{"name":"Soutien aux personnes en situation d'handicap", "id":16},
{"name":"Accueil des primo-arrivants", "id":197},
{"name":"Coopération et solidarité internationale", "id":209},
{"name":"Associations pour l'égalité des genres", "id":202},
{"name":"Centres communautaires NL", "id":204},
{"name":"Initiatives de récup' alimentaire", "id":201},
{"name":"Soutien aux personnes précaires", "id":193},
{"name":"Systèmes d'échange locaux (SEL)", "id":184}
], "color":"#C6B117"},
{"id":2, "name":"7. Apprendre, se former", "subcategories":[
{"name":"Association d'écologie urbaine", "id":90},
{"name":"Espaces de travail partagés, co-working", "id":87},
{"name":"Soutien à l'économie locale", "id":12},
{"name":"Associations d'éducation permanente", "id":13}
], "color":"#7E8AE0"},
{"id":13, "name":"8. S'exprimer, communiquer", "subcategories":[
{"name":"Bornes d'accès à Internet", "id":124},
{"name":"Hackerspaces", "id":43},
{"name":"Médias indépendants", "id":109},
{"name":"Écrivains publics", "id":177},
{"name":"Lieux de promotion du logiciel libre", "id":174}
], "color":"#677362"},
/* {"id":9, "name":"9. Bouger, se déplacer", "subcategories":[
{"name":"Cours de vélo", "id":179},
{"name":"Ateliers vélo", "id":71},
{"name":"Vélos partagés", "id":147},
{"name":"Livraisons à vélo", "id":140},
{"name":"Pompes à vélos", "id":212},
{"name":"Taxis collectifs (Collecto)", "id":196},
{"name":"Voitures partagées", "id":141}
*/
{"id": 9, "name": "9. Initiatives de transition", "subcategories":[
{"name":"Associées au Réseau Transition", "id":999}
], "color":"#D3A9B5"}
]
// Store known sections (refs #3)
const KnownSections = []
// Our map -- We customize attribution down below
const map = L.map('map', {attributionControl: false})
const mcg = L.markerClusterGroup({
disableClusteringAtZoom: 18,
spiderfyOnMaxZoom: false,
showCoverageOnHover: false,
zoomToBoundsOnClick: true,
removeOutsideVisibleBounds: true
})
// GeoJSON cache of markers by section
const Markers = []
const SectionLayers = []
// Prepare Markers for lazy-loading
Categories.forEach(function(c) {
c.subcategories.forEach(function(s) {
KnownSections.push(s.id) // refs #3
Markers[s.id] = []
if (USE_GEOJSON === true) {
SectionLayers[s.id] = L.geoJSON([], {
filter: function(feature, layer) {
console.log('layer filter for ' + s.id)
return feature.properties.subcategories[0].id == s.id
},
onEachFeature: function(feature, layer) {
if (feature.properties && feature.properties.popup) {
console.log(feature.properties.popup)
layer.bindPopup(feature.properties.popup)
}
}
})
} else {
SectionLayers[s.id] = L.featureGroup.subGroup(mcg, [])
}
})
})
// Where do we get our JSON data from
function apiBaseUrl() {
if (location.href.includes('localhost')) {
return 'http://localhost/data/'
}
return 'https://map.incommon.cc/data/'
}
const api_base = apiBaseUrl()
const cat_uri = api_base + '/categories.json'
/**
* Setup the navigation to filter markers on map by category and
* section. It creates an unordered list of categories to toggle
* section display.
**/
function navSetup() {
var nav = $('body > aside > nav')
var ul = $('
')
Categories.forEach(function(c, i) {
var li = $('- ')
var h3 = $('
')
var cs = $('')
cs.click(function(e) {
$(this).toggleClass('selected')
toggleAllSections(c.id, $(this).hasClass('selected'))
return false
})
h3.html(c.name)
.append(cs)
li.append(h3)
.attr('id', 'c-' + c.id)
.click(function(e) {
if ($(this).hasClass('active')) {
hideCategoryMarkers(c.id)
} else {
showActiveCategoryMarkers(c.id)
}
$(this).toggleClass('active')
})
.append(listSections(c.subcategories))
ul.append(li)
})
nav.append(ul)
}
function listSections(sections) {
var ul = $('')
sections.forEach(function(s, i) {
var li = $('- ')
li.attr('id', 's-' + s.id)
li.html(s.name)
li.mouseover(function(e) {
// console.log('mouseover: fetching markers')
var sec_id = $(this).attr('id').substr(2)
fetchMarkers(sec_id)
}).click(function(e) {
var sec_id = $(this).attr('id').substr(2)
$(this).toggleClass('active')
toggleMarkers(sec_id)
return false
})
ul.append(li)
})
return ul
}
//
function toggleMarkers(sec_id) {
var section = $('#s-' + sec_id)
if (section.hasClass('on-map')) {
// console.log('remove markers for section ' + sec_id)
sectionOffMap(sec_id)
} else {
// console.log('showing markers for section ' + sec_id)
sectionOnMap(sec_id)
}
}
/**
* Select or deselect all sections in the navbar for a given category
**/
function toggleAllSections(cat_id, active) {
console.log('toggleAllSections(' + cat_id + ', ' + active + ')')
categorySections(cat_id).forEach(function(s) {
var section = $('#s-' + s.id)
if (active) {
sectionOnMap(s.id)
section.addClass('active')
} else {
sectionOffMap(s.id)
section.removeClass('active')
}
})
}
function categorySections(cat_id) {
return Categories.find(function(c) { return c.id == cat_id }).subcategories
}
/**
* Retrieve markers for given section from the JSON API
*
* @param String sec_id HTML attribute id for the wanted section
*
* @return Boolean true if remote JSON, false if available locally
**/
function fetchMarkers(sec_id) {
if ($.isEmptyObject(Markers[sec_id])) {
var uri = api_base + 'markers-' + sec_id + '.json'
$.getJSON(uri, function(data) {
console.log('loaded JSON from ' + uri)
}).fail(function() {
console.log('failed to get JSON from ' + uri)
}).done(function(data) {
// Must be in .done() otherwise data is lost
Markers[sec_id] = toGeoJSON(data)
})
return true
}
return false
}
/**
* Grossly convert incoming JSON objects to GeoJSON
*
* @param Array of JSON objects with lat and lon coordinates
* @return Array of GeoJSON features
**/
function toGeoJSON(data) {
if (USE_GEOJSON === false) {
return data
}
var myFeatures = $.map(data, function(o, i) {
return {
"type": "feature",
"properties": o,
"geometry": {
"type": "Point",
"coordinates": [o.lat, o.lon]
}
}
})
return {
"type": "FeatureCollection",
"features": myFeatures
}
}
/**
* Hide markers in all sections for given category
*
* @param Integer cat_id The Category ID
*
* @return Boolean true if any markers were hidden
**/
function hideCategoryMarkers(cat_id) {
// console.log('hideCategoryMarkers(' + cat_id + ')')
Categories.forEach(function(c) {
if (c.id == cat_id) {
c.subcategories.forEach(function(s) {
sectionOffMap(s.id)
})
return true
}
})
return false
}
/**
* Show markers for all active sections in given category
**/
function showActiveCategoryMarkers(cat_id) {
// console.log('showActiveCategoryMarkers(' + cat_id + ')')
Categories.forEach(function(c) {
if (c.id == cat_id) {
console.log(cat_id)
c.subcategories.forEach(function(s) {
var section = $('#s-' + s.id)
if (section.hasClass('active')) {
// console.log(' - showing active section ' + s.id)
sectionOnMap(s.id)
}
})
return true
}
})
return false
}
/**
* Display section markers on map
**/
function sectionOnMap(sec_id) {
// console.log('sectionOnMap(' + sec_id + ')')
var section = $('#s-' + sec_id)
if (USE_GEOJSON === true) {
SectionLayers[sec_id].addData(Markers[sec_id]).addTo(map)
} else {
var markers = []
Markers[sec_id].forEach(function(m) {
var layer = markerFor(m)
markers.push(layer)
})
}
SectionLayers[sec_id] = L.featureGroup.subGroup(mcg, markers)
// mcg.addTo(map)
SectionLayers[sec_id].addTo(map)
section.addClass('on-map')
}
/**
* Hide section markers from map
**/
function sectionOffMap(sec_id) {
// console.log('sectionOffMap(' + sec_id + ')')
var section = $('#s-' + sec_id)
if (section.hasClass('on-map')) {
// console.log('sectionOffMap(' + sec_id + ')')
map.removeLayer(SectionLayers[sec_id])
section.removeClass('on-map')
}
}
// Careful with icons and MarkerCluster
// Use iconCreateFunction for styling
// https://github.com/Leaflet/Leaflet.markercluster/blob/master/example/marker-clustering-custom.html
// https://github.com/Leaflet/Leaflet/issues/534
function markerFor(data) {
var icon = iconFor(data)
var marker = L.marker([data.lat, data.lon], {
title: data.name,
alt: data.type + ' marker ' + data.id,
})
// Override pre-made popup data
data.popup = markerPopupFor(data)
marker.bindPopup(data.popup)
return marker
}
const asset_uri = $('script[src$="mapper.js"]' ).attr( 'src' ).replace( 'mapper.js', '' )
// Find parent Category ID from data.subcategories (fixes #3)
function categoryIdFromData(data) {
var section = data.subcategories.find(function(s) {
return KnownSections.includes(s.id)
})
return $('#s-' + section.id).parents('li').attr('id').substr(2)
}
function iconFor(data) {
var cat_id = categoryIdFromData(data)
var color = Categories.find(function(el){ return el.id == cat_id }).color
// console.log(asset_uri + 'img/')
var icon = new L.Icon({
iconUrl: asset_uri + 'img/marker-' + cat_id + '.png',
iconAnchor: new L.Point(16, 16),
})
}
function markerPopupFor(data) {
var template = $('#popup-template').html()
var cat_id = categoryIdFromData(data)
return Mustache.render(template, {
marker: data,
cat_id: 'c-' + cat_id
})
}
function selected_sections() {
var s = [];
$('.on-map').each(function() {
s.push($(this).attr('id').split('s-')[1]);
});
return s;
}
function current_state() {
var c = map.getCenter();
var z = map.getZoom();
var s = selected_sections();
console.log('current_state: ' + [ c.lat, c.lng, z, s ]);
return [ c.lat, c.lng, z, s ];
}
/**
* Add Watermark
**/
L.Control.Watermark = L.Control.extend({
onAdd: function(map) {
var img = L.DomUtil.create('img');
img.src = '/assets/logo-incommon.png';
return img;
},
onRemove: function(map) {
// Nothing to do here
}
});
L.control.watermark = function(opts) {
return new L.Control.Watermark(opts);
}
L.control.watermark({ position: 'bottomleft' }).addTo(map);
/**
* Collapsable attribution
**/
const attribution = L.control({ position: 'bottomright' })
attribution.onAdd = function(map) {
var div = L.DomUtil.create('div')
div.id = 'attribution'
div.innerHTML = '
©
'
div.setAttribute('onmouseenter', 'expandAttribution()')
div.setAttribute('onmouseleave', 'collapseAttribution()')
return div
}
function expandAttribution() {
$('#attribution').addClass('expanded')
.html(renderAttribution())
}
function collapseAttribution() {
$('#attribution').removeClass('expanded')
.html('©
')
}
function renderAttribution() {
var template = $('#attribution-template').html()
var attribution = {
leaflet_url: 'http://leafletjs.com/',
osm_url: 'https://osm.org/',
rt_url: 'http://www.reseautransition.be/',
incommon_url: 'https://incommon.cc/'
}
return Mustache.render(template, { data: attribution })
}
/*
function show_markers(sec_id) {
// Create corresponding layer
console.log(section_markers[sec_id])
section_layers[sec_id] = L.geoJSON(section_markers[sec_id], {
filter: function(feature, layer) {
return feature.properties.subcategories[0].id == sec_id
},
onEachFeature: function(feature, layer) {
if (feature.properties && feature.properties.popup) {
console.log(feature.properties.popup)
layer.bindPopup(feature.properties.popup)
}
}
}).addTo(map)
}
*/
function locate () {
map.locate({ setView: true, maxZoom: 13 })
map.on('locationfound', onLocationFound)
}
function unlocate () {
map.locate({ setView: false, maxZoom: 0 })
map.off('locationfound')
}
function onLocationFound (e) {
var radius = e.accuracy / 2
if (radius > 1000) {
// We must be on a desktop
$('#b-locate').remove()
return false
}
L.marker(e.latlng).addTo(map)
.bindPopup("Vous vous trouvez dans les " + radius + " mètres autour de ce point.").openPopup()
L.circle(e.latlng, radius).addTo(map)
}
$(document).ready(function() {
map.setView([50.83906, 4.35308], 8)
var OpenStreetMap_HOT =
L.tileLayer('https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap, Tiles courtesy of Humanitarian OpenStreetMap Team'
});
/*
var OpenStreetMap_BlackAndWhite =
L.tileLayer('http://{s}.tiles.wmflabs.org/bw-mapnik/{z}/{x}/{y}.png', {
maxZoom: 18,
attribution: '© OpenStreetMap'
});
var Stamen_TonerLite =
L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}.{ext}', {
attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap',
subdomains: 'abcd',
minZoom: 0,
maxZoom: 20,
ext: 'png'
});
var Stamen_Toner =
L.tileLayer('https://stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.{ext}', {
attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap',
subdomains: 'abcd',
minZoom: 0,
maxZoom: 20,
ext: 'png'
});
*//*
L.tileLayer('//stamen-tiles-{s}.a.ssl.fastly.net/toner/{z}/{x}/{y}.png', {
attribution: 'Map data © OpenStreetMap contributors, CC-BY-SA, Imagery © Mapbox',
maxZoom: 18,
id: 'c4ptaincrunch.ka5engdh',
accessToken: 'pk.eyJ1IjoiYzRwdGFpbmNydW5jaCIsImEiOiJUdWVRSENNIn0.qssi5TBLeBinBsXkZKiI6Q'
}).addTo(map)
*/
/*
const BaseLayers = {
'OSM HOT': OpenStreetMap_HOT,
'OSM B&W': OpenStreetMap_BlackAndWhite,
'Stamen TonerLite': Stamen_TonerLite,
'Stamen Toner': Stamen_Toner
}
L.control.layers(BaseLayers).addTo(map)
*/
OpenStreetMap_HOT.addTo(map)
attribution.addTo(map)
mcg.addTo(map)
navSetup()
var btnLocate = $('