celestial-chart 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1177 @@
1
+ import * as d3 from "./d3.js";
2
+ import { geoZoom } from "../lib/geo-zoom.js";
3
+ import { hasCallback } from "./add.js";
4
+ import { Canvas } from "./canvas.js";
5
+ import { arrayfy, bvcolor, formats, projections, settings } from "./config.js";
6
+ import { form } from "./form.js";
7
+ import { exportSVG } from "./svg.js";
8
+ import { getConstellationList, getData, getGridValues, getMwbackground, getPlanet, getPlanets } from "./get.js";
9
+ import { geo } from "./location.js";
10
+ import { Celestial } from "./core.js";
11
+ import { poles, projectionTween } from "./projection.js";
12
+ import { euler, getAngles, halfπ, transformDeg } from "./transform.js";
13
+ import { Round, has, interpolateAngle, isArray, isNumber, loadJson, px } from "./util.js";
14
+
15
+ var ANIMDISTANCE = 0.035, // Rotation animation threshold, ~2deg in radians
16
+ ANIMSCALE = 1.4, // Zoom animation threshold, scale factor
17
+ ANIMINTERVAL_R = 2000, // Rotation duration scale in ms
18
+ ANIMINTERVAL_P = 2500, // Projection duration in ms
19
+ ANIMINTERVAL_Z = 1500; // Projection duration in ms
20
+
21
+ // The most recently created map.
22
+ //
23
+ // Kept only for the few places that still need a "current" map. Drawing itself
24
+ // runs entirely on per-instance state, which is what allows several independent
25
+ // maps on one page (upstream #96, #131).
26
+ export var current = null;
27
+
28
+ // One sky-map instance.
29
+ //
30
+ // The body is unchanged from the original `Celestial.display`: that was already
31
+ // written as a constructor (it ends with `this.clip = ...`, `this.rotate = ...`),
32
+ // only it was called as `Celestial.display(...)` — so `this` was the global
33
+ // object itself. That is what limited a page to one map (upstream #96, #131).
34
+ // As a class the interface is the same, but every call gets its own `this`.
35
+ export class SkyMap {
36
+ // config: the usual settings object.
37
+ // options.standalone: when true the map is built ONLY from the defaults and
38
+ // the config passed in, not from the accumulated global settings — this is
39
+ // what several independent maps need. Without it the original behaviour
40
+ // stands.
41
+ constructor(config, options) {
42
+ var animationID,
43
+ container = null,
44
+ animations = [],
45
+ current = 0,
46
+ repeat = false,
47
+ zoomextent = 10, // Default maximum extent of zoom (max/min)
48
+ zoomlevel = 1; // Default zoom level, 1 = 100%
49
+
50
+ // Per-instance state. These used to be module-level variables, which is why
51
+ // every map on a page shared them.
52
+ var cfg, mapProjection, parentElement, zoom, map, circle, daylight,
53
+ starnames = {}, dsonames = {};
54
+
55
+ var base = (options && options.standalone) ? settings : undefined;
56
+
57
+ // The variables below are REASSIGNED during drawing (cfg = cfg.set(...),
58
+ // mapProjection = projectionTween(…), container = …append("container")).
59
+ // With a plain assignment the instance would keep the stale value — for
60
+ // example Celestial.mapProjection would be out of date after a projection
61
+ // change. A getter always exposes the current internal state.
62
+ var instance = this;
63
+
64
+ // Element lookup inside this map's own parent element. This used to live in
65
+ // util.js, reached through a shared "current map" pointer — so two maps would
66
+ // have found each other's elements.
67
+ function $(id) { return document.querySelector(parentElement + " #" + id); }
68
+ ["cfg", "mapProjection", "container", "map", "parentElement", "starnames", "dsonames"]
69
+ .forEach(function (name_) {
70
+ Object.defineProperty(instance, name_, {
71
+ get: function () {
72
+ switch (name_) {
73
+ case "cfg": return cfg;
74
+ case "mapProjection": return mapProjection;
75
+ case "container": return container;
76
+ case "map": return map;
77
+ case "parentElement": return parentElement;
78
+ case "starnames": return starnames;
79
+ default: return dsonames;
80
+ }
81
+ },
82
+ enumerable: true, configurable: true
83
+ });
84
+ });
85
+
86
+ current = this;
87
+
88
+
89
+ //Mash config with default settings, todo: if globalConfig exists, make another one
90
+ cfg = settings.set(config, base).applyDefaults(config, base);
91
+ if (isNumber(cfg.zoomextend)) zoomextent = cfg.zoomextend;
92
+ if (isNumber(cfg.zoomlevel)) zoomlevel = cfg.zoomlevel;
93
+ //if (cfg.disableAnimations) ANIMDISTANCE = Infinity;
94
+
95
+ var parent = document.getElementById(cfg.container);
96
+ if (parent) {
97
+ parentElement = "#" + cfg.container;
98
+ var st = window.getComputedStyle(parent, null);
99
+ if (!parseInt(st.width) && !cfg.width) parent.style.width = px(parent.parentNode.clientWidth);
100
+ } else {
101
+ parentElement = "body";
102
+ parent = null;
103
+ }
104
+
105
+ var margin = [16, 16],
106
+ width = getWidth(),
107
+ canvaswidth = isNumber(cfg.background.width) ? width + cfg.background.width : width,
108
+ pixelRatio = window.devicePixelRatio || 1,
109
+ projectionSetting = getProjection(cfg.projection, cfg.projectionRatio);
110
+
111
+ if (!projectionSetting) return;
112
+
113
+ if (cfg.lines.graticule.lat && cfg.lines.graticule.lat.pos[0] === "outline") projectionSetting.scale -= 2;
114
+
115
+ var ratio = projectionSetting.ratio,
116
+ height = Math.round(width / ratio),
117
+ canvasheight = Math.round(canvaswidth / ratio),
118
+ scale = projectionSetting.scale * width/1024,
119
+ starbase = cfg.stars.size,
120
+ dsobase = cfg.dsos.size || starbase,
121
+ starexp = cfg.stars.exponent,
122
+ dsoexp = cfg.dsos.exponent || starexp, //Object size base & exponent
123
+ adapt = 1,
124
+ rotation = getAngles(cfg.center),
125
+ path = cfg.datapath;
126
+
127
+ if (parentElement !== "body") parent.style.height = px(canvasheight);
128
+
129
+ // This map's own settings-form interface. One per instance, so that two
130
+ // interactive maps on a page do not write each other's fields.
131
+ //
132
+ // It has to exist this early: setting up the zoom (below) immediately triggers
133
+ // a redraw(), which calls formApi.setCenter(). The field lookups themselves
134
+ // are guarded against a missing form, but the object must be there.
135
+ var formApi = form(this);
136
+ this.form = formApi;
137
+
138
+ mapProjection = Celestial.projection(cfg.projection).rotate(rotation).translate([canvaswidth/2, canvasheight/2]).scale(scale * zoomlevel);
139
+
140
+ zoom = geoZoom().projection(mapProjection).center([canvaswidth/2, canvasheight/2]).scaleExtent([scale, scale * zoomextent]).on("zoom.redraw", redraw);
141
+ // Set initial zoom level
142
+ scale *= zoomlevel;
143
+
144
+ var canvas = d3.select(parentElement).selectAll("canvas"),
145
+ culture = (cfg.culture !== "" && cfg.culture !== "iau") ? cfg.culture : "";
146
+
147
+ if (canvas.empty()) canvas = d3.select(parentElement).append("canvas");
148
+ //canvas.attr("width", width).attr("height", height);
149
+ canvas.style("width", px(canvaswidth)).style("height", px(canvasheight)).attr("width", canvaswidth * pixelRatio).attr("height", canvasheight * pixelRatio);
150
+ var context = canvas.node().getContext("2d");
151
+ context.setTransform(pixelRatio,0,0,pixelRatio,0,0);
152
+
153
+ var graticule = d3.geoGraticule().stepMinor([15,10]);
154
+
155
+ map = d3.geoPath().projection(mapProjection).context(context);
156
+
157
+ //parent div with id #celestial-map or body
158
+ // Look for the container inside THIS map's parent. It used to take over the
159
+ // global Celestial.container, so a second map would have drawn into the
160
+ // rajzolt volna (#96, #131).
161
+ container = d3.select(parentElement).select("container");
162
+ if (container.empty()) container = d3.select(parentElement).append("container");
163
+ else container.selectAll(parentElement + " *").remove();
164
+
165
+ if (cfg.interactive) {
166
+ canvas.call(zoom);
167
+ d3.select(parentElement).on('dblclick', function () { zoomBy(1.5625); return false; });
168
+ } else {
169
+ canvas.attr("style", "cursor: default!important");
170
+ }
171
+
172
+ setClip(projectionSetting.clip);
173
+
174
+ // In v7 the listener's first argument is the event object; in v3 it was the
175
+ // datum (undefined here). Passed straight through, the "nothing changed, do
176
+ // nothing" guard in resize(set) would never fire, and every resize event
177
+ // would reset the user's zoom level.
178
+ d3.select(window).on('resize', function () { resize(); });
179
+
180
+ if (cfg.interactive === true && cfg.controls === true && $("celestial-zoomin") === null) {
181
+ d3.select(parentElement).append("input").attr("type", "button").attr("id", "celestial-zoomin").attr("value", "\u002b").on("click", function () { zoomBy(1.25); return false; });
182
+ d3.select(parentElement).append("input").attr("type", "button").attr("id", "celestial-zoomout").attr("value", "\u2212").on("click", function () { zoomBy(0.8); return false; });
183
+ }
184
+
185
+ circle = d3.geoCircle().radius(90);
186
+ daylight = d3.geoCircle().radius(179.9);
187
+
188
+
189
+ if ($("error") === null) d3.select("body").append("div").attr("id", "error");
190
+
191
+ if ($("loc") === null) geo(this);
192
+ else if (cfg.location === true && cfg.follow === "zenith") rotate({center: Celestial.zenith()});
193
+
194
+ if (cfg.location === true || cfg.formFields.location === true) {
195
+ d3.select(parentElement + " #location").style("display", "inline-block");
196
+ formApi.fldEnable("horizon-show", projectionSetting.clip);
197
+ formApi.fldEnable("daylight-show", !projectionSetting.clip);
198
+ }
199
+
200
+ function load() {
201
+ //Background
202
+ setClip(projectionSetting.clip);
203
+ container.append("path").datum(graticule.outline).attr("class", "outline");
204
+ container.append("path").datum(circle).attr("class", "horizon");
205
+ container.append("path").datum(daylight).attr("class", "daylight");
206
+ //Celestial planes
207
+ if (cfg.transform === "equatorial") graticule.stepMinor([15,10]);
208
+ else graticule.stepMinor([10,10]);
209
+ for (var key in cfg.lines) {
210
+ if (!has(cfg.lines, key)) continue;
211
+ if (key === "graticule") {
212
+ container.append("path").datum(graticule).attr("class", "graticule");
213
+ if (has(cfg.lines.graticule, "lon") && cfg.lines.graticule.lon.pos.length > 0)
214
+ container.selectAll(parentElement + " .gridvalues_lon")
215
+ .data(getGridValues("lon", cfg.lines.graticule.lon.pos, instance))
216
+ .enter().append("path")
217
+ .attr("class", "graticule_lon");
218
+ if (has(cfg.lines.graticule, "lat") && cfg.lines.graticule.lat.pos.length > 0)
219
+ container.selectAll(parentElement + " .gridvalues_lat")
220
+ .data(getGridValues("lat", cfg.lines.graticule.lat.pos, instance))
221
+ .enter().append("path")
222
+ .attr("class", "graticule_lat");
223
+ } else {
224
+ container.append("path")
225
+ .datum(d3.geoCircle().radius(90).center(transformDeg(poles[key], euler[cfg.transform])) )
226
+ .attr("class", key);
227
+ }
228
+ }
229
+
230
+ //Milky way outline
231
+ loadJson(path + "mw.json", function(error, json) {
232
+ if (error) {
233
+ window.alert("Data file could not be loaded or doesn't exist. See readme.md");
234
+ return console.warn(error);
235
+ }
236
+
237
+ var mw = getData(json, cfg.transform);
238
+ var mw_back = getMwbackground(mw);
239
+
240
+ container.selectAll(parentElement + " .mway")
241
+ .data(mw.features)
242
+ .enter().append("path")
243
+ .attr("class", "mw");
244
+ container.selectAll(parentElement + " .mwaybg")
245
+ .data(mw_back.features)
246
+ .enter().append("path")
247
+ .attr("class", "mwbg");
248
+ redraw();
249
+ });
250
+
251
+ //Constellation names or designation
252
+ loadJson(path + filename("constellations"), function(error, json) {
253
+ if (error) return console.warn(error);
254
+
255
+ var con = getData(json, cfg.transform);
256
+ container.selectAll(parentElement + " .constnames")
257
+ .data(con.features)
258
+ .enter().append("text")
259
+ .attr("class", "constname");
260
+
261
+ instance.constellations = getConstellationList(con);
262
+ redraw();
263
+ });
264
+
265
+ //Constellation boundaries
266
+ loadJson(path + filename("constellations", "borders"), function(error, json) {
267
+ if (error) return console.warn(error);
268
+
269
+ //var cb = getData(topojson.feature(json, json.objects.constellations_bounds), cfg.transform);
270
+ var cb = getData(json, cfg.transform);
271
+
272
+ container.selectAll(parentElement + " .bounds")
273
+ .data(cb.features)
274
+ .enter().append("path")
275
+ .attr("class", "boundaryline");
276
+ redraw();
277
+ });
278
+
279
+ //Constellation lines
280
+ loadJson(path + filename("constellations", "lines"), function(error, json) {
281
+ if (error) return console.warn(error);
282
+
283
+ var conl = getData(json, cfg.transform);
284
+
285
+ container.selectAll(parentElement + " .lines")
286
+ .data(conl.features)
287
+ .enter().append("path")
288
+ .attr("class", "constline");
289
+
290
+ formApi.listConstellations();
291
+ redraw();
292
+ });
293
+
294
+ //Stars
295
+ loadJson(path + cfg.stars.data, function(error, json) {
296
+ if (error) return console.warn(error);
297
+
298
+ var st = getData(json, cfg.transform);
299
+
300
+ container.selectAll(parentElement + " .stars")
301
+ .data(st.features)
302
+ .enter().append("path")
303
+ .attr("class", "star");
304
+ redraw();
305
+
306
+ });
307
+
308
+ //Star names
309
+ loadJson(path + filename("starnames"), function(error, json) {
310
+ if (error) return console.warn(error);
311
+ Object.assign(starnames, json);
312
+ redraw();
313
+ });
314
+
315
+ //Deep space objects
316
+ loadJson(path + cfg.dsos.data, function(error, json) {
317
+ if (error) return console.warn(error);
318
+
319
+ var ds = getData(json, cfg.transform);
320
+
321
+ container.selectAll(parentElement + " .dsos")
322
+ .data(ds.features)
323
+ .enter().append("path")
324
+ .attr("class", "dso" );
325
+ redraw();
326
+ });
327
+
328
+ //DSO names
329
+ loadJson(path + filename("dsonames"), function(error, json) {
330
+ if (error) return console.warn(error);
331
+ Object.assign(dsonames, json);
332
+ redraw();
333
+ });
334
+
335
+ //Planets, Sun & Moon
336
+ loadJson(path + filename("planets"), function(error, json) {
337
+ if (error) return console.warn(error);
338
+
339
+ var pl = getPlanets(json, instance);
340
+
341
+ container.selectAll(parentElement + " .planets")
342
+ .data(pl)
343
+ .enter().append("path")
344
+ .attr("class", "planet");
345
+ redraw();
346
+ });
347
+
348
+ if (Celestial.data.length > 0) {
349
+ Celestial.data.forEach( function(d) {
350
+ if (has(d, "file")) loadJson(d.file, d.callback);
351
+ else setTimeout(d.callback, 0);
352
+ }, this);
353
+ }
354
+
355
+ if (cfg.lang && cfg.lang != "") apply(Celestial.setLanguage(cfg.lang));
356
+ //redraw();
357
+ }
358
+
359
+ // Zoom by factor; >1 larger <1 smaller
360
+ function zoomBy(factor) {
361
+ if (!factor || factor === 1) return;
362
+ var sc0 = mapProjection.scale(),
363
+ sc1 = sc0 * factor,
364
+ ext = zoom.scaleExtent(),
365
+ interval = ANIMINTERVAL_Z * Math.sqrt(Math.abs(1-factor));
366
+
367
+ if (sc1 < ext[0]) sc1 = ext[0];
368
+ if (sc1 > ext[1]) sc1 = ext[1];
369
+ if (cfg.disableAnimations === true) {
370
+ mapProjection.scale(sc1);
371
+ zoom.scale(sc1);
372
+ redraw();
373
+ return 0;
374
+ }
375
+ var zTween = d3.interpolateNumber(sc0, sc1);
376
+ d3.select({}).transition().duration(interval).tween("scale", function () {
377
+ return function(t) {
378
+ var z = zTween(t);
379
+ mapProjection.scale(z);
380
+ redraw();
381
+ };
382
+ }).transition().duration(0).tween("scale", function () {
383
+ zoom.scale(sc1);
384
+ redraw();
385
+ });
386
+ return interval;
387
+ }
388
+
389
+ function apply(config) {
390
+ cfg = settings.set(config);
391
+ redraw();
392
+ }
393
+
394
+
395
+ function rotate(config) {
396
+ var cFrom = cfg.center,
397
+ rot = mapProjection.rotate(),
398
+ sc = mapProjection.scale(),
399
+ interval = ANIMINTERVAL_R,
400
+ keep = false,
401
+ cTween, zTween, oTween,
402
+ oof = cfg.orientationfixed;
403
+
404
+ if (Round(rot[1], 1) === -Round(config.center[1], 1)) keep = true; //keep lat fixed if equal
405
+ cfg = cfg.set(config);
406
+ var d = Round(d3.geoDistance(cFrom, cfg.center), 2);
407
+ var o = d3.geoDistance([cFrom[2],0], [cfg.center[2],0]);
408
+ if ((d < ANIMDISTANCE && o < ANIMDISTANCE) || cfg.disableAnimations === true) {
409
+ rotation = getAngles(cfg.center);
410
+ mapProjection.rotate(rotation);
411
+ redraw();
412
+ return 0;
413
+ }
414
+ // Zoom interpolator
415
+ if (sc > scale * ANIMSCALE) zTween = d3.interpolateNumber(sc, scale);
416
+ else zTween = function () { return sc; };
417
+ // Orientation interpolator
418
+ if (o === 0) oTween = function () { return rot[2]; };
419
+ else oTween = interpolateAngle(cFrom[2], cfg.center[2]);
420
+ // Round(d, 2) can be at most 3.14 (pi rounded), so the original condition
421
+ // "d > 3.14" was never satisfied — the guard failed to fire in precisely the
422
+ // one case it was written for. Nudge the starting point rather than the
423
+ // target: that keeps the endpoint exact and does not accumulate over
424
+ // repeated calls. (upstream #157)
425
+ if (d >= 3.14) cFrom = [cFrom[0] + 0.01, cFrom[1], cFrom[2]]; //180deg turn is ambiguous
426
+ cfg.orientationfixed = false;
427
+ // Rotation interpolator
428
+ if (d === 0) cTween = function () { return cfg.center; };
429
+ else cTween = d3.geoInterpolate(cFrom, cfg.center);
430
+ interval = (d !== 0) ? interval * d : interval * o; // duration scaled by ang. distance
431
+ d3.select({}).transition().duration(interval).tween("center", function () {
432
+ return function(t) {
433
+ var c = getAngles(cTween(t));
434
+ c[2] = oTween(t);
435
+ var z = t < 0.5 ? zTween(t) : zTween(1-t);
436
+ if (keep) c[1] = rot[1];
437
+ mapProjection.scale(z);
438
+ mapProjection.rotate(c);
439
+ redraw();
440
+ };
441
+ }).transition().duration(0).tween("center", function () {
442
+ cfg.orientationfixed = oof;
443
+ rotation = getAngles(cfg.center);
444
+ mapProjection.rotate(rotation);
445
+ redraw();
446
+ });
447
+ return interval;
448
+ }
449
+
450
+ function resize(set) {
451
+ width = getWidth();
452
+ if (cfg.width === width && !set) return;
453
+ height = width/ratio;
454
+ canvaswidth = isNumber(cfg.background.width) ? width + cfg.background.width : width;
455
+ canvasheight = Math.round(canvaswidth / ratio);
456
+
457
+ scale = projectionSetting.scale * width/1024;
458
+ //canvas.attr("width", width).attr("height", height);
459
+ canvas.style("width", px(canvaswidth)).style("height", px(canvasheight)).attr("width", canvaswidth * pixelRatio).attr("height", canvasheight * pixelRatio);
460
+ zoom.scaleExtent([scale, scale * zoomextent]).scale(scale * zoomlevel);
461
+ mapProjection.translate([canvaswidth/2, canvasheight/2]).scale(scale * zoomlevel);
462
+ if (parent) parent.style.height = px(height);
463
+ scale *= zoomlevel;
464
+ redraw();
465
+ }
466
+
467
+ function reproject(config) {
468
+ var prj = getProjection(config.projection, config.projectionRatio);
469
+ if (!prj) return;
470
+
471
+ var rot = mapProjection.rotate(), ctr = mapProjection.center(), sc = mapProjection.scale(), ext = zoom.scaleExtent(), clip = [],
472
+ prjFrom = Celestial.projection(cfg.projection).center(ctr).translate([canvaswidth/2, canvasheight/2]).scale([ext[0]]),
473
+ interval = ANIMINTERVAL_P,
474
+ delay = 0, clipTween = null,
475
+ rTween = d3.interpolateNumber(ratio, prj.ratio);
476
+
477
+ if (projectionSetting.clip != prj.clip || cfg.disableAnimations === true) {
478
+ interval = 0; // Different clip = no transition
479
+ }
480
+ /*if (projectionSetting.clip !== prj.clip) {
481
+ clipTween = d3.interpolateNumber(projectionSetting.clip ? 90 : 180, prj.clip ? 90 : 180); // Clipangle from - to
482
+ } else*/ setClip(prj.clip);
483
+
484
+ var prjTo = Celestial.projection(config.projection).center(ctr).translate([canvaswidth/2, canvaswidth/prj.ratio/2]).scale([prj.scale * width/1024]);
485
+ var bAdapt = cfg.adaptable;
486
+
487
+ if (sc > ext[0]) {
488
+ delay = zoomBy(0.1);
489
+ setTimeout(reproject, delay, config);
490
+ return delay + interval;
491
+ }
492
+
493
+ if (cfg.location || cfg.formFields.location) {
494
+ formApi.fldEnable("horizon-show", prj.clip);
495
+ formApi.fldEnable("daylight-show", !prj.clip);
496
+ }
497
+
498
+ mapProjection = projectionTween(prjFrom, prjTo);
499
+ cfg.adaptable = false;
500
+
501
+ d3.select({}).transition().duration(interval).tween("projection", function () {
502
+ return function(_) {
503
+ mapProjection.alpha(_).rotate(rot);
504
+ map.projection(mapProjection);
505
+ /*if (clipTween) mapProjection.clipAngle(clipTween(_));
506
+ else*/setClip(prj.clip);
507
+ ratio = rTween(_);
508
+ height = width/ratio;
509
+ //canvas.attr("width", width).attr("height", height);
510
+ canvas.style("width", px(canvaswidth)).style("height", px(canvasheight)).attr("width", canvaswidth * pixelRatio).attr("height", canvasheight * pixelRatio);
511
+ if (parent) parent.style.height = px(canvasheight);
512
+ redraw();
513
+ };
514
+ }).transition().duration(0).tween("projection", function () {
515
+ projectionSetting = prj;
516
+ ratio = projectionSetting.ratio;
517
+ height = width / projectionSetting.ratio;
518
+ canvasheight = isNumber(cfg.background.width) ? height + cfg.background.width : height;
519
+ scale = projectionSetting.scale * width/1024;
520
+ //canvas.attr("width", width).attr("height", height);
521
+ canvas.style("width", px(canvaswidth)).style("height", px(canvasheight)).attr("width", canvaswidth * pixelRatio).attr("height", canvasheight * pixelRatio);
522
+ if (parent) parent.style.height = px(canvasheight);
523
+ cfg.projection = config.projection;
524
+ mapProjection = Celestial.projection(config.projection).rotate(rot).translate([canvaswidth/2, canvasheight/2]).scale(scale * zoomlevel);
525
+ map.projection(mapProjection);
526
+ setClip(projectionSetting.clip);
527
+ zoom.projection(mapProjection).scaleExtent([scale, scale * zoomextent]).scale(scale * zoomlevel);
528
+ cfg.adaptable = bAdapt;
529
+ scale *= zoomlevel;
530
+ redraw();
531
+ });
532
+ return interval;
533
+ }
534
+
535
+
536
+ function redraw() {
537
+ var rot = mapProjection.rotate();
538
+
539
+ context.setTransform(pixelRatio,0,0,pixelRatio,0,0);
540
+ if (cfg.adaptable) adapt = Math.sqrt(mapProjection.scale()/scale);
541
+ if (!adapt) adapt = 1;
542
+ starbase = cfg.stars.size;
543
+ starexp = cfg.stars.exponent;
544
+ dsobase = cfg.dsos.size || starbase;
545
+ dsoexp = cfg.dsos.exponent;
546
+
547
+ if (cfg.orientationfixed && cfg.center.length > 2) {
548
+ rot[2] = cfg.center[2];
549
+ mapProjection.rotate(rot);
550
+ }
551
+ cfg.center = [-rot[0], -rot[1], rot[2]];
552
+
553
+ formApi.setCenter(cfg.center, cfg.transform);
554
+ clear();
555
+
556
+ drawOutline();
557
+
558
+ //Draw all types of objects on the canvas
559
+ if (cfg.mw.show) {
560
+ container.selectAll(parentElement + " .mw").each(function(d) {
561
+ setStyle(cfg.mw.style); map(d);
562
+ if (wrongWinding(false)) { setStyle(cfg.mw.style); map(reversed(d)); }
563
+ context.fill();
564
+ });
565
+ // paint mw-outside in background color
566
+ if (cfg.transform !== "supergalactic" && cfg.background.opacity > 0.95)
567
+ container.selectAll(parentElement + " .mwbg").each(function(d) {
568
+ setStyle(cfg.background); map(d);
569
+ if (wrongWinding(true)) { setStyle(cfg.background); map(reversed(d)); }
570
+ context.fill();
571
+ });
572
+ }
573
+
574
+ for (var key in cfg.lines) {
575
+ if (!has(cfg.lines, key)) continue;
576
+ if (cfg.lines[key].show !== true) continue;
577
+ setStyle(cfg.lines[key]);
578
+ container.selectAll(parentElement + " ." + key).attr("d", map);
579
+ context.stroke();
580
+ }
581
+
582
+ if (has(cfg.lines.graticule, "lon")) {
583
+ setTextStyle(cfg.lines.graticule.lon);
584
+ container.selectAll(parentElement + " .graticule_lon").each(function(d, i) {
585
+ if (clip(d.geometry.coordinates)) {
586
+ var pt = mapProjection(d.geometry.coordinates);
587
+ gridOrientation(pt, d.properties.orientation);
588
+ context.fillText(d.properties.value, pt[0], pt[1]);
589
+ }
590
+ });
591
+ }
592
+
593
+ if (has(cfg.lines.graticule, "lat")) {
594
+ setTextStyle(cfg.lines.graticule.lat);
595
+ container.selectAll(parentElement + " .graticule_lat").each(function(d, i) {
596
+ if (clip(d.geometry.coordinates)) {
597
+ var pt = mapProjection(d.geometry.coordinates);
598
+ gridOrientation(pt, d.properties.orientation);
599
+ context.fillText(d.properties.value, pt[0], pt[1]);
600
+ }
601
+ });
602
+ }
603
+
604
+ if (cfg.constellations.bounds) {
605
+ container.selectAll(parentElement + " .boundaryline").each(function(d) {
606
+ setStyle(cfg.constellations.boundStyle);
607
+ if (Celestial.constellation) {
608
+ var re = new RegExp("\\b" + Celestial.constellation + "\\b");
609
+ if (d.ids.search(re) !== -1) {
610
+ context.lineWidth *= 1.5;
611
+ context.globalAlpha = 1;
612
+ context.setLineDash([]);
613
+ }
614
+ }
615
+ map(d);
616
+ context.stroke();
617
+ });
618
+ context.setLineDash([]);
619
+ }
620
+
621
+ if (cfg.constellations.lines) {
622
+ container.selectAll(parentElement + " .constline").each(function(d) {
623
+ setStyleA(d.properties.rank, cfg.constellations.lineStyle);
624
+ map(d);
625
+ context.stroke();
626
+ });
627
+ }
628
+
629
+ drawOutline(true);
630
+
631
+ if (cfg.constellations.names) {
632
+ //setTextStyle(cfg.constellations.nameStyle);
633
+ container.selectAll(parentElement + " .constname").each( function(d) {
634
+ if (clip(d.geometry.coordinates)) {
635
+ setStyleA(d.properties.rank, cfg.constellations.nameStyle);
636
+ var pt = mapProjection(d.geometry.coordinates);
637
+ context.fillText(constName(d), pt[0], pt[1]);
638
+ }
639
+ });
640
+ }
641
+
642
+
643
+ if (cfg.stars.show) {
644
+ setStyle(cfg.stars.style);
645
+ container.selectAll(parentElement + " .star").each(function(d) {
646
+ if (clip(d.geometry.coordinates) && d.properties.mag <= cfg.stars.limit) {
647
+ var pt = mapProjection(d.geometry.coordinates),
648
+ r = starSize(d);
649
+ context.fillStyle = starColor(d);
650
+ context.beginPath();
651
+ context.arc(pt[0], pt[1], r, 0, 2 * Math.PI);
652
+ context.closePath();
653
+ context.fill();
654
+ if (cfg.stars.designation && d.properties.mag <= cfg.stars.designationLimit*adapt) {
655
+ setTextStyle(cfg.stars.designationStyle);
656
+ context.fillText(starDesignation(d.id), pt[0]+r, pt[1]);
657
+ }
658
+ if (cfg.stars.propername && d.properties.mag <= cfg.stars.propernameLimit*adapt) {
659
+ setTextStyle(cfg.stars.propernameStyle);
660
+ context.fillText(starPropername(d.id), pt[0]-r, pt[1]);
661
+ }
662
+ }
663
+ });
664
+ }
665
+
666
+ if (cfg.dsos.show) {
667
+ container.selectAll(parentElement + " .dso").each(function(d) {
668
+ if (clip(d.geometry.coordinates) && dsoDisplay(d.properties, cfg.dsos.limit)) {
669
+ var pt = mapProjection(d.geometry.coordinates),
670
+ type = d.properties.type;
671
+ if (cfg.dsos.colors === true) setStyle(cfg.dsos.symbols[type]);
672
+ else setStyle(cfg.dsos.style);
673
+ var r = dsoSymbol(d, pt);
674
+ if (has(cfg.dsos.symbols[type], "stroke")) context.stroke();
675
+ else context.fill();
676
+
677
+ if (cfg.dsos.names && dsoDisplay(d.properties, cfg.dsos.nameLimit*adapt)) {
678
+ setTextStyle(cfg.dsos.nameStyle);
679
+ if (cfg.dsos.colors === true) context.fillStyle = cfg.dsos.symbols[type].fill;
680
+ context.fillText(dsoName(d), pt[0]+r, pt[1]-r);
681
+ }
682
+ }
683
+ });
684
+ }
685
+
686
+ if ((cfg.location || cfg.formFields.location) && cfg.planets.show && Celestial.origin) {
687
+ var dt = Celestial.date(),
688
+ o = Celestial.origin(dt).spherical();
689
+ container.selectAll(parentElement + " .planet").each(function(d) {
690
+ var id = d.id(), r = 12 * adapt,
691
+ p = d(dt).equatorial(o),
692
+ pos = transformDeg(p.ephemeris.pos, euler[cfg.transform]); //transform;
693
+ if (clip(pos)) {
694
+ var pt = mapProjection(pos),
695
+ sym = cfg.planets.symbols[id];
696
+ if (cfg.planets.symbolType === "letter") {
697
+ setTextStyle(cfg.planets.symbolStyle);
698
+ context.fillStyle = sym.fill;
699
+ context.fillText(sym.letter, pt[0], pt[1]);
700
+ } else if (id === "lun") {
701
+ if (has(sym, "size") && isNumber(sym.size)) r = sym.size * adapt;
702
+ context.fillStyle = sym.fill;
703
+ Canvas.symbol().type("crescent").size(r*r).age(p.ephemeris.age).position(pt)(context);
704
+ } else if (cfg.planets.symbolType === "disk") {
705
+ r = has(sym, "size") && isNumber(sym.size) ? sym.size * adapt : planetSize(p.ephemeris);
706
+ context.fillStyle = sym.fill;
707
+ Canvas.symbol().type("circle").size(r*r).position(pt)(context);
708
+ context.fill();
709
+ } else if (cfg.planets.symbolType === "symbol") {
710
+ setTextStyle(cfg.planets.symbolStyle);
711
+ context.font = planetSymbol(cfg.planets.symbolStyle.font);
712
+ context.fillStyle = sym.fill;
713
+ context.fillText(sym[cfg.planets.symbolType], pt[0], pt[1]);
714
+ }
715
+ //name
716
+ if (cfg.planets.names) {
717
+ var name = p[cfg.planets.namesType];
718
+ setTextStyle(cfg.planets.nameStyle);
719
+ //context.direction = "ltr" || "rtl" ar il ir
720
+ context.fillStyle = sym.fill;
721
+ context.fillText(name, pt[0] - r/2, pt[1] + r/2);
722
+ }
723
+ }
724
+ });
725
+ }
726
+
727
+ if (Celestial.data.length > 0) {
728
+ Celestial.data.forEach( function(d) {
729
+ d.redraw();
730
+ });
731
+ }
732
+
733
+ if ((cfg.location || cfg.formFields.location) && cfg.daylight.show && projectionSetting.clip) {
734
+ var sol = getPlanet("sol", undefined, instance);
735
+ if (sol) {
736
+ var up = Celestial.zenith(),
737
+ solpos = sol.ephemeris.pos,
738
+ dist = d3.geoDistance(up, solpos),
739
+ pt = mapProjection(solpos);
740
+
741
+ daylight.center(solpos);
742
+ setSkyStyle(dist, pt);
743
+ container.selectAll(parentElement + " .daylight").datum(daylight).attr("d", map);
744
+ context.fill();
745
+ context.fillStyle = "#fff";
746
+ if (clip(solpos)) {
747
+ context.beginPath();
748
+ context.arc(pt[0], pt[1], 6, 0, 2 * Math.PI);
749
+ context.closePath();
750
+ context.fill();
751
+ }
752
+ }
753
+ }
754
+
755
+ if ((cfg.location || cfg.formFields.location) && cfg.horizon.show && !projectionSetting.clip) {
756
+ circle.center(Celestial.nadir());
757
+ setStyle(cfg.horizon);
758
+ container.selectAll(parentElement + " .horizon").datum(circle).attr("d", map);
759
+ context.fill();
760
+ if (cfg.horizon.stroke) context.stroke();
761
+ }
762
+
763
+ if (cfg.controls) {
764
+ zoomState(mapProjection.scale());
765
+ }
766
+
767
+ if (hasCallback) {
768
+ Celestial.runCallback();
769
+ }
770
+
771
+ //Celestial.updateForm();
772
+
773
+ }
774
+
775
+
776
+ function drawOutline(stroke) {
777
+ var rot = mapProjection.rotate(),
778
+ prj = getProjection(cfg.projection, config.projectionRatio);
779
+
780
+ mapProjection.rotate([0,0]);
781
+ setStyle(cfg.background);
782
+ container.selectAll(parentElement + " .outline").attr("d", map);
783
+ if (stroke === true) {
784
+ context.globalAlpha = 1;
785
+ context.stroke();
786
+ } else {
787
+ context.fill();
788
+ }
789
+ mapProjection.rotate(rot);
790
+ }
791
+
792
+ // Helper functions -------------------------------------------------
793
+
794
+ function clip(coords) {
795
+ return projectionSetting.clip && d3.geoDistance(cfg.center, coords) > halfπ ? 0 : 1;
796
+ }
797
+
798
+ // At some rotations d3-geo fills the COMPLEMENT of the Milky Way polygon: the
799
+ // map greys out and the Milky Way shows black. Typically when the band moves
800
+ // towards the rim of the disc. Deciding the interior of a spherical polygon is
801
+ // numerically fragile there — the ol1 outline is a thin shell: an outer ring of
802
+ // area 1.697*pi containing a hole of 1.623*pi, the difference between them
803
+ // being the 0.069*pi Milky Way band.
804
+ //
805
+ // There is however one point whose correct answer we know for certain: the
806
+ // galactic pole. The Milky Way outlines are bands around the galactic equator,
807
+ // so the pole lies OUTSIDE every one of them — and INSIDE their complement
808
+ // (mwbg). The two poles are antipodal, so with a clipped projection exactly
809
+ // one of them is visible.
810
+ //
811
+ // We ask the already-built path (isPointInPath), and when its verdict
812
+ // contradicts what we know, we redraw with the rings reversed.
813
+ function galacticPole() {
814
+ var p = transformDeg(poles.galactic, euler[cfg.transform]);
815
+ if (!clip(p)) p = [p[0] + 180, -p[1]]; // the other pole is the visible one
816
+ return mapProjection(p);
817
+ }
818
+
819
+ function wrongWinding(expected) {
820
+ var pt = galacticPole();
821
+ if (!pt || !isFinite(pt[0]) || !isFinite(pt[1])) return false;
822
+ // isPointInPath expects the point in device coordinates, while the context
823
+ // has setTransform(pixelRatio, ...) applied — hence the multiplication.
824
+ return context.isPointInPath(pt[0] * pixelRatio, pt[1] * pixelRatio) !== expected;
825
+ }
826
+
827
+ function reversed(d) {
828
+ if (!d.reversedCopy) {
829
+ d.reversedCopy = { type: d.type, geometry: { type: d.geometry.type,
830
+ coordinates: d.geometry.coordinates.map(function (pol) {
831
+ return pol.map(function (ring) { return ring.slice().reverse(); });
832
+ })}};
833
+ }
834
+ return d.reversedCopy;
835
+ }
836
+
837
+ function setStyle(s) {
838
+ context.fillStyle = s.fill || null;
839
+ context.strokeStyle = s.stroke || null;
840
+ context.lineWidth = s.width || null;
841
+ context.globalAlpha = s.opacity !== null ? s.opacity : 1;
842
+ context.font = s.font || null;
843
+ if (has(s, "dash")) context.setLineDash(s.dash); else context.setLineDash([]);
844
+ context.beginPath();
845
+ }
846
+
847
+ function setTextStyle(s) {
848
+ context.fillStyle = s.fill;
849
+ context.textAlign = s.align || "left";
850
+ context.textBaseline = s.baseline || "bottom";
851
+ context.globalAlpha = s.opacity !== null ? s.opacity : 1;
852
+ context.font = s.font;
853
+ }
854
+
855
+ function setStyleA(rank, s) {
856
+ rank = rank || 1;
857
+ context.fillStyle = isArray(s.fill) ? s.fill[rank-1] : null;
858
+ context.strokeStyle = isArray(s.stroke) ? s.stroke[rank-1] : null;
859
+ context.lineWidth = isArray(s.width) ? s.width[rank-1] : null;
860
+ context.globalAlpha = isArray(s.opacity) ? s.opacity[rank-1] : 1;
861
+ context.font = isArray(s.font) ? s.font[rank-1] : null;
862
+ if (has(s, "dash")) context.setLineDash(s.dash); else context.setLineDash([]);
863
+ context.textAlign = s.align || "left";
864
+ context.textBaseline = s.baseline || "bottom";
865
+ context.beginPath();
866
+ }
867
+
868
+ function setSkyStyle(dist, pt) {
869
+ var factor, color1, color2, color3,
870
+ upper = 1.36,
871
+ lower = 1.885;
872
+
873
+ if (dist > lower) {
874
+ context.fillStyle = "transparent";
875
+ context.globalAlpha = 0;
876
+ return;
877
+ }
878
+
879
+ if (dist <= upper) {
880
+ color1 = "#daf1fa";
881
+ color2 = "#93d7f0";
882
+ color3 = "#57c0e8";
883
+ factor = -(upper-dist) / 10;
884
+ } else {
885
+ factor = (dist - upper) / (lower - upper);
886
+ color1 = d3.interpolateLab("#daf1fa", "#e8c866")(factor);
887
+ color2 = d3.interpolateLab("#93c7d0", "#ff854a")(factor);
888
+ color3 = d3.interpolateLab("#57b0c8", "#6caae2")(factor);
889
+ }
890
+ var grad = context.createRadialGradient(pt[0],pt[1],0, pt[0],pt[1],300);
891
+ grad.addColorStop(0, color1);
892
+ grad.addColorStop(0.2+0.4*factor, color2);
893
+ grad.addColorStop(1, color3);
894
+ context.fillStyle = grad;
895
+ context.globalAlpha = 0.9 * (1 - skyTransparency(factor, 1.4));
896
+ }
897
+
898
+ function skyTransparency(t, a) {
899
+ return (Math.pow(Math.E, t*a) - 1) / (Math.pow(Math.E, a) - 1);
900
+ }
901
+
902
+ function zoomState(sc) {
903
+ var czi = $("celestial-zoomin"),
904
+ czo = $("celestial-zoomout"),
905
+ defscale = projectionSetting.scale * width/1024;
906
+ if (!czi || !czo) return;
907
+ czi.disabled = sc >= defscale * zoomextent * 0.99;
908
+ czo.disabled = sc <= defscale;
909
+ }
910
+
911
+ function setClip(setit) {
912
+ if (setit) { mapProjection.clipAngle(90); }
913
+ else { mapProjection.clipAngle(null); }
914
+ }
915
+
916
+ function filename(what, sub, ext) {
917
+ var cult = (has(formats[what], culture)) ? "." + culture : "";
918
+ ext = ext ? "." + ext : ".json";
919
+ sub = sub ? "." + sub : "";
920
+ return what + sub + cult + ext;
921
+ }
922
+
923
+ function dsoDisplay(prop, limit) {
924
+ return prop.mag === 999 && Math.sqrt(parseInt(prop.dim)) > limit ||
925
+ prop.mag !== 999 && prop.mag <= limit;
926
+ }
927
+
928
+ function dsoSymbol(d, pt) {
929
+ var prop = d.properties;
930
+ var size = dsoSize(prop) || 9,
931
+ type = dsoShape(prop.type);
932
+ Canvas.symbol().type(type).size(size).position(pt)(context);
933
+ return Math.sqrt(size)/2;
934
+ }
935
+
936
+ function dsoShape(type) {
937
+ if (!type || !has(cfg.dsos.symbols, type)) return "circle";
938
+ else return cfg.dsos.symbols[type].shape;
939
+ }
940
+
941
+ function dsoSize(prop) {
942
+ if (!prop.mag || prop.mag === 999) return Math.pow(parseInt(prop.dim) * dsobase * adapt / 7, 0.5);
943
+ return Math.pow(2 * dsobase * adapt - prop.mag, dsoexp);
944
+ }
945
+
946
+
947
+ function dsoName(d) {
948
+ //return d.properties[cfg.dsos.namesType];
949
+ var lang = cfg.dsos.namesType, id = d.id;
950
+ if (lang === "desig" || !has(dsonames, id)) return d.properties.desig;
951
+ return has(dsonames[id], lang) ? dsonames[id][lang] : d.properties.desig;
952
+ }
953
+
954
+ /* Star designation */
955
+ function starDesignation(id) {
956
+ if (!has(starnames, id)) return "";
957
+ return starnames[id][cfg.stars.designationType];
958
+ }
959
+
960
+ function starPropername(id) {
961
+ var lang = cfg.stars.propernameType;
962
+ if (!has(starnames, id)) return "";
963
+ return has(starnames[id], lang) ? starnames[id][lang] : starnames[id].name;
964
+ }
965
+
966
+ function starSize(d) {
967
+ var mag = d.properties.mag;
968
+ if (mag === null) return 0.1;
969
+ var r = starbase * adapt * Math.exp(starexp * (mag+2));
970
+ return Math.max(r, 0.1);
971
+ }
972
+
973
+
974
+ function starColor(d) {
975
+ var bv = d.properties.bv;
976
+ if (!cfg.stars.colors || isNaN(bv)) {return cfg.stars.style.fill; }
977
+ return bvcolor(bv);
978
+ }
979
+
980
+ function constName(d) {
981
+ return d.properties[cfg.constellations.namesType];
982
+ }
983
+
984
+ function planetSize(d) {
985
+ var mag = d.mag;
986
+ if (mag === null) return 2;
987
+ var r = 4 * adapt * Math.exp(-0.05 * (mag+2));
988
+ return Math.max(r, 2);
989
+ }
990
+
991
+ function planetSymbol(s) {
992
+ var size = s.replace(/(^\D*)(\d+)(\D.+$)/i,'$2');
993
+ size = Math.round(adapt * size);
994
+ return s.replace(/(^\D*)(\d+)(\D.+$)/i,'$1' + size + '$3');
995
+ }
996
+
997
+ function gridOrientation(pos, orient) {
998
+ var o = orient.split(""), h = "center", v = "middle";
999
+ for (var i = o.length-1; i >= 0; i--) {
1000
+ switch(o[i]) {
1001
+ case "N": v = "bottom"; break;
1002
+ case "S": v = "top"; break;
1003
+ case "E": h = "left"; pos[0] += 2; break;
1004
+ case "W": h = "right"; pos[0] -= 2; break;
1005
+ }
1006
+ }
1007
+ context.textAlign = h;
1008
+ context.textBaseline = v;
1009
+ return pos;
1010
+ }
1011
+
1012
+ function clear() {
1013
+ context.clearRect(0, 0, canvaswidth + margin[0], canvasheight + margin[1]);
1014
+ }
1015
+
1016
+ function getWidth() {
1017
+ var w = 0;
1018
+ if (isNumber(cfg.width) && cfg.width > 0) w = cfg.width;
1019
+ else if (parent) w = parent.getBoundingClientRect().width - margin[0] *2;
1020
+ // No container element: the map goes into the body, so the body's width is
1021
+ // what counts. This originally read window.getBoundingClientRect(), which is
1022
+ // not a method that exists — with neither a container nor an explicit width
1023
+ // the map did not render at all, display() threw.
1024
+ else w = document.body.getBoundingClientRect().width - margin[0]*2;
1025
+ //if (isNumber(cfg.background.width)) w -= cfg.background.width;
1026
+ return w;
1027
+ }
1028
+
1029
+ function getProjection(p, ratioOverride) {
1030
+ if (!has(projections, p)) return;
1031
+ var res = projections[p];
1032
+ if (!has(res, "ratio")) res.ratio = 2; // Default w/h ratio 2:1
1033
+ res.ratio = ratioOverride ? ratioOverride : res.ratio;
1034
+ return res;
1035
+ }
1036
+
1037
+
1038
+ function animate() {
1039
+ if (!animations || animations.length < 1) return;
1040
+
1041
+ var d, a = animations[current];
1042
+
1043
+ switch (a.param) {
1044
+ case "projection": d = reproject({projection:a.value}); break;
1045
+ case "center": d = rotate({center:a.value}); break;
1046
+ case "zoom": d = zoomBy(a.value);
1047
+ }
1048
+ if (a.callback) setTimeout(a.callback, d);
1049
+ current++;
1050
+ if (repeat === true && current === animations.length) current = 0;
1051
+ d = a.duration === 0 || a.duration < d ? d : a.duration;
1052
+ if (current < animations.length) animationID = setTimeout(animate, d);
1053
+ }
1054
+
1055
+ function stop() {
1056
+ clearTimeout(animationID);
1057
+ //current = 0;
1058
+ //repeat = false;
1059
+ }
1060
+
1061
+
1062
+ // Exported objects and functions for adding data
1063
+ //
1064
+ this.clip = clip;
1065
+ this.context = context;
1066
+ // Exports this map. It calls the module function directly rather than
1067
+ // Celestial.exportSVG: the backwards-compatible interface copies this very
1068
+ // method onto itself, so going through it would recurse.
1069
+ this.exportSVG = function (callback) { return exportSVG(instance, callback); };
1070
+ this.metrics = function() {
1071
+ return {"width": width, "height": height, "margin": margin, "scale": mapProjection.scale()};
1072
+ };
1073
+ this.setStyle = setStyle;
1074
+ this.setTextStyle = setTextStyle;
1075
+ this.setStyleA = setStyleA;
1076
+ this.setConstStyle = function(rank, font) {
1077
+ var f = arrayfy(font);
1078
+ context.font = f[rank];
1079
+ };
1080
+ this.symbol = Canvas.symbol;
1081
+ this.dsoSymbol = dsoSymbol;
1082
+ this.redraw = redraw;
1083
+ this.resize = function(config) {
1084
+ if (config !== undefined) {
1085
+ if (has(config, "width")) cfg.width = config.width;
1086
+ else if (isNumber(config)) cfg.width = config;
1087
+ }
1088
+ resize(true);
1089
+ return cfg.width;
1090
+ };
1091
+ this.reload = function(config) {
1092
+ var ctr;
1093
+ //if (!config || !has(config, "transform")) return;
1094
+ //cfg.transform = config.transform;
1095
+ if (config) Object.assign(cfg, settings.set(config));
1096
+ if (cfg.follow === "center" && has(cfg, "center")) {
1097
+ ctr = getAngles(cfg.center);
1098
+ } else if (cfg.follow === "zenith") {
1099
+ ctr = getAngles(Celestial.zenith());
1100
+ }
1101
+ if (ctr) mapProjection.rotate(ctr);
1102
+ container.selectAll(parentElement + " *").remove();
1103
+ load();
1104
+ };
1105
+ this.apply = function(config) { apply(config); };
1106
+ this.reproject = function(config) { return reproject(config); };
1107
+ this.rotate = function(config) { if (!config) return cfg.center; return rotate(config); };
1108
+ this.zoomBy = function(factor) { if (!factor) return mapProjection.scale()/scale; return zoomBy(factor); };
1109
+ this.color = function(type) {
1110
+ if (!type) return "#000";
1111
+ if (has(cfg.dsos.symbols, type)) return cfg.dsos.symbols[type].fill;
1112
+ return "#000";
1113
+ };
1114
+ this.starColor = starColor;
1115
+ this.animate = function(anims, dorepeat) {
1116
+ if (!anims) return;
1117
+ animations = anims;
1118
+ current = 0;
1119
+ repeat = dorepeat ? true : false;
1120
+ animate();
1121
+ };
1122
+ this.stop = function(wipe) {
1123
+ stop();
1124
+ if (wipe === true) animations = [];
1125
+ };
1126
+ this.go = function(index) {
1127
+ if (animations.length < 1) return;
1128
+ if (index && index < animations.length) current = index;
1129
+ animate();
1130
+ };
1131
+
1132
+ /* obsolete
1133
+ if (!has(this, "date"))
1134
+ this.date = function() { console.log("Celestial.date() needs config.location = true to work." ); };
1135
+ */
1136
+ load();
1137
+ }
1138
+ }
1139
+
1140
+ // Backwards-compatible interface.
1141
+ //
1142
+ // `Celestial.display(config)` behaves exactly as before: it creates an instance
1143
+ // and spreads that instance's interface onto itself. The difference is that the
1144
+ // instance is now also returned — `var sky = Celestial.display(cfg)` — which is
1145
+ // how several maps on one page become possible.
1146
+ //
1147
+ // Two names receive their value AFTER loading (the constellation list, and the
1148
+ // selected constellation), by which time the global interface has already been
1149
+ // copied. Those two get forwarding accessors, so that Celestial.constellations
1150
+ // keeps working the way it used to.
1151
+ ["constellations", "constellation"].forEach(function (name_) {
1152
+ Object.defineProperty(Celestial, name_, {
1153
+ get: function () { return current ? current[name_] : undefined; },
1154
+ set: function (value_) { if (current) current[name_] = value_; },
1155
+ enumerable: true, configurable: true
1156
+ });
1157
+ });
1158
+
1159
+ Celestial.display = function (config) {
1160
+ var instance = new SkyMap(config);
1161
+ // Copy property descriptors rather than values: several properties on the
1162
+ // instance are getters (cfg, mapProjection, container and so on), and copying
1163
+ // by value would leave Celestial.mapProjection stale after a projection change.
1164
+ Object.defineProperties(instance, {});
1165
+ var descriptors = Object.getOwnPropertyDescriptors(instance);
1166
+ for (var key_ in descriptors) {
1167
+ // do not overwrite the names that have forwarding accessors
1168
+ if (key_ === "constructor" || key_ === "constellations" || key_ === "constellation") continue;
1169
+ Object.defineProperty(Celestial, key_, descriptors[key_]);
1170
+ }
1171
+ return instance;
1172
+ };
1173
+
1174
+ // The CommonJS export has been taken over by the build: build/celestial.cjs and
1175
+ // build/celestial.mjs expose Celestial in whichever form the host needs.
1176
+
1177
+ export { Celestial };