snapshots.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. /**
  2. * Copyright (c) 2018-2020 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. * @author Alexander Rose <alexander.rose@weirdbyte.de>
  6. */
  7. import { List } from 'immutable';
  8. import { UUID } from '../../mol-util';
  9. import { PluginState } from '../../mol-plugin/state';
  10. import { StatefulPluginComponent } from '../component';
  11. import { PluginContext } from '../../mol-plugin/context';
  12. import { utf8ByteCount, utf8Write } from '../../mol-io/common/utf8';
  13. import { Asset } from '../../mol-util/assets';
  14. import { zip } from '../../mol-util/zip/zip';
  15. import { readFromFile } from '../../mol-util/data-source';
  16. import { objectForEach } from '../../mol-util/object';
  17. import { PLUGIN_VERSION } from '../../mol-plugin/version';
  18. export { PluginStateSnapshotManager };
  19. class PluginStateSnapshotManager extends StatefulPluginComponent<{
  20. current?: UUID | undefined,
  21. entries: List<PluginStateSnapshotManager.Entry>,
  22. isPlaying: boolean,
  23. nextSnapshotDelayInMs: number
  24. }> {
  25. static DefaultNextSnapshotDelayInMs = 1500;
  26. private entryMap = new Map<string, PluginStateSnapshotManager.Entry>();
  27. readonly events = {
  28. changed: this.ev()
  29. };
  30. currentGetSnapshotParams: PluginState.GetSnapshotParams = PluginState.DefaultGetSnapshotParams as any;
  31. getIndex(e: PluginStateSnapshotManager.Entry) {
  32. return this.state.entries.indexOf(e);
  33. }
  34. getEntry(id: string | undefined) {
  35. if (!id) return;
  36. return this.entryMap.get(id);
  37. }
  38. remove(id: string) {
  39. const e = this.entryMap.get(id);
  40. if (!e) return;
  41. this.entryMap.delete(id);
  42. this.updateState({
  43. current: this.state.current === id ? void 0 : this.state.current,
  44. entries: this.state.entries.delete(this.getIndex(e))
  45. });
  46. this.events.changed.next();
  47. }
  48. add(e: PluginStateSnapshotManager.Entry) {
  49. this.entryMap.set(e.snapshot.id, e);
  50. this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) });
  51. this.events.changed.next();
  52. }
  53. replace(id: string, snapshot: PluginState.Snapshot) {
  54. const old = this.getEntry(id);
  55. if (!old) return;
  56. const idx = this.getIndex(old);
  57. // The id changes here!
  58. const e = PluginStateSnapshotManager.Entry(snapshot, {
  59. name: old.name,
  60. description: old.description
  61. });
  62. this.entryMap.set(snapshot.id, e);
  63. this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) });
  64. this.events.changed.next();
  65. }
  66. move(id: string, dir: -1 | 1) {
  67. const len = this.state.entries.size;
  68. if (len < 2) return;
  69. const e = this.getEntry(id);
  70. if (!e) return;
  71. const from = this.getIndex(e);
  72. let to = (from + dir) % len;
  73. if (to < 0) to += len;
  74. const f = this.state.entries.get(to);
  75. const entries = this.state.entries.asMutable();
  76. entries.set(to, e);
  77. entries.set(from, f);
  78. this.updateState({ current: e.snapshot.id, entries: entries.asImmutable() });
  79. this.events.changed.next();
  80. }
  81. clear() {
  82. if (this.state.entries.size === 0) return;
  83. this.entryMap.clear();
  84. this.updateState({ current: void 0, entries: List<PluginStateSnapshotManager.Entry>() });
  85. this.events.changed.next();
  86. }
  87. setCurrent(id: string) {
  88. const e = this.getEntry(id);
  89. if (e) {
  90. this.updateState({ current: id as UUID });
  91. this.events.changed.next();
  92. }
  93. return e && e.snapshot;
  94. }
  95. getNextId(id: string | undefined, dir: -1 | 1) {
  96. const len = this.state.entries.size;
  97. if (!id) {
  98. if (len === 0) return void 0;
  99. const idx = dir === -1 ? len - 1 : 0;
  100. return this.state.entries.get(idx).snapshot.id;
  101. }
  102. const e = this.getEntry(id);
  103. if (!e) return;
  104. let idx = this.getIndex(e);
  105. if (idx < 0) return;
  106. idx = (idx + dir) % len;
  107. if (idx < 0) idx += len;
  108. return this.state.entries.get(idx).snapshot.id;
  109. }
  110. async setStateSnapshot(snapshot: PluginStateSnapshotManager.StateSnapshot): Promise<PluginState.Snapshot | undefined> {
  111. if (snapshot.version !== PLUGIN_VERSION) {
  112. // TODO
  113. console.warn('state snapshot version mismatch');
  114. }
  115. this.clear();
  116. const entries = List<PluginStateSnapshotManager.Entry>().asMutable();
  117. for (const e of snapshot.entries) {
  118. this.entryMap.set(e.snapshot.id, e);
  119. entries.push(e);
  120. }
  121. const current = snapshot.current
  122. ? snapshot.current
  123. : snapshot.entries.length > 0
  124. ? snapshot.entries[0].snapshot.id
  125. : void 0;
  126. this.updateState({
  127. current,
  128. entries: entries.asImmutable(),
  129. isPlaying: false,
  130. nextSnapshotDelayInMs: snapshot.playback ? snapshot.playback.nextSnapshotDelayInMs : PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
  131. });
  132. this.events.changed.next();
  133. if (!current) return;
  134. const entry = this.getEntry(current);
  135. const next = entry && entry.snapshot;
  136. if (!next) return;
  137. await this.plugin.state.setSnapshot(next);
  138. if (snapshot.playback && snapshot.playback.isPlaying) this.play(true);
  139. return next;
  140. }
  141. private syncCurrent(options?: { name?: string, description?: string, params?: PluginState.GetSnapshotParams }) {
  142. const snapshot = this.plugin.state.getSnapshot(options?.params);
  143. if (this.state.entries.size === 0 || !this.state.current) {
  144. this.add(PluginStateSnapshotManager.Entry(snapshot, { name: options?.name, description: options?.description }));
  145. } else {
  146. this.replace(this.state.current, snapshot);
  147. }
  148. }
  149. getStateSnapshot(options?: { name?: string, description?: string, playOnLoad?: boolean, params?: PluginState.GetSnapshotParams }): PluginStateSnapshotManager.StateSnapshot {
  150. // TODO: diffing and all that fancy stuff
  151. // TODO: the options need to be handled better, particularky options.params
  152. this.syncCurrent(options);
  153. return {
  154. timestamp: +new Date(),
  155. version: PLUGIN_VERSION,
  156. name: options && options.name,
  157. description: options && options.description,
  158. current: this.state.current,
  159. playback: {
  160. isPlaying: !!(options && options.playOnLoad),
  161. nextSnapshotDelayInMs: this.state.nextSnapshotDelayInMs
  162. },
  163. entries: this.state.entries.valueSeq().toArray()
  164. };
  165. }
  166. async serialize(type: 'json' | 'zip' = 'json') {
  167. const json = JSON.stringify(this.getStateSnapshot(), null, 2);
  168. if (type === 'json') {
  169. return new Blob([json], {type : 'application/json;charset=utf-8'});
  170. } else {
  171. const state = new Uint8Array(utf8ByteCount(json));
  172. utf8Write(state, 0, json);
  173. const zipDataObj: { [k: string]: Uint8Array } = {
  174. 'state.json': state
  175. };
  176. const assets: [UUID, Asset][] = [];
  177. // TODO: there can be duplicate entries: check for this?
  178. for (const { asset, file } of this.plugin.managers.asset.assets) {
  179. assets.push([asset.id, asset]);
  180. zipDataObj[`assets/${asset.id}`] = new Uint8Array(await file.arrayBuffer());
  181. }
  182. if (assets.length > 0) {
  183. const index = JSON.stringify(assets, null, 2);
  184. const data = new Uint8Array(utf8ByteCount(index));
  185. utf8Write(data, 0, index);
  186. zipDataObj['assets.json'] = data;
  187. }
  188. const zipFile = zip(zipDataObj);
  189. return new Blob([zipFile], {type : 'application/zip'});
  190. }
  191. }
  192. async open(file: File) {
  193. try {
  194. const fn = file.name.toLowerCase();
  195. if (fn.endsWith('json') || fn.endsWith('molj')) {
  196. const data = await this.plugin.runTask(readFromFile(file, 'string'));
  197. const snapshot = JSON.parse(data);
  198. return this.setStateSnapshot(snapshot);
  199. } else {
  200. const data = await this.plugin.runTask(readFromFile(file, 'zip'));
  201. const assets = Object.create(null);
  202. objectForEach(data, (v, k) => {
  203. if (k === 'state.json' || k === 'assets.json') return;
  204. const name = k.substring(k.indexOf('/') + 1);
  205. assets[name] = new File([v], name);
  206. });
  207. const stateFile = new File([data['state.json']], 'state.json');
  208. const stateData = await this.plugin.runTask(readFromFile(stateFile, 'string'));
  209. if (data['assets.json']) {
  210. const file = new File([data['assets.json']], 'assets.json');
  211. const json = JSON.parse(await this.plugin.runTask(readFromFile(file, 'string')));
  212. for (const [id, asset] of json) {
  213. this.plugin.managers.asset.set(asset, assets[id]);
  214. }
  215. }
  216. const snapshot = JSON.parse(stateData);
  217. return this.setStateSnapshot(snapshot);
  218. }
  219. } catch (e) {
  220. this.plugin.log.error(`Reading state: ${e}`);
  221. }
  222. }
  223. private timeoutHandle: any = void 0;
  224. private next = async () => {
  225. this.timeoutHandle = void 0;
  226. const next = this.getNextId(this.state.current, 1);
  227. if (!next || next === this.state.current) {
  228. this.stop();
  229. return;
  230. }
  231. const snapshot = this.setCurrent(next)!;
  232. await this.plugin.state.setSnapshot(snapshot);
  233. const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
  234. if (this.state.isPlaying) this.timeoutHandle = setTimeout(this.next, delay);
  235. };
  236. play(delayFirst: boolean = false) {
  237. this.updateState({ isPlaying: true });
  238. if (delayFirst) {
  239. const e = this.getEntry(this.state.current);
  240. if (!e) {
  241. this.next();
  242. return;
  243. }
  244. this.events.changed.next();
  245. const snapshot = e.snapshot;
  246. const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
  247. this.timeoutHandle = setTimeout(this.next, delay);
  248. } else {
  249. this.next();
  250. }
  251. }
  252. stop() {
  253. this.updateState({ isPlaying: false });
  254. if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
  255. this.timeoutHandle = void 0;
  256. this.events.changed.next();
  257. }
  258. togglePlay() {
  259. if (this.state.isPlaying) {
  260. this.stop();
  261. this.plugin.managers.animation.stop();
  262. } else {
  263. this.play();
  264. }
  265. }
  266. constructor(private plugin: PluginContext) {
  267. super({
  268. current: void 0,
  269. entries: List(),
  270. isPlaying: false,
  271. nextSnapshotDelayInMs: PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
  272. });
  273. // TODO make nextSnapshotDelayInMs editable
  274. }
  275. }
  276. namespace PluginStateSnapshotManager {
  277. export interface Entry {
  278. timestamp: number,
  279. name?: string,
  280. description?: string,
  281. snapshot: PluginState.Snapshot
  282. }
  283. export function Entry(snapshot: PluginState.Snapshot, params: {name?: string, description?: string }): Entry {
  284. return { timestamp: +new Date(), snapshot, ...params };
  285. }
  286. export interface StateSnapshot {
  287. timestamp: number,
  288. version: string,
  289. name?: string,
  290. description?: string,
  291. current: UUID | undefined,
  292. playback: {
  293. isPlaying: boolean,
  294. nextSnapshotDelayInMs: number,
  295. },
  296. entries: Entry[]
  297. }
  298. }