Blame view

web/static/js/swapp.ls 14.9 KB
f1f1e797   Goutte   Rewrite ugly java...
1
2
3
4
5
# Livescript transpiles to javascript, and is easier on the eyes and brain.
# http://livescript.net

const GOLDEN_RATIO = 2 / (1 + Math.sqrt(5))

ae0aa7d2   Goutte   Add an x axis lab...
6
7
8
9
###############################################################################

export class SpaceWeather

f75faf5f   Goutte   WIP
10
  (@configuration) ->
fe3132dd   Goutte   Refactor even more.
11
    console.info "Creating Space Weather app...", @configuration
f75faf5f   Goutte   WIP
12
    @sources = {}
fe3132dd   Goutte   Refactor even more.
13
14
15
16
    configs = [@configuration.sources[k] for k of @configuration.sources]
    configs.forEach((source_config) ~>
      @sources[source_config.slug] = new Source(source_config.slug, source_config.name, source_config)
    )
ae0aa7d2   Goutte   Add an x axis lab...
17

f75faf5f   Goutte   WIP
18
19
20
21
  buildDataUrlForSource: (source_slug) ->
    url = @configuration['api']['data_for_interval']
    url = url.replace('<source>', source_slug)
    url
ae0aa7d2   Goutte   Add an x axis lab...
22

f75faf5f   Goutte   WIP
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
  addSource: (source) ->
    @sources[source.slug] = source
    this

  showAllSources: ->
    for slug, source of @sources
      source.show()
    this

  showSource: (source) ->
    source.show()
    this

  hideSource: (source) ->
    source.hide()
    this

fe3132dd   Goutte   Refactor even more.
40
41
42
43
44
45
46
47
48
49
50
  resize: ->
    timeSeries.forEach((ts) -> ts.resize())
    orbits?.resize();

  init: ->
    active_sources = [ @sources[k] for k of @sources when @sources[k].config.active ]
    active_sources.forEach((source) ~>
      @loadData(source.slug, '2016-01-01T00:00:00', '2023-01-01T00:00:00').then(
        (data) ~>
          @createTimeSeries(source, data)
          # fixme: don't create a new Orbits instance every time
8bd715ad   Goutte   Use a pixel art i...
51
          @orbits = new Orbits(@configuration.sources, data['hci'], @configuration.orbits_container, @configuration)
fe3132dd   Goutte   Refactor even more.
52
53
54
55
56
        ,
        (data) ~> console.error('Failed to load SW data.', data)
      )
    )

f75faf5f   Goutte   WIP
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
  loadData: (source_slug, started_at, stopped_at) ->
    sw = this
    promise = new Promise((resolve, reject) ->
      url = sw.buildDataUrlForSource(source_slug)
      d3.csv(url+"?started_at=#{started_at}&stopped_at=#{stopped_at}", (csv) ->
        timeFormat = d3.timeParse('%Y-%m-%dT%H:%M:%S%Z')
        data = {'hci': []};
        configuration['parameters'].forEach((parameter) ->
            data[parameter['id']] = []
        )
        csv.forEach((d) ->
          dtime = timeFormat(d['time'])
          configuration['parameters'].forEach((parameter) ->
            id = parameter['id']
            data[id].push({x: dtime, y: parseFloat(d[id])})
          )
          if (d['xhci'] && d['yhci'])
            data['hci'].push({t: dtime, x: parseFloat(d['xhci']), y: parseFloat(d['yhci'])});
        )
        resolve(data)
      )
    )
    promise
ae0aa7d2   Goutte   Add an x axis lab...
80

fe3132dd   Goutte   Refactor even more.
81
  createTimeSeries: (source, data) ->
4816cef4   Goutte   Refactor some more.
82
83
84
85
86
    timeSeries = []
    @configuration['parameters'].forEach((parameter) ->
      container = @configuration['time_series_container']
      id = parameter['id'] ; title = parameter['title']
      if id not of data then console.error("No data for id '#{id}'.", data)
fe3132dd   Goutte   Refactor even more.
87
      timeSeries.push(new TimeSeries(id, title, source, data[id], container))
4816cef4   Goutte   Refactor some more.
88
    )
