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.
- package/CHANGELOG.md +105 -0
- package/LICENSE +13 -0
- package/NOTICE.md +41 -0
- package/README.md +192 -0
- package/build/celestial.cjs +15354 -0
- package/build/celestial.js +15330 -0
- package/build/celestial.min.js +7 -0
- package/build/celestial.mjs +15333 -0
- package/celestial.css +111 -0
- package/images/dtpick.png +0 -0
- package/lib/geo-zoom.js +116 -0
- package/package.json +89 -0
- package/src/add.js +46 -0
- package/src/canvas.js +269 -0
- package/src/celestial.js +1177 -0
- package/src/config.js +537 -0
- package/src/core.js +14 -0
- package/src/d3.js +28 -0
- package/src/datetimepicker.js +233 -0
- package/src/form.js +837 -0
- package/src/get.js +210 -0
- package/src/global.js +4 -0
- package/src/horizontal.js +98 -0
- package/src/index.js +8 -0
- package/src/kepler.js +481 -0
- package/src/location.js +359 -0
- package/src/moon.js +522 -0
- package/src/projection.js +205 -0
- package/src/svg.js +879 -0
- package/src/timezones.js +71 -0
- package/src/transform.js +94 -0
- package/src/util.js +275 -0
- package/types/celestial.d.ts +545 -0
package/src/celestial.js
ADDED
|
@@ -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 };
|