OpenDroneMap-WebODM/app/static/app/js/components/Map.jsx

361 wiersze
11 KiB
React
Czysty Zwykły widok Historia

import React from 'react';
import '../css/Map.scss';
import 'leaflet/dist/leaflet.css';
import Leaflet from 'leaflet';
2016-11-26 15:27:54 +00:00
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 Dropzone from '../vendor/dropzone';
import $ from 'jquery';
import ErrorMessage from './ErrorMessage';
import SwitchModeButton from './SwitchModeButton';
2017-12-01 18:52:36 +00:00
import ShareButton from './ShareButton';
import AssetDownloads from '../classes/AssetDownloads';
import {addTempLayer} from '../classes/TempLayer';
import PropTypes from 'prop-types';
2018-02-09 17:38:42 +00:00
import PluginsAPI from '../classes/plugins/API';
2018-09-04 23:06:04 +00:00
import Basemaps from '../classes/Basemaps';
import Standby from './Standby';
import update from 'immutability-helper';
class Map extends React.Component {
static defaultProps = {
maxzoom: 18,
minzoom: 0,
showBackground: false,
opacity: 100,
mapType: "orthophoto",
public: false
};
static propTypes = {
maxzoom: PropTypes.number,
minzoom: PropTypes.number,
showBackground: PropTypes.bool,
tiles: PropTypes.array.isRequired,
opacity: PropTypes.number,
mapType: PropTypes.oneOf(['orthophoto', '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
};
this.imageryLayers = [];
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);
}
updatePopupFor(layer){
const popup = layer.getPopup();
$('#layerOpacity', popup.getContent()).val(layer.options.opacity);
}
loadImageryLayers(forceAddLayers = false){
const { tiles } = this.props,
assets = AssetDownloads.excludeSeparators(),
layerId = layer => {
const meta = layer[Symbol.for("meta")];
2017-12-01 18:52:36 +00:00
return meta.task.project + "_" + meta.task.id;
};
// Remove all previous imagery layers
// and keep track of which ones were selected
const prevSelectedLayers = [];
this.imageryLayers.forEach(layer => {
this.autolayers.removeLayer(layer);
if (this.map.hasLayer(layer)) prevSelectedLayers.push(layerId(layer));
layer.remove();
});
this.imageryLayers = [];
// Request new tiles
return new Promise((resolve, reject) => {
this.tileJsonRequests = [];
async.each(tiles, (tile, done) => {
const { url, meta } = tile;
this.tileJsonRequests.push($.getJSON(url)
.done(info => {
const bounds = Leaflet.latLngBounds(
[info.bounds.slice(0, 2).reverse(), info.bounds.slice(2, 4).reverse()]
);
const layer = Leaflet.tileLayer(info.tiles[0], {
bounds,
minZoom: info.minzoom,
maxZoom: L.Browser.retina ? (info.maxzoom + 1) : info.maxzoom,
maxNativeZoom: L.Browser.retina ? (info.maxzoom - 1) : info.maxzoom,
tms: info.scheme === 'tms',
opacity: this.props.opacity / 100,
detectRetina: true
});
// Associate metadata with this layer
meta.name = info.name;
layer[Symbol.for("meta")] = meta;
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){
2017-12-01 18:52:36 +00:00
this.setState({singleTask: meta.task});
}
// For some reason, getLatLng is not defined for tileLayer?
// We need this function if other code calls layer.openPopup()
layer.getLatLng = function(){
return this.options.bounds.getCenter();
};
var popup = L.DomUtil.create('div', 'infoWindow');
popup.innerHTML = `<div class="title">
${info.name}
</div>
<div class="popup-opacity-slider">Opacity: <input id="layerOpacity" type="range" value="${layer.options.opacity}" min="0" max="1" step="0.01" /></div>
<div>Bounds: [${layer.options.bounds.toBBoxString().split(",").join(", ")}]</div>
<ul class="asset-links">
${assets.map(asset => {
2017-12-01 18:52:36 +00:00
return `<li><a href="${asset.downloadUrl(meta.task.project, meta.task.id)}">${asset.label}</a></li>`;
}).join("")}
</ul>
<button
2017-12-01 18:52:36 +00:00
onclick="location.href='/3d/project/${meta.task.project}/task/${meta.task.id}/';"
type="button"
2017-11-24 20:20:38 +00:00
class="switchModeButton btn btn-sm btn-secondary">
<i class="fa fa-cube"></i> 3D
</button>`;
2018-03-27 18:35:16 +00:00
layer.bindPopup(popup);
$('#layerOpacity', popup).on('change input', function() {
layer.setOpacity($('#layerOpacity', popup).val());
});
this.imageryLayers.push(layer);
let mapBounds = this.mapBounds || Leaflet.latLngBounds();
mapBounds.extend(bounds);
this.mapBounds = mapBounds;
// Add layer to layers control
this.autolayers.addOverlay(layer, info.name);
done();
})
.fail((_, __, err) => done(err))
);
}, err => {
if (err){
this.setState({error: err.message || JSON.stringify(err)});
reject(err);
}else{
resolve();
}
});
});
}
componentDidMount() {
var mapTempLayerDrop = new Dropzone(this.container, {url : "/", clickable : false});
mapTempLayerDrop.on("addedfile", (file) => {
this.setState({showLoading: true});
addTempLayer(file, (err, tempLayer, filename) => {
if (!err){
tempLayer.addTo(this.map);
//add layer to layer switcher with file name
this.autolayers.addOverlay(tempLayer, filename);
//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", function(file) {
mapTempLayerDrop.removeFile(file);
});
2018-05-04 16:37:34 +00:00
const { showBackground, tiles } = this.props;
this.map = Leaflet.map(this.container, {
scrollWheelZoom: true,
2018-02-09 17:38:42 +00:00
positionControl: true,
zoomControl: false
});
2018-02-09 17:38:42 +00:00
PluginsAPI.Map.triggerWillAddControls({
2018-05-04 16:37:34 +00:00
map: this.map,
tiles
});
2018-02-09 17:38:42 +00:00
Leaflet.control.scale({
maxWidth: 250,
}).addTo(this.map);
//add zoom control with your options
Leaflet.control.zoom({
position:'bottomleft'
}).addTo(this.map);
if (showBackground) {
2018-09-06 12:18:49 +00:00
this.basemaps = {};
Basemaps.forEach((src, idx) => {
2018-09-04 23:06:04 +00:00
const { url, ...props } = src;
const layer = L.tileLayer(url, props);
if (idx === 0) {
layer.addTo(this.map);
}
2018-09-06 12:18:49 +00:00
this.basemaps[props.label] = layer;
2018-09-04 23:06:04 +00:00
});
2019-02-19 21:15:28 +00:00
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: 21,
minZoom: 0
});
customLayer.addLayer(l);
l.bringToBack();
}
});
this.basemaps["Custom"] = customLayer;
this.basemaps["None"] = L.layerGroup();
}
this.autolayers = Leaflet.control.autolayers({
overlays: {},
selectedOverlays: [],
baseLayers: this.basemaps
}).addTo(this.map);
this.map.fitWorld();
this.map.attributionControl.setPrefix("");
this.loadImageryLayers(true).then(() => {
2016-11-21 21:32:37 +00:00
this.map.fitBounds(this.mapBounds);
this.map.on('click', e => {
// Find first tile layer at the selected coordinates
for (let layer of this.imageryLayers){
if (layer._map && layer.options.bounds.contains(e.latlng)){
this.updatePopupFor(layer);
2016-11-21 21:32:37 +00:00
layer.openPopup();
break;
}
2016-11-21 21:32:37 +00:00
}
});
});
2018-02-09 17:38:42 +00:00
PluginsAPI.Map.triggerDidAddControls({
2018-05-04 16:37:34 +00:00
map: this.map,
tiles: tiles
});
2018-04-29 20:43:45 +00:00
PluginsAPI.Map.triggerAddActionButton({
2018-05-04 16:37:34 +00:00
map: this.map,
tiles
}, (button) => {
this.setState(update(this.state, {
pluginActionButtons: {$push: [button]}
}));
});
}
componentDidUpdate(prevProps) {
this.imageryLayers.forEach(imageryLayer => {
imageryLayer.setOpacity(this.props.opacity / 100);
this.updatePopupFor(imageryLayer);
});
if (prevProps.tiles !== this.props.tiles){
2018-05-04 16:37:34 +00:00
this.loadImageryLayers();
}
}
componentWillUnmount() {
this.map.remove();
if (this.tileJsonRequests) {
this.tileJsonRequests.forEach(tileJsonRequest => this.tileJsonRequest.abort());
this.tileJsonRequests = [];
}
}
handleMapMouseDown(e){
// Make sure the share popup closes
if (this.shareButton) this.shareButton.hidePopup();
}
render() {
return (
<div style={{height: "100%"}} className="map">
<ErrorMessage bind={[this, 'error']} />
<Standby
message="Loading..."
show={this.state.showLoading}
/>
<div
style={{height: "100%"}}
ref={(domNode) => (this.container = domNode)}
onMouseDown={this.handleMapMouseDown}
>
</div>
<div className="actionButtons">
{this.state.pluginActionButtons.map((button, i) => <div key={i}>{button}</div>)}
{(!this.props.public && this.state.singleTask !== null) ?
<ShareButton
ref={(ref) => { this.shareButton = ref; }}
task={this.state.singleTask}
linksTarget="map"
/>
: ""}
<SwitchModeButton
task={this.state.singleTask}
type="mapToModel"
public={this.props.public} />
</div>
</div>
);
}
}
2017-11-24 20:20:38 +00:00
export default Map;