'use strict'; const path = require('path'); const fs = require('fs'); const ol = require('openlayers'); const xml2js = require('xml2js'); const Store = require('electron-store'); const store = new Store(); const { dialog } = require("@electron/remote"); const MSPChainerClass = require('./../js/msp/MSPchainer'); const mspHelper = require('./../js/msp/MSPHelper'); const MSPCodes = require('./../js/msp/MSPCodes'); const MSP = require('./../js/msp'); const mspQueue = require('./../js/serial_queue'); const { GUI, TABS } = require('./../js/gui'); const FC = require('./../js/fc'); const CONFIGURATOR = require('./../js/data_storage'); const i18n = require('./../js/localization'); const { globalSettings } = require('./../js/globalSettings'); const MWNP = require('./../js/mwnp'); const Waypoint = require('./../js/waypoint') const WaypointCollection = require('./../js/waypointCollection'); const Safehome = require('./../js/safehome'); const SafehomeCollection = require('./../js/safehomeCollection'); const { ApproachDirection, FwApproach } = require('./../js/fwApproach'); const FwApproachCollection = require('./../js/fwApproachCollection'); const SerialBackend = require('./../js/serial_backend'); const { distanceOnLine, wrap_360, calculate_new_cooridatnes } = require('./../js/helpers'); const Plotly = require('./../js/libraries/plotly-latest.min'); const interval = require('./../js/intervals'); var MAX_NEG_FW_LAND_ALT = -2000; // cm // Dictionary of Parameter 1,2,3 definition depending on type of action selected (refer to MWNP.WPTYPE) var dictOfLabelParameterPoint = { 1: {parameter1: 'Speed (cm/s)', parameter2: '', parameter3: 'Sea level Ref'}, 2: {parameter1: '', parameter2: '', parameter3: ''}, 3: {parameter1: 'Wait time (s)', parameter2: 'Speed (cm/s)', parameter3: 'Sea level Ref'}, 4: {parameter1: 'Force land (non zero)', parameter2: '', parameter3: ''}, 5: {parameter1: '', parameter2: '', parameter3: ''}, 6: {parameter1: 'Target WP number', parameter2: 'Number of repeat (-1: infinite)', parameter3: ''}, 7: {parameter1: 'Heading (deg)', parameter2: '', parameter3: ''}, 8: {parameter1: '', parameter2: '', parameter3: 'Sea level Ref'} }; var waypointOptions = ['JUMP','SET_HEAD','RTH']; //////////////////////////////////// // // Tab mission control block // //////////////////////////////////// TABS.mission_control = {}; TABS.mission_control.isYmapLoad = false; TABS.mission_control.initialize = function (callback) { let cursorInitialized = false; let curPosStyle; let curPosGeo; let rthGeo; let breadCrumbLS; let breadCrumbFeature; let breadCrumbStyle; let breadCrumbSource; let breadCrumbVector; let textStyle; let textFeature; var textGeom; let isOffline = false; let selectedSafehome; let $safehomeContentBox; let $waypointOptionsTableBody; let settings = {speed: 0, alt: 5000, safeRadiusSH: 50, fwApproachAlt: 60, fwLandAlt: 5, maxDistSH: 0, fwApproachLength: 0, fwLoiterRadius: 0, bingDemModel: false}; if (GUI.active_tab != 'mission_control') { GUI.active_tab = 'mission_control'; } if (CONFIGURATOR.connectionValid) { var loadChainer = new MSPChainerClass(); loadChainer.setChain([ mspHelper.getMissionInfo, //mspHelper.loadWaypoints, mspHelper.loadSafehomes, mspHelper.loadFwApproach, function (callback) { mspHelper.getSetting("nav_fw_land_approach_length").then((data) => { settings.fwApproachLength = parseInt(data.value); }).then(callback); }, function (callback) { mspHelper.getSetting("safehome_max_distance").then((data) => { settings.maxDistSH = parseInt(data.value) / 100; }).then(callback); }, function (callback) { mspHelper.getSetting(("nav_fw_loiter_radius")).then((data) => { settings.fwLoiterRadius = parseInt(data.value); }).then(callback); } ]); loadChainer.setExitPoint(loadHtml); loadChainer.execute(); } else { // FC not connected, load page anyway loadHtml(); if (!FC.FW_APPROACH) { FC.FW_APPROACH = new FwApproachCollection(); } if (!FC.SAFEHOMES) { FC.SAFEHOMES = new SafehomeCollection(); } for (let i = 0; i < FC.FW_APPROACH.getMaxFwApproachCount(); i++){ FC.FW_APPROACH.put(new FwApproach(i)); } } function loadHtml() { GUI.load(path.join(__dirname, "mission_control.html"), process_html); } function process_html() { // set GUI for offline operations if (!CONFIGURATOR.connectionValid) { $('#infoAvailablePoints').hide(); $('#infoMissionValid').hide(); $('#loadMissionButton').hide(); $('#saveMissionButton').hide(); $('#loadEepromMissionButton').hide(); $('#saveEepromMissionButton').hide(); isOffline = true; } $safehomeContentBox = $('#SafehomeContentBox'); $waypointOptionsTableBody = $('#waypointOptionsTableBody'); if (typeof require !== "undefined") { loadSettings(); // let the dom load finish, avoiding the resizing of the map setTimeout(initMap, 200); if (!isOffline) { setTimeout(() => { if (FC.SAFEHOMES.safehomeCount() >= 1) { updateSelectedShAndFwAp(0); } else { selectedSafehome = null; selectedFwApproachSh = null; } renderSafehomesOnMap(); updateSafehomeInfo(); }, 500); } } else { $('#missionMap, #missionControls').hide(); $('#notLoadMap').show(); } i18n.localize(); function get_raw_gps_data() { MSP.send_message(MSPCodes.MSP_RAW_GPS, false, false, get_comp_gps_data); } function get_comp_gps_data() { MSP.send_message(MSPCodes.MSP_COMP_GPS, false, false, get_altitude_data); } function get_altitude_data() { MSP.send_message(MSPCodes.MSP_ALTITUDE, false, false, get_attitude_data); } function get_attitude_data() { MSP.send_message(MSPCodes.MSP_ATTITUDE, false, false, update_gpsTrack); } function update_gpsTrack() { let lat = FC.GPS_DATA.lat / 10000000; let lon = FC.GPS_DATA.lon / 10000000; //Update map if (FC.GPS_DATA.fix >= 2) { if (!cursorInitialized) { cursorInitialized = true; ///////////////////////////////////// //create layer for current position curPosStyle = new ol.style.Style({ image: new ol.style.Icon(({ anchor: [0.5, 0.5], opacity: 1, scale: 0.6, src: './images/icons/icon_mission_airplane.png' })) }); let currentPositionLayer; curPosGeo = new ol.geom.Point(ol.proj.fromLonLat([lon, lat])); let curPosFeature = new ol.Feature({ geometry: curPosGeo }); curPosFeature.setStyle(curPosStyle); let vectorSource = new ol.source.Vector({ features: [curPosFeature] }); currentPositionLayer = new ol.layer.Vector({ source: vectorSource }); /////////////////////////// //create layer for RTH Marker let rthStyle = new ol.style.Style({ image: new ol.style.Icon(({ anchor: [0.5, 1.0], opacity: 1, scale: 0.5, src: './images/icons/cf_icon_RTH.png' })) }); rthGeo = new ol.geom.Point(ol.proj.fromLonLat([90, 0])); let rthFeature = new ol.Feature({ geometry: rthGeo }); rthFeature.setStyle(rthStyle); let rthVector = new ol.source.Vector({ features: [rthFeature] }); let rthLayer = new ol.layer.Vector({ source: rthVector }); ////////////////////////////// //create layer for bread crumbs breadCrumbLS = new ol.geom.LineString([ol.proj.fromLonLat([lon, lat]), ol.proj.fromLonLat([lon, lat])]); breadCrumbStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: '#ffcc33', width: 6 }) }); breadCrumbFeature = new ol.Feature({ geometry: breadCrumbLS }); breadCrumbFeature.setStyle(breadCrumbStyle); breadCrumbSource = new ol.source.Vector({ features: [breadCrumbFeature] }); breadCrumbVector = new ol.layer.Vector({ source: breadCrumbSource }); ///////////////////////////// //create layer for heading, alt, groundspeed textGeom = new ol.geom.Point([0,0]); textStyle = new ol.style.Style({ text: new ol.style.Text({ font: 'bold 35px Calibri,sans-serif', fill: new ol.style.Fill({ color: '#fff' }), offsetX: map.getSize()[0]-260, offsetY: 80, textAlign: 'left', backgroundFill: new ol.style.Fill({ color: '#000' }), stroke: new ol.style.Stroke({ color: '#fff', width: 2 }), text: 'H: XXX\nAlt: XXXm\nSpeed: XXXcm/s' }) }); textFeature = new ol.Feature({ geometry: textGeom }); textFeature.setStyle(textStyle); var textSource = new ol.source.Vector({ features: [textFeature] }); var textVector = new ol.layer.Vector({ source: textSource }); map.addLayer(rthLayer); map.addLayer(breadCrumbVector); map.addLayer(currentPositionLayer); map.addControl(textVector); } let gpsPos = ol.proj.fromLonLat([lon, lat]); curPosGeo.setCoordinates(gpsPos); breadCrumbLS.appendCoordinate(gpsPos); var coords = breadCrumbLS.getCoordinates(); if(coords.length > 100) { coords.shift(); breadCrumbLS.setCoordinates(coords); } curPosStyle.getImage().setRotation((FC.SENSOR_DATA.kinematics[2]/360.0) * 6.28318); //update data text textGeom.setCoordinates(map.getCoordinateFromPixel([0,0])); let tmpText = textStyle.getText(); tmpText.setText(' \n' + 'H: ' + FC.SENSOR_DATA.kinematics[2] + '\nAlt: ' + FC.SENSOR_DATA.altitude + 'm\nSpeed: ' + FC.GPS_DATA.speed + 'cm/s\n' + 'Dist: ' + FC.GPS_DATA.distanceToHome + 'm'); } } /* * enable data pulling if not offline * Refreshing data at 5Hz... Could slow this down if we have performance issues */ if(!isOffline) { interval.add('gps_pull', function gps_update() { // avoid usage of the GPS commands until a GPS sensor is detected for targets that are compiled without GPS support. if (!SerialBackend.have_sensor(FC.CONFIG.activeSensors, 'gps')) { update_gpsTrack(); return; } get_raw_gps_data(); }, 200); } GUI.content_ready(callback); } /////////////////////////////////////////////// // // define & init parameters // /////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// // define & init parameters for Map Layer ////////////////////////////////////////////////////////////////////////////////////////////// var markers = []; // Layer for Waypoints var lines = []; // Layer for lines between waypoints var safehomeMarkers = []; // layer for Safehome points var safehomeMarkers = []; // layer for Safehome points var approachLayers = [] // Layers for FW approach var safehomeMarkers = []; // layer for Safehome points var approachLayers = [] // Layers for FW approach var map; ////////////////////////////////////////////////////////////////////////////////////////////// // define & init parameters for Selected Marker ////////////////////////////////////////////////////////////////////////////////////////////// var selectedMarker = null; var selectedFeature = null; var tempMarker = null; var disableMarkerEdit = false; var selectedFwApproachWp = null; var selectedFwApproachSh = null; var lockShExclHeading = false; ////////////////////////////////////////////////////////////////////////////////////////////// // define & init parameters for default Settings ////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////// // define & init Waypoints parameters ////////////////////////////////////////////////////////////////////////////////////////////// var mission = new WaypointCollection(); ////////////////////////////////////////////////////////////////////////////////////////////// // define & init Multi Mission parameters ////////////////////////////////////////////////////////////////////////////////////////////// var multimission = new WaypointCollection(); var multimissionCount = 0; var maxMultimissionCount = 9; ////////////////////////////////////////////////////////////////////////////////////////////// // define & init home parameters ////////////////////////////////////////////////////////////////////////////////////////////// var HOME = new Waypoint(0,0,0,0); var homeMarkers =[]; // layer for home point ////////////////////////////////////////////////////////////////////////////////////////////// // define & init Safehome parameters ////////////////////////////////////////////////////////////////////////////////////////////// //var FC.SAFEHOMES = new SafehomeCollection(); // TO COMMENT FOR RELEASE : DECOMMENT FOR DEBUG //FC.SAFEHOMES.inflate(); // TO COMMENT FOR RELEASE : DECOMMENT FOR DEBUG //var safehomeRangeRadius = 200; //meters //var safehomeSafeRadius = 50; //meters ///////////////////////////////////////////// // // Reinit Jquery Form // ///////////////////////////////////////////// function clearEditForm() { $('#pointLat').val(''); $('#pointLon').val(''); $('#pointAlt').val(''); $('#pointP1').val(''); $('#pointP2').val(''); $('#pointP3Alt').val(''); $('#missionDistance').text(0); $('#MPeditPoint').fadeOut(300); } function clearFilename() { $('#missionFilename').text(''); } ///////////////////////////////////////////// // // Manage Settings // ///////////////////////////////////////////// function loadSettings() { var missionPlannerSettings = store.get('missionPlannerSettings', false); if (missionPlannerSettings) { if (!missionPlannerSettings.fwApproachLength && settings.fwApproachLength) { missionPlannerSettings.fwApproachLength = settings.fwApproachLength; missionPlannerSettings.maxDistSH = settings.maxDistSH; missionPlannerSettings.fwLoiterRadius = settings.fwLoiterRadius; } saveSettings(); settings = missionPlannerSettings; } refreshSettings(); } function saveSettings() { store.set('missionPlannerSettings', settings); } function refreshSettings() { $('#MPdefaultPointAlt').val(String(settings.alt)); $('#MPdefaultPointSpeed').val(String(settings.speed)); $('#MPdefaultSafeRangeSH').val(String(settings.safeRadiusSH)); $('#MPdefaultFwApproachAlt').val(String(settings.fwApproachAlt)); $('#MPdefaultLandAlt').val(String(settings.fwLandAlt)); } function closeSettingsPanel() { $('#missionPlannerSettings').hide(); } ///////////////////////////////////////////// // // Manage Safehome // ///////////////////////////////////////////// function closeSafehomePanel() { $('#missionPlannerSafehome').hide(); cleanSafehomeLayers(); } function checkApproachAltitude(altitude, isSeaLevelRef, sealevel) { if (altitude - (isSeaLevelRef ? sealevel * 100 : 0 ) < 0) { GUI.alert(i18n.getMessage('MissionPlannerAltitudeChangeReset')); return false; } return true; } function checkLandingAltitude(altitude, isSeaLevelRef, sealevel) { if (altitude - (isSeaLevelRef ? sealevel * 100 : 0 ) < MAX_NEG_FW_LAND_ALT) { GUI.alert(i18n.getMessage('MissionPlannerFwLAndingAltitudeChangeReset')); return false; } return true; } function updateSafehomeInfo(){ let freeSamehomes = FC.SAFEHOMES.getMaxSafehomeCount() - FC.SAFEHOMES.safehomeCount() $('#availableSafehomes').text(freeSamehomes + '/' + FC.SAFEHOMES.getMaxSafehomeCount()); } function renderSafehomesOnMap() { /* * Process safehome on Map */ FC.SAFEHOMES.get().forEach(safehome => { addFwApproach(safehome.getLonMap(), safehome.getLatMap(), FC.FW_APPROACH.get()[safehome.getNumber()], safehomeMarkers); }); FC.SAFEHOMES.get().forEach(safehome => { addSafehomeCircles(safehome); addSafeHomeMarker(safehome); }); } function cleanSafehomeLayers() { for (var i in safehomeMarkers) { map.removeLayer(safehomeMarkers[i]); } safehomeMarkers = []; } function getSafehomeIcon(safehome) { /* * Process Safehome Icon */ return new ol.style.Style({ image: new ol.style.Icon(({ anchor: [0.5, 1], opacity: 1, scale: 0.5, src: './images/icons/cf_icon_safehome' + (safehome.isUsed() ? '_used' : '')+ '.png' })), text: new ol.style.Text(({ text: String(Number(safehome.getNumber())+1), font: '12px sans-serif', offsetY: -15, offsetX: -2, fill: new ol.style.Fill({ color: '#FFFFFF' }), stroke: new ol.style.Stroke({ color: '#FFFFFF' }), })) }); } function paintApproachLine(pos1, pos2, color, layers) { var line = new ol.geom.LineString([ol.proj.fromLonLat([pos1.lon, pos1.lat]), ol.proj.fromLonLat([pos2.lon, pos2.lat])]); var feature = new ol.Feature({ geometry: line }); var styles = [ new ol.style.Style({ stroke: new ol.style.Stroke({ color: color, width: 3, }), }) ]; var geometry = feature.getGeometry(); geometry.forEachSegment(function (start, end) { var dx = end[0] - start[0]; var dy = end[1] - start[1]; var rotation = Math.atan2(dy, dx); styles.push(new ol.style.Style({ geometry: new ol.geom.Point(distanceOnLine(start, end, -8)), image: new ol.style.RegularShape({ fill: new ol.style.Fill({color}), points: 3, radius: 8, rotation: -rotation, angle: Math.PI / 2 // rotate -90° }) })); }); feature.setStyle(styles); var vectorSource = new ol.source.Vector({ features: [feature] }); var vectorLayer = new ol.layer.Vector({ source: vectorSource }); vectorLayer.kind = "approachline"; vectorLayer.selection = false; approachLayers.push(vectorLayer); approachLayers.push(vectorLayer); map.addLayer(vectorLayer); layers.push(vectorLayer); return vectorLayer; } function paintApproach(landCoord, approachLength, bearing, approachDirection, layers) { var pos1 = calculate_new_cooridatnes(landCoord, bearing, approachLength); let direction; if (approachDirection == ApproachDirection.LEFT) { direction = wrap_360(bearing + 90); } else { direction = wrap_360(bearing - 90); } var pos2 = calculate_new_cooridatnes(pos1, direction, Math.max(settings.fwLoiterRadius * 4, settings.fwApproachLength / 2)); paintApproachLine(landCoord, pos2, '#0025a1', layers); paintApproachLine(pos2, pos1, '#0025a1', layers); paintApproachLine(pos1, landCoord, '#f78a05', layers); } function addFwApproach(lon, lat, fwApproach, layers) { if (fwApproach.getLandHeading1() != 0) { let bearing = wrap_360(Math.abs(fwApproach.getLandHeading1()) + 180); paintApproach({lat: lat, lon: lon}, settings.fwApproachLength, bearing, fwApproach.getApproachDirection(), layers); } if (fwApproach.getLandHeading1() > 0) { let direction = fwApproach.getApproachDirection() == ApproachDirection.LEFT ? ApproachDirection.RIGHT : ApproachDirection.LEFT; paintApproach({lat: lat, lon: lon}, settings.fwApproachLength, fwApproach.getLandHeading1(), direction, layers); } if (fwApproach.getLandHeading2() != 0) { let bearing = wrap_360(Math.abs(fwApproach.getLandHeading2()) + 180); paintApproach({lat: lat, lon: lon}, settings.fwApproachLength, bearing, fwApproach.getApproachDirection(), layers); } if (fwApproach.getLandHeading2() > 0) { let direction = fwApproach.getApproachDirection() == ApproachDirection.LEFT ? ApproachDirection.RIGHT : ApproachDirection.LEFT; paintApproach({lat: lat, lon: lon}, settings.fwApproachLength, fwApproach.getLandHeading2(), direction, layers); } } function addSafehomeCircles(safehome) { /* * add safehome on Map */ let coord = ol.proj.fromLonLat([safehome.getLonMap(), safehome.getLatMap()]); var iconFeature = new ol.Feature({ geometry: new ol.geom.Point(coord), name: 'safehome' }); //iconFeature.setStyle(getSafehomeIcon(safehome, safehome.isUsed())); let circleStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(144, 12, 63, 0.5)', width: 3, lineDash : [10] }), // fill: new ol.style.Fill({ // color: 'rgba(251, 225, 155, 0.1)' // }) }); let circleSafeStyle = new ol.style.Style({ stroke: new ol.style.Stroke({ color: 'rgba(136, 204, 62, 1)', width: 3, lineDash : [10] }), /* fill: new ol.style.Fill({ color: 'rgba(136, 204, 62, 0.1)' }) */ }); var vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ features: [iconFeature] }), style : function(iconFeature) { let styles = [getSafehomeIcon(safehome)]; if (safehome.isUsed()) { circleStyle.setGeometry(new ol.geom.Circle(iconFeature.getGeometry().getCoordinates(), getProjectedRadius(settings.maxDistSH))); circleSafeStyle.setGeometry(new ol.geom.Circle(iconFeature.getGeometry().getCoordinates(), getProjectedRadius(Number(settings.safeRadiusSH)))); styles.push(circleSafeStyle); styles.push(circleStyle); } return styles; } }); vectorLayer.kind = "safehome"; vectorLayer.number = safehome.getNumber(); vectorLayer.selection = false; safehomeMarkers.push(vectorLayer); map.addLayer(vectorLayer); } function addSafeHomeMarker(safehome) { let coord = ol.proj.fromLonLat([safehome.getLonMap(), safehome.getLatMap()]); var iconFeature = new ol.Feature({ geometry: new ol.geom.Point(coord), name: 'safehome' }); var vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ features: [iconFeature] }), style : function(iconFeature) { return [getSafehomeIcon(safehome)]; } }); vectorLayer.kind = "safehome"; vectorLayer.number = safehome.getNumber(); vectorLayer.selection = true; safehomeMarkers.push(vectorLayer); map.addLayer(vectorLayer); } function getProjectedRadius(radius) { let projection = map.getView().getProjection(); let resolutionAtEquator = map.getView().getResolution(); let resolutionRate = resolutionAtEquator / ol.proj.getPointResolution(projection, resolutionAtEquator, map.getView().getCenter()); let radiusProjected = (radius / ol.proj.METERS_PER_UNIT.m) * resolutionRate; return radiusProjected; } ///////////////////////////////////////////// // // Manage Take Off Home // ///////////////////////////////////////////// function closeHomePanel() { $('#missionPlannerHome').hide(); $('#missionPlannerElevation').hide(); cleanHomeLayers(); } function cleanHomeLayers() { for (var i in homeMarkers) { map.removeLayer(homeMarkers[i]); } homeMarkers = []; } function renderHomeTable() { /* * Process home table UI */ $(".home-lat").val(HOME.getLatMap()).on('change', function () { HOME.setLat(Math.round(Number($(this).val()) * 10000000)); cleanHomeLayers(); renderHomeOnMap(); }); $(".home-lon").val(HOME.getLonMap()).on('change', function () { HOME.setLon(Math.round(Number($(this).val()) * 10000000)); cleanHomeLayers(); renderHomeOnMap(); }); if (HOME.getLatMap() == 0 && HOME.getLonMap() == 0) { HOME.setAlt("N/A"); } else { (async () => { const elevationAtHome = await HOME.getElevation(globalSettings); $('#elevationValueAtHome').text(elevationAtHome+' m'); HOME.setAlt(elevationAtHome); })() } if (globalSettings.mapProviderType == 'bing') { $('#elevationEarthModelclass').fadeIn(300); changeSwitchery($('#elevationEarthModel'), settings.bingDemModel); } else { $('#elevationEarthModelclass').fadeOut(300); } } function renderHomeOnMap() { /* * Process home on Map */ map.addLayer(addHomeMarker(HOME)); } function addHomeMarker(home) { /* * add safehome on Map */ let coord = ol.proj.fromLonLat([home.getLonMap(), home.getLatMap()]); var iconFeature = new ol.Feature({ geometry: new ol.geom.Point(coord), name: 'home' }); //iconFeature.setStyle(getSafehomeIcon(safehome, safehome.isUsed())); var vectorLayer = new ol.layer.Vector({ source: new ol.source.Vector({ features: [iconFeature] }), style : function(iconFeature) { let styles = [getHomeIcon(home)]; return styles; } }); vectorLayer.kind = "home"; vectorLayer.number = home.getNumber(); vectorLayer.selection = false; homeMarkers.push(vectorLayer); return vectorLayer; } function getHomeIcon(home) { /* * Process Safehome Icon */ return new ol.style.Style({ image: new ol.style.Icon(({ anchor: [0.5, 1], opacity: 1, scale: 0.5, src: './images/icons/cf_icon_home.png' })), }); } function updateHome() { renderHomeTable(); cleanHomeLayers(); renderHomeOnMap(); plotElevation(); } ///////////////////////////////////////////// // // Manage Multi Mission // ///////////////////////////////////////////// /* Multi Mission working method: * 'multimission' waypoint collection is a repository for all multi missions. * 'mission' WP collection remains as the WP source for the map display. * All missions can be displayed on the map or only a single mission. With all missions displayed 'mission' and * 'multimission' are copies containing all missions. When a single mission is displayed 'multimission' contains all * missions except the currently displayed mission. * On update to display all missions the current dislayed mission is merged back into 'multimission' and 'mission' * updated as a copy of 'multimission'. * When all missions are displayed WP data can be viewed but mission edit is disabled. * Mission WPs can be edited only when a single mission is loaded on the map. */ var startWPCount = 0; function renderMultimissionTable() { $('#multimissionOptionList').prop('options').length = 1; for (var i = 1; i <= multimissionCount; i++) { $('#multimissionOptionList').append($('