From d4a10497fcd162019daf46c750d9032837469b60 Mon Sep 17 00:00:00 2001 From: j Date: Tue, 4 Dec 2018 09:50:29 +0100 Subject: [PATCH] CAMP can capture canals --- .gitignore | 1 + action.py | 380 ++ server.py | 228 ++ static/css/campcamcontrol.css | 210 + static/index.html | 126 + static/js/cccc.js | 498 +++ static/sg/MIT-LICENSE.txt | 20 + static/sg/README.md | 27 + static/sg/controls/slick.columnpicker.css | 31 + static/sg/controls/slick.columnpicker.js | 152 + static/sg/controls/slick.pager.css | 41 + static/sg/controls/slick.pager.js | 154 + .../images/ui-bg_flat_0_aaaaaa_40x100.png | Bin 0 -> 180 bytes .../images/ui-bg_flat_75_ffffff_40x100.png | Bin 0 -> 178 bytes .../images/ui-bg_glass_55_fbf9ee_1x400.png | Bin 0 -> 120 bytes .../images/ui-bg_glass_65_ffffff_1x400.png | Bin 0 -> 105 bytes .../images/ui-bg_glass_75_dadada_1x400.png | Bin 0 -> 111 bytes .../images/ui-bg_glass_75_e6e6e6_1x400.png | Bin 0 -> 110 bytes .../images/ui-bg_glass_95_fef1ec_1x400.png | Bin 0 -> 119 bytes .../ui-bg_highlight-soft_75_cccccc_1x100.png | Bin 0 -> 101 bytes .../images/ui-icons_222222_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_2e83ff_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_454545_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_888888_256x240.png | Bin 0 -> 4369 bytes .../images/ui-icons_cd0a0a_256x240.png | Bin 0 -> 4369 bytes .../smoothness/jquery-ui-1.8.16.custom.css | 409 ++ .../sg/examples/.example2-formatters.html.swp | Bin 0 -> 12288 bytes .../.example3a-compound-editors.html.swp | Bin 0 -> 16384 bytes static/sg/examples/example-autotooltips.html | 78 + .../examples/example-checkbox-row-select.html | 96 + static/sg/examples/example-colspan.html | 91 + ...example-composite-editor-item-details.html | 236 ++ ...example-custom-column-value-extractor.html | 77 + .../example-explicit-initialization.html | 83 + static/sg/examples/example-grouping.html | 402 ++ static/sg/examples/example-header-row.html | 139 + .../examples/example-multi-column-sort.html | 99 + .../examples/example-optimizing-dataview.html | 182 + .../example-plugin-headerbuttons.html | 167 + .../examples/example-plugin-headermenu.html | 154 + static/sg/examples/example-spreadsheet.html | 175 + .../example-totals-via-data-provider.html | 134 + static/sg/examples/example1-simple.html | 68 + .../examples/example10-async-post-render.html | 134 + static/sg/examples/example11-autoheight.html | 87 + static/sg/examples/example12-fillbrowser.html | 116 + .../examples/example13-getItem-sorting.html | 125 + .../sg/examples/example14-highlighting.html | 157 + static/sg/examples/example2-formatters.html | 93 + static/sg/examples/example3-editing.html | 113 + .../examples/example3a-compound-editors.html | 149 + .../examples/example3b-editing-with-undo.html | 113 + static/sg/examples/example4-model.html | 356 ++ static/sg/examples/example5-collapsing.html | 278 ++ static/sg/examples/example6-ajax-loading.html | 171 + static/sg/examples/example7-events.html | 141 + .../example8-alternative-display.html | 176 + .../sg/examples/example9-row-reordering.html | 321 ++ static/sg/examples/examples.css | 230 ++ static/sg/examples/index.html | 61 + static/sg/examples/slick.compositeeditor.js | 211 + static/sg/images/actions.gif | Bin 0 -> 170 bytes static/sg/images/ajax-loader-small.gif | Bin 0 -> 1849 bytes static/sg/images/arrow_redo.png | Bin 0 -> 572 bytes static/sg/images/arrow_right_peppermint.png | Bin 0 -> 128 bytes static/sg/images/arrow_right_spearmint.png | Bin 0 -> 128 bytes static/sg/images/arrow_undo.png | Bin 0 -> 578 bytes static/sg/images/bullet_blue.png | Bin 0 -> 241 bytes static/sg/images/bullet_star.png | Bin 0 -> 279 bytes static/sg/images/bullet_toggle_minus.png | Bin 0 -> 154 bytes static/sg/images/bullet_toggle_plus.png | Bin 0 -> 156 bytes static/sg/images/calendar.gif | Bin 0 -> 1035 bytes static/sg/images/collapse.gif | Bin 0 -> 846 bytes static/sg/images/comment_yellow.gif | Bin 0 -> 257 bytes static/sg/images/down.gif | Bin 0 -> 59 bytes static/sg/images/drag-handle.png | Bin 0 -> 1130 bytes static/sg/images/editor-helper-bg.gif | Bin 0 -> 1164 bytes static/sg/images/expand.gif | Bin 0 -> 851 bytes static/sg/images/header-bg.gif | Bin 0 -> 872 bytes static/sg/images/header-columns-bg.gif | Bin 0 -> 836 bytes static/sg/images/header-columns-over-bg.gif | Bin 0 -> 823 bytes static/sg/images/help.png | Bin 0 -> 345 bytes static/sg/images/info.gif | Bin 0 -> 80 bytes static/sg/images/listview.gif | Bin 0 -> 2380 bytes static/sg/images/pencil.gif | Bin 0 -> 914 bytes static/sg/images/row-over-bg.gif | Bin 0 -> 823 bytes static/sg/images/sort-asc.gif | Bin 0 -> 830 bytes static/sg/images/sort-asc.png | Bin 0 -> 105 bytes static/sg/images/sort-desc.gif | Bin 0 -> 833 bytes static/sg/images/sort-desc.png | Bin 0 -> 107 bytes static/sg/images/stripes.png | Bin 0 -> 1125 bytes static/sg/images/tag_red.png | Bin 0 -> 537 bytes static/sg/images/tick.png | Bin 0 -> 484 bytes static/sg/images/user_identity.gif | Bin 0 -> 905 bytes static/sg/images/user_identity_plus.gif | Bin 0 -> 546 bytes static/sg/lib/firebugx.js | 9 + static/sg/lib/jquery-1.7.min.js | 4 + static/sg/lib/jquery-ui-1.8.16.custom.min.js | 611 +++ static/sg/lib/jquery.event.drag-2.2.js | 402 ++ static/sg/lib/jquery.event.drop-2.2.js | 302 ++ static/sg/lib/jquery.jsonp-2.4.min.js | 3 + static/sg/lib/jquery.simulate.js | 150 + static/sg/lib/jquery.sparkline.min.js | 79 + static/sg/lib/qunit.css | 119 + static/sg/lib/qunit.js | 1069 +++++ static/sg/plugins/slick.autotooltips.js | 83 + static/sg/plugins/slick.cellcopymanager.js | 86 + static/sg/plugins/slick.cellrangedecorator.js | 66 + static/sg/plugins/slick.cellrangeselector.js | 113 + static/sg/plugins/slick.cellselectionmodel.js | 154 + .../sg/plugins/slick.checkboxselectcolumn.js | 153 + static/sg/plugins/slick.headerbuttons.css | 39 + static/sg/plugins/slick.headerbuttons.js | 177 + static/sg/plugins/slick.headermenu.css | 59 + static/sg/plugins/slick.headermenu.js | 275 ++ static/sg/plugins/slick.rowmovemanager.js | 138 + static/sg/plugins/slick.rowselectionmodel.js | 187 + static/sg/slick-default-theme.css | 118 + static/sg/slick.core.js | 467 +++ static/sg/slick.dataview.js | 1126 ++++++ static/sg/slick.editors.js | 512 +++ static/sg/slick.formatters.js | 59 + static/sg/slick.grid.css | 157 + static/sg/slick.grid.js | 3422 +++++++++++++++++ static/sg/slick.groupitemmetadataprovider.js | 158 + static/sg/slick.remotemodel.js | 173 + static/sg/tests/dataview/dataview.js | 843 ++++ static/sg/tests/dataview/index.html | 24 + static/sg/tests/grid/grid.js | 68 + static/sg/tests/grid/index.html | 34 + static/sg/tests/index.html | 40 + static/sg/tests/init benchmark.html | 57 + static/sg/tests/model benchmarks.html | 110 + static/sg/tests/plugins/autotooltips.html | 34 + static/sg/tests/plugins/autotooltips.js | 133 + static/sg/tests/scrolling benchmark raf.html | 154 + static/sg/tests/scrolling benchmarks.html | 135 + 137 files changed, 19592 insertions(+) create mode 100644 .gitignore create mode 100644 action.py create mode 100644 server.py create mode 100644 static/css/campcamcontrol.css create mode 100644 static/index.html create mode 100644 static/js/cccc.js create mode 100644 static/sg/MIT-LICENSE.txt create mode 100644 static/sg/README.md create mode 100644 static/sg/controls/slick.columnpicker.css create mode 100644 static/sg/controls/slick.columnpicker.js create mode 100644 static/sg/controls/slick.pager.css create mode 100644 static/sg/controls/slick.pager.js create mode 100644 static/sg/css/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png create mode 100644 static/sg/css/smoothness/images/ui-bg_flat_75_ffffff_40x100.png create mode 100644 static/sg/css/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png create mode 100644 static/sg/css/smoothness/images/ui-bg_glass_65_ffffff_1x400.png create mode 100644 static/sg/css/smoothness/images/ui-bg_glass_75_dadada_1x400.png create mode 100644 static/sg/css/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png create mode 100644 static/sg/css/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png create mode 100644 static/sg/css/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png create mode 100644 static/sg/css/smoothness/images/ui-icons_222222_256x240.png create mode 100644 static/sg/css/smoothness/images/ui-icons_2e83ff_256x240.png create mode 100644 static/sg/css/smoothness/images/ui-icons_454545_256x240.png create mode 100644 static/sg/css/smoothness/images/ui-icons_888888_256x240.png create mode 100644 static/sg/css/smoothness/images/ui-icons_cd0a0a_256x240.png create mode 100644 static/sg/css/smoothness/jquery-ui-1.8.16.custom.css create mode 100644 static/sg/examples/.example2-formatters.html.swp create mode 100644 static/sg/examples/.example3a-compound-editors.html.swp create mode 100644 static/sg/examples/example-autotooltips.html create mode 100644 static/sg/examples/example-checkbox-row-select.html create mode 100644 static/sg/examples/example-colspan.html create mode 100644 static/sg/examples/example-composite-editor-item-details.html create mode 100644 static/sg/examples/example-custom-column-value-extractor.html create mode 100644 static/sg/examples/example-explicit-initialization.html create mode 100644 static/sg/examples/example-grouping.html create mode 100644 static/sg/examples/example-header-row.html create mode 100644 static/sg/examples/example-multi-column-sort.html create mode 100644 static/sg/examples/example-optimizing-dataview.html create mode 100644 static/sg/examples/example-plugin-headerbuttons.html create mode 100644 static/sg/examples/example-plugin-headermenu.html create mode 100644 static/sg/examples/example-spreadsheet.html create mode 100644 static/sg/examples/example-totals-via-data-provider.html create mode 100644 static/sg/examples/example1-simple.html create mode 100644 static/sg/examples/example10-async-post-render.html create mode 100644 static/sg/examples/example11-autoheight.html create mode 100644 static/sg/examples/example12-fillbrowser.html create mode 100644 static/sg/examples/example13-getItem-sorting.html create mode 100644 static/sg/examples/example14-highlighting.html create mode 100644 static/sg/examples/example2-formatters.html create mode 100644 static/sg/examples/example3-editing.html create mode 100644 static/sg/examples/example3a-compound-editors.html create mode 100644 static/sg/examples/example3b-editing-with-undo.html create mode 100644 static/sg/examples/example4-model.html create mode 100644 static/sg/examples/example5-collapsing.html create mode 100644 static/sg/examples/example6-ajax-loading.html create mode 100644 static/sg/examples/example7-events.html create mode 100644 static/sg/examples/example8-alternative-display.html create mode 100644 static/sg/examples/example9-row-reordering.html create mode 100644 static/sg/examples/examples.css create mode 100644 static/sg/examples/index.html create mode 100644 static/sg/examples/slick.compositeeditor.js create mode 100644 static/sg/images/actions.gif create mode 100644 static/sg/images/ajax-loader-small.gif create mode 100644 static/sg/images/arrow_redo.png create mode 100644 static/sg/images/arrow_right_peppermint.png create mode 100644 static/sg/images/arrow_right_spearmint.png create mode 100644 static/sg/images/arrow_undo.png create mode 100644 static/sg/images/bullet_blue.png create mode 100644 static/sg/images/bullet_star.png create mode 100644 static/sg/images/bullet_toggle_minus.png create mode 100644 static/sg/images/bullet_toggle_plus.png create mode 100644 static/sg/images/calendar.gif create mode 100644 static/sg/images/collapse.gif create mode 100644 static/sg/images/comment_yellow.gif create mode 100644 static/sg/images/down.gif create mode 100644 static/sg/images/drag-handle.png create mode 100644 static/sg/images/editor-helper-bg.gif create mode 100644 static/sg/images/expand.gif create mode 100644 static/sg/images/header-bg.gif create mode 100644 static/sg/images/header-columns-bg.gif create mode 100644 static/sg/images/header-columns-over-bg.gif create mode 100644 static/sg/images/help.png create mode 100644 static/sg/images/info.gif create mode 100644 static/sg/images/listview.gif create mode 100644 static/sg/images/pencil.gif create mode 100644 static/sg/images/row-over-bg.gif create mode 100644 static/sg/images/sort-asc.gif create mode 100644 static/sg/images/sort-asc.png create mode 100644 static/sg/images/sort-desc.gif create mode 100644 static/sg/images/sort-desc.png create mode 100644 static/sg/images/stripes.png create mode 100644 static/sg/images/tag_red.png create mode 100644 static/sg/images/tick.png create mode 100644 static/sg/images/user_identity.gif create mode 100644 static/sg/images/user_identity_plus.gif create mode 100644 static/sg/lib/firebugx.js create mode 100644 static/sg/lib/jquery-1.7.min.js create mode 100644 static/sg/lib/jquery-ui-1.8.16.custom.min.js create mode 100644 static/sg/lib/jquery.event.drag-2.2.js create mode 100644 static/sg/lib/jquery.event.drop-2.2.js create mode 100644 static/sg/lib/jquery.jsonp-2.4.min.js create mode 100644 static/sg/lib/jquery.simulate.js create mode 100644 static/sg/lib/jquery.sparkline.min.js create mode 100644 static/sg/lib/qunit.css create mode 100644 static/sg/lib/qunit.js create mode 100644 static/sg/plugins/slick.autotooltips.js create mode 100644 static/sg/plugins/slick.cellcopymanager.js create mode 100644 static/sg/plugins/slick.cellrangedecorator.js create mode 100644 static/sg/plugins/slick.cellrangeselector.js create mode 100644 static/sg/plugins/slick.cellselectionmodel.js create mode 100644 static/sg/plugins/slick.checkboxselectcolumn.js create mode 100644 static/sg/plugins/slick.headerbuttons.css create mode 100644 static/sg/plugins/slick.headerbuttons.js create mode 100644 static/sg/plugins/slick.headermenu.css create mode 100644 static/sg/plugins/slick.headermenu.js create mode 100644 static/sg/plugins/slick.rowmovemanager.js create mode 100644 static/sg/plugins/slick.rowselectionmodel.js create mode 100644 static/sg/slick-default-theme.css create mode 100644 static/sg/slick.core.js create mode 100644 static/sg/slick.dataview.js create mode 100644 static/sg/slick.editors.js create mode 100644 static/sg/slick.formatters.js create mode 100644 static/sg/slick.grid.css create mode 100644 static/sg/slick.grid.js create mode 100644 static/sg/slick.groupitemmetadataprovider.js create mode 100644 static/sg/slick.remotemodel.js create mode 100644 static/sg/tests/dataview/dataview.js create mode 100644 static/sg/tests/dataview/index.html create mode 100755 static/sg/tests/grid/grid.js create mode 100644 static/sg/tests/grid/index.html create mode 100644 static/sg/tests/index.html create mode 100644 static/sg/tests/init benchmark.html create mode 100644 static/sg/tests/model benchmarks.html create mode 100644 static/sg/tests/plugins/autotooltips.html create mode 100644 static/sg/tests/plugins/autotooltips.js create mode 100644 static/sg/tests/scrolling benchmark raf.html create mode 100644 static/sg/tests/scrolling benchmarks.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..def97dd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +camera.json diff --git a/action.py b/action.py new file mode 100644 index 0000000..422b75a --- /dev/null +++ b/action.py @@ -0,0 +1,380 @@ +#!/usr/bin/python3 +import json +import time +import xml.etree.ElementTree +import requests +from requests.auth import HTTPDigestAuth + + +def PTZData(d, root="PTZData"): + op = lambda tag: '<' + tag + '>' + cl = lambda tag: '\n' + ml = lambda v, xml: xml + op(key) + str(v) + cl(key) + xml = op(root) + '\n' if root else "" + if isinstance(d, list): + for v in d: + xml = ml(v, xml) + else: + for key, vl in d.items(): + if isinstance(vl, list): + for v in vl: + xml = ml(v, xml) + if isinstance(vl, dict): + xml = ml('\n' + PTZData(vl, None), xml) + if not isinstance(vl, (list, dict)): + xml = ml(vl, xml) + xml += cl(root) if root else "" + return xml + +def etree_to_list(t): + d = [] + k = t.tag.split('}')[-1] + v = list(map(etree_to_dict, t.getchildren())) + if len(v) == 1: + v = v[0] + elif v and not isinstance(v[0], dict): + v_ = {} + for kv in v: + for key, value in kv.items(): + if isinstance(value, str) and (value.isdigit() or value[1:].isdigit()): + value = int(value) + v_[key] = value + v = v_ + return v + +def etree_to_dict(t): + d = {} + k = t.tag.split('}')[-1] + v = list(map(etree_to_dict, t.getchildren())) + if len(v) == 1: + v = v[0] + #elif v and not isinstance(v[0], dict): + else: + v_ = {} + for kv in v: + for key, value in kv.items(): + if isinstance(value, str) and (value.isdigit() or value[1:].isdigit()): + value = int(value) + v_[key] = value + v = v_ + d = {k: v} + #d.update(('@' + k, v) for k, v in t.attrib.items()) + if not v: + d[k] = t.text + elif t.text.strip(): + d['text'] = t.text + return d + +class Camera: + STOP = {'pan': 0, 'tilt': 0, 'zoom': 0} + LEFT = {'pan': -1, 'tilt': 0, 'zoom': 0} + RIGHT = {'pan': 1, 'tilt': 0, 'zoom': 0} + UP = {'pan': 0, 'tilt': 1, 'zoom': 0} + DOWN = {'pan': 0, 'tilt': -1, 'zoom': 0} + LEFT_UP = {'pan': -1, 'tilt': 1, 'zoom': 0} + LEFT_DOWN = {'pan': -1, 'tilt': -1, 'zoom': 0} + RIGHT_UP = {'pan': 1, 'tilt': 1, 'zoom': 0} + RIGHT_DOWN = {'pan': 1, 'tilt': -1, 'zoom': 0} + + IN = {'pan': 1, 'tilt': 0, 'zoom': 1} + OUT = {'pan': 1, 'tilt': 0, 'zoom': -1} + + abort = False + sequence_time = 0 + segment_times = {} + next_target = None + last_position = {} + + def __init__(self, ip='192.168.1.64', username='admin', password='admin', channel=1): + self.ip = ip + self.username = username + self.password = password + self.channel = channel + self.auth = HTTPDigestAuth(self.username, self.password) + + def url(self, method): + return 'http://{}/ISAPI/PTZCtrl/channels/{}/{}'.format(self.ip, self.channel, method) + + def parse_xml(self, data): + return etree_to_dict(xml.etree.ElementTree.fromstring(data)) + + def status(self): + s = self.parse_xml(self.get('status')) + if 'PTZStatus' in s: + self.last_position = s['PTZStatus']['AbsoluteHigh'] + return self.last_position + return {} + + def get(self, method): + return requests.get(self.url(method), auth=self.auth).text + + def put(self, method, data): + if isinstance(data, dict): + data = PTZData(data) + return requests.put(self.url(method), data=data, auth=self.auth).text + + def momentary(self, cmd, duration=None): + if duration: + cmd['Momentary'] = { + 'duration': int(duration * 1000) + } + r = self.put('momentary', cmd) + if '1' not in r: + print(r) + + def continuous(self, cmd, duration=None): + r = self.put('continuous', cmd) + if '1' not in r: + print(r) + elif duration: + time.sleep(duration) + self.continuous(self.STOP) + + def set_preset(self, id, name): + data = PTZData({ + 'id': str(id), + 'presetName': name + }, 'PTZPreset') + r = self.put('presets/%s' % id, data) + if '1' not in r: + print(r) + + def set_presets(self, presets): + for preset in presets: + self.absolute(**preset['position']) + time.sleep(5) + self.absolute(**preset['position']) + time.sleep(5) + self.set_preset(preset['id'], preset['name']) + + def sequence(self, steps=[], speed=12, goto_first=True): + self.sequence_start = 0 + if goto_first: + #self.goto_preset(steps[0], pan=100, tilt=100, zoom=100) + first = steps[0] + if isinstance(first, dict): + if 'seqid' in first and first['seqid'] not in self.segment_times: + self.segment_times[first['seqid']] = 0 + first = first['preset'] + self.fast_preset(first, True) + #self.goto_preset(first, pan=speed, tilt=speed) + if self.abort: + return + time.sleep(3) + steps = steps[1:] + self.sequence_start = t0 = time.time() + for step in steps: + self.next_target = step + segment_t0 = time.time() + if self.abort: + return + if isinstance(step, dict): + if 'fast' in step or ('speed' in step and step['speed'] > 100): + self.fast_preset(step.get('fast', step.get('preset')), True) + else: + if 'preset' in step: + kwargs = { + 'pan': step['speed'], + 'tilt': step['speed'], + } + if step.get('zoom'): + kwargs['zoom'] = step['zoom'] + if 'zoom_last' in step: + kwargs['zoom_last'] = step['zoom_last'] + self.goto_preset(step['preset'], **kwargs) + if 'sleep' in step: + if self.abort: + return + time.sleep(float(step['sleep'])) + else: + self.goto_preset(step, pan=speed, tilt=speed) + + segment_time = time.time() - segment_t0 + if isinstance(step, dict) and 'seqid' in step: + self.segment_times[step['seqid']] = segment_time + self.sequence_time = time.time() - t0 + self.next_target = None + + def fast_preset(self, id, wait=False): + self.put('presets/%s/goto' % id, None) + if wait: + preset = self.get_preset(id)['position'] + last = self.status() + + def is_close_enough(a, b): + return abs(a['elevation'] - b['elevation']) < 2 \ + and abs(a['absoluteZoom'] - b['absoluteZoom']) < 2 \ + and abs(a['azimuth'] - b['azimuth']) < 2 + + while not is_close_enough(last, preset): + time.sleep(0.1) + last = self.status() + + def goto_preset(self, id, pan, tilt, zoom=1, zoom_last=False): + if self.abort: + return + presets = self.get_presets(True) + if isinstance(id, str): + preset = [p for p in presets if p['name'] == id] + if not preset: + raise Exception('unknown preset %s' % id) + id = preset[0]['id'] + else: + preset = [p for p in presets if p['id'] == id] + if not preset: + raise Exception('unknown preset %s' % id) + preset = preset[0] + i = preset['position'] + #info = etree_to_dict(xml.etree.ElementTree.fromstring(self.get('presets/%s' % id))) + #i = info['PTZPreset']['AbsoluteHigh'] + print('goto preset', id, i['elevation'], i['azimuth'], i['absoluteZoom']) + return self.goto(i['elevation'], i['azimuth'], i['absoluteZoom'], pan=pan, tilt=tilt, zoom=zoom, zoom_last=zoom_last) + + def goto(self, elevation, azimuth, absoluteZoom, speed=1, pan=None, tilt=None, zoom=None, zoom_last=False): + t0 = time.time() + start = self.status() + target = {'elevation': elevation, 'azimuth': azimuth, 'absoluteZoom': absoluteZoom} + print('start:', start, 'target:', target) + + if zoom_last: + goto_last = { + 'speed': speed, + 'pan': pan, + 'tilt': tilt, + 'zoom': zoom, + } + goto_last.update(target) + target['absoluteZoom'] = start['absoluteZoom'] + + def get_delta(): + current = self.status() + delta = {} + for key in current: + delta[key] = target[key] - current[key] + return current, delta + + current, delta = get_delta() + + if pan is None: + pan = speed + if tilt is None: + tilt = speed + if zoom is None: + zoom = 1 + if delta['elevation'] > 0: + tilt = -tilt + if delta['azimuth'] < 0: + pan = -pan + if delta['absoluteZoom'] < 0: + zoom = -zoom + if pan and not delta['azimuth']: + pan = 0 + if tilt and not delta['elevation']: + tilt = 0 + if zoom and not delta['absoluteZoom']: + zoom = 0 + + move = {'pan': pan, 'tilt': tilt, 'zoom': zoom} + direction = {k: v >= 0 for k, v in delta.items()} + print(move, direction) + self.continuous(move) + n = 0 + while sum(map(abs, move.values())): + n += 1 + #print(current, delta, move) + current, delta = get_delta() + changed = False + if pan and (not delta['azimuth'] or (delta['azimuth'] >= 0) != direction['azimuth']): + pan = 0 + changed = True + ''' + if pan > 0: + pan = min(pan, delta['azimuth']) + change = True + elif pan < 0 and delta['azimuth'] < 0: + pan = max(pan, delta['azimuth']) + changed = True + ''' + if tilt and (not delta['elevation'] or (delta['elevation'] >= 0) != direction['elevation']): + tilt = 0 + changed = True + ''' + if tilt > 0: + tilt = min(tilt, delta['elevation']) + change = True + elif tilt < 0 and delta['elevation'] < 0: + tilt = max(tilt, delta['elevation']) + changed = True + ''' + if zoom and (not delta['absoluteZoom'] or (delta['absoluteZoom'] >= 0) != direction['absoluteZoom']): + zoom = 0 + changed = True + + if changed or not n % 500: + print('update move', move, current) + move = {'pan': pan, 'tilt': tilt, 'zoom': zoom} + self.continuous(self.STOP) + self.continuous(move) + if sum(map(abs, move.values())) and not self.abort: + time.sleep(0.1) + if self.abort: + self.continuous(self.STOP) + return + self.continuous(self.STOP) + print(time.time() - t0) + if zoom_last: + return self.goto(**goto_last) + else: + return get_delta() + + def get_preset(self, id): + presets = self.get_presets(True) + if isinstance(id, str): + preset = [p for p in presets if p['name'] == id] + if not preset: + raise Exception('unknown preset %s' % id) + id = preset[0]['id'] + else: + preset = [p for p in presets if p['id'] == id] + if not preset: + raise Exception('unknown preset %s' % id) + preset = preset[0] + return preset + + def absolute(self, elevation, azimuth, absoluteZoom): + cmd = { + 'AbsoluteHigh': { + 'elevation': elevation, + 'azimuth': azimuth, + 'absoluteZoom': absoluteZoom + } + } + print('!!', cmd) + r = self.put('absolute', cmd) + if '1' not in r: + print(r) + return False + return True + + def get_presets_xml(self): + return self.get('presets') + + + def get_presets(self, details=False): + system_presets = list(range(33, 47)) + list(range(90, 106)) + presets = etree_to_list(xml.etree.ElementTree.fromstring(self.get('presets'))) + presets = [k['PTZPreset'] for k in presets if k['PTZPreset']['id'] not in system_presets] + for preset in presets: + del preset['enabled'] + if not details: + del preset['AbsoluteHigh'] + else: + preset['position'] = preset.pop('AbsoluteHigh') + preset['name'] = preset.pop('presetName') + return presets + +#/PTZCtrl/channels//patterns//recordstart +#/PTZCtrl/channels//patterns//recordstop + + +cam = Camera() diff --git a/server.py b/server.py new file mode 100644 index 0000000..42e9e00 --- /dev/null +++ b/server.py @@ -0,0 +1,228 @@ +from functools import wraps +from urllib.parse import unquote +import json +import logging +import os +import queue +import shutil +import threading +import time + +import requests +from tornado.httpserver import HTTPServer +from tornado.ioloop import IOLoop +from tornado.web import StaticFileHandler, Application, HTTPError +import tornado.gen +import tornado.web + + +from action import Camera + + +logger = logging.getLogger(__name__) + +STATIC_PATH = 'static' +PORT = 8000 +ADDRESS = '127.0.0.1' + +with open('camera.json') as fd: + CAMERA = json.load(fd) + +state = { + 'time': {}, + 'status': 'Idle' +} + +def run_async(func): + @wraps(func) + def async_func(*args, **kwargs): + func_hl = Thread(target=func, args=args, kwargs=kwargs) + func_hl.start() + return func_hl + + return async_func + +def _to_json(python_object): + if isinstance(python_object, datetime.datetime): + if python_object.year < 1900: + tt = python_object.timetuple() + return '%d-%02d-%02dT%02d:%02d%02dZ' % tuple(list(tt)[:6]) + return python_object.strftime('%Y-%m-%dT%H:%M:%SZ') + raise TypeError('%s %s is not JSON serializable' % (repr(python_object), type(python_object))) + +def json_dumps(obj): + return json.dumps(obj, indent=4, default=_to_json, ensure_ascii=False, sort_keys=True).encode() + +class ControlQueue: + shutdown = False + + def worker(self): + while True: + item = self.q.get() + if item is None or self.shutdown: + break + state['status'] = 'Active' + self.camera.sequence(**item) + if self.camera.abort: + self.camera.abort = False + state['status'] = 'Idle' + + def __init__(self): + self.q = queue.Queue() + self.camera = Camera(**CAMERA) + self._worker = threading.Thread(target=self.worker) + self._worker.start() + + def put(self, filename): + self.q.put(filename) + + def join(self): + self.shutdown = True + self.camera.abort = True + # block until all tasks are done + self.q.join() + + self.q.put(None) + self._worker.join() + + +class API(object): + + def __call__(self, method, kwargs): + return getattr(self, method)(**kwargs) + + def getPresets(self, **data): + result = {} + result['presets'] = ctl.camera.get_presets(True) + return result + + def camera(self, **data): + result = {} + for key, value in data.items(): + getattr(ctl.camera, key)(**value) + return result + + def run(self, **data): + result = {} + ctl.put(data) + return result + + def stop(self, **data): + result = {} + ctl.camera.abort = True + ctl.camera.continuous(ctl.camera.STOP) + return result + + def status(self, **data): + result = {} + result['time'] = ctl.camera.segment_times + result['status'] = state['status'] + if result['status'] == 'Active': + if ctl.camera.sequence_start: + result['duration'] = int(time.time() - ctl.camera.sequence_start) + else: + result['duration'] = '...' + result['next'] = ctl.camera.next_target + else: + result['duration'] = ctl.camera.sequence_time + result['position'] = ctl.camera.last_position + return result + +#@run_async +def api_task(request, callback): + api = API() + response = { + 'result': api(request['method'], request.get('params', {})) + } + callback(response) + +class RPCHandler(tornado.web.RequestHandler): + + def get(self): + self.write(BANNER) + + @tornado.gen.coroutine + def post(self): + error = None + request = None + try: + request = json.loads(self.request.body.decode()) + if request['method'][0] == '_' or not hasattr(API, request['method']): + raise Exception('unknown method') + except: + error = {'error': {'code': -32700, 'message': 'Parse error'}} + if not error: + try: + response = yield tornado.gen.Task(api_task, request) + except: + logger.error("ERROR: %s", request, exc_info=True) + error = {'error': {'code': -32000, 'message': 'Server error'}} + if error: + response = error + if request and 'id' in request: + response['id'] = request['id'] + response['jsonrpc'] = '2.0' + response = json_dumps(response) + self.write(response) + +def prepare(encoding): + if not os.path.exists(settings['prefix']): + print('please create "%s" and start again' % settings['prefix']) + sys.exit(1) + index = os.path.join(settings['prefix'], 'index.html') + if not os.path.exists(index): + try: + with open(index, 'w') as fd: + fd.write(BANNER_PUBLIC) + except: + print('can not write to "%s"' % settings['prefix']) + sys.exit(1) + load_files(encoding) + registered = False + while not registered: + try: + register_server() + except: + logging.error('failed to register') + time.sleep(10) + registered = True + +class MainHandler(tornado.web.RequestHandler): + + def get(self, path): + path = os.path.join(STATIC_PATH, 'index.html') + with open(path) as fd: + content = fd.read() + self.set_header('Content-Type', 'text/html') + self.set_header('Content-Length', str(len(content))) + self.set_header('Cache-Control', 'no-cache, no-store, must-revalidate') + self.set_header('Pragma', 'no-cache') + self.set_header('Expires', '0') + self.write(content) + +def main(): + global ctl + ctl = ControlQueue() + handlers = [ + (r'/api/', RPCHandler), + (r'/static/(.*)', StaticFileHandler, {'path': STATIC_PATH}), + (r"(.*)", MainHandler), + ] + + options = { + 'debug': False, + 'gzip': True, + } + app = Application(handlers, **options) + app.listen(PORT, ADDRESS) + + main = IOLoop.instance() + try: + main.start() + except: + print('shutting down...') + ctl.join() + + +if __name__ == '__main__': + main() diff --git a/static/css/campcamcontrol.css b/static/css/campcamcontrol.css new file mode 100644 index 0000000..b6fb4c6 --- /dev/null +++ b/static/css/campcamcontrol.css @@ -0,0 +1,210 @@ +@import url('../sg/slick-default-theme.css'); + +* { + font-family: arial; + font-size: 8pt; +} + +body { + padding: 0; + margin: 8px; +} + +h2 { + font-size: 10pt; + border-bottom: 1px dotted gray; +} + +ul { + margin-left: 0; + padding: 0; + cursor: default; +} + +li { + background: url("../../sg/images/arrow_right_spearmint.png") no-repeat center left; + padding: 0 0 0 14px; + + list-style: none; + margin: 0; +} + +#myGrid { + background: white; + outline: 0; + border: 1px solid gray; +} + +.grid-header { + border: 1px solid gray; + border-bottom: 0; + border-top: 0; + background: url('../sg/images/header-bg.gif') repeat-x center top; + color: black; + height: 24px; + line-height: 24px; +} + +.grid-header label { + display: inline-block; + font-weight: bold; + margin: auto auto auto 6px; +} + +.grid-header .ui-icon { + margin: 4px 4px auto 6px; + background-color: transparent; + border-color: transparent; +} + +.grid-header .ui-icon.ui-state-hover { + background-color: white; +} + +.grid-header #txtSearch { + margin: 0 4px 0 4px; + padding: 2px 2px; + -moz-border-radius: 2px; + -webkit-border-radius: 2px; + border: 1px solid silver; +} + +/* Individual cell styles */ +.slick-cell.task-name { + font-weight: bold; + text-align: right; +} + +.slick-cell.task-percent { + text-align: right; +} + +.slick-cell.cell-move-handle { + font-weight: bold; + text-align: right; + border-right: solid gray; + + background: #efefef; + cursor: move; +} + +.cell-move-handle:hover { + background: #b6b9bd; +} + +.slick-row.selected .cell-move-handle { + background: #D5DC8D; +} + +.slick-row .cell-actions { + text-align: left; +} + +.slick-row.complete { + background-color: #DFD; + color: #555; +} + +.percent-complete-bar { + display: inline-block; + height: 6px; + -moz-border-radius: 3px; + -webkit-border-radius: 3px; +} + +/* Slick.Editors.Text, Slick.Editors.Date */ +input.editor-text { + width: 100%; + height: 100%; + border: 0; + margin: 0; + background: transparent; + outline: 0; + padding: 0; + +} + +.ui-datepicker-trigger { + margin-top: 2px; + padding: 0; + vertical-align: top; +} + +/* Slick.Editors.PercentComplete */ +input.editor-percentcomplete { + width: 100%; + height: 100%; + border: 0; + margin: 0; + background: transparent; + outline: 0; + padding: 0; + + float: left; +} + +.editor-percentcomplete-picker { + position: relative; + display: inline-block; + width: 16px; + height: 100%; + background: url("../sg/images/pencil.gif") no-repeat center center; + overflow: visible; + z-index: 1000; + float: right; +} + +.editor-percentcomplete-helper { + border: 0 solid gray; + position: absolute; + top: -2px; + left: -9px; + background: url("../sg/images/editor-helper-bg.gif") no-repeat top left; + padding-left: 9px; + + width: 120px; + height: 140px; + display: none; + overflow: visible; +} + +.editor-percentcomplete-wrapper { + background: beige; + padding: 20px 8px; + + width: 100%; + height: 98px; + border: 1px solid gray; + border-left: 0; +} + +.editor-percentcomplete-buttons { + float: right; +} + +.editor-percentcomplete-buttons button { + width: 80px; +} + +.editor-percentcomplete-slider { + float: left; +} + +.editor-percentcomplete-picker:hover .editor-percentcomplete-helper { + display: block; +} + +.editor-percentcomplete-helper:hover { + display: block; +} + + +/* Slick.Editors.Checkbox */ +input.editor-checkbox { + margin: 0; + height: 100%; + padding: 0; + border: 0; +} + + diff --git a/static/index.html b/static/index.html new file mode 100644 index 0000000..d9b93b1 --- /dev/null +++ b/static/index.html @@ -0,0 +1,126 @@ + + + + + CAMP can capture canals + + + + + + +
+
+
+
+ +
+

CAMP cam canal:

+
    +
  • +
  • +
  • +
  • +
  • +
+

Status:

+
  • Idle
  • +
  • +
  • + +

    Edit:

    +
  • +
  • +
  • +
  • Global Speed:
  • +

    Export:

    +
  • +
  • +

    Import:

    +
  • import sequence
  • +
  • import presets
  • +
    +
    + + + + + + + + + + + + + + + + + + diff --git a/static/js/cccc.js b/static/js/cccc.js new file mode 100644 index 0000000..dbd5065 --- /dev/null +++ b/static/js/cccc.js @@ -0,0 +1,498 @@ +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 presets = []; + +function PresetEditor(args) { + var $preset; + var scope = this; + + this.init = function () { + var options = '' + presets.forEach(function(preset) { + options += ''; + }) + $preset = $('') + .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 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: "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) { + $('#status').html(response.result.status) + 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() + } + }) + localStorage.sequence = JSON.stringify(data) + + 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}) +} + +$(function () { + api('getPresets', {}, function(response) { + presets = response.result.presets + loadData(JSON.parse(localStorage.sequence || '[]')) + updateStatus() + setInterval(updateStatus, 1000) + }) +}) + +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.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) { + 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: 'text/plain; charset=utf-8'}); + 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 + }) +} + +$('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.all_presets').on({click: function() { + loadData(presets.map(function(preset) { + return { + preset: preset.id, + speed: 20 + } + })) +}}) + +$('input.import_sequence').on({change: function() { + var reader = new FileReader() + reader.onload = function(event) { + localStorage.sequence = reader.result + loadData(JSON.parse(reader.result)) + } + reader.readAsText(this.files[0]); +}}) +$('input.import_presets').on({change: function() { + console.log('import', this) +}}) diff --git a/static/sg/MIT-LICENSE.txt b/static/sg/MIT-LICENSE.txt new file mode 100644 index 0000000..60f6542 --- /dev/null +++ b/static/sg/MIT-LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2010 Michael Leibman, http://github.com/mleibman/slickgrid + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/static/sg/README.md b/static/sg/README.md new file mode 100644 index 0000000..7eb6fda --- /dev/null +++ b/static/sg/README.md @@ -0,0 +1,27 @@ +# Welcome to SlickGrid + +Find documentation and examples in [the wiki](https://github.com/mleibman/SlickGrid/wiki). + + +**UPDATE: March 5th, 2014 - I have too many things going on in my life right now to really give SlickGrid support and development the time and attention it deserves. I am not stopping it, but I will most likely be unresponsive for some time. Sorry.** + +**UPDATE: This repo hasn't been updated in a while. https://github.com/6pac/SlickGrid/wiki seems to be the most active fork at the moment.** + +## SlickGrid is an advanced JavaScript grid/spreadsheet component + +Some highlights: + +* Adaptive virtual scrolling (handle hundreds of thousands of rows with extreme responsiveness) +* Extremely fast rendering speed +* Supports jQuery UI Themes +* Background post-rendering for richer cells +* Configurable & customizable +* Full keyboard navigation +* Column resize/reorder/show/hide +* Column autosizing & force-fit +* Pluggable cell formatters & editors +* Support for editing and creating new rows. +* Grouping, filtering, custom aggregators, and more! +* Advanced detached & multi-field editors with undo/redo support. +* “GlobalEditorLock” to manage concurrent edits in cases where multiple Views on a page can edit the same data. +* Support for [millions of rows](http://stackoverflow.com/a/2569488/1269037) diff --git a/static/sg/controls/slick.columnpicker.css b/static/sg/controls/slick.columnpicker.css new file mode 100644 index 0000000..bcbb375 --- /dev/null +++ b/static/sg/controls/slick.columnpicker.css @@ -0,0 +1,31 @@ +.slick-columnpicker { + border: 1px solid #718BB7; + background: #f0f0f0; + padding: 6px; + -moz-box-shadow: 2px 2px 2px silver; + -webkit-box-shadow: 2px 2px 2px silver; + box-shadow: 2px 2px 2px silver; + min-width: 100px; + cursor: default; +} + +.slick-columnpicker li { + list-style: none; + margin: 0; + padding: 0; + background: none; +} + +.slick-columnpicker input { + margin: 4px; +} + +.slick-columnpicker li a { + display: block; + padding: 4px; + font-weight: bold; +} + +.slick-columnpicker li a:hover { + background: white; +} diff --git a/static/sg/controls/slick.columnpicker.js b/static/sg/controls/slick.columnpicker.js new file mode 100644 index 0000000..dc16720 --- /dev/null +++ b/static/sg/controls/slick.columnpicker.js @@ -0,0 +1,152 @@ +(function ($) { + function SlickColumnPicker(columns, grid, options) { + var $menu; + var columnCheckboxes; + + var defaults = { + fadeSpeed:250 + }; + + function init() { + grid.onHeaderContextMenu.subscribe(handleHeaderContextMenu); + grid.onColumnsReordered.subscribe(updateColumnOrder); + options = $.extend({}, defaults, options); + + $menu = $("