Experimental work with mapbox gl js, web workers and geojsonhint to create new layers by drag and dropping geojson data.
Last active
May 2, 2017 14:17
-
-
Save fxi/b7f1af5981432296bfafec70a95fd9b6 to your computer and use it in GitHub Desktop.
Drop multiple geojson files on a mapbox gl js map
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { margin:0; padding:0; } | |
#map { position:absolute; top:0; bottom:0; width:100%; } | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// mapbox gl init | |
mapboxgl.accessToken = 'pk.eyJ1IjoidW5lcGdyaWQiLCJhIjoiY2lzZnowenUwMDAzdjJubzZyZ3R1bjIzZyJ9.uyP-RWjY-94qCVajU0u8KA'; | |
var map = new mapboxgl.Map({ | |
container: 'map', | |
style: 'mapbox://styles/mapbox/dark-v9', | |
center: [-14.66,-23.64], | |
zoom: 3 | |
}); | |
// object to hold geojson | |
var data = {}; | |
// test if file api is available | |
if (window.File && window.FileReader && window.FileList && window.Blob) { | |
// handle read geojson | |
// Update progress | |
var updateProgress = function(theFile) { | |
return function(e) { | |
// evt is an ProgressEvent. 100/2 as loading is ~ half the process | |
if (e.lengthComputable) { | |
var percentLoaded = Math.round((e.loaded / e.total) * 50); | |
progressScreen( | |
true, | |
theFile.name, | |
percentLoaded, | |
theFile.name + " loading (" + percentLoaded + "%)" | |
); | |
} | |
}; | |
}; | |
// init progress bar | |
var startProgress = function(theFile) { | |
return function(e) { | |
progressScreen( | |
true, | |
theFile.name, | |
0, | |
theFile.name + " init .. " | |
); | |
}; | |
}; | |
// on error, set progress to 100 (remove it) | |
var errorProgress = function(theFile) { | |
return function(e) { | |
progressScreen( | |
true, | |
theFile.name, | |
100, | |
theFile.name + "stop .. " | |
); | |
}; | |
}; | |
// handle worker | |
var startWorker = function(theFile) { | |
return function(e) { | |
// Create a worker to handle this file | |
var w = new Worker("handleReadJson.js"); | |
// parse file content before passing to worker. | |
var gJson = JSON.parse(e.target.result); | |
// Message to pass to the worker | |
var res = { | |
json: gJson, | |
fileName: theFile.name | |
}; | |
// handle message received | |
w.onmessage = function(e) { | |
var m = e.data; | |
if ( m.progress ) { | |
progressScreen( | |
true, | |
theFile.name, | |
m.progress, | |
theFile.name + ": " + m.message | |
); | |
} | |
// send alert for errors message | |
if( m.errorMessage ){ | |
alert(m.errorMessage); | |
} | |
// If extent is received | |
if (m.extent) { | |
map.fitBounds(m.extent); | |
} | |
// If layer is valid and returned | |
if (m.layer) { | |
try { | |
progressScreen( | |
true, | |
theFile.name, | |
100, | |
theFile.name + " done" | |
); | |
// add source to map | |
map.addSource(m.id, { | |
"type": "geojson", | |
"data": gJson | |
}); | |
// add layer | |
map.addLayer(m.layer,"place-village"); | |
// set progress to max | |
data[m.id] = gJson; | |
} | |
catch(err){ | |
alert(err); | |
} | |
// close worker | |
w.terminate(); | |
} | |
}; | |
// launch process | |
try { | |
w.postMessage(res); | |
}catch(err){ | |
alert("An error occured, quick ! check the console !"); | |
console.log({ | |
res : res, | |
err : err | |
}); | |
} | |
}; | |
}; | |
var updateLayerList = function(theFile) { | |
return function(e) {}; | |
}; | |
// handle drop event | |
var handleDropGeojson = function(evt) { | |
evt.stopPropagation(); | |
evt.preventDefault(); | |
var files = evt.dataTransfer.files; | |
var nFiles = files.length; | |
var incFile = 100 / nFiles; | |
var progressBar = 0; | |
// In case of multiple file, loop on them | |
for (var i = 0; i < nFiles; i++) { | |
f = files[i]; | |
// Only process geojson files. Validate later. | |
if (f.name.toLowerCase().indexOf(".geojson") == -1) { | |
alert(f.name + " not supported"); | |
continue; | |
} | |
// get a new reader | |
var reader = new FileReader(); | |
// handle events | |
reader.onloadstart = (startProgress)(f); | |
reader.onprogress = (updateProgress)(f); | |
reader.onerror = (errorProgress)(f); | |
reader.onload = (startWorker)(f); | |
reader.onloadend = (updateLayerList)(f); | |
// read the geojson | |
reader.readAsText(f); | |
} | |
}; | |
var handleDragOver = function(evt) { | |
evt.stopPropagation(); | |
evt.preventDefault(); | |
evt.dataTransfer.dropEffect = 'copy'; // Explicitly show this is a copy. | |
}; | |
// Set events | |
mapEl = document.getElementById("map"); | |
mapEl.addEventListener('dragover', handleDragOver, false); | |
mapEl.addEventListener('drop', handleDropGeojson, false); | |
} else { | |
alert('The File APIs are not fully supported in this browser.'); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Importation of helpers | |
importScripts( | |
"https://npmcdn.com/geojsonhint@latest/geojsonhint.js", | |
"turf_bbox.js", | |
"helpers.js" | |
); | |
// Inital message | |
postMessage({ | |
progress: 0, | |
message: "start" | |
}); | |
// handle message send from the main thread | |
onmessage = function(e) { | |
try { | |
/** | |
* Initialisation : set local helper and variables | |
*/ | |
// init variables | |
var errorMsg = ""; | |
var warningMsg = ""; | |
var dat = e.data; | |
var gJson = dat.json; | |
var fileName = dat.fileName; | |
// set basic timing function | |
timerVal = 0; | |
// start timer | |
var timerStart = function() { | |
timerVal = new Date(); | |
}; | |
// give intermediate time, reset timer | |
var timerLap = function() { | |
var lap = new Date() - timerVal; | |
timerStart(); | |
return lap; | |
}; | |
// printable version of timerLaè | |
var timerLapString = function() { | |
return " " + timerLap() + " ms "; | |
}; | |
// start timer | |
timerStart(); | |
/** | |
* validation : geojson validation with geojsonhint | |
*/ | |
// Validation. Is that a valid geojson ? | |
var messages = geojsonhint.hint(gJson); | |
// extract errors | |
var errors = messages.filter(function(x){ | |
return x.level == "error"; | |
}); | |
// extract message | |
var warnings = messages.filter(function(x){ | |
return x.level == "message"; | |
}); | |
// set a message with summary | |
var logMessage = " geojson validation " + | |
" n errors = " + errors.length + | |
" n warnings = " + warnings.length + " done in" + | |
timerLapString(); | |
console.log(fileName + ":" + logMessage); | |
// send message | |
postMessage({ | |
progress: 60, | |
message: logMessage | |
}); | |
// validation : warnings | |
if (warnings.length > 0) { | |
warningMsg = warnings.length + " warning message(s) found. Check the console for more info"; | |
postMessage({ | |
progress: 75, | |
msssage: warningMsg | |
}); | |
warnings.forEach(function(x) { | |
console.log({file:fileName,warnings:x}); | |
}); | |
} | |
// varlidation: errors | |
if (errors.length > 0) { | |
errorMsg = errors.length + " errors found. check the console for more info"; | |
postMessage({ | |
progress: 100, | |
msssage: errorMsg, | |
errorMessage: errorMsg | |
}); | |
errors.forEach(function(x) { | |
console.log({file:fileName,errors:x}); | |
}); | |
return; | |
} | |
/** | |
* Get extent : get extent using a Turf bbox | |
*/ | |
var extent = turf.bbox(gJson); | |
// Quick extent validation | |
if ( | |
extent[0] > 180 || extent[0] < -180 || | |
extent[1] > 89 || extent[1] < -89 || | |
extent[2] > 180 || extent[2] < -180 || | |
extent[3] > 89 || extent[3] < -89 | |
) { | |
errorMsg = fileName + " : extent seems to be out of range: " + extent; | |
postMessage({ | |
progress: 100, | |
msssage: errorMsg, | |
errorMessage: errorMsg | |
}); | |
console.log({ | |
"errors": errorMsg | |
}); | |
return; | |
} | |
postMessage({ | |
progress: 80, | |
message: " extent found in " + timerLapString() | |
}); | |
/** | |
* Avoid multi type : we don't handle them for now | |
*/ | |
var geomType = []; | |
if( gJson.features ){ | |
// array of types in data | |
geomTypes = gJson.features | |
.map(function(x){ | |
return typeSwitcher[x.geometry.type]; | |
}) | |
.filter(function(v,i,s){ | |
return s.indexOf(v) === i; | |
}); | |
}else{ | |
geomTypes = [typeSwitcher[gJson.geometry.type]]; | |
} | |
postMessage({ | |
progress: 90, | |
message: " geom types =" + geomTypes + " found in " + timerLapString() | |
}); | |
// if more than one type, return an error | |
if ( geomTypes.length>1) { | |
var msg = "Multi geometry not yet implemented"; | |
postMessage({ | |
progress: 100, | |
msssage: msg, | |
errorMessage: fileName + ": " + msg | |
}); | |
console.log({ | |
"errors": fileName + ": " + msg + ".(" + geomTypes + ")" | |
}); | |
return; | |
} | |
/** | |
* Set default for a new layer | |
*/ | |
// Set random id for source and layer | |
var id = "mgl_drop_" + randomString(5) + "_" + fileName ; | |
// Set random color | |
var ran = Math.random(); | |
var colA = randomHsl(0.1, ran); | |
var colB = randomHsl(0.5, ran); | |
// Set default type from geojson type | |
var typ = geomTypes[0]; | |
// Set up default style | |
var dummyStyle = { | |
"circle": { | |
"id": id, | |
"source": id, | |
"type": typ, | |
"paint": { | |
"circle-color": colA | |
} | |
}, | |
"fill": { | |
"id": id, | |
"source": id, | |
"type": typ, | |
"paint": { | |
"fill-color": colA, | |
"fill-outline-color": colB | |
} | |
}, | |
"line": { | |
"id": id, | |
"source": id, | |
"type": typ, | |
"paint": { | |
"line-color": colA, | |
"line-width": 10 | |
} | |
} | |
}; | |
postMessage({ | |
progress: 99, | |
message: "Add layer", | |
id: id, | |
extent: extent, | |
layer: dummyStyle[typ] | |
}); | |
} | |
catch(err) { | |
console.log(err); | |
postMessage({ | |
progress: 100, | |
errorMessage : "An error occured, yey ! Quick, check the console !" | |
}); | |
} | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* Generate a random string of the given length | |
* @param {integer} n Number of character | |
* @return {string} random string | |
*/ | |
var randomString = function(n) { | |
var result = ""; | |
if (!n) n = 5; | |
var characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; | |
for (var i = 0; i < n; i++) | |
result += characters.charAt(Math.floor(Math.random() * characters.length)); | |
return result; | |
}; | |
/** | |
* Generate a random hsla color string, with fixed saturation and lightness | |
* @param {number} opacity opacity from 0 to 1 | |
* @param {number} random value from 0 to 1 | |
* @param {number} saturation from 0 to 100 | |
* @param {number} lightness from 0 to 100 | |
*/ | |
var randomHsl = function(opacity, random, saturation, lightness) { | |
if (!opacity) opacity = 1; | |
if (!saturation) saturation = 100; | |
if (!lightness) lightness = 50; | |
if (!random) random = Math.random(); | |
res = "hsla(" + (random * 360) + | |
", " + saturation + "% " + | |
", " + lightness + "% " + | |
", " + opacity + ")"; | |
return res; | |
}; | |
/** | |
* Remove multiple layers by prefix | |
* @param {object} map Map object | |
* @param {string} prefix Prefix to search for in layers, if something found, remove it | |
* @return {array} List of removed layer | |
*/ | |
var removeLayersByPrefix = function(map, prefix) { | |
var result = []; | |
if (map) { | |
// no method to get all layers ? | |
var layers = map.style._layers; | |
for (var l in layers) { | |
if (l.indexOf(prefix) > -1) { | |
map.removeLayer(l); | |
result.push(l); | |
} | |
} | |
} | |
return result; | |
}; | |
/** | |
* Create and manage multiple progression bar | |
* @param {boolean} enable Enable the screen | |
* @param {string} id Identifier of the given item | |
* @param {number} percent Progress bar percentage | |
* @param {string} text Optional text | |
*/ | |
var progressScreen = function(enable, id, percent, text ) { | |
lScreen = document.getElementsByClassName("loading-screen")[0]; | |
if (!enable) { | |
if (lScreen) lScreen.remove(); | |
return; | |
} | |
if (!id || !percent || !text) return; | |
if (!lScreen && enable) { | |
lBody = document.getElementsByTagName("body")[0]; | |
lScreen = document.createElement("div"); | |
lScreen.className = "loading-screen"; | |
lScreenContainer = document.createElement("div"); | |
lScreenContainer.className = "loading-container"; | |
lScreen.appendChild(lScreenContainer); | |
lBody.appendChild(lScreen); | |
} | |
lItem = document.getElementById(id); | |
if (!lItem) { | |
lItem = document.createElement("div"); | |
lItem.className = "loading-item"; | |
lItem.setAttribute("id", id); | |
pBarIn = document.createElement("div"); | |
pBarIn.className = "loading-bar-in"; | |
pBarOut = document.createElement("div"); | |
pBarOut.className = "loading-bar-out"; | |
pBarTxt = document.createElement("div"); | |
pBarTxt.className = "loading-bar-txt"; | |
pBarOut.appendChild(pBarIn); | |
lItem.appendChild(pBarTxt); | |
lItem.appendChild(pBarOut); | |
lScreenContainer.appendChild(lItem); | |
} else { | |
pBarIn = lItem.getElementsByClassName("loading-bar-in")[0]; | |
pBarTxt = lItem.getElementsByClassName("loading-bar-txt")[0]; | |
} | |
if (percent >= 100) { | |
lItem = document.getElementById(id); | |
if (lItem) lItem.remove(); | |
} else { | |
pBarIn.style.width = percent + "%"; | |
pBarTxt.innerHTML = text; | |
} | |
lItems = lScreenContainer.getElementsByClassName("loading-item"); | |
if (lItems.length === 0) progressScreen(false); | |
}; | |
// geojson type to mapbox gl type | |
var typeSwitcher = { | |
"Point": "circle", | |
"MultiPoint": "line", | |
"LineString": "line", | |
"MultiLineString": "line", | |
"Polygon": "fill", | |
"MultiPolygon": "fill", | |
"GeometryCollection": "fill" | |
}; | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset='utf-8' /> | |
<title>Drop geojson</title> | |
<meta name='viewport' content='initial-scale=1,maximum-scale=1,user-scalable=no' /> | |
<meta http-equiv="cache-control" content="Public"> | |
<link href='https://api.tiles.mapbox.com/mapbox-gl-js/v0.23.0/mapbox-gl.css' rel='stylesheet' /> | |
<link href='progress.css' rel='stylesheet' /> | |
<link href='app.css' rel='stylesheet' /> | |
</head> | |
<body> | |
<div id='map'></div> | |
<script src='https://api.tiles.mapbox.com/mapbox-gl-js/v0.23.0/mapbox-gl.js'></script> | |
<script src='helpers.js'></script> | |
<script src='app.js'></script> | |
</body> | |
</html> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.loading-screen { | |
background-color : rgba(47,47,47,0.6); | |
top: 0px; | |
width: 100%; | |
bottom:0px; | |
display: block; | |
position:absolute; | |
} | |
.loading-container { | |
font-family : "Lucida Console", Monaco, monospace; | |
color: #0f0; | |
position:absolute; | |
width: 40%; | |
left: 50%; | |
padding: 10px; | |
} | |
.loading-item { | |
padding: 5px; | |
width: 99%; | |
} | |
.loading-bar-out { | |
height:5px; | |
width:100%; | |
border:1px solid #0f0; | |
} | |
.loading-bar-in { | |
width:0px; | |
height:100%; | |
background-color: #0f0; | |
} | |
.loading-bar-txt { | |
font-size: 14px; | |
} | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.turf = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){ | |
var each=require("turf-meta").coordEach;module.exports=function(r){var e=[1/0,1/0,-(1/0),-(1/0)];return each(r,function(r){e[0]>r[0]&&(e[0]=r[0]),e[1]>r[1]&&(e[1]=r[1]),e[2]<r[0]&&(e[2]=r[0]),e[3]<r[1]&&(e[3]=r[1])}),e}; | |
},{"turf-meta":2}],2:[function(require,module,exports){ | |
function coordEach(e,o,t){var r,n,c,u,l,p,a,i,f,h,d=0,s="FeatureCollection"===e.type,y="Feature"===e.type,g=s?e.features.length:1;for(r=0;r<g;r++)for(f=s?e.features[r].geometry:y?e.geometry:e,h="GeometryCollection"===f.type,a=h?f.geometries.length:1,u=0;u<a;u++)if(p=h?f.geometries[u]:f,i=p.coordinates,d=!t||"Polygon"!==p.type&&"MultiPolygon"!==p.type?0:1,"Point"===p.type)o(i);else if("LineString"===p.type||"MultiPoint"===p.type)for(n=0;n<i.length;n++)o(i[n]);else if("Polygon"===p.type||"MultiLineString"===p.type)for(n=0;n<i.length;n++)for(c=0;c<i[n].length-d;c++)o(i[n][c]);else{if("MultiPolygon"!==p.type)throw new Error("Unknown Geometry Type");for(n=0;n<i.length;n++)for(c=0;c<i[n].length;c++)for(l=0;l<i[n][c].length-d;l++)o(i[n][c][l])}}function coordReduce(e,o,t,r){return coordEach(e,function(e){t=o(t,e)},r),t}function propEach(e,o){var t;switch(e.type){case"FeatureCollection":for(t=0;t<e.features.length;t++)o(e.features[t].properties);break;case"Feature":o(e.properties)}}function propReduce(e,o,t){return propEach(e,function(e){t=o(t,e)}),t}function featureEach(e,o){if("Feature"===e.type)return o(e);if("FeatureCollection"===e.type)for(var t=0;t<e.features.length;t++)o(e.features[t])}function coordAll(e){var o=[];return coordEach(e,function(e){o.push(e)}),o}module.exports.coordEach=coordEach,module.exports.coordReduce=coordReduce,module.exports.propEach=propEach,module.exports.propReduce=propReduce,module.exports.featureEach=featureEach,module.exports.coordAll=coordAll; | |
},{}],3:[function(require,module,exports){ | |
module.exports={bbox:require("turf-bbox")}; | |
},{"turf-bbox":1}]},{},[3])(3) | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment