import React from 'react'; import '../css/Map.scss'; import 'leaflet/dist/leaflet.css'; import Leaflet from 'leaflet'; import async from 'async'; import '../vendor/leaflet/L.Control.MousePosition.css'; import '../vendor/leaflet/L.Control.MousePosition'; import '../vendor/leaflet/Leaflet.Autolayers/css/leaflet.auto-layers.css'; import '../vendor/leaflet/Leaflet.Autolayers/leaflet-autolayers'; import '../vendor/leaflet/L.TileLayer.NoGap'; import Dropzone from '../vendor/dropzone'; import $ from 'jquery'; import ErrorMessage from './ErrorMessage'; import SwitchModeButton from './SwitchModeButton'; import ShareButton from './ShareButton'; import AssetDownloads from '../classes/AssetDownloads'; import {addTempLayer} from '../classes/TempLayer'; import PropTypes from 'prop-types'; import PluginsAPI from '../classes/plugins/API'; import Basemaps from '../classes/Basemaps'; import Standby from './Standby'; import LayersControl from './LayersControl'; import update from 'immutability-helper'; import Utils from '../classes/Utils'; class Map extends React.Component { static defaultProps = { showBackground: false, mapType: "orthophoto", public: false }; static propTypes = { showBackground: PropTypes.bool, tiles: PropTypes.array.isRequired, mapType: PropTypes.oneOf(['orthophoto', 'plant', 'dsm', 'dtm']), public: PropTypes.bool }; constructor(props) { super(props); this.state = { error: "", singleTask: null, // When this is set to a task, show a switch mode button to view the 3d model pluginActionButtons: [], showLoading: false, // for drag&drop of files opacity: 100, imageryLayers: [], overlays: [] }; this.basemaps = {}; this.mapBounds = null; this.autolayers = null; this.loadImageryLayers = this.loadImageryLayers.bind(this); this.updatePopupFor = this.updatePopupFor.bind(this); this.handleMapMouseDown = this.handleMapMouseDown.bind(this); } updateOpacity = (evt) => { this.setState({ opacity: parseFloat(evt.target.value), }); } updatePopupFor(layer){ const popup = layer.getPopup(); $('#layerOpacity', popup.getContent()).val(layer.options.opacity); } loadImageryLayers(forceAddLayers = false){ const { tiles } = this.props, layerId = layer => { const meta = layer[Symbol.for("meta")]; return meta.task.project + "_" + meta.task.id; }; // Remove all previous imagery layers // and keep track of which ones were selected const prevSelectedLayers = []; this.state.imageryLayers.forEach(layer => { if (this.map.hasLayer(layer)) prevSelectedLayers.push(layerId(layer)); layer.remove(); }); this.setState({imageryLayers: []}); // Request new tiles return new Promise((resolve, reject) => { this.tileJsonRequests = []; async.each(tiles, (tile, done) => { const { url, meta, type } = tile; let metaUrl = url + "metadata"; if (type == "plant") metaUrl += "?formula=NDVI&bands=RGN&color_map=rdylgn"; if (type == "dsm" || type == "dtm") metaUrl += "?hillshade=3&color_map=jet_r"; this.tileJsonRequests.push($.getJSON(metaUrl) .done(mres => { const { scheme, name, maxzoom, statistics } = mres; const bounds = Leaflet.latLngBounds( [mres.bounds.value.slice(0, 2).reverse(), mres.bounds.value.slice(2, 4).reverse()] ); // Build URL let tileUrl = mres.tiles[0]; // Certain types need the rescale parameter if (["plant", "dsm", "dtm"].indexOf(type) !== -1 && statistics){ const params = Utils.queryParams({search: tileUrl.slice(tileUrl.indexOf("?"))}); if (statistics["1"]){ // Add rescale params["rescale"] = encodeURIComponent(`${statistics["1"]["min"]},${statistics["1"]["max"]}`); }else{ console.warn("Cannot find min/max statistics for dataset, setting to -1,1"); params["rescale"] = encodeURIComponent("-1,1"); } tileUrl = Utils.buildUrlWithQuery(tileUrl, params); } const layer = Leaflet.tileLayer(tileUrl, { bounds, minZoom: 0, maxZoom: maxzoom + 99, maxNativeZoom: maxzoom, tms: scheme === 'tms', opacity: this.state.opacity / 100, detectRetina: true }); // Associate metadata with this layer meta.name = name; meta.metaUrl = metaUrl; layer[Symbol.for("meta")] = meta; layer[Symbol.for("tile-meta")] = mres; if (forceAddLayers || prevSelectedLayers.indexOf(layerId(layer)) !== -1){ layer.addTo(this.map); } // Show 3D switch button only if we have a single orthophoto if (tiles.length === 1){ this.setState({singleTask: meta.task}); } // For some reason, getLatLng is not defined for tileLayer? // We need this function if other code calls layer.openPopup() let self = this; layer.getLatLng = function(){ let latlng = self.lastClickedLatLng ? self.lastClickedLatLng : this.options.bounds.getCenter(); return latlng; }; var popup = L.DomUtil.create('div', 'infoWindow'); popup.innerHTML = `
${name}
Bounds: [${layer.options.bounds.toBBoxString().split(",").join(", ")}]
`; layer.bindPopup(popup); $('#layerOpacity', popup).on('change input', function() { layer.setOpacity($('#layerOpacity', popup).val()); }); this.setState(update(this.state, { imageryLayers: {$push: [layer]} })); let mapBounds = this.mapBounds || Leaflet.latLngBounds(); mapBounds.extend(bounds); this.mapBounds = mapBounds; done(); }) .fail((_, __, err) => done(err)) ); }, err => { if (err){ this.setState({error: err.message || JSON.stringify(err)}); } resolve(); }); }); } componentDidMount() { const { showBackground, tiles } = this.props; this.map = Leaflet.map(this.container, { scrollWheelZoom: true, positionControl: true, zoomControl: false, minZoom: 0, maxZoom: 24 }); PluginsAPI.Map.triggerWillAddControls({ map: this.map, tiles }); let scaleControl = Leaflet.control.scale({ maxWidth: 250, }).addTo(this.map); //add zoom control with your options let zoomControl = Leaflet.control.zoom({ position:'bottomleft' }).addTo(this.map); if (showBackground) { this.basemaps = {}; Basemaps.forEach((src, idx) => { const { url, ...props } = src; const layer = L.tileLayer(url, props); if (idx === 0) { layer.addTo(this.map); } this.basemaps[props.label] = layer; }); const customLayer = L.layerGroup(); customLayer.on("add", a => { let url = window.prompt(`Enter a tile URL template. Valid tokens are: {z}, {x}, {y} for Z/X/Y tile scheme {-y} for flipped TMS-style Y coordinates Example: https://a.tile.openstreetmap.org/{z}/{x}/{y}.png `, 'https://a.tile.openstreetmap.org/{z}/{x}/{y}.png'); if (url){ customLayer.clearLayers(); const l = L.tileLayer(url, { maxZoom: 24, minZoom: 0 }); customLayer.addLayer(l); l.bringToBack(); } }); this.basemaps["Custom"] = customLayer; this.basemaps["None"] = L.layerGroup(); } this.layersControl = new LayersControl({ layers: this.state.imageryLayers, overlays: this.state.overlays }).addTo(this.map); this.autolayers = Leaflet.control.autolayers({ overlays: {}, selectedOverlays: [], baseLayers: this.basemaps }).addTo(this.map); // Drag & Drop overlays const addDnDZone = (container, opts) => { const mapTempLayerDrop = new Dropzone(container, opts); mapTempLayerDrop.on("addedfile", (file) => { this.setState({showLoading: true}); addTempLayer(file, (err, tempLayer, filename) => { if (!err){ tempLayer.addTo(this.map); tempLayer[Symbol.for("meta")] = {name: filename}; this.setState(update(this.state, { overlays: {$push: [tempLayer]} })); //zoom to all features this.map.fitBounds(tempLayer.getBounds()); }else{ this.setState({ error: err.message || JSON.stringify(err) }); } this.setState({showLoading: false}); }); }); mapTempLayerDrop.on("error", (file) => { mapTempLayerDrop.removeFile(file); }); }; addDnDZone(this.container, {url : "/", clickable : false}); const AddOverlayCtrl = Leaflet.Control.extend({ options: { position: 'topright' }, onAdd: function () { this.container = Leaflet.DomUtil.create('div', 'leaflet-control-add-overlay leaflet-bar leaflet-control'); Leaflet.DomEvent.disableClickPropagation(this.container); const btn = Leaflet.DomUtil.create('a', 'leaflet-control-add-overlay-button'); btn.setAttribute("title", "Add a temporary GeoJSON (.json) or ShapeFile (.zip) overlay"); this.container.append(btn); addDnDZone(btn, {url: "/", clickable: true}); return this.container; } }); new AddOverlayCtrl().addTo(this.map); this.map.fitWorld(); this.map.attributionControl.setPrefix(""); this.loadImageryLayers(true).then(() => { this.map.fitBounds(this.mapBounds); this.map.on('click', e => { // Find first tile layer at the selected coordinates for (let layer of this.state.imageryLayers){ if (layer._map && layer.options.bounds.contains(e.latlng)){ this.lastClickedLatLng = this.map.mouseEventToLatLng(e.originalEvent); this.updatePopupFor(layer); layer.openPopup(); break; } } }).on('popupopen', e => { // Load task assets links in popup if (e.popup && e.popup._source && e.popup._content){ const infoWindow = e.popup._content; if (typeof infoWindow === 'string') return; const $assetLinks = $("ul.asset-links", infoWindow); if ($assetLinks.length > 0 && $assetLinks.hasClass('loading')){ const {id, project} = (e.popup._source[Symbol.for("meta")] || {}).task; $.getJSON(`/api/projects/${project}/tasks/${id}/`) .done(res => { const { available_assets } = res; const assets = AssetDownloads.excludeSeparators(); const linksHtml = assets.filter(a => available_assets.indexOf(a.asset) !== -1) .map(asset => { return `
  • ${asset.label}
  • `; }) .join(""); $assetLinks.append($(linksHtml)); }) .fail(() => { $assetLinks.append($("
  • Error: cannot load assets list.
  • ")); }) .always(() => { $assetLinks.removeClass('loading'); }); } } }); }); PluginsAPI.Map.triggerDidAddControls({ map: this.map, tiles: tiles, controls:{ autolayers: this.autolayers, scale: scaleControl, zoom: zoomControl } }); PluginsAPI.Map.triggerAddActionButton({ map: this.map, tiles }, (button) => { this.setState(update(this.state, { pluginActionButtons: {$push: [button]} })); }); } componentDidUpdate(prevProps, prevState) { this.state.imageryLayers.forEach(imageryLayer => { imageryLayer.setOpacity(this.state.opacity / 100); this.updatePopupFor(imageryLayer); }); if (prevProps.tiles !== this.props.tiles){ this.loadImageryLayers(); } if (this.layersControl && (prevState.imageryLayers !== this.state.imageryLayers || prevState.overlays !== this.state.overlays)){ this.layersControl.update(this.state.imageryLayers, this.state.overlays); } } componentWillUnmount() { this.map.remove(); if (this.tileJsonRequests) { this.tileJsonRequests.forEach(tileJsonRequest => tileJsonRequest.abort()); this.tileJsonRequests = []; } } handleMapMouseDown(e){ // Make sure the share popup closes if (this.shareButton) this.shareButton.hidePopup(); } render() { return (
    Opacity:
    (this.container = domNode)} onMouseDown={this.handleMapMouseDown} />
    {this.state.pluginActionButtons.map((button, i) =>
    {button}
    )} {(!this.props.public && this.state.singleTask !== null) ? { this.shareButton = ref; }} task={this.state.singleTask} linksTarget="map" /> : ""}
    ); } } export default Map;