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/location.js
ADDED
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
import * as d3 from "./d3.js";
|
|
2
|
+
|
|
3
|
+
import { formats, settings } from "./config.js";
|
|
4
|
+
import { datetimepicker } from "./datetimepicker.js";
|
|
5
|
+
import { testNumber } from "./form.js";
|
|
6
|
+
import { horizontal } from "./horizontal.js";
|
|
7
|
+
import { Celestial } from "./core.js";
|
|
8
|
+
import { Round, has, hasParent, isArray, isNumber, isValidDate, loadJson, pad, styles_ } from "./util.js";
|
|
9
|
+
|
|
10
|
+
var geoInfo = null;
|
|
11
|
+
|
|
12
|
+
// Observer location and time controls for one map. It receives the instance
|
|
13
|
+
// because its input fields live on that map's settings form.
|
|
14
|
+
function geo(sky) {
|
|
15
|
+
var cfg = sky.cfg;
|
|
16
|
+
var $form = sky.form.$form,
|
|
17
|
+
enable = sky.form.enable,
|
|
18
|
+
showAdvanced = sky.form.showAdvanced;
|
|
19
|
+
var dtFormat = d3.timeFormat("%Y-%m-%d %H:%M:%S"),
|
|
20
|
+
// in v3 the formatter could also parse; in v7 that is a separate function
|
|
21
|
+
dtParse = d3.timeParse("%Y-%m-%d %H:%M:%S"),
|
|
22
|
+
zenith = [0,0],
|
|
23
|
+
geopos = [0,0],
|
|
24
|
+
date = new Date(),
|
|
25
|
+
localZone = -date.getTimezoneOffset(),
|
|
26
|
+
timeZone = localZone,
|
|
27
|
+
config = settings.set(cfg),
|
|
28
|
+
frm = d3.select(sky.parentElement + " ~ #celestial-form form").insert("div", "div#general").attr("id", "loc");
|
|
29
|
+
|
|
30
|
+
var dtpick = new datetimepicker(sky, function(date, tz) {
|
|
31
|
+
$form("datetime").value = dateFormat(date, tz);
|
|
32
|
+
timeZone = tz;
|
|
33
|
+
go();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (has(config, "geopos") && config.geopos !== null && config.geopos.length === 2) geopos = config.geopos;
|
|
37
|
+
var col = frm.append("div").attr("class", "col").attr("id", "location").style("display", "none");
|
|
38
|
+
//Latitude & longitude fields
|
|
39
|
+
col.append("label").attr("title", "Location coordinates long/lat").attr("for", "lat").html("Location");
|
|
40
|
+
col.append("input").attr("type", "number").attr("id", "lat").attr("title", "Latitude").attr("placeholder", "Latitude").attr("max", "90").attr("min", "-90").attr("step", "0.0001").attr("value", geopos[0]).on("change", function () {
|
|
41
|
+
if (testNumber(this) === true) go();
|
|
42
|
+
});
|
|
43
|
+
col.append("span").html("\u00b0");
|
|
44
|
+
col.append("input").attr("type", "number").attr("id", "lon").attr("title", "Longitude").attr("placeholder", "Longitude").attr("max", "180").attr("min", "-180").attr("step", "0.0001").attr("value", geopos[1]).on("change", function () {
|
|
45
|
+
if (testNumber(this) === true) go();
|
|
46
|
+
});
|
|
47
|
+
col.append("span").html("\u00b0");
|
|
48
|
+
//Here-button if supported
|
|
49
|
+
if ("geolocation" in navigator) {
|
|
50
|
+
col.append("input").attr("type", "button").attr("value", "Here").attr("id", "here").on("click", here);
|
|
51
|
+
}
|
|
52
|
+
//Datetime field with dtpicker-button
|
|
53
|
+
col.append("label").attr("title", "Local date/time").attr("for", "datetime").html(" Date/time");
|
|
54
|
+
col.append("input").attr("type", "button").attr("id", "day-left").attr("title", "One day back").on("click", function () {
|
|
55
|
+
date.setDate(date.getDate() - 1);
|
|
56
|
+
$form("datetime").value = dateFormat(date, timeZone);
|
|
57
|
+
go();
|
|
58
|
+
});
|
|
59
|
+
col.append("input").attr("type", "text").attr("id", "datetime").attr("title", "Date and time").attr("value", dateFormat(date, timeZone))
|
|
60
|
+
.on("click", showpick, true).on("input", function () {
|
|
61
|
+
this.value = dateFormat(date, timeZone);
|
|
62
|
+
if (!dtpick.isVisible()) showpick();
|
|
63
|
+
});
|
|
64
|
+
col.append("div").attr("id", "datepick").on("click", showpick);
|
|
65
|
+
col.append("input").attr("type", "button").attr("id", "day-right").attr("title", "One day forward").on("click", function () {
|
|
66
|
+
date.setDate(date.getDate() + 1);
|
|
67
|
+
$form("datetime").value = dateFormat(date, timeZone);
|
|
68
|
+
go();
|
|
69
|
+
});
|
|
70
|
+
//Now -button sets current time & date of device
|
|
71
|
+
col.append("input").attr("type", "button").attr("value", "Now").attr("id", "now").on("click", now);
|
|
72
|
+
//Horizon marker
|
|
73
|
+
col.append("br");
|
|
74
|
+
col.append("label").attr("title", "Show horizon marker").attr("for", "horizon-show").html(" Horizon marker");
|
|
75
|
+
col.append("input").attr("type", "checkbox").attr("id", "horizon-show").property("checked", config.horizon.show).on("change", apply);
|
|
76
|
+
//Daylight
|
|
77
|
+
col.append("label").attr("title", "Show daylight").attr("for", "daylight-show").html("Daylight sky");
|
|
78
|
+
col.append("input").attr("type", "checkbox").attr("id", "daylight-show").property("checked", config.daylight.show).on("change", apply);col.append("br");
|
|
79
|
+
|
|
80
|
+
//Show planets
|
|
81
|
+
col.append("label").attr("title", "Show solar system objects").attr("for", "planets-show").html(" Planets, Sun & Moon");
|
|
82
|
+
col.append("input").attr("type", "checkbox").attr("id", "planets-show").property("checked", config.planets.show).on("change", apply);
|
|
83
|
+
//Planet names
|
|
84
|
+
var names = formats.planets[config.culture] || formats.planets.iau;
|
|
85
|
+
|
|
86
|
+
for (var fld in names) {
|
|
87
|
+
if (!has(names, fld)) continue;
|
|
88
|
+
var keys = Object.keys(names[fld]);
|
|
89
|
+
if (keys.length > 1) {
|
|
90
|
+
//Select List
|
|
91
|
+
var txt = (fld === "symbol") ? "as" : "with";
|
|
92
|
+
col.append("label").attr("for", "planets-" + fld + "Type").html(txt);
|
|
93
|
+
|
|
94
|
+
var selected = 0;
|
|
95
|
+
col.append("label").attr("title", "Type of planet name").attr("for", "planets-" + fld + "Type").attr("class", "advanced").html("");
|
|
96
|
+
var sel = col.append("select").attr("id", "planets-" + fld + "Type").on("change", apply);
|
|
97
|
+
var list = keys.map(function (key, i) {
|
|
98
|
+
if (key === config.planets[fld + "Type"]) selected = i;
|
|
99
|
+
return {o:key, n:names[fld][key]};
|
|
100
|
+
});
|
|
101
|
+
sel.selectAll("option").data(list).enter().append('option')
|
|
102
|
+
.attr("value", function (d) { return d.o; })
|
|
103
|
+
.text(function (d) { return d.n; });
|
|
104
|
+
sel.property("selectedIndex", selected);
|
|
105
|
+
|
|
106
|
+
if (fld === "names") {
|
|
107
|
+
sel.attr("class", "advanced");
|
|
108
|
+
col.append("label").attr("for", "planets-" + fld).html("names");
|
|
109
|
+
col.append("input").attr("type", "checkbox").attr("id", "planets-" + fld).property("checked", config.planets[fld]).on("change", apply);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
enable($form("planets-show"));
|
|
115
|
+
showAdvanced(config.advanced);
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
d3.select(document).on("mousedown", function (event_) {
|
|
119
|
+
if (!hasParent(event_.target, "celestial-date") && dtpick.isVisible()) dtpick.hide();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function now() {
|
|
123
|
+
date.setTime(Date.now());
|
|
124
|
+
$form("datetime").value = dateFormat(date, timeZone);
|
|
125
|
+
go();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function here() {
|
|
129
|
+
navigator.geolocation.getCurrentPosition( function(pos) {
|
|
130
|
+
geopos = [Round(pos.coords.latitude, 4), Round(pos.coords.longitude, 4)];
|
|
131
|
+
$form("lat").value = geopos[0];
|
|
132
|
+
$form("lon").value = geopos[1];
|
|
133
|
+
go();
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function showpick() {
|
|
138
|
+
dtpick.show(date, timeZone);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function dateFormat(dt, tz) {
|
|
142
|
+
var tzs;
|
|
143
|
+
if (!tz || tz === "0") tzs = " ±0000";
|
|
144
|
+
else {
|
|
145
|
+
var h = Math.floor(Math.abs(tz) / 60),
|
|
146
|
+
m = Math.abs(tz) - (h * 60),
|
|
147
|
+
s = tz > 0 ? " +" : " −";
|
|
148
|
+
tzs = s + pad(h) + pad(m);
|
|
149
|
+
}
|
|
150
|
+
return dtFormat(dt) + tzs;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
function isValidLocation(loc) {
|
|
155
|
+
//[lat, lon] expected
|
|
156
|
+
if (!loc || !isArray(loc) || loc.length < 2) return false;
|
|
157
|
+
if (!isNumber(loc[0]) || loc[0] < -90 || loc[0] > 90) return false;
|
|
158
|
+
if (!isNumber(loc[1]) || loc[1] < -180 || loc[1] > 180) return false;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function isValidTimezone(tz) {
|
|
163
|
+
if (tz === undefined || tz === null) return false;
|
|
164
|
+
if (!isNumber(tz) && Math.abs(tz) > 840) return false;
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function apply() {
|
|
169
|
+
Object.assign(config, settings.set());
|
|
170
|
+
config.horizon.show = !!$form("horizon-show").checked;
|
|
171
|
+
config.daylight.show = !!$form("daylight-show").checked;
|
|
172
|
+
config.planets.show = !!$form("planets-show").checked;
|
|
173
|
+
config.planets.names = !!$form("planets-names").checked;
|
|
174
|
+
config.planets.namesType = $form("planets-namesType").value;
|
|
175
|
+
config.planets.symbolType = $form("planets-symbolType").value;
|
|
176
|
+
enable($form("planets-show"));
|
|
177
|
+
|
|
178
|
+
sky.apply(config);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function go() {
|
|
182
|
+
var lon = parseFloat($form("lon").value),
|
|
183
|
+
lat = parseFloat($form("lat").value),
|
|
184
|
+
tz;
|
|
185
|
+
//Get current configuration
|
|
186
|
+
Object.assign(config, settings.set());
|
|
187
|
+
|
|
188
|
+
date = dtParse($form("datetime").value.slice(0,-6));
|
|
189
|
+
|
|
190
|
+
//sky.apply(config);
|
|
191
|
+
|
|
192
|
+
if (!isNaN(lon) && !isNaN(lat)) {
|
|
193
|
+
if (lat !== geopos[0] || lon !== geopos[1]) {
|
|
194
|
+
geopos = [lat, lon];
|
|
195
|
+
setPosition([lat, lon], true);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
//if (!tz) tz = date.getTimezoneOffset();
|
|
199
|
+
$form("datetime").value = dateFormat(date, timeZone);
|
|
200
|
+
|
|
201
|
+
var dtc = new Date(date.valueOf() - (timeZone - localZone) * 60000);
|
|
202
|
+
|
|
203
|
+
zenith = Celestial.getPoint(horizontal.inverse(dtc, [90, 0], geopos), config.transform);
|
|
204
|
+
zenith[2] = 0;
|
|
205
|
+
if (config.follow === "zenith") {
|
|
206
|
+
sky.rotate({center:zenith});
|
|
207
|
+
} else {
|
|
208
|
+
sky.redraw();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
// Longitude-based estimate: 15 degrees per hour. This is what the code has
|
|
215
|
+
// always fallen back to when the lookup failed ("location at sea"); now it is
|
|
216
|
+
// also what happens when no lookup is configured at all.
|
|
217
|
+
function estimateZone(p, timestamp) {
|
|
218
|
+
timeZone = Math.round(p[1] / 15) * 60;
|
|
219
|
+
geoInfo = { gmtOffset: timeZone * 60, message: "Estimated from longitude", timestamp: timestamp };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function applyZone() {
|
|
223
|
+
$form("datetime").value = dateFormat(date, timeZone);
|
|
224
|
+
go();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Resolving the UTC offset of a position needs a time zone database, which is
|
|
228
|
+
// not something a browser exposes for an arbitrary point on Earth — only for
|
|
229
|
+
// the viewer's own zone. So it takes an outside source.
|
|
230
|
+
//
|
|
231
|
+
// The library ships neither an API key nor an endpoint. Upstream baked in the
|
|
232
|
+
// author's TimeZoneDB account id, which meant every page using the library
|
|
233
|
+
// sent its visitors' coordinates to a third party, by default, on a shared
|
|
234
|
+
// quota. Configure one of these instead:
|
|
235
|
+
//
|
|
236
|
+
// timezoneResolver: (lat, lon, whenSeconds) => Promise<offsetInMinutes>
|
|
237
|
+
// timezoneid: "<your own TimeZoneDB key>" (the upstream route, unchanged)
|
|
238
|
+
//
|
|
239
|
+
// With neither, no request is made and the offset is estimated from longitude.
|
|
240
|
+
function setPosition(p, settime) {
|
|
241
|
+
if (!p || !has(config, "settimezone") || config.settimezone === false) return;
|
|
242
|
+
var timestamp = Math.floor(date.getTime() / 1000);
|
|
243
|
+
|
|
244
|
+
if (typeof config.timezoneResolver === "function") {
|
|
245
|
+
Promise.resolve(config.timezoneResolver(p[0], p[1], timestamp)).then(function (offset) {
|
|
246
|
+
if (Number.isFinite(offset)) {
|
|
247
|
+
timeZone = offset;
|
|
248
|
+
geoInfo = { gmtOffset: offset * 60, message: "From timezoneResolver", timestamp: timestamp };
|
|
249
|
+
} else {
|
|
250
|
+
estimateZone(p, timestamp);
|
|
251
|
+
}
|
|
252
|
+
applyZone();
|
|
253
|
+
}, function (err) {
|
|
254
|
+
console.warn("timezoneResolver failed, estimating from longitude:", err);
|
|
255
|
+
estimateZone(p, timestamp);
|
|
256
|
+
applyZone();
|
|
257
|
+
});
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!config.timezoneid) {
|
|
262
|
+
estimateZone(p, timestamp);
|
|
263
|
+
applyZone();
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
var protocol = window && window.location.protocol === "https:" ? "https" : "http",
|
|
268
|
+
url = protocol + "://api.timezonedb.com/v2.1/get-time-zone?key=" + config.timezoneid + "&format=json&by=position" +
|
|
269
|
+
"&lat=" + p[0] + "&lng=" + p[1] + "&time=" + timestamp;
|
|
270
|
+
|
|
271
|
+
loadJson(url, function(error, json) {
|
|
272
|
+
if (error) return console.warn(error);
|
|
273
|
+
if (json.status === "FAILED") {
|
|
274
|
+
estimateZone(p, timestamp);
|
|
275
|
+
} else {
|
|
276
|
+
timeZone = json.gmtOffset / 60;
|
|
277
|
+
geoInfo = json;
|
|
278
|
+
}
|
|
279
|
+
applyZone();
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
sky.dateFormat = dateFormat;
|
|
284
|
+
|
|
285
|
+
sky.date = function (dt, tz) {
|
|
286
|
+
if (!dt) return date;
|
|
287
|
+
if (isValidTimezone(tz)) timeZone = tz;
|
|
288
|
+
Object.assign(config, settings.set());
|
|
289
|
+
if (dtpick.isVisible()) dtpick.hide();
|
|
290
|
+
date.setTime(dt.valueOf());
|
|
291
|
+
$form("datetime").value = dateFormat(dt, timeZone);
|
|
292
|
+
go();
|
|
293
|
+
};
|
|
294
|
+
sky.timezone = function (tz) {
|
|
295
|
+
if (!tz) return timeZone;
|
|
296
|
+
if (isValidTimezone(tz)) timeZone = tz;
|
|
297
|
+
Object.assign(config, settings.set());
|
|
298
|
+
if (dtpick.isVisible()) dtpick.hide();
|
|
299
|
+
$form("datetime").value = dateFormat(date, timeZone);
|
|
300
|
+
go();
|
|
301
|
+
};
|
|
302
|
+
sky.position = function () { return geopos; };
|
|
303
|
+
sky.location = function (loc, tz) {
|
|
304
|
+
if (!loc || loc.length < 2) return geopos;
|
|
305
|
+
if (isValidLocation(loc)) {
|
|
306
|
+
geopos = loc.slice();
|
|
307
|
+
$form("lat").value = geopos[0];
|
|
308
|
+
$form("lon").value = geopos[1];
|
|
309
|
+
if (isValidTimezone(tz)) timeZone = tz;
|
|
310
|
+
else setPosition(geopos, true);
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
//{"date":dt, "location":loc, "timezone":tz}
|
|
314
|
+
sky.skyview = function (cfg) {
|
|
315
|
+
if (!cfg) return {"date": date, "location": geopos, "timezone": timeZone};
|
|
316
|
+
var valid = false;
|
|
317
|
+
if (dtpick.isVisible()) dtpick.hide();
|
|
318
|
+
if (has(cfg, "timezone") && isValidTimezone(cfg.timezone)) {
|
|
319
|
+
timeZone = cfg.timezone;
|
|
320
|
+
valid = true;
|
|
321
|
+
}
|
|
322
|
+
if (has(cfg, "date") && isValidDate(cfg.date)) {
|
|
323
|
+
date.setTime(cfg.date.valueOf());
|
|
324
|
+
$form("datetime").value = dateFormat(cfg.date, timeZone);
|
|
325
|
+
valid = true;
|
|
326
|
+
}
|
|
327
|
+
if (has(cfg, "location") && isValidLocation(cfg.location)) {
|
|
328
|
+
geopos = cfg.location.slice();
|
|
329
|
+
$form("lat").value = geopos[0];
|
|
330
|
+
$form("lon").value = geopos[1];
|
|
331
|
+
if (!has(cfg, "timezone")) {
|
|
332
|
+
setPosition(geopos, !has(cfg, "date"));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
//sky.updateForm();
|
|
337
|
+
if (valid === false) return {"date": date, "location": geopos, "timezone": timeZone};
|
|
338
|
+
if (config.follow === "zenith") go();
|
|
339
|
+
else sky.redraw();
|
|
340
|
+
};
|
|
341
|
+
sky.dtLoc = sky.skyview;
|
|
342
|
+
sky.zenith = function () { return zenith; };
|
|
343
|
+
sky.nadir = function () {
|
|
344
|
+
var b = -zenith[1],
|
|
345
|
+
l = zenith[0] + 180;
|
|
346
|
+
if (l > 180) l -= 360;
|
|
347
|
+
return [l, b-0.001];
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
if (has(config, "formFields") && (config.location === true || config.formFields.location === true)) {
|
|
351
|
+
styles_(d3.select(sky.parentElement + " ~ #celestial-form").select("#location"), {"display": "inline-block"});
|
|
352
|
+
}
|
|
353
|
+
//only if appropriate
|
|
354
|
+
if (isValidLocation(geopos) && (config.location === true || config.formFields.location === true) && config.follow === "zenith")
|
|
355
|
+
setTimeout(go, 1000);
|
|
356
|
+
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export { geo };
|