Skip to content

Instantly share code, notes, and snippets.

@fxi
Last active May 2, 2017 14:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save fxi/b7f1af5981432296bfafec70a95fd9b6 to your computer and use it in GitHub Desktop.
Save fxi/b7f1af5981432296bfafec70a95fd9b6 to your computer and use it in GitHub Desktop.
Drop multiple geojson files on a mapbox gl js map

Experimental work with mapbox gl js, web workers and geojsonhint to create new layers by drag and dropping geojson data.

body { margin:0; padding:0; }
#map { position:absolute; top:0; bottom:0; width:100%; }
// 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.');
}
// 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 !"
});
}
};
/**
* 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"
};
<!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>
.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;
}
(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