From 526c4543f7299ac47cbfe3fbc5f329713b07e06b Mon Sep 17 00:00:00 2001 From: Tariq Soliman Date: Mon, 14 Aug 2023 10:41:03 -0700 Subject: [PATCH] #410 Draw Tool - Time Integration (#411) * ts-410 DrawTool - Temporal part 1 * #410 DrawlTool - Temporal Integration * #410 Clearer DrawTool Template Date Start/End Icons --- src/css/mmgisUI.css | 48 ++++ src/essence/Ancillary/Coordinates.js | 4 + src/essence/Ancillary/TimeControl.js | 5 + src/essence/Basics/Formulae_/Formulae_.js | 14 ++ src/essence/Basics/Layers_/Layers_.js | 17 ++ src/essence/Tools/Draw/DrawTool.css | 29 ++- src/essence/Tools/Draw/DrawTool.js | 206 +++++++++++++++++- src/essence/Tools/Draw/DrawTool_Files.js | 8 +- src/essence/Tools/Draw/DrawTool_Templater.css | 61 +++++- src/essence/Tools/Draw/DrawTool_Templater.js | 84 +++++++ src/essence/Tools/Draw/config.json | 1 + 11 files changed, 470 insertions(+), 7 deletions(-) diff --git a/src/css/mmgisUI.css b/src/css/mmgisUI.css index 809db950..768fc0a8 100644 --- a/src/css/mmgisUI.css +++ b/src/css/mmgisUI.css @@ -800,6 +800,54 @@ color: var(--color-c2); } +/*vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv toggle switch vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv*/ +/* Use as:
*/ +.mmgisToggleSwitch { + position: relative; +} +.mmgisToggleSwitch input[type='checkbox'] { + height: 0; + width: 0; + visibility: hidden; +} + +.mmgisToggleSwitch label { + cursor: pointer; + text-indent: -9999px; + width: 36px; + height: 18px; + background: grey; + display: block; + border-radius: 2px; + position: relative; + top: -18px; +} + +.mmgisToggleSwitch label:after { + content: ''; + position: absolute; + top: 2px; + left: 2px; + width: 14px; + height: 14px; + background: #fff; + border-radius: 2px; + transition: 0.3s; +} + +.mmgisToggleSwitch input:checked + label { + background: var(--color-c); +} + +.mmgisToggleSwitch input:checked + label:after { + left: calc(100% - 2px); + transform: translateX(-100%); +} + +.mmgisToggleSwitch label:active:after { + width: 20px; +} + /*vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv switch slider vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv*/ .mmgisMultirange { diff --git a/src/essence/Ancillary/Coordinates.js b/src/essence/Ancillary/Coordinates.js index d76684ea..fd14484a 100644 --- a/src/essence/Ancillary/Coordinates.js +++ b/src/essence/Ancillary/Coordinates.js @@ -811,6 +811,10 @@ function toggleTimeUI() { $('#timeUI').css({ bottom: timeBottom + (UserInterface.pxIsTools || 0) + 'px', }) + + Object.keys(L_._onTimeUIToggleSubscriptions).forEach((k) => { + L_._onTimeUIToggleSubscriptions[k](!active) + }) } export default Coordinates diff --git a/src/essence/Ancillary/TimeControl.js b/src/essence/Ancillary/TimeControl.js index 3f3c535a..3b11fa76 100644 --- a/src/essence/Ancillary/TimeControl.js +++ b/src/essence/Ancillary/TimeControl.js @@ -332,6 +332,11 @@ function timeInputChange(startTime, endTime, currentTime, skipUpdate) { TimeControl.currentTime = currentTime == null ? endTime : currentTime TimeControl.endTime = endTime + if (L_?._timeChangeSubscriptions) + Object.keys(L_._timeChangeSubscriptions).forEach((k) => { + L_._timeChangeSubscriptions[k]({ startTime, currentTime, endTime }) + }) + if (skipUpdate !== true) { // Update layer times and reload TimeControl.updateLayersTime() diff --git a/src/essence/Basics/Formulae_/Formulae_.js b/src/essence/Basics/Formulae_/Formulae_.js index c2fd3147..0c569fa0 100644 --- a/src/essence/Basics/Formulae_/Formulae_.js +++ b/src/essence/Basics/Formulae_/Formulae_.js @@ -98,6 +98,20 @@ var Formulae_ = { return '' } }, + addTimeZoneOffset(timestamp) { + const date = new Date(timestamp) + const addedOffset = new Date( + date.getTime() + date.getTimezoneOffset() * 60000 + ) + return addedOffset + }, + removeTimeZoneOffset(timestamp) { + const date = new Date(timestamp) + const removedOffset = new Date( + date.getTime() - date.getTimezoneOffset() * 60000 + ) + return removedOffset + }, // Returns an array of timestamps between startTime and endTime timestamps that fall along the unit getTimeStartsBetweenTimestamps: function (startTime, endTime, unit) { const timeStarts = [] diff --git a/src/essence/Basics/Layers_/Layers_.js b/src/essence/Basics/Layers_/Layers_.js index 687d20a1..7fee3b4b 100644 --- a/src/essence/Basics/Layers_/Layers_.js +++ b/src/essence/Basics/Layers_/Layers_.js @@ -188,6 +188,23 @@ const L_ = { } } else console.log('Failure updating to new site') }, + _timeChangeSubscriptions: {}, + subscribeTimeChange: function (fid, func) { + if (typeof func === 'function') L_._timeChangeSubscriptions[fid] = func + }, + unsubscribeTimeChange: function (fid) { + if (L_._timeChangeSubscriptions[fid] != null) + delete L_._timeChangeSubscriptions[fid] + }, + _onTimeUIToggleSubscriptions: {}, + subscribeOnTimeUIToggle: function (fid, func) { + if (typeof func === 'function') + L_._onTimeUIToggleSubscriptions[fid] = func + }, + unsubscribeOnTimeUIToggle: function (fid) { + if (L_._onTimeUIToggleSubscriptions[fid] != null) + delete L_._onTimeUIToggleSubscriptions[fid] + }, _onLayerToggleSubscriptions: {}, subscribeOnLayerToggle: function (fid, func) { if (typeof func === 'function') diff --git a/src/essence/Tools/Draw/DrawTool.css b/src/essence/Tools/Draw/DrawTool.css index e7e62e23..fc3772a9 100644 --- a/src/essence/Tools/Draw/DrawTool.css +++ b/src/essence/Tools/Draw/DrawTool.css @@ -985,6 +985,9 @@ color: black; font-weight: bold; } +.drawToolShapeLi.temporallyHidden { + display: none !important; +} .drawToolShapeLiItem { cursor: pointer; @@ -2602,11 +2605,11 @@ background: var(--color-c); } -#cmExportGeoJSON, +#cmExportGeoJSON, #cmExportSourceGeoJSON { display: block; } -#cmExportGeoJSON > div:last-child, +#cmExportGeoJSON > div:last-child, #cmExportSourceGeoJSON > div:last-child { font-size: 12px; text-align: right; @@ -2694,3 +2697,25 @@ #drawToolFilesLoadingSpinner.on { opacity: 1; } + +#DrawTool_TimeToggle { + position: absolute; + bottom: 45px; + left: 50%; + transform: translateX(-50%); + display: flex; + background: var(--color-a); + border-radius: 2px; + height: 24px; + line-height: 18px; + padding: 3px 3px; + font-size: 13px; + box-shadow: 0px 4px 3px 0px rgba(0,0,0,0.2); +} +#DrawTool_TimeToggle > div:first-child { + padding: 0px 6px 0px 5px; + letter-spacing: 1px; +} +#DrawTool_TimeToggle_switch { + top: -14px; +} diff --git a/src/essence/Tools/Draw/DrawTool.js b/src/essence/Tools/Draw/DrawTool.js index 25e6fa12..9270939f 100644 --- a/src/essence/Tools/Draw/DrawTool.js +++ b/src/essence/Tools/Draw/DrawTool.js @@ -17,6 +17,7 @@ import Viewer_ from '../../Basics/Viewer_/Viewer_' import ToolController_ from '../../Basics/ToolController_/ToolController_' import CursorInfo from '../../Ancillary/CursorInfo' import Description from '../../Ancillary/Description' +import TimeControl from '../../Ancillary/TimeControl' import { Kinds } from '../../../pre/tools' import turf from 'turf' @@ -80,7 +81,7 @@ var markup = [ "
Draw Clipping
", "
", "
Over
", - "
Under
", + "
Under
", "
Off
", "
", "", @@ -280,6 +281,7 @@ var DrawTool = { allTags: {}, //: count, ... tags: [], labelsOn: [], + fileGeoJSONFeatures: {}, palettes: [ [ '#26a8ff', @@ -1229,8 +1231,154 @@ var DrawTool = { geojson.features = templateEnforcedFeatures return geojson }, -} + _isFeatureTemporallyVisible(feature, startField, endField) { + if (DrawTool.timeToggledOn !== true) return true + const startTime = F_.removeTimeZoneOffset( + new Date(L_.TimeControl_.getStartTime()).getTime() + ) + const endTime = F_.removeTimeZoneOffset( + new Date(L_.TimeControl_.getEndTime()).getTime() + ) + + let startTimeValue = false + if (startField) + startTimeValue = F_.getIn(feature.properties, startField, 0) + let endTimeValue = false + if (endField) + endTimeValue = F_.getIn(feature.properties, endField, false) + + // No prop, won't show + if (endTimeValue === false) return false + else if ( + typeof endTimeValue === 'string' && + endTimeValue.indexOf('T') != -1 + ) + endTimeValue += 'Z' + + if (startTimeValue === false) { + //Single Point in time, just compare end times + let endDate = new Date(endTimeValue) + if (endDate === 'Invalid Date') return false + + endDate = endDate.getTime() + if (endDate <= endTime && endDate >= startTime) return true + return false + } else { + if ( + typeof startTimeValue === 'string' && + startTimeValue.indexOf('T') != -1 + ) + startTimeValue += 'Z' + // Then we have a range + let startDate = new Date(startTimeValue) + let endDate = new Date(endTimeValue) + + // Bad prop value, won't show + if (startDate === 'Invalid Date' || endDate === 'Invalid Date') + return false + startDate = startDate.getTime() + endDate = endDate.getTime() + if (endTime < startDate) return false + if (startTime > endDate) return false + + return true + } + }, + timeFilterDrawingLayer(fileId) { + if (L_.layers.layer[`DrawTool_${fileId}`]) { + const file = DrawTool.getFileObjectWithId(fileId) + + let startField + let endField + if (file?.template?.template) { + file.template.template.forEach((t) => { + if (startField == null && t.isStart === true) + startField = t.field + if (endField == null && t.isEnd === true) endField = t.field + }) + } + L_.layers.layer[`DrawTool_${fileId}`].forEach((l, index) => { + if (l == null) return + if (l.feature == null) { + if (l._layers) { + Object.keys(l._layers).forEach((l2) => { + l2 = l._layers[l2] + if (l2.feature) { + const isVisible = + DrawTool._isFeatureTemporallyVisible( + l2.feature, + startField, + endField + ) + if (l2.savedOptions == null) + l2.savedOptions = JSON.parse( + JSON.stringify(l2.options) + ) + l2.temporallyHidden = !isVisible + if (l2.temporallyHidden) + $( + `#drawToolShapeLiItem_DrawTool_${fileId}_${index}` + ).addClass('temporallyHidden') + else + $( + `#drawToolShapeLiItem_DrawTool_${fileId}_${index}` + ).removeClass('temporallyHidden') + if (l2.temporallyHidden) { + l2.setStyle({ + opacity: 0, + fillOpacity: 0, + }) + if (l2._path?.style) + l2._path.style.pointerEvents = 'none' + } else if (l2.savedOptions) { + l2.setStyle({ + opacity: l2.savedOptions.opacity, + fillOpacity: + l2.savedOptions.fillOpacity, + }) + if (l2._path?.style) + l2._path.style.pointerEvents = 'all' + } + } + }) + } + } else { + const isVisible = DrawTool._isFeatureTemporallyVisible( + l.feature, + startField, + endField + ) + if (l.savedOptions == null) + l.savedOptions = JSON.parse(JSON.stringify(l.options)) + + l.temporallyHidden = !isVisible + if (l.temporallyHidden) + $( + `#drawToolShapeLiItem_DrawTool_${fileId}_${index}` + ).addClass('temporallyHidden') + else + $( + `#drawToolShapeLiItem_DrawTool_${fileId}_${index}` + ).removeClass('temporallyHidden') + if (l.temporallyHidden) { + l.setStyle({ + opacity: 0, + fillOpacity: 0, + }) + if (l._path?.style) l._path.style.pointerEvents = 'none' + } else if (l.savedOptions) { + l.setStyle({ + opacity: l.savedOptions.opacity, + fillOpacity: l.savedOptions.fillOpacity, + }) + if (l._path?.style) l._path.style.pointerEvents = 'all' + } + } + }) + } + }, +} // function interfaceWithMMGIS() { this.separateFromMMGIS = function () { @@ -1244,9 +1392,61 @@ function interfaceWithMMGIS() { tools.selectAll('*').remove() //Add a semantic container tools = tools.append('div').style('height', '100%') + //Add the markup to tools or do it manually tools.html(markup) + // Set defaultDrawClipping if any + $( + `#drawToolDrawSettingsTier > [value="${ + ['over', 'under', 'off'].includes(DrawTool.vars.defaultDrawClipping) + ? DrawTool.vars.defaultDrawClipping + : 'under' + }"]` + ).addClass('active') + + // Add time indicator + if (L_.configData?.time?.enabled === true) { + // prettier-ignore + $('body').append([ + `
`, + `
Temporal Drawings
`, + `
`, + ``, + ``, + `
`, + `
` + ].join('\n')) + $('#DrawTool_TimeToggle').css( + 'display', + $('#toggleTimeUI.active').length > 0 ? 'flex' : 'none' + ) + + $('#DrawTool_TimeToggle_switch').on('input', function (e) { + // Toggle edit panel off + $('.drawToolContextMenuHeaderClose').click() + DrawTool.timeToggledOn = $(this).is(':checked') + DrawTool.filesOn.forEach((fileId) => { + DrawTool.timeFilterDrawingLayer(fileId) + }) + }) + L_.subscribeTimeChange('DrawTool', (times) => { + DrawTool.filesOn.forEach((fileId) => { + DrawTool.timeFilterDrawingLayer(fileId) + }) + }) + L_.subscribeOnTimeUIToggle('DrawTool', (active) => { + $('#DrawTool_TimeToggle').css('display', active ? 'flex' : 'none') + }) + tippy('#DrawTool_TimeToggle', { + content: + 'Only display drawings whose templated dates fall within the current time window.', + placement: 'bottom', + theme: 'blue', + maxWidth: 700, + }) + } + tippy('#drawToolDrawFilesNew', { content: 'New File', placement: 'right', @@ -1611,6 +1811,8 @@ function interfaceWithMMGIS() { function separateFromMMGIS() { DrawTool.endDrawing() $('.drawToolContextMenuHeaderClose').click() + L_.unsubscribeTimeChange('DrawTool') + L_.unsubscribeOnTimeUIToggle('DrawTool') DrawTool.open = false } } diff --git a/src/essence/Tools/Draw/DrawTool_Files.js b/src/essence/Tools/Draw/DrawTool_Files.js index 64a4ded1..50f51dc5 100644 --- a/src/essence/Tools/Draw/DrawTool_Files.js +++ b/src/essence/Tools/Draw/DrawTool_Files.js @@ -1520,12 +1520,13 @@ var Files = { //Highlight layer if on $('.drawToolDrawFilesListElem').off('mouseenter') $('.drawToolDrawFilesListElem').on('mouseenter', function () { + if (DrawTool.timeToggledOn) return $(this).find('.drawToolFileEdit').addClass('shown') var fileId = parseInt($(this).attr('file_id')) var l = L_.layers.layer['DrawTool_' + fileId] if (!l) return for (var i = 0; i < l.length; i++) { - if (l[i] != null) { + if (l[i] != null && l[i].temporallyHidden != true) { if (typeof l[i].setStyle === 'function') l[i].setStyle({ color: '#7fff00' }) else if (l[i].hasOwnProperty('_layers')) { @@ -1544,6 +1545,8 @@ var Files = { }) $('.drawToolDrawFilesListElem').off('mouseleave') $('.drawToolDrawFilesListElem').on('mouseleave', function () { + if (DrawTool.timeToggledOn) return + $(this).find('.drawToolFileEdit').removeClass('shown') var fileId = parseInt($(this).attr('file_id')) var l = L_.layers.layer['DrawTool_' + fileId] @@ -1743,6 +1746,8 @@ var Files = { } let features = data.geojson.features + DrawTool.fileGeoJSONFeatures[index] = features + let coreFeatures = JSON.parse(JSON.stringify(data.geojson)) coreFeatures.features = [] @@ -1904,6 +1909,7 @@ var Files = { L_.enforceVisibilityCutoffs([layerId]) DrawTool.maintainLayerOrder() + DrawTool.timeFilterDrawingLayer(index) DrawTool.refreshMasterCheckbox() diff --git a/src/essence/Tools/Draw/DrawTool_Templater.css b/src/essence/Tools/Draw/DrawTool_Templater.css index 7b983b2c..60ccfd27 100644 --- a/src/essence/Tools/Draw/DrawTool_Templater.css +++ b/src/essence/Tools/Draw/DrawTool_Templater.css @@ -24,6 +24,52 @@ font-size: 14px; } +#drawToolTemplater_setTime { + border-top: 1px solid var(--color-a1); + padding-top: 8px; + display: flex; +} +#drawToolTemplater_setTime > div:first-child { + display: flex; + margin-right: 4px; +} +#drawToolTemplater_setTime > div:last-child { + display: flex; + margin-left: 4px; +} +#drawToolTemplater_setTime > div { + display: flex !important; + justify-content: center; + text-align: center; + flex: 1; + background: var(--color-a1); + line-height: 26px !important; + cursor: pointer; + opacity: 0.4; + pointer-events: none; + transition: all 0.2s ease-in-out; +} +#drawToolTemplater_setTime > div > div { + line-height: 32px; + font-size: 13px; +} +#drawToolTemplater_setTime > div:hover { + color: white; + background: var(--color-a2); +} +#drawToolTemplater_setTime > div.active { + opacity: 1; + pointer-events: all; +} +#drawToolTemplater_setTime > div:first-child > i { + margin-right: 4px; + line-height: 30px; +} +#drawToolTemplater_setTime > div:last-child > i { + margin-left: 4px; + line-height: 30px; +} + .drawToolTemplatercheckbox .mmgis-checkbox { margin-right: 5px; margin-top: 5px; @@ -155,6 +201,11 @@ background: var(--color-a1-5); color: var(--color-a7); } +.drawToolTemplaterLiBody_slider_default, +.drawToolTemplaterLiBody_number_default, +.drawToolTemplaterLiBody_text_regex { + flex: 1; +} .drawToolTemplaterLi { list-style-type: none; @@ -192,13 +243,16 @@ background: var(--color-a2); } .drawToolTemplaterLiBodyDropdown_format { - width: 200px; + width: 160px; } .drawToolTemplaterLiBodyDropdown_format .dropy, .drawToolTemplaterLiType > div .dropy { margin-bottom: 0; } -.drawToolTemplaterLiBodyDropdown_format .dropy__title, +.drawToolTemplaterLiBodyDropdown_format .dropy__title { + line-height: 21px; + font-size: 11px; +} .drawToolTemplaterLiType > div .dropy__title { line-height: 13px; font-size: 14px; @@ -207,6 +261,9 @@ .drawToolTemplaterLiType > div .dropy__title > i { transform: translateY(30%); } +.drawToolTemplaterLiBody_date_default > input { + width: 125px; +} .drawToolTemplaterDesignHeadingRemove { width: 30px; height: 30px; diff --git a/src/essence/Tools/Draw/DrawTool_Templater.js b/src/essence/Tools/Draw/DrawTool_Templater.js index 62336481..d8480f30 100644 --- a/src/essence/Tools/Draw/DrawTool_Templater.js +++ b/src/essence/Tools/Draw/DrawTool_Templater.js @@ -17,6 +17,7 @@ const DrawTool_Templater = { properties = properties || {} const template = JSON.parse(JSON.stringify(templateObj.template)) + let hasStartTime, hasEndTime // prettier-ignore const markup = [ "" ].join('\n') $(`#${containerId}`).append(markup) const helperStates = {} + let startTime, endTime // Attach events template.forEach((t, idx) => { + if (startTime == null && t.isStart) startTime = t.field + if (endTime == null && t.isEnd) endTime = t.field + switch (t.type) { case 'range': case 'slider': @@ -220,6 +234,19 @@ const DrawTool_Templater = { } }) + $(`#drawToolTemplater_setTimeStart`).on('click', () => { + L_.TimeControl_.setTime( + properties[startTime], + L_.TimeControl_.getEndTime() + ) + }) + $(`#drawToolTemplater_setTimeEnd`).on('click', () => { + L_.TimeControl_.setTime( + L_.TimeControl_.getStartTime(), + properties[endTime] + ) + }) + return { getValues: (layer, existingProperties, onlyIfChanged) => { const values = {} @@ -806,6 +833,14 @@ const DrawTool_Templater = { `
Format:
`, ``, "", + `
`, + `
S:
`, + `
`, + "
", + `
`, + `
E:
`, + `
`, + "
", `
`, `
Req:
`, `
`, @@ -1041,6 +1076,12 @@ const DrawTool_Templater = { item.format = $(this) .find('.drawToolTemplaterLiBodyDropdown_format') .attr('value') + item.isStart = $(this) + .find('.drawToolTemplaterLiBody_date_isStart input') + .prop('checked') + item.isEnd = $(this) + .find('.drawToolTemplaterLiBody_date_isEnd input') + .prop('checked') item.required = $(this) .find( '.drawToolTemplaterLiBody_date_required input' @@ -1107,6 +1148,10 @@ const DrawTool_Templater = { } } + // Only allow one of each: + let hasADateStartTime = false + let hasADateEndTime = false + for (let i = 0; i < template.template.length; i++) { const t = template.template[i] if (t.field == null || t.field == '') { @@ -1131,6 +1176,45 @@ const DrawTool_Templater = { ) return false } + if (t.type === 'date') { + if (t.isStart && t.isEnd) { + CursorInfo.update( + `Template cannot use same date field as Start Time and End Time.`, + 6000, + true, + { x: 305, y: 6 }, + '#e9ff26', + 'black' + ) + return false + } else if (t.isStart) { + if (hasADateStartTime === false) hasADateStartTime = true + else { + CursorInfo.update( + `Template cannot use multiple date fields as Start Times.`, + 6000, + true, + { x: 305, y: 6 }, + '#e9ff26', + 'black' + ) + return false + } + } else if (t.isEnd) { + if (hasADateEndTime === false) hasADateEndTime = true + else { + CursorInfo.update( + `Template cannot use multiple date fields as End Times.`, + 6000, + true, + { x: 305, y: 6 }, + '#e9ff26', + 'black' + ) + return false + } + } + } if (t.regex != null) { try { new RegExp(t.regex) diff --git a/src/essence/Tools/Draw/config.json b/src/essence/Tools/Draw/config.json index 293c34cf..fc5f96c7 100644 --- a/src/essence/Tools/Draw/config.json +++ b/src/essence/Tools/Draw/config.json @@ -12,6 +12,7 @@ "Point_Alias", "All_Alias" ], + "defaultDrawClipping": "over || under || off", "leadsCanEditFileInfo": false, "hoverLengthOnLines": false, "templates": {