fe3132dd   Goutte   Refactor even more.
89
    timeSeries.forEach((ts) ~>
4816cef4   Goutte   Refactor some more.
90
91
92
93
      ts.options['onMouseOver'] = ->
        timeSeries.forEach((ts2) -> ts2.showCursor())
      ts.options['onMouseOut'] = ->
        timeSeries.forEach((ts2) -> ts2.hideCursor())
fe3132dd   Goutte   Refactor even more.
94
      ts.options['onMouseMove'] = (t) ~>
4816cef4   Goutte   Refactor some more.
95
        timeSeries.forEach((ts2) -> ts2.moveCursor(t))
fe3132dd   Goutte   Refactor even more.
96
        @orbits?.moveToDate(t)
4816cef4   Goutte   Refactor some more.
97
98
99
    )


ae0aa7d2   Goutte   Add an x axis lab...
100
101
102


class Source
fe3132dd   Goutte   Refactor even more.
103
  (@slug, @name, @config) ->
ae0aa7d2   Goutte   Add an x axis lab...
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
    @time_series = {}

  addTimeSeries: (ts) ->
    @time_series[ts.slug] = ts

  show: ->
    for slug, ts of @time_series
      $(ts.svg).show()
  hide: ->
    for slug, ts of @time_series
      $(ts.svg).hide()




###############################################################################
###############################################################################

f1f1e797   Goutte   Rewrite ugly java...
122
123
124
125
export class TimeSeries
  # Time in x-axis
  # Data in y-axis

fe3132dd   Goutte   Refactor even more.
126
  (@slug, @title, @source, @data, @container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
127
128
    # title : string
    # data : list of {x: <datetime>, y: <float>}
ae0aa7d2   Goutte   Add an x axis lab...
129
    console.info "Creating time series '#{@title}'..."
f1f1e797   Goutte   Rewrite ugly java...
130
131
132
    @init()

  init: ->
ae0aa7d2   Goutte   Add an x axis lab...
133
    console.info "Initializing time series '#{@title}'...", @data, @options
f1f1e797   Goutte   Rewrite ugly java...
134
135
136
137
138

    @margin = {
      top: 30,
      right: 20,
      bottom: 30,
fe3132dd   Goutte   Refactor even more.
139
      left: 80
f1f1e797   Goutte   Rewrite ugly java...
140
141
142
143
144
    }

    @xScale = d3.scaleTime().domain(d3.extent(@data, (d) -> d.x))
    @yScale = d3.scaleLinear().domain(d3.extent(@data, (d) -> d.y))

541e2936   Goutte   Synchronize the t...
145
    #console.info("Y domain #{@title}", d3.extent(@data, (d) -> d.y))
2463bd16   Goutte   Add a circle foll...
146

f1f1e797   Goutte   Rewrite ugly java...
147
148
149
150
151
152
153
154
155
156
157
    @xAxis = d3.axisBottom()
               .ticks(7, ",f")
               .tickFormat(d3.timeFormat("%Y-%m-%d"))
    @yAxis = d3.axisLeft()
               .ticks(10)

    @line = d3.line()
              .x((d) ~> @xScale(d.x))
              .y((d) ~> @yScale(d.y))

    @svg = d3.select(@container).append('svg')
6f9a3852   Goutte   Hide time series ...
158
    @svg.attr("class", @slug)
f1f1e797   Goutte   Rewrite ugly java...
159

2463bd16   Goutte   Add a circle foll...
160

f1f1e797   Goutte   Rewrite ugly java...
161
162
163
    @plotWrapper = @svg.append('g')
    @plotWrapper.attr('transform', 'translate(' + @margin.left + ',' + @margin.top + ')')

f1f1e797   Goutte   Rewrite ugly java...
164
165
166
167
    @path = @plotWrapper.append('path')
                        .datum(@data)
                        .classed('line', true)

2463bd16   Goutte   Add a circle foll...
168
169
170
171
    @mouseCanvas = @plotWrapper.append("rect")
                               .style("fill", "none")
                               .style("pointer-events", "all")
    @mouseCanvas
541e2936   Goutte   Synchronize the t...
172
173
174
        .on("mouseover", @onMouseOver)
        .on("mouseout",  @onMouseOut)
        .on("mousemove", @onMouseMove)
2463bd16   Goutte   Add a circle foll...
175

f1f1e797   Goutte   Rewrite ugly java...
176
177
178
179
180
181
182
    @plotWrapper.append('g').classed('x axis', true)
    @plotWrapper.append('g').classed('y axis', true)
    @yAxisText = @plotWrapper.append("text")
        .attr("transform", "rotate(-90)")
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .text(@title)
fe3132dd   Goutte   Refactor even more.
183
184
185
186
187
188
    @yAxisTextSource = @plotWrapper.append("text")
        .attr("transform", "rotate(-90)")
        .attr("dy", "1em")
        .style("text-anchor", "middle")
        .style("font-style", "oblique")
        .text(@source.name)
f1f1e797   Goutte   Rewrite ugly java...
189

81c9b2e8   Goutte   Add the values to...
190
191
192
193
194
195
    @focus = @plotWrapper.append('g').style("display", "none")

    @cursorCircle = @focus.append("circle")
                         .attr("class", "cursor-circle")
                         .attr("r", 3)

541e2936   Goutte   Synchronize the t...
196
    dx = 8
81c9b2e8   Goutte   Add the values to...
197
    @cursorValueShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
198
199
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
200
                              .attr("dy", "-.3em")
541e2936   Goutte   Synchronize the t...
201

81c9b2e8   Goutte   Add the values to...
202
203
    @cursorValue = @focus.append("text")
                        .attr("class", "cursor-text cursor-value")
541e2936   Goutte   Synchronize the t...
204
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
205
206
                        .attr("dy", "-.3em")

541e2936   Goutte   Synchronize the t...
207

81c9b2e8   Goutte   Add the values to...
208
    @cursorDateShadow = @focus.append("text")
541e2936   Goutte   Synchronize the t...
209
210
                              .attr("class", "cursor-text cursor-text-shadow")
                              .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
211
                              .attr("dy", "1em")
541e2936   Goutte   Synchronize the t...
212

81c9b2e8   Goutte   Add the values to...
213
214
    @cursorDate = @focus.append("text")
                        .attr("class", "cursor-text cursor-date")
541e2936   Goutte   Synchronize the t...
215
                        .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
216
217
                        .attr("dy", "1em")

f1f1e797   Goutte   Rewrite ugly java...
218
219
220
221
    @resize()

  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
541e2936   Goutte   Synchronize the t...
222
223
224
225
    height = GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * GOLDEN_RATIO * width

    @plotWidth = width
    @plotHeight = height
f1f1e797   Goutte   Rewrite ugly java...
226
227
228
229
230
231
232
233
234
235
236
237
238
239

    console.log("Resize time series #{@title} : #{width} x #{height}")

    @xScale.range([0, width]);
    @yScale.range([height, 0]);

    @svg.attr('width',  width + @margin.right + @margin.left)
        .attr('height', height + @margin.top + @margin.bottom)

    @path.attr('d', @line) # wip, do we need to put this in resize ?

    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

2463bd16   Goutte   Add a circle foll...
240
241
242
    #if width < 600 then @xAxis.ticks(3) else @xAxis.ticks(7, ",f")
    @xAxis.ticks(Math.floor(width / 60))
    @yAxis.ticks(Math.floor(height / 18))
f1f1e797   Goutte   Rewrite ugly java...
243
244
245
246
247
248
249
250

    @svg.select('.x.axis')
        .attr('transform', 'translate(0,' + height + ')')
        .call(@xAxis)

    @svg.select('.y.axis')
        .call(@yAxis)

fe3132dd   Goutte   Refactor even more.
251
    @yAxisText.attr("y", 20 - @margin.left)
f1f1e797   Goutte   Rewrite ugly java...
252
253
              .attr("x", 0 - (height / 2))

fe3132dd   Goutte   Refactor even more.
254
255
256
    @yAxisTextSource.attr("y", 0 - @margin.left)
                    .attr("x", 0 - (height / 2))

2463bd16   Goutte   Add a circle foll...
257
258
259
    @mouseCanvas.attr("width", width)
                .attr("height", height)

f1f1e797   Goutte   Rewrite ugly java...
260
261
    this

541e2936   Goutte   Synchronize the t...
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
  onMouseMove: ~>
    x = @xScale.invert(d3.mouse(@mouseCanvas.node())[0])
    if @options.onMouseMove?
      @options.onMouseMove(x)
    else
      @moveCursor(x)

  onMouseOver: ~>
    if @options.onMouseOver?
      @options.onMouseOver()
    else
      @showCursor()

  onMouseOut: ~>
    if @options.onMouseOut?
      @options.onMouseOut()
    else
      @hideCursor()

  showCursor: ->
    @focus.style("display", null)

  hideCursor: ->
    @focus.style("display", "none")

  bisectDate: d3.bisector((d) -> d.x).left # /!\ complex
81c9b2e8   Goutte   Add the values to...
288
  timeFormat: d3.timeFormat("%Y-%m-%d %Hh")
2463bd16   Goutte   Add a circle foll...
289

541e2936   Goutte   Synchronize the t...
290
  moveCursor: (x0) ->
2463bd16   Goutte   Add a circle foll...
291
292
293
    i = @bisectDate(@data, x0, 1)
    d0 = @data[i - 1]
    d1 = @data[i]
541e2936   Goutte   Synchronize the t...
294
    return unless d1 and d0
2463bd16   Goutte   Add a circle foll...
295
296
297
298
    d = if x0 - d0.x > d1.x - x0 then d1 else d0
    xx = @xScale(d.x)
    yy = @yScale(d.y)

81c9b2e8   Goutte   Add the values to...
299
300
    transform = "translate(#{xx}, #{yy})"

541e2936   Goutte   Synchronize the t...
301
302
303
304
305
306
    mirrored = if @plotWidth? and xx > @plotWidth / 2 then true else false
#    console.log("xx", xx)

    dx = 8
    dx = -1 * dx if mirrored

81c9b2e8   Goutte   Add the values to...
307
308
    @cursorCircle.attr("transform", transform)
    @cursorValue.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
309
310
                .attr('text-anchor', if mirrored then 'end' else 'start')
                .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
311
    @cursorValueShadow.attr("transform", transform).text(d.y)
541e2936   Goutte   Synchronize the t...
312
313
                      .attr('text-anchor', if mirrored then 'end' else 'start')
                      .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
314
    @cursorDate.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
315
316
               .attr('text-anchor', if mirrored then 'end' else 'start')
               .attr("dx", dx)
81c9b2e8   Goutte   Add the values to...
317
    @cursorDateShadow.attr("transform", transform).text(@timeFormat(d.x))
541e2936   Goutte   Synchronize the t...
318
319
                     .attr('text-anchor', if mirrored then 'end' else 'start')
                     .attr("dx", dx)
2463bd16   Goutte   Add a circle foll...
320
321

    this
f1f1e797   Goutte   Rewrite ugly java...
322

ae0aa7d2   Goutte   Add an x axis lab...
323
324
325
326

###############################################################################
###############################################################################

f1f1e797   Goutte   Rewrite ugly java...
327
export class Orbits
ae0aa7d2   Goutte   Add an x axis lab...
328
329
  # View of the solar system from above, with orbits segments for selected time
  # interval, from real data.
f1f1e797   Goutte   Rewrite ugly java...
330

438929a4   Goutte   Rewrite the orbit...
331
  (@orbiters, @data, @container, @options = {}) ->
f1f1e797   Goutte   Rewrite ugly java...
332
333
334
335
336
337
338
339
340
    console.log "Create orbits"
    @init()

  init: ->
    console.log "Initialize orbits", @data, @options

    @margin = {
      top: 30,
      right: 20,
11662eed   Goutte   Add Y axis label ...
341
      bottom: 42,
f1f1e797   Goutte   Rewrite ugly java...
342
343
344
345
346
347
348
349
350
351
352
353
354
      left: 60
    }
    
    @extremum = 1.11 * d3.max(@data, (d) ->
      Math.max(Math.abs(d.x), Math.abs(d.y))
    )

    @xScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])
    @yScale = d3.scaleLinear().domain([-1 * @extremum, @extremum])

    @xAxis = d3.axisBottom().ticks(10)
    @yAxis = d3.axisLeft().ticks(10)

f1f1e797   Goutte   Rewrite ugly java...
355
356
357
358
359
    @svg = d3.select(@container).append('svg')

    @plotWrapper = @svg.append('g')
    @plotWrapper.attr('transform', 'translate(' + @margin.left + ',' + @margin.top + ')')

ae0aa7d2   Goutte   Add an x axis lab...
360
361
362
363
364
365
366
367
368
    @xAxisLine = @plotWrapper.append('g').classed('x axis', true)
    @yAxisLine = @plotWrapper.append('g').classed('y axis', true)

    @xAxisTitle = @xAxisLine.append('text').attr('fill', '#000')
    @xAxisTitle.style("text-anchor", "middle")
    @xAxisTitle.append('tspan').text('X')
    # No : https://bugzilla.mozilla.org/show_bug.cgi?id=308338
    # @xAxisTitle.append('tspan').attr('baseline-shift', 'sub').text('HEE')
    # Also, don't use em as dy units
11662eed   Goutte   Add Y axis label ...
369
    @xAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
ae0aa7d2   Goutte   Add an x axis lab...
370
    @xAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
f1f1e797   Goutte   Rewrite ugly java...
371

11662eed   Goutte   Add Y axis label ...
372
373
374
375
376
377
378
    @yAxisTitle = @yAxisLine.append('text').attr('fill', '#000')
    @yAxisTitle.style("text-anchor", "middle")
    @yAxisTitle.append('tspan').text('Y')
    @yAxisTitle.append('tspan').attr('dy', '3px').text('HEE').attr('font-size', '8px')
    @yAxisTitle.append('tspan').attr('dy', '-3px').text('   (AU)')
    @yAxisTitle.attr('transform', 'rotate(-90)')

8bd715ad   Goutte   Use a pixel art i...
379
380
381
    @sun = @plotWrapper.append("svg:image")
                       .attr('xlink:href', @options.sun.img)
                       .attr('width', '32px').attr('height', '32px')
f1f1e797   Goutte   Rewrite ugly java...
382
    @sun.append('svg:title').text("Sol")
f1f1e797   Goutte   Rewrite ugly java...
383

438929a4   Goutte   Rewrite the orbit...
384
385
386
    for slug, config of @orbiters
      @initOrbiter(slug, config)

f1f1e797   Goutte   Rewrite ugly java...
387
388
    @resize()

438929a4   Goutte   Rewrite the orbit...
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
  orbitersElements: {}
  initOrbiter: (slug, config) ->
    if slug of @orbitersElements then throw new Error("Second init of #{slug}")

    # The order is important, as it will define the default z-order
    orbit_ellipse = @plotWrapper.append("svg:ellipse")
                                .classed('orbit orbit_ellipse', true)
    orbiter = @plotWrapper.append("svg:image")
                          .attr('xlink:href', config['img'])
                          .attr('width', '32px').attr('height', '32px')

    orbit_line = d3.line()
                   .x((d) ~> @xScale(d.x))
                   .y((d) ~> @yScale(d.y))

    orbit_section = @plotWrapper.append('path')
                                .datum(@data)  # fixme
                                .classed('orbit orbit_section', true)

    @orbitersElements[slug] =
      orbiter: orbiter
      orbit_ellipse: orbit_ellipse
      orbit_section: orbit_section
      orbit_line: orbit_line

    this

f1f1e797   Goutte   Rewrite ugly java...
416
417
418
419
420
421
422
423
424
425
426
427
  resize: ->
    width = jQuery(@container).width() - @margin.left - @margin.right
    height = 1.0 * width

    console.log("Resize orbits : #{width} x #{height}")

    @xScale.range([0, width]);
    @yScale.range([height, 0]);

    @svg.attr('width',  width + @margin.right + @margin.left)
        .attr('height', height + @margin.top + @margin.bottom)

8bd715ad   Goutte   Use a pixel art i...
428
    @sun.attr("x", width / 2 - 16).attr("y", height / 2 - 16)
f1f1e797   Goutte   Rewrite ugly java...
429

438929a4   Goutte   Rewrite the orbit...
430
431
432
    for slug, config of @orbiters
      @resizeOrbiter(slug, config)

f1f1e797   Goutte   Rewrite ugly java...
433
434
435
436
437
438
439
440
441
442
    @xAxis.scale(@xScale)
    @yAxis.scale(@yScale)

    @svg.select('.x.axis')
        .attr('transform', 'translate(0,' + height + ')')
        .call(@xAxis)

    @svg.select('.y.axis')
        .call(@yAxis)

ae0aa7d2   Goutte   Add an x axis lab...
443
    @xAxisTitle.attr("x", width / 2)
11662eed   Goutte   Add Y axis label ...
444
445
446
               .attr("y", 37)
    @yAxisTitle.attr("x", -1 * height / 2)
               .attr("y", -30)
ae0aa7d2   Goutte   Add an x axis lab...
447

f1f1e797   Goutte   Rewrite ugly java...
448
449
    this

438929a4   Goutte   Rewrite the orbit...
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
  resizeOrbiter: (slug, config) ->
    width = jQuery(@container).width() - @margin.left - @margin.right
    height = 1.0 * width

    console.log("Resize orbiter #{slug}")

    el = @orbitersElements[slug]
    el['orbit_section'].attr('d', el['orbit_line'])

    a = config['orbit']['a']
    b = config['orbit']['b']
    c = Math.sqrt(a*a - b*b)
    cx = (width / 2) - c
    cy = (height / 2)
    @yScale.range([0, height])
    el['orbit_ellipse'].attr('cx', cx).attr('cy', cy)
        .attr('rx', @xScale(a) - @xScale(0))
        .attr('ry', @yScale(b) - @yScale(0))
        .attr('transform', 'rotate(66,'+(cx+c)+', '+cy+')')
    @yScale.range([height, 0])

ae0aa7d2   Goutte   Add an x axis lab...
471
    data = @data  # todo: multiple orbiters
438929a4   Goutte   Rewrite the orbit...
472
473
474
475
476
477

    el['orbiter'].attr('x', @xScale(data[data.length - 1].x) - 16)
    el['orbiter'].attr('y', @yScale(data[data.length - 1].y) - 16)

    this

ae0aa7d2   Goutte   Add an x axis lab...
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
  repositionOrbiter: (slug, datum) ->
    data = @data  # todo
    datum ?= data[data.length - 1]
    el = @orbitersElements[slug]
    el['orbiter'].attr('x', @xScale(datum.x) - 16)
    el['orbiter'].attr('y', @yScale(datum.y) - 16)

    this

  bisectDate: d3.bisector((d) -> d.t).left

  moveToDate: (t) ->
    for slug, el of @orbitersElements
      data = @data  # todo: multiple orbiters
      i = @bisectDate(data, t, 1)
      d0 = data[i - 1]
      d1 = data[i]
      continue unless d1 and d0
      d = if t - d0.t > d1.t - t then d1 else d0
      @repositionOrbiter(slug, d)  # fixme