зеркало из https://github.com/microsoft/landcover.git
461 строка
21 KiB
HTML
461 строка
21 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
|
<meta name="description" content="">
|
|
<meta name="author" content="">
|
|
<link rel="icon" href="favicon.ico">
|
|
|
|
<title>Microsoft AI for Earth - Land cover mapping platform</title>
|
|
|
|
<!-- Core CSS -->
|
|
<link href="css/leaflet.css" rel="stylesheet" />
|
|
<link href="css/leaflet-slider.css" rel="stylesheet" />
|
|
<link href="css/leaflet-sidebar.css" rel="stylesheet" />
|
|
<link href="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.css" rel="stylesheet" />
|
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.4.14/leaflet.draw.css">
|
|
<link href="css/noty.css" rel="stylesheet">
|
|
|
|
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
|
|
<link href="css/main.css" rel="stylesheet">
|
|
|
|
|
|
</head>
|
|
|
|
<body>
|
|
|
|
<div id="map"></div>
|
|
|
|
<div id="privacy" class="leaflet-container">
|
|
<a href="https://privacy.microsoft.com/en-US/privacystatement" target="_blank">Privacy Statement</a>
|
|
</div>
|
|
|
|
<div id="sidebar" class="sidebar">
|
|
|
|
<!-- Tab panes -->
|
|
<div class="sidebar-content">
|
|
|
|
<!-- Start of "home" tab -->
|
|
<div class="sidebar-pane" id="home">
|
|
<div style="text-align: center; margin-bottom:10px;" id="inputImages">
|
|
<!-- <h3 id="lblModelInput">Model Input</h3> -->
|
|
<img id="inputImage">
|
|
<div style="display:inline-block" id="exampleImages"><div style="display:inline-block" id="exampleImageList"></div></div>
|
|
</div>
|
|
<!--
|
|
<div style="text-align: center; margin-bottom:10px; width:100%;" id="exampleImages">
|
|
<h3>Land Cover Predictions</h3>
|
|
<div id="exampleImageList">
|
|
</div>
|
|
</div>
|
|
-->
|
|
<div style="display:none;text-align: left; margin-bottom:10px; width:100%; padding-left:30px;">
|
|
<span>Name of zone: <span id="lblZoneName">none selected</span></span>
|
|
</div>
|
|
|
|
<div style="margin-bottom:10px;">
|
|
|
|
<h3 style="display:none;text-align: center; margin-top:20px;">Correction type:</h3>
|
|
<div style="padding-left:20px">
|
|
<div id="classList"></div>
|
|
<div style="text-align: center; margin-top:10px; margin-bottom:20px;">
|
|
<button id="btnNewClass" style="background-color: gray; border-color: white;"> Add new class</button>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top:10px;">
|
|
<button id="btnRetrain">Retrain (<span id="label-retrains">0</span> times)</button>
|
|
</div>
|
|
<div style="text-align: center; margin-top:10px;">
|
|
<button id="btnUndo">Undo</button>
|
|
</div>
|
|
<div style="text-align: center; margin-top:10px;">
|
|
<button id="btnReset">Reset</button>
|
|
</div>
|
|
<div style="text-align: center; margin-top:10px;">
|
|
<button id="btnDownload">Download</button>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top:10px;">
|
|
<input type="text" id="lblURL" value="" placeholder="Visit this URL to start from a saved checkpoint" readonly="readonly"/>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top:5px;">
|
|
<div id="lblPNG"></div>
|
|
<div id="lblTIFF"></div>
|
|
<div id="lblStatistics"></div>
|
|
</div>
|
|
|
|
<div style="text-align: center; margin-top:10px;">
|
|
<button id="btnKillSession">End Session</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- End of "home" tab -->
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Core JavaScript
|
|
================================================== -->
|
|
<script src="js/jquery-3.3.1.min.js"></script>
|
|
<script src="js/noty.js" type="text/javascript"></script>
|
|
<script src="js/jscolor.js" type="text/javascript"></script>
|
|
|
|
<!-- Leaflet JavaScript
|
|
================================================== -->
|
|
<script src="js/leaflet.js" type="text/javascript"></script>
|
|
<script src="js/leaflet-slider.js" type="text/javascript"></script>
|
|
<script src="js/leaflet-sidebar.min.js" type="text/javascript"></script>
|
|
<script src="js/leaflet.filelayer.js" type="text/javascript"></script>
|
|
<script src="https://unpkg.com/leaflet-control-geocoder/dist/Control.Geocoder.js" type="text/javascript"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/leaflet-easybutton@2/src/easy-button.js" type="text/javascript"></script>
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/0.4.14/leaflet.draw.js" type="text/javascript"></script>
|
|
|
|
<!-- Application JavaScript
|
|
================================================== -->
|
|
<script src="js/components.js" type="text/javascript"></script>
|
|
<script src="js/handlers.js" type="text/javascript"></script>
|
|
<script src="js/main.js" type="text/javascript"></script>
|
|
<script src="js/utils.js" type="text/javascript"></script>
|
|
<script src="js/globals.js" type="text/javascript"></script>
|
|
|
|
<!-- List of backend URLS to query
|
|
================================================== -->
|
|
<script src="endpoints.mine.js" type="text/javascript"></script>
|
|
|
|
<script type="text/javascript">
|
|
|
|
var zonePickerControl;
|
|
//----------------------------------------------------------------------
|
|
// Runtime entrance
|
|
//----------------------------------------------------------------------
|
|
$(document).ready(function(){
|
|
|
|
//----------------------------------------------------------------------
|
|
// Parse URL arguments
|
|
//----------------------------------------------------------------------
|
|
let args = getURLArguments()
|
|
EXP_NAME = args.userID
|
|
gBackendURL = SERVERS[args.backendID]["url"];
|
|
|
|
if(!(args.dataset in DATASETS) && (args.dataset !== null)){
|
|
args.dataset = null;
|
|
new Noty({
|
|
type: "error",
|
|
text: "Requested dataset doesn't exist, using default",
|
|
layout: 'topCenter',
|
|
timeout: 5000,
|
|
theme: 'metroui'
|
|
}).show();
|
|
}
|
|
if(args.dataset === null){
|
|
args.dataset = "esri_world_imagery";
|
|
}
|
|
gCurrentDataset = args.dataset;
|
|
gCurrentModel = args.model;
|
|
|
|
if(args.cachedModel !== null){
|
|
doLoad(args.cachedModel); // load a cached version of the model
|
|
}else{
|
|
//doReset(false, true); // reset the backend server
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Start user study if necessary
|
|
//----------------------------------------------------------------------
|
|
var userStudy = false;
|
|
if(args.maxTime !== null){
|
|
runUserStudyTimer(args.maxTime);
|
|
userStudy = true;
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Load the dataset specific shapes
|
|
//----------------------------------------------------------------------
|
|
// TODO: This needs to be refactored out of main() somehow. e.g. If I want to create a new interface that handles zone clicks differently then it should be easy to do, while currently I would have to break everything about how zones are currently handled.
|
|
if(DATASETS[gCurrentDataset]["shapeLayers"] !== null && DATASETS[gCurrentDataset]["shapeLayers"].length !== 0){
|
|
for(zoneSetId in DATASETS[gCurrentDataset]["shapeLayers"]){
|
|
// for each shapeLayer in the current dataset we need to load the GeoJSON in `zoneMaps`
|
|
|
|
var zoneName = DATASETS[gCurrentDataset]["shapeLayers"][zoneSetId]["name"];
|
|
gZoneMapsWeight[zoneName] = DEFAULT_ZONE_LINE_WEIGHTS[zoneSetId];
|
|
gZonemaps[zoneName] = L.geoJSON(null, {
|
|
style: DEFAULT_ZONE_STYLE(gZoneMapsWeight[zoneName]),
|
|
onEachFeature: forEachFeatureOnClick,
|
|
pane: "polygons"
|
|
});
|
|
|
|
getZoneMap(zoneSetId, zoneName, DATASETS[gCurrentDataset]["shapeLayers"][zoneSetId]["shapesFn"]);
|
|
}
|
|
}
|
|
|
|
//----------------------------------------------------------------------
|
|
// Update the correction pixel size to the resolution of the dataset
|
|
//----------------------------------------------------------------------
|
|
CORRECTION_WINDOW_SIZE = DATASETS[gCurrentDataset]["dataLayer"]["resolution"];
|
|
|
|
//----------------------------------------------------------------------
|
|
// Load the specific model classes
|
|
//----------------------------------------------------------------------
|
|
|
|
for(var i=0;i<MODELS[gCurrentModel]["classes"].length;i++){
|
|
var currentClass = MODELS[gCurrentModel]["classes"][i];
|
|
|
|
var newClassIdx = CLASSES.length;
|
|
var newClassName = currentClass["name"];
|
|
var newColor = currentClass["color"];
|
|
|
|
var newClassElement = $("<div class='radio'>");
|
|
var newLabel = $(" \
|
|
<label><input type='radio' name='radClasses' class='radClasses' value='"+newClassName+"'><span class='className'>"+newClassName+"</span> (<span class='classCounts'>0</span> samples since last retrain)<i class='fa fa-edit ml-1 classNameEdit'></i></label> \
|
|
");
|
|
|
|
var newPicker = document.createElement('button');
|
|
newPicker.classList.add("circle");
|
|
newPicker.classList.add("jscolor");
|
|
newPicker.setAttribute("data-class-label", newClassName);
|
|
newPicker.setAttribute("data-class-idx", newClassIdx);
|
|
var output = new jscolor(newPicker, {
|
|
valueElement: null,
|
|
value: newColor.substr(1),
|
|
position:'left',
|
|
zIndex:2001,
|
|
closable:true,
|
|
closeText:'Close',
|
|
onFineChange:'updateClassColor(this)'
|
|
});
|
|
|
|
newClassElement.append(newPicker);
|
|
newClassElement.append(newLabel);
|
|
|
|
$("#classList").append(newClassElement);
|
|
|
|
CLASSES.push({
|
|
"name": newClassName,
|
|
"color": newColor,
|
|
"count": 0
|
|
});
|
|
}
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// Setup map layers
|
|
//----------------------------------------------------------------------
|
|
|
|
// Create the basemap for the current dataset
|
|
var baseLayer = L.tileLayer(DATASETS[gCurrentDataset]["basemapLayer"]["url"], DATASETS[gCurrentDataset]["basemapLayer"]["args"]);
|
|
|
|
// This is the list of basemaps that will be displayed on the control in bottom left of GUI
|
|
gCurrentBasemapLayerName = DATASETS[gCurrentDataset]["metadata"]["imageryName"];
|
|
gBasemaps[DATASETS[gCurrentDataset]["metadata"]["imageryName"]] = baseLayer;
|
|
gBasemaps["OpenStreetMap"] = getOSMLayer();
|
|
gBasemaps["ESRI World Imagery"] = getESRILayer();
|
|
|
|
|
|
// Setup the initial layers to be displayed on the map. This consists of the basemap from the dataset and, if defined, the first shape layer.
|
|
var initialLayers = [baseLayer];
|
|
if(DATASETS[gCurrentDataset]["shapeLayers"] !== null && DATASETS[gCurrentDataset]["shapeLayers"].length !== 0){
|
|
initialLayers.push(gZonemaps[DATASETS[gCurrentDataset]["shapeLayers"][0]["name"]]);
|
|
gCurrentZoneLayerName = DATASETS[gCurrentDataset]["shapeLayers"][0]["name"];
|
|
}
|
|
|
|
// Setup the global map object
|
|
gMap = L.map('map', {
|
|
zoomControl: false,
|
|
crs: L.CRS.EPSG3857, // this is the projection CRS (EPSG:3857), but it is different than the data CRS (EPSG:4326). See https://gis.stackexchange.com/questions/225765/leaflet-map-crs-is-3857-but-coordinates-4326/225786.
|
|
center: DATASETS[gCurrentDataset]["basemapLayer"]["initialLocation"],
|
|
zoom: DATASETS[gCurrentDataset]["basemapLayer"]["initialZoom"],
|
|
keyboard: false,
|
|
minZoom: baseLayer.options.minZoom,
|
|
layers: initialLayers
|
|
});
|
|
gMap.createPane('customPolygons');
|
|
gMap.getPane('customPolygons').style.zIndex = 2001;
|
|
|
|
gMap.createPane('polygons');
|
|
gMap.getPane('polygons').style.zIndex = 2000;
|
|
|
|
gMap.createPane('labels');
|
|
gMap.getPane('labels').style.zIndex = 201;
|
|
|
|
gMap.createPane('downloadOutput');
|
|
gMap.getPane('downloadOutput').style.zIndex = 200;
|
|
|
|
|
|
|
|
//----------------------------------------------------------------------
|
|
// Create UI components. These are all defined in `js/components.js`.
|
|
//----------------------------------------------------------------------
|
|
addCustomLogoControl();
|
|
addZoomControls();
|
|
//addDrawControls();
|
|
var basemapPickerControl = addBasemapPickerControl(gBasemaps);
|
|
var basemapPickerControlContainer = basemapPickerControl.getContainer();
|
|
|
|
zonePickerControl = addZonePickerControl(gZonemaps);
|
|
var zonePickerControlContainer = zonePickerControl.getContainer();
|
|
|
|
var opacitySlider = addOpacitySlider();
|
|
addInferenceWindowSizeSlider();
|
|
// addCorrectionWindowSizeSlider();
|
|
// addSharpnessToggleSlider();
|
|
|
|
addSideBar();
|
|
|
|
|
|
var uploadControl = addUploadControl();
|
|
uploadControl.loader.on('data:loaded', function (e) {
|
|
var zoneName = e.filename;
|
|
|
|
if(zoneName in gZonemaps){ // if there is an entry in gZonemaps with the same filename then we have already loaded this file
|
|
console.debug("Error, already loaded this file");
|
|
}else{
|
|
if(gCurrentZoneLayerName !== null){
|
|
console.debug("Removing existing zone layer");
|
|
gMap.removeLayer(gZonemaps[gCurrentZoneLayerName]);
|
|
}
|
|
gZoneMapsWeight[zoneName] = 3;
|
|
gZonemaps[zoneName] = e.layer;
|
|
e.layer.addTo(gMap);
|
|
zonePickerControl.addBaseLayer(e.layer, zoneName);
|
|
}
|
|
});
|
|
uploadControl.loader.on('data:error', function (error) {
|
|
console.error(error); //TODO: alert a reasonable error message
|
|
});
|
|
|
|
//----------------------------------------------------------------------
|
|
// Register handlers. These are all defined in `js/handlers.js`
|
|
//----------------------------------------------------------------------
|
|
|
|
addInferenceMouseHandlers();
|
|
addDrawControlHandlers();
|
|
addOpacityKeyHandlers(opacitySlider);
|
|
addRetrainKeyHandler();
|
|
addGUIKeyHandlers();
|
|
|
|
|
|
// $(zonePickerControlContainer).find('input[type=radio]').change(function() {
|
|
// var name = $(this).siblings()[0].innerHTML.trim();
|
|
// gCurrentZoneLayerName = name;
|
|
// console.debug("Switched zone layer to: " + name);
|
|
// });
|
|
|
|
// $(basemapPickerControlContainer).find('input[type=radio]').change(function() {
|
|
// var name = $(this).siblings()[0].innerHTML.trim();
|
|
// gCurrentBasemapLayerName = name;
|
|
// console.debug("Switched basemap layer to: " + name);
|
|
// });
|
|
|
|
gMap.on('baselayerchange', function(e) {
|
|
//https://gis.stackexchange.com/questions/171911/getting-current-layer-in-control-event-of-leaflet
|
|
console.log(e);
|
|
});
|
|
|
|
$("#btnRetrain").click(doRetrain);
|
|
$("#btnUndo").click(doUndo);
|
|
$("#btnReset").click(function(){doReset(true, initialReset=false)});
|
|
$("#btnDownload").click(doDownloadTile);
|
|
|
|
$("#btnKillSession").click(doKillSession)
|
|
|
|
|
|
$("#btnNewClass").click(function(){
|
|
|
|
var newClassIdx = CLASSES.length;
|
|
var newClassName = "Class " + (newClassIdx + 1);
|
|
var newColor = getRandomColor();
|
|
|
|
var newClassElement = $("<div class='radio'>");
|
|
var newLabel = $(" \
|
|
<label><input type='radio' name='radClasses' class='radClasses' value='"+newClassName+"'><span class='className'>"+newClassName+"</span> (<span class='classCounts'>0</span> samples since last retrain)<i class='fa fa-edit ml-1 classNameEdit'></i></label> \
|
|
");
|
|
|
|
var newPicker = document.createElement('button');
|
|
newPicker.classList.add("circle");
|
|
newPicker.classList.add("jscolor");
|
|
newPicker.setAttribute("data-class-label", newClassName);
|
|
newPicker.setAttribute("data-class-idx", newClassIdx);
|
|
var output = new jscolor(newPicker, {
|
|
valueElement: null,
|
|
value: newColor.substr(1),
|
|
position:'left',
|
|
zIndex:2001,
|
|
closable:true,
|
|
closeText:'Close',
|
|
onFineChange:'updateClassColor(this)'
|
|
});
|
|
|
|
newClassElement.append(newPicker);
|
|
newClassElement.append(newLabel);
|
|
|
|
$("#classList").append(newClassElement);
|
|
|
|
CLASSES.push({
|
|
"name": newClassName,
|
|
"color": newColor,
|
|
"count": 0
|
|
});
|
|
});
|
|
|
|
//----------------------------------------------------------------------
|
|
// Setup radio button change detection
|
|
//----------------------------------------------------------------------
|
|
$(document).on('change', '.radClasses', function(){
|
|
|
|
$('.radio label').removeClass("selected");
|
|
$(this).parent().addClass("selected");
|
|
|
|
gSelectedClassIdx = findClassByName(this.value);
|
|
});
|
|
|
|
$(document).on('click', '.classNameEdit', function(){
|
|
var oldName = $(this).siblings(".className").html();
|
|
var newName = prompt("New label name for '"+oldName+"'");
|
|
$(this).siblings(".className").html(newName);
|
|
$(this).siblings(".radClasses").val(newName);
|
|
|
|
$(this).parent().siblings(".jscolor").attr("data-class-name", newName);
|
|
|
|
var classIdx = findClassByName(oldName);
|
|
console.debug(classIdx);
|
|
CLASSES[classIdx]["name"] = newName;
|
|
});
|
|
|
|
//----------------------------------------------------------------------
|
|
// Setup the example images list
|
|
//----------------------------------------------------------------------
|
|
|
|
//for(var i=0; i<SERVERS.length; i++){
|
|
for(var i=0; i<3; i++){
|
|
var img = $("<img class='exampleImage'>");
|
|
img.attr("im-id", i);
|
|
img.attr("id", "exampleImage_"+i);
|
|
$("#exampleImageList").append(img);
|
|
}
|
|
|
|
$(".exampleImage").click(function(){
|
|
$(".exampleImage").removeClass("active");
|
|
$(this).addClass("active");
|
|
|
|
var idx = gCurrentPatches.length-1;
|
|
gActiveImgIdx = $(this).attr("im-id");
|
|
|
|
var srcs = gCurrentPatches[idx]["patches"][gActiveImgIdx]["srcs"];
|
|
let tSelection = gDisplayHard ? "hard" : "soft";
|
|
gCurrentPatches[idx]["imageLayer"].setUrl(srcs[tSelection]);
|
|
gCurrentPatches[idx]["activeImgIdx"] = gActiveImgIdx;
|
|
$(this).attr("src", srcs[tSelection]);
|
|
});
|
|
|
|
});
|
|
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|