Home Reference Source

ourthings/Queueable/Mapbox.js

import Queueable from "../Queueable";
import MapboxGL from 'mapbox-gl';
import centroid from '@turf/centroid';
import bbox from '@turf/bbox';
//import MapboxDraw from '@mapbox/mapbox-gl-draw';
//import MapboxEdit from 'mapbox-gl-edit';

export default class Mapbox extends Queueable {

	init(queue) {
		this.queue = queue;
		this.maps = {};
		this.overlays = {};
		this.ready = true;
	}

	/**
	 * Create a new mapbox gl map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - name for the map (used to reference)
	 * @param {string} json.target - id of element in the page to target
	 * @param {string} json.style - mapbox style for the map
	 * @param {int} json.zoom - Set the initial zoom of the map
	 * @param {int} json.maxZoom - Set the max zoom for the map (default 34)
	 * @param {array} json.center - Center on
	 * @example
	 * mapbox.addMap({"map": "testMap", "target":"mapboxMap", "style": "mapbox://styles/mapbox/streets-v11", "zoom": 8, "center": [-70, 41.2]});
	 */
	addMap(pid, json) {
		const options = Object.assign({
			map: 'default',
			zoom: 0,
			center: [-74.5, 40],
			style: 'mapbox://styles/mapbox/streets-v11',
			target: 'map',
			token: 'fail',
			pitch: 0,
			mapZoom: 24
		}, json);

		MapboxGL.accessToken = options.token;
		const map = new MapboxGL.Map({
			container: options.target, // container id
			style: options.style, // stylesheet location
			center: options.center, // starting position [lng, lat]
			zoom: options.zoom, // starting zoom
			pitch: options.pitch,
			maxZoom: options.maxZoom
		});

		this.maps[options.map] = {map, layers: {}};
		this.maps[options.map].sources = {};
		this.maps[options.map].controls = {};

		map.on('load', () => {
			this.finished(pid, self.queue.DEFINE.FIN_OK);
		});

		/*
		 * On idle run a queue (this is needed for blocking access to data before its loaded). We delay the start of this
		 * monitor to allow time for setup commands to run
		 */
		/*
				setTimeout(function () {
		*/
		map.on('idle', function () {
			self.queue.setRegister(options.map + 'Idle');
			//self.queue.execute(options.map+'Idle');
		});
		map.on('dataloading', function (data) {
			if (data.isSourceLoaded)
				self.queue.setRegister(options.map + 'DataLoaded');
			//self.queue.execute(options.map+'Idle');
		})
		/*
				}, 5000);
		*/

	};

	/**
	 * Add a new source and layer to the map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - name for the map (used to reference)
	 * @param {string} json.name - Name for the layer/source
	 * @param {string} json.type - The type of feature for the layer one of ['Point', 'Line', 'Polygon', 'MultiLineString']
	 * @param {string} json.filter - The layer filter
	 * @param {string|object} json.data - Set the data of the source, this could also be a url for the data
	 * @example
	 * mapbox.addSource({"map": "testMap", "name": "newLayer", "featureType": "Point", "data": "https://d2ad6b4ur7yvpq.cloudfront.net/naturalearth-3.3.0/ne_10m_ports.geojson"});
	 */
	addSource(pid, json) {


		const options = Object.assign({
			map: 'default',
			data: {
				type: 'FeatureCollection',
				features: [],
			},
			name: 'default'
		}, json);
		this.queue.deleteRegister(options.map + 'Idle');

		this.maps[options.map].map.addSource(options.name, {
			type: 'geojson',
			data: options.data,
		});

		//this._addLayer(options);

		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Add the layer to the map once a source has been created.
	 * @param {object} options
	 * @param {string} options.type - The type of feature that the layer is.
	 * @param {string} options.name - The name for the layer
	 * @param {object} options.paint - The paint styling for the layer
	 * @param {object} options.layout - The layout styling for the layer
	 * @private
	 */
	addLayer(pid, json) {

		const options = Object.assign({
			map: 'default',
			type: 'point',
			source: 'default',
			paint: {},
		}, json);

		let mapOptions = {
			id: options.name,
			type: options.type,
			source: options.source,
			paint: options.paint
		};

		if (options.layout) {
			if (typeof options.layout !== 'object')
				options.layout = window[options.layout];
			mapOptions.layout = options.layout;
		}

		if (json.filter)
			mapOptions.filter = options.filter;

		this.maps[options.map].map.addLayer(mapOptions);
		this.finished(pid, self.queue.DEFINE.FIN_OK);

	}

	/**
	 * Load images for use as icons (needs to be run prior to layer addition)
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - name for the map (used to reference)
	 * @param {object} json.images - array of images to load in format [{"url":"url to image ","id":"id to use"}]
	 */
	addImages(pid, json) {
		const options = Object.assign({
			map: 'default',
			images: []
		}, json);
		let self = this;
		Promise.all(
			options.images.map(img => new Promise((resolve, reject) => {
				self.maps[options.map].map.loadImage(img.url, function (error, res) {
					if (error) {
						console.error('IMAGE: ' + img.url + ' - ' + img.id);
						throw error;
					}
					self.maps[options.map].map.addImage(img.id, res);
					resolve();
				})
			}))
		).then(function () {
			self.finished(pid, self.queue.DEFINE.FIN_OK);
		})
	}

	/**
	 * Add a select control to a layer
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - name for the map (used to reference)
	 * @param {string} json.layers - name for the map layers in []
	 * @param {object} json.queue - queue to call upon select
	 * @param {object} json.unselectQueue - queue to call upon unselect
	 */
	addSelect(pid, json) {
		let self = this;
		const options = Object.assign({
			map: 'default',
			queue: "select",
			unselectQueue: "unselect"
		}, json);

		self.maps[options.map].map.on('click', function (e) {
			let f = self.maps[options.map].map.queryRenderedFeatures(e.point, {layers: options.layers});
			//debugger;
			//mapbox converts multi depth objects to strings. Deserialize this
			if (f.length >= 1) {

				for (let i in f[0].properties) {
					try {
						f[0].properties[i] = JSON.parse(f[0].properties[i]);
					} catch (e) {
						//do nothing (this was not a multi depth)
					}
				}
				const selectDetails = {
					coordinates: f[0].geometry.coordinates.slice(),
					centroid: centroid(f[0].geometry).geometry.coordinates,
					properties: f[0].properties,
					featureJSON: f[0].toJSON()
				};

				self.queue.setMemory("select", selectDetails, "Session");

				self.queue.execute(options.queue, selectDetails);
			} else {
				self.queue.setMemory("select", {}, "Session");
				self.queue.execute(options.queue);

			}

		});
		self.finished(pid, self.queue.DEFINE.FIN_OK);

	}
	/**
	 * Set filter on a layer
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - name for the map (used to reference)
	 * @param {string} json.layer - name for the map layer
	 * @param {array} json.filter - new filter
	 */
	setFilter(pid, json) {
		const options = Object.assign({
			map: 'default',
			layer: 'default',
			filter: []
		}, json);
		this.maps[options.map].map.setFilter(options.layer, options.filter);
		this.finished(pid, this.queue.DEFINE.FIN_OK);

	}

	/**
	 * Await the source to be valid on a layer because mapbox has no event for this *WARNING SHIT HACK*
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - name for the map (used to reference)
	 * @param {string} json.layer - name for the map layer
	 */
	awaitSourceFeatures(pid, json) {
		let self = this;
		const options = Object.assign({
			map: 'default',
			layer: 'default'
		}, json);
		const features = this.maps[options.map].map.querySourceFeatures(options.layer);
		if (features.length > 0) {
			this.finished(pid, this.queue.DEFINE.FIN_OK);
		} else {
			setTimeout(function () {
				self.awaitSourceFeatures(pid, json);
			}, 500);
		}
	}

	manualSelect(pid, json) {
		const options = Object.assign({
			map: 'default',
			source: 'default',
			queue: "select",
			filter: []
		}, json);
		let selectDetails = {};
		//const features = this.maps[options.map].map.querySourceFeatures(options.layer);
		const features = this.maps[options.map].sources[options.source];
		features.forEach(function (feature) {
			let res = eval(feature.properties[options.filter[1]] + ' ' + options.filter[0] + ' ' + options.filter[2]);
			if (res) {
				selectDetails.properties = feature.properties;
				selectDetails.coordinates = feature.geometry.coordinates.slice();
				selectDetails.centroid = centroid(feature.geometry).geometry.coordinates;
				selectDetails.featureJSON = JSON.stringify(feature);
			}
		});

		self.queue.setMemory("select", selectDetails, "Session");
		self.queue.execute(options.queue, selectDetails);
		this.finished(pid, this.queue.DEFINE.FIN_OK);

	}

	addClick(pid, json) {
		let self = this;
		const options = Object.assign({
			map: 'default',
			queue: 'clicked'
		}, json);
		this.maps[options.map].map.on('click', function (e) {
			let data = {location: e.lngLat};
			self.queue.setMemory("click", data, "Session");
			self.queue.execute(options.queue, data);
		});
		this.finished(pid, this.queue.DEFINE.FIN_OK);
	}

	removeClick(pid, json) {
		let self = this;
		const options = Object.assign({
			map: 'default'
		}, json);
		this.maps[options.map].map.off('click', function (e) {
		});
		this.finished(pid, this.queue.DEFINE.FIN_OK);
	}

	/**
	 * Set the data for a layer
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The name of the map that the layer is on
	 * @param {string} json.source - The name of the source that the data will be set on
	 * @param {object|string} json.data - The data to set the layer to (this will override old data)
	 */
	setData(pid, json) {

		const options = Object.assign({
			map: 'default',
			source: 'default',
			data: {
				type: 'FeatureCollection',
				features: [],
			},
			mode: 'clear',
			index: 'id'
		}, json);
		this.queue.deleteRegister(options.map + 'Idle');
		if(options.mode==='append') {
			for(let i in this.maps[options.map].sources[options.source]) {
				options.data.features.push(this.maps[options.map].sources[options.source][i]);
			}
		}
		let newFeatures=[];
		if(options.mode==='delete') {
			for(let i in this.maps[options.map].sources[options.source]) {
				let fdelete=false;
				for(let j in options.data.features) {
					if(options.data.features[j].properties[options.index]===this.maps[options.map].sources[options.source][i].properties[options.index]) {
						fdelete=true;
						break;
					}
				}
				if(fdelete===false)
					newFeatures.push(this.maps[options.map].sources[options.source][i]);
			}
			options.data.features=newFeatures;
		}

		this.maps[options.map].map.getSource(options.source).setData(options.data);
		// Make a copy of the source data because the internal call is not reliable
		this.maps[options.map].sources[options.source] = options.data.features;
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	setPaintProperty(pid, json) {
		const options = Object.assign({
			map: 'default',
			layer: 'default'
		}, json);
		this.maps[options.map].map.setPaintProperty(options.layer, options.property, options.value);
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	setLayoutProperty(pid, json) {
		const options = Object.assign({
			map: 'default',
			layer: 'default'
		}, json);
		this.maps[options.map].map.setLayoutProperty(options.layer, options.property, options.value);
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Set the data for a source
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The name of the map that the layer is on
	 * @param {string} json.name - The name of the layer to clear
	 */
	clearSource(pid, json) {

		const options = Object.assign({
			map: 'default',
			source: 'default'
		}, json);
		this.queue.deleteRegister(options.map + 'Idle');

		this.maps[options.map].map.getSource(options.source).setData({
			type: 'FeatureCollection',
			features: []
		});
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Zoom to the bounds of a layer
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - Name of the map the layer is within
	 * @param {string} json.source - Name of the layer to zoom in to
	 * @param {int} json.options - options as per: https://docs.mapbox.com/mapbox-gl-js/api/#map#fitbounds
	 */
	zoomToBounds(pid, json) {
		const options = Object.assign({
			map: 'default',
			source: 'default',
			options: {}
		}, json);

		let bounds = new MapboxGL.LngLatBounds();

		this.maps[options.map].sources[options.source].forEach(function (feature) {
			let fbbox = bbox(feature.geometry);
			bounds.extend([fbbox[0], fbbox[1]], [fbbox[2], fbbox[3]]);
		});
		if(bounds._ne)
			this.maps[options.map].map.fitBounds(bounds, options.options);
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * move to a feature
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 * @param {string} json.name - The name of the layer to query
	 * @param {string} json.property - The property key to check against
	 * @param {string} json.value - The value of the property that we'll be looking for
	 */
	moveToFeature(pid, json) {
		const options = Object.assign({
			map: 'default',
			layer: 'default',
			property: '',
			value: ''
		}, json);

		let features = this.maps[options.map].map.getSource(options.layer)._data.features;
		let pointGeom = null;

		for (const feature of features) {
			if (feature.properties.hasOwnProperty(options.property))
				/* Note the == match, this is lose on purpose, due to int/string casting
				 */
				if (feature.properties[options.property] == options.value) {
					pointGeom = feature;
					break;
				}
		}
		if (pointGeom !== null) {
			this.maps[options.map].map.flyTo({
				center: centroid(pointGeom.geometry).geometry.coordinates
			})
		}

		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Zoom /MapboxGL move to a feature
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 * @param {string} json.name - The name of the layer to query
	 * @param {string} json.property - The property key to check against
	 * @param {string} json.value - The value of the property that we'll be looking for
	 * @param {int} json.zoom - The final zoom level for viewing the point
	 * @param {int} json.minZoom - The furthest out the map will zoom whilst flying to the point
	 */
	zoomToFeature(pid, json) {
		const options = Object.assign({
			map: 'default',
			source: 'default',
			property: '',
			value: ''
		}, json);

		let features = this.maps[options.map].map.getSource(options.source)._data.features;
		let pointGeom = null;

		for (const feature of features) {
			if (feature.properties.hasOwnProperty(options.property))
				/* Note the == match, this is lose on purpose, due to int/string casting
				 */
				if (feature.properties[options.property] == options.value) {
					pointGeom = feature;
					break;
				}
		}
		if (pointGeom !== null) {
			this.maps[options.map].map.flyTo({
				center: centroid(pointGeom.geometry).geometry.coordinates,
				zoom: options.zoom,
				minZoom: options.minZoom,
			})
		}

		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Move to a lat long
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 * @param {string} json.coordinates - lat long in array format
	 */
	moveToLocation(pid, json) {
		const options = Object.assign({
			map: 'default'
		}, json);

		this.maps[options.map].map.flyTo({
			center: options.coordinates
		});

		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Resize the visible map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 */
	resize(pid, json) {
		const options = Object.assign({
			map: 'default'
		}, json);
		this.maps[options.map].map.resize();
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Set pitch of the visible map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 * @param {int} json.pitch - The map pitch
	 */
	setPitch(pid, json) {
		const options = Object.assign({
			map: 'default',
			pitch: 0
		}, json);
		this.maps[options.map].map.setPitch(options.pitch);
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Set a memory mapDetails to have current info on the state of the map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 */
	getMapDetails(pid, json) {
		const options = Object.assign({
			map: 'default'
		}, json);
		let data = {
			center: this.maps[options.map].map.getCenter(),
			zoom: this.maps[options.map].map.getZoom()
		};
		self.queue.setMemory("mapDetails", data, "Session");
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Zoom in the map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 */
	zoomIn(pid, json) {
		const options = Object.assign({
			map: 'default'
		}, json);
		this.maps[options.map].map.zoomIn();
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Zoom the map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 */
	zoomTo(pid, json) {
		const options = Object.assign({
			map: 'default',
			zoom: 1
		}, json);
		this.maps[options.map].map.zoomTo(parseInt(options.zoom));
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Zoom out the map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 */
	zoomOut(pid, json) {
		const options = Object.assign({
			map: 'default'
		}, json);
		this.maps[options.map].map.zoomOut();
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

	/**
	 * Add a popup to the map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 * @param {array} json.lngLat - The long Lat to place the popup at
	 * @param {string} json.template - The template to use
	 */
	addPopup(pid, json) {
		const options = Object.assign({
			map: 'default',
			lngLat: [-96, 37.8],
			options:{closeOnClick: false}
		}, json);

		if (this.popup !== undefined)
			this.popup.remove();
		this.popup = new MapboxGL.Popup(options.options)
			.setLngLat(options.lngLat)
			.setHTML(`<div id="pu_${pid}"></div>`)
			.addTo(this.maps[options.map].map);
		this.queue.templateProcessor(options.template, `#pu_${pid}`, this.queue.DEFINE.RENDER_REPLACE);
		this.finished(pid, self.queue.DEFINE.FIN_OK);

	}

	/**
	 * Remove a popup from the map
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 */
	removePopup(pid, json) {
		const options = Object.assign({
			map: 'default'
		}, json);

		if (this.popup !== undefined)
			this.popup.remove();
		this.finished(pid, self.queue.DEFINE.FIN_OK);

	}

	/**
	 * Query and highlight feature depending on the paint features
	 * @param {int} pid
	 * @param {object} json
	 * @param {string} json.map - The map that the querying layer is on
	 * @param {string} json.name - The name of the layer to query
	 * @param {object} json.paint - The pain object for querying ans styling
	 * @param {string} json.paint.type - The styling type that will be changed for highlighting
	 * @param {array} json.paint.value - The list of paint styles for querying e.g. [["get", "feature_name"], "test_name", "#333399"] This will be appended to the current style
	 */
	paintQueryFeatures(pid, json) {
		const options = Object.assign({
			map: 'default',
			name: '',
			paint: {},
		}, json);

		let paint = JSON.parse(JSON.stringify(this.maps[options.map].layers[options.name])).defaultStyle[options.paint.type];
		let final = null;
		if (typeof paint === 'object') {
			final = paint.splice([paint.length - 1], 1);
			for (let value of options.paint.value) {
				for (let i = 0; i < paint.length; i += 2) {
					if (value === paint[i]) {
						paint.splice(i, 2);
					}
				}
			}
			// Edge doesn't support ... notation ffs. Don't want to bable edge so changed for now
			// Untested
			//paint = [...paint, ...options.paint.value];
			paint.push(options.paint.value)
			paint.push(final[0]);
		} else {
			paint = options.paint.value;
		}

		this.maps[options.map].map.setPaintProperty(options.name, options.paint.type, paint);

		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}

/*	addDrawTools(pid,json) {
		const options = Object.assign({
			map: 'default'
		}, json);
		let Draw = new MapboxDraw();
		this.maps[options.map].map.addControl(Draw, 'top-left');
		this.finished(pid, self.queue.DEFINE.FIN_OK);

	}*/

/*
	addEditTools(pid,json) {
		const options = Object.assign({
			map: 'default',
			source: 'default'
		}, json);
		this.maps[options.map].controls['Edit']= new MapboxEdit(options);
		this.maps[options.map].map.addControl(this.maps[options.map].controls['Edit']);
		this.finished(pid, self.queue.DEFINE.FIN_OK);
	}
*/

	removeEditTools(pid,json) {
		const options = Object.assign({
			map: 'default'
		}, json);
		this.maps[options.map].map.removeControl(this.maps[options.map].controls['Edit']);
		this.finished(pid, self.queue.DEFINE.FIN_OK);

	}
}