function uuidv4() { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8); return v.toString(16); }); } function api(method, params, callback) { var request = new XMLHttpRequest() request.addEventListener('load', function (event) { var response try { response = JSON.parse(event.target.responseText) } catch(e) { response = { error: {code: -32700, message: 'Parse error'} } } callback && callback(response) }, false) request.addEventListener('error', function (evt) { callback && callback({error: {code: -32000, message: 'Server error'}}) }, false) request.open('POST', '/api/') request.setRequestHeader('Content-type', 'application/json') request.send(JSON.stringify( {method: method, params: params, jsonrpc: '2.0'} )) } function requiredFieldValidator(value) { if (value == null || value == undefined || !value.length) { return {valid: false, msg: "This is a required field"}; } else { return {valid: true, msg: null}; } } function isInt(value) { return !isNaN(value) && parseInt(Number(value)) == value && !isNaN(parseInt(value, 10)); } function integerValidator(value) { if (value == null || value == undefined || !value.length || !isInt(value)) { return {valid: false, msg: "Value must be a number"}; } else { return {valid: true, msg: null}; } } var currentSequence; var presets = []; var system_presets = [ 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105 ]; function presetSelect(all) { var options = '' if (all) { for (var id=1;id<=300;id++) { if (system_presets.indexOf(id) == -1) { var preset = getPreset(id) if (preset) { options += ''; } else { options += ''; } } } } else { presets.forEach(function(preset) { options += ''; }) } return $('') } function PresetEditor(args) { var $preset; var scope = this; this.init = function () { $preset = presetSelect() .appendTo(args.container); scope.focus(); }; this.destroy = function () { $(args.container).empty(); }; this.focus = function () { $preset.focus(); }; this.serializeValue = function () { return parseInt($preset.val(), 10); }; this.applyValue = function (item, state) { console.log('apply', item, state) item.preset = state; }; this.loadValue = function (item) { $preset.val(item.preset); }; this.isValueChanged = function () { return args.item.preset != parseInt($preset.val(), 10); }; this.validate = function () { if (isNaN(parseInt($preset.val(), 10))) { return {valid: false, msg: "Invalid preset."}; } return {valid: true, msg: null}; }; this.init(); } function PresetFormatter(row, cell, value, columnDef, dataContext) { preset = presets.filter(function(preset) { return preset.id == value })[0]; if (preset) { return preset.id + ': ' + preset.name; } else { return value } } function formatNumber(number, decimals) { var array = [], abs = Math.abs(number), split = abs.toFixed(decimals).split('.'); while (split[0]) { array.unshift(split[0].slice(-3)); split[0] = split[0].slice(0, -3); } split[0] = array.join(','); return (number < 0 ? '-' : '') + split.join('.'); }; function formatDuration(seconds) { if (seconds == '...' || !seconds) { return seconds } var values = [ Math.floor(seconds / 31536000), Math.floor(seconds % 31536000 / 86400), Math.floor(seconds % 86400 / 3600), Math.floor(seconds % 3600 / 60), formatNumber(seconds % 60, 3) ]; var labels = ['y', 'd', 'h', 'm', 's']; var duration = ''; values.forEach(function(v, i) { if (v) { if (labels[i] == 's') { v = v.replace('.', 's ').replace(' 000', '') duration += v; } else { duration += v + labels[i] + ' '; } } }); return duration } function formatTime(row, cell, value, columnDef, dataContext) { var time = ''; if (row == 0) { return 0 } if (data[row].duration) { time = data.slice(0, row + 1).map(function(row) { return row.duration || 0 }).reduce(function(a, b) { return a+b }, 0) if (data[row].sleep) { time -= data[row].sleep } } return formatDuration(time) } function formatShotTime(row, cell, value, columnDef, dataContext) { if (data[row].duration) { return formatDuration(data[row].duration) } return '' } function CheckmarkFormatter(row, cell, value, columnDef, dataContext) { return value ? "" : ""; } function StatusFormatter(row, cell, value, columnDef, dataContext) { return value ? "Next" : ""; } var grid; var data = []; var nextStep; var columns = [ { id: "#", name: "", width: 10, behavior: "selectAndMove", selectable: false, resizable: false, cssClass: "cell-reorder dnd" }, {id: "preset", name: "Preset", field: "preset", width: 180, cssClass: "cell-title", formatter: PresetFormatter, editor: PresetEditor, validator: requiredFieldValidator }, {id: "speed", name: "Speed", field: "speed", editor: Slick.Editors.Integer, validator: integerValidator, width: 60 }, {id: "sleep", name: "Sleep", field: "sleep", editor: Slick.Editors.Integer, validator: integerValidator, width: 60 }, {id: "zoom", name: "Zoom Speed", field: "zoom", editor: Slick.Editors.Integer, validator: integerValidator, width: 75}, {id: "zoom_last", name: "Zoom Last", field: "zoom_last", cssClass: "cell-status", formatter: CheckmarkFormatter, editor: Slick.Editors.Checkbox, width: 60}, //{id: "duration", name: "Time", field: "duration", editor: Slick.Editors.Text, formatter: formatTime}, {id: "duration", name: "Time", field: "duration", formatter: formatTime}, {id: "shot_duration", name: "Duration", field: "shot_duration", formatter: formatShotTime}, {id: "status", name: "Status", width: 80, minWidth: 20, maxWidth: 80, cssClass: "cell-status", field: "status", formatter: StatusFormatter} ]; var options = { editable: true, enableAddRow: true, enableCellNavigation: true, asyncEditorLoading: false, autoEdit: false }; function loadData(sequence) { data = sequence; grid = new Slick.Grid("#myGrid", data, columns, options); grid.setSelectionModel(new Slick.CellSelectionModel()); var moveRowsPlugin = new Slick.RowMoveManager({ cancelEditOnDrag: true }); moveRowsPlugin.onBeforeMoveRows.subscribe(function (e, data) { for (var i = 0; i < data.rows.length; i++) { // no point in moving before or after itself if (data.rows[i] == data.insertBefore || data.rows[i] == data.insertBefore - 1) { e.stopPropagation(); return false; } } return true; }); moveRowsPlugin.onMoveRows.subscribe(function (e, args) { var extractedRows = [], left, right; var rows = args.rows; var insertBefore = args.insertBefore; left = data.slice(0, insertBefore); right = data.slice(insertBefore, data.length); rows.sort(function(a,b) { return a-b; }); for (var i = 0; i < rows.length; i++) { extractedRows.push(data[rows[i]]); } rows.reverse(); for (var i = 0; i < rows.length; i++) { var row = rows[i]; if (row < insertBefore) { left.splice(row, 1); } else { right.splice(row - insertBefore, 1); } } data = left.concat(extractedRows.concat(right)); var selectedRows = []; for (var i = 0; i < rows.length; i++) selectedRows.push(left.length + i); grid.resetActiveCell(); grid.setData(data); grid.setSelectedRows(selectedRows); grid.render(); }); grid.registerPlugin(moveRowsPlugin); grid.onAddNewRow.subscribe(function (e, args) { var item = args.item; item.speed = item.speed || data[data.length-1].speed item.sleep = item.sleep || 0 grid.invalidateRow(data.length); data.push(item); grid.updateRowCount(); grid.render(); }); } function totalDuration() { return data.map(function(row) { return row.duration || 0 }).reduce(function(a, b) { return a+b }, 0) } function updateStatus() { api('status', {}, function(response) { if (response.result) { var gotSelection = grid.getSelectedRows().length > 0 var disabled = !gotSelection || response.result.status == 'Active' $('#status').html(response.result.status) $('button.run').attr({disabled: response.result.status == 'Active'}) $('button.run_from').attr({disabled: disabled}) $('button.continue_from').attr({disabled: disabled}) $('button.stop').attr({disabled: response.result.status != 'Active'}) if (response.result.duration && response.result.status == 'Active') { $('#duration').html(formatDuration(response.result.duration)) } else { $('#duration').html(formatDuration(totalDuration())) } var update = false; if (response.result.time) { data.forEach(function(row) { if (row.seqid in response.result.time && response.result.time[row.seqid] != row.duration) { if (row.seqid == data[0].seqid || response.result.time[row.seqid]) { console.log(row.duration, response.result.time[row.seqid]); row.duration = response.result.time[row.seqid]; update = true } } }) } if (response.result.position) { $('#position').html(JSON.stringify(response.result.position)) } data.forEach(function(row) { var s = (response.result.next && row.seqid == response.result.next.seqid); if (row.status != s) { update = true row.status = s } }) if (response.result.next) { nextStep = response.result.next if (response.result.next.seqid) { delete response.result.next.seqid } if (response.result.next.duration) { delete response.result.next.duration } $('#next').html(JSON.stringify(response.result.next)) } if (update) { grid.invalidate(); grid.render(); } } }) data.forEach(function(seq) { if (!seq.seqid) { seq.seqid = uuidv4() } }) var sequence = JSON.stringify(data.map(step => { step = Object.assign({}, step) delete step.duration delete step.status return step })) if (!currentSequence) { currentSequence = sequence } if (sequence != currentSequence) { console.log('updateSequence') currentSequence = sequence api('updateSequence', {sequence: data}, function() {}) } var gotSelection = grid.getSelectedRows().length > 0 $('button.goto').attr({disabled: !gotSelection}) $('button.run_from').attr({disabled: !gotSelection}) $('button.continue_from').attr({disabled: !gotSelection}) $('button.insert').attr({disabled: !gotSelection}) $('button.delete').attr({disabled: !gotSelection}) } var preset_offset = {}, current_position = {}; function updateShiftStatus() { api('camera', { 'status': {} }, function(response) { if (response.result) { if (response.result.status) { current_position = response.result.status; var preset = presets.filter(function(p) { return p.id == parseInt($('select[name=preset-editor]').val(), 10) })[0]; preset_offset['azimuth'] = preset['position']['azimuth'] - current_position['azimuth']; preset_offset['elevation'] = preset['position']['elevation'] - current_position['elevation']; preset_offset['absoluteZoom'] = preset['position']['absoluteZoom'] - current_position['absoluteZoom']; updateDelta() } } }) } function updateDelta() { $('#delta').html(JSON.stringify(preset_offset, null, ' ')) } function deleteRows() { var result = confirm("Are you sure you want to delete " + grid.getSelectedRows().length + " row(s)?"); if (result) { var rowsToDelete = grid.getSelectedRows().sort().reverse(); for (var i = 0; i < rowsToDelete.length; i++) { data.splice(rowsToDelete[i], 1); } grid.invalidate(); grid.setSelectedRows([]); } } $('button.goto').on({click: function() { var selected = grid.getSelectedRows()[0]; api('camera', { 'fast_preset': {id: data[selected].preset} }, function(response) { //console.log(response) }) }}) $('button.run').on({click: function() { data.forEach(function(seq) { if (!seq.seqid) { seq.seqid = uuidv4() } }) api('run', { 'steps': data }, function(response) { //console.log(response) }) }}) $('button.loop').on({click: function() { data.forEach(function(seq) { if (!seq.seqid) { seq.seqid = uuidv4() } }) api('run', { 'steps': data, 'loop': true }, function(response) { //console.log(response) }) }}) $('button.run_from').on({click: function() { var selected = grid.getSelectedRows()[0]; api('run', { 'steps': data.slice(selected) }, function(response) { //console.log(response) }) }}) $('button.continue_from').on({click: function() { var selected = grid.getSelectedRows()[0]; api('run', { 'steps': data.slice(selected), 'goto_first': false }, function(response) { console.log(response) }) }}) $('button.stop').on({click: function() { api('stop', {}, function(response) { if (nextStep && grid.getSelectedRows().length == 0) { var selected = data.map(function(row) { return row.preset }).indexOf(nextStep.preset) grid.setSelectedRows([selected]); } }) }}) $('button.delete').on({click: deleteRows}) $('button.insert').on({click: function() { var selected = grid.getSelectedRows()[0]; data.splice(selected, 0, { preset: data[selected].preset, speed: data[selected].speed, seqid: uuidv4() }); grid.invalidate(); grid.setSelectedRows([selected+1]); }}) $('button.set_speed').on({click: function() { var speed = parseInt($('input.default_speed').val(), 10) data.forEach(function(row) { row.speed = speed }) grid.invalidate() }}) function textBlob(data, type) { type = type || 'text/plain; charset=utf-8'; var byteNumbers = new Array(data.length); for (var i = 0; i < data.length; i++) { byteNumbers[i] = data.charCodeAt(i); } var byteArray = new Uint8Array(byteNumbers); var blob = new Blob([byteArray], {type: type}); return blob; } function exportSequence() { return data.map(function(row) { var r = {}; Object.keys(row).forEach(function(key) { if (['status'].indexOf(key) == -1) { r[key] = row[key]; } }) return r }) } function getPreset(id) { return presets.filter(function(preset) { return preset.id == id; })[0] } function exportSequenceCSV() { var position = 0; return [ ['preset', 'name', 'time', 'duration', 'speed', 'sleep', 'elevation', 'azimuth', 'zoom'] ].concat(data.map(function(row) { var r = []; r.push(row.preset) var preset = getPreset(row.preset) if (preset) { r.push(preset.name) } else { r.push(row.preset) } if (row.duration) { position += row.duration r.push(formatDuration(position)) r.push(formatDuration(row.duration)) } else { r.push('') r.push('') } r.push(row.speed) r.push(row.sleep) if (preset) { r.push(preset.position.elevation) r.push(preset.position.azimuth) r.push(preset.position.absoluteZoom) } else { r.push('') r.push('') r.push('') } return r.join(','); })).join('\n'); } $('button.export_sequence_csv').on({click: function() { data.forEach(function(seq) { if (!seq.seqid) { seq.seqid = uuidv4() } }) var blob = textBlob(exportSequenceCSV(), 'text/csv; charset=utf-8') var url = window.URL.createObjectURL(blob); $(this).parent().attr({ href: url, download: 'sequence.csv' }); }}) $('button.export_sequence').on({click: function() { data.forEach(function(seq) { if (!seq.seqid) { seq.seqid = uuidv4() } }) var blob = textBlob(JSON.stringify(exportSequence(), null, ' ')) var url = window.URL.createObjectURL(blob); $(this).parent().attr({ href: url, download: 'sequence.json' }); }}) $('button.export_presets').on({click: function() { var blob = textBlob(JSON.stringify(presets, null, ' ')) var url = window.URL.createObjectURL(blob); $(this).parent().attr({ href: url, download: 'presets.json' }); }}) $('button.shift_presets').on({click: function() { document.location.href = '/static/shift_presets.html'; }}); $('button.all_presets').on({click: function() { api('getPresets', {}, function(response) { presets = response.result.presets var seq = [].concat(data, presets.filter(function(preset) { return data.filter(function(row) { return row.preset == preset.id }).length == 0 }).map(function(preset) { return { preset: preset.id, speed: parseInt($('input.default_speed').val(), 10) } })) loadData(seq) }) }}) $('input.import_sequence').on({change: function() { var reader = new FileReader() reader.onload = function(event) { var sequence = JSON.parse(reader.result) api('updateSequence', {sequence: sequence}, function() {}) loadData(sequence) } reader.readAsText(this.files[0]); }}) $('input.import_presets').on({change: function() { var reader = new FileReader() reader.onload = function(event) { var data = JSON.parse(reader.result) api('setPresets', { 'presets': data }, function(response) { presets = response.result.presets }) } reader.readAsText(this.files[0]); }}) function shiftPresets(offset, callback) { api('getPresets', {}, function(response) { presets = response.result.presets presets.forEach(function(preset) { if (offset.azimuth) { preset['position']['azimuth'] -= offset.azimuth } if (offset.elevation) { preset['position']['elevation'] -= offset.elevation } if (offset.absoluteZoom) { preset['position']['absoluteZoom'] -= offset.absoluteZoom } }) api('setPresets', { 'presets': presets }, function(response) { presets = response.result.presets callback && callback() }) }) } function init() { api('getPresets', {}, function(response) { presets = response.result.presets api('getSequence', {}, function(response) { loadData(response.result.sequence) updateStatus() setInterval(updateStatus, 1000) }) }) } function init_shift() { api('getPresets', {}, function(response) { presets = response.result.presets presetSelect().on({ change: updateShiftStatus }).appendTo($('body')) $('
').attr({id: 'delta'}).appendTo($('body')) $('