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,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 };