snapshots.ts 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. /**
  2. * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
  3. *
  4. * @author David Sehnal <david.sehnal@gmail.com>
  5. */
  6. import { List } from 'immutable';
  7. import { UUID } from 'mol-util';
  8. import { PluginState } from '../state';
  9. import { PluginComponent } from 'mol-plugin/component';
  10. import { PluginContext } from 'mol-plugin/context';
  11. export { PluginStateSnapshotManager }
  12. class PluginStateSnapshotManager extends PluginComponent<{
  13. current?: UUID | undefined,
  14. entries: List<PluginStateSnapshotManager.Entry>,
  15. isPlaying: boolean,
  16. nextSnapshotDelayInMs: number
  17. }> {
  18. static DefaultNextSnapshotDelayInMs = 1500;
  19. private entryMap = new Map<string, PluginStateSnapshotManager.Entry>();
  20. readonly events = {
  21. changed: this.ev()
  22. };
  23. currentGetSnapshotParams: PluginState.GetSnapshotParams = PluginState.DefaultGetSnapshotParams as any;
  24. getIndex(e: PluginStateSnapshotManager.Entry) {
  25. return this.state.entries.indexOf(e);
  26. }
  27. getEntry(id: string | undefined) {
  28. if (!id) return;
  29. return this.entryMap.get(id);
  30. }
  31. remove(id: string) {
  32. const e = this.entryMap.get(id);
  33. if (!e) return;
  34. this.entryMap.delete(id);
  35. this.updateState({
  36. current: this.state.current === id ? void 0 : this.state.current,
  37. entries: this.state.entries.delete(this.getIndex(e))
  38. });
  39. this.events.changed.next();
  40. }
  41. add(e: PluginStateSnapshotManager.Entry) {
  42. this.entryMap.set(e.snapshot.id, e);
  43. this.updateState({ current: e.snapshot.id, entries: this.state.entries.push(e) });
  44. this.events.changed.next();
  45. }
  46. replace(id: string, snapshot: PluginState.Snapshot) {
  47. const old = this.getEntry(id);
  48. if (!old) return;
  49. const idx = this.getIndex(old);
  50. // The id changes here!
  51. const e = PluginStateSnapshotManager.Entry(snapshot, {
  52. name: old.name,
  53. description: old.description
  54. });
  55. this.entryMap.set(snapshot.id, e);
  56. this.updateState({ current: e.snapshot.id, entries: this.state.entries.set(idx, e) });
  57. this.events.changed.next();
  58. }
  59. move(id: string, dir: -1 | 1) {
  60. const len = this.state.entries.size;
  61. if (len < 2) return;
  62. const e = this.getEntry(id);
  63. if (!e) return;
  64. const from = this.getIndex(e);
  65. let to = (from + dir) % len;
  66. if (to < 0) to += len;
  67. const f = this.state.entries.get(to);
  68. const entries = this.state.entries.asMutable();
  69. entries.set(to, e);
  70. entries.set(from, f);
  71. this.updateState({ current: e.snapshot.id, entries: entries.asImmutable() });
  72. this.events.changed.next();
  73. }
  74. clear() {
  75. if (this.state.entries.size === 0) return;
  76. this.entryMap.clear();
  77. this.updateState({ current: void 0, entries: List<PluginStateSnapshotManager.Entry>() });
  78. this.events.changed.next();
  79. }
  80. setCurrent(id: string) {
  81. const e = this.getEntry(id);
  82. if (e) {
  83. this.updateState({ current: id as UUID });
  84. this.events.changed.next();
  85. }
  86. return e && e.snapshot;
  87. }
  88. getNextId(id: string | undefined, dir: -1 | 1) {
  89. const len = this.state.entries.size;
  90. if (!id) {
  91. if (len === 0) return void 0;
  92. const idx = dir === -1 ? len - 1 : 0;
  93. return this.state.entries.get(idx).snapshot.id;
  94. }
  95. const e = this.getEntry(id);
  96. if (!e) return;
  97. let idx = this.getIndex(e);
  98. if (idx < 0) return;
  99. idx = (idx + dir) % len;
  100. if (idx < 0) idx += len;
  101. return this.state.entries.get(idx).snapshot.id;
  102. }
  103. async setRemoteSnapshot(snapshot: PluginStateSnapshotManager.RemoteSnapshot): Promise<PluginState.Snapshot | undefined> {
  104. this.clear();
  105. const entries = List<PluginStateSnapshotManager.Entry>().asMutable()
  106. for (const e of snapshot.entries) {
  107. this.entryMap.set(e.snapshot.id, e);
  108. entries.push(e);
  109. }
  110. const current = snapshot.current
  111. ? snapshot.current
  112. : snapshot.entries.length > 0
  113. ? snapshot.entries[0].snapshot.id
  114. : void 0;
  115. this.updateState({
  116. current,
  117. entries: entries.asImmutable(),
  118. isPlaying: false,
  119. nextSnapshotDelayInMs: snapshot.playback ? snapshot.playback.nextSnapshotDelayInMs : PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
  120. });
  121. this.events.changed.next();
  122. if (!current) return;
  123. const entry = this.getEntry(current);
  124. const next = entry && entry.snapshot;
  125. if (!next) return;
  126. await this.plugin.state.setSnapshot(next);
  127. if (snapshot.playback && snapshot.playback.isPlaying) this.play();
  128. return next;
  129. }
  130. getRemoteSnapshot(options?: { name?: string, description?: string }): PluginStateSnapshotManager.RemoteSnapshot {
  131. // TODO: diffing and all that fancy stuff
  132. return {
  133. timestamp: +new Date(),
  134. name: options && options.name,
  135. description: options && options.description,
  136. current: this.state.current,
  137. playback: {
  138. isPlaying: this.state.isPlaying,
  139. nextSnapshotDelayInMs: this.state.nextSnapshotDelayInMs
  140. },
  141. entries: this.state.entries.valueSeq().toArray()
  142. };
  143. }
  144. private timeoutHandle: any = void 0;
  145. private next = async () => {
  146. this.timeoutHandle = void 0;
  147. const next = this.getNextId(this.state.current, 1);
  148. if (!next || next === this.state.current) {
  149. this.stop();
  150. return;
  151. }
  152. const snapshot = this.setCurrent(next)!;
  153. await this.plugin.state.setSnapshot(snapshot);
  154. const delay = typeof snapshot.durationInMs !== 'undefined' ? snapshot.durationInMs : this.state.nextSnapshotDelayInMs;
  155. if (this.state.isPlaying) this.timeoutHandle = setTimeout(this.next, delay);
  156. };
  157. play() {
  158. this.updateState({ isPlaying: true });
  159. this.next();
  160. }
  161. stop() {
  162. this.updateState({ isPlaying: false });
  163. if (typeof this.timeoutHandle !== 'undefined') clearTimeout(this.timeoutHandle);
  164. this.timeoutHandle = void 0;
  165. this.events.changed.next();
  166. }
  167. togglePlay() {
  168. if (this.state.isPlaying) {
  169. this.stop();
  170. this.plugin.state.animation.stop();
  171. }
  172. else this.play();
  173. }
  174. constructor(private plugin: PluginContext) {
  175. super({
  176. current: void 0,
  177. entries: List(),
  178. isPlaying: false,
  179. nextSnapshotDelayInMs: PluginStateSnapshotManager.DefaultNextSnapshotDelayInMs
  180. });
  181. // TODO make nextSnapshotDelayInMs editable
  182. }
  183. }
  184. namespace PluginStateSnapshotManager {
  185. export interface Entry {
  186. timestamp: number,
  187. name?: string,
  188. description?: string,
  189. snapshot: PluginState.Snapshot
  190. }
  191. export function Entry(snapshot: PluginState.Snapshot, params: {name?: string, description?: string }): Entry {
  192. return { timestamp: +new Date(), snapshot, ...params };
  193. }
  194. export interface RemoteSnapshot {
  195. timestamp: number,
  196. name?: string,
  197. description?: string,
  198. current: UUID | undefined,
  199. playback: {
  200. isPlaying: boolean,
  201. nextSnapshotDelayInMs: number,
  202. },
  203. entries: Entry[]
  204. }
  205. }