From 1f406503f4f1e75dbe29e1d0605b208bba42edc6 Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Mon, 30 Sep 2024 11:35:04 -0400 Subject: [PATCH 1/4] Add configurable circle showing airmass limit on sky chart --- .../FormElements/AirmassAltitudeInput.vue | 135 ++++++++++++++++++ src/components/celestialmap/TheSkyChart.vue | 29 +++- .../celestialmap/add_custom_data.js | 36 ++++- src/components/sitepages/SiteTargets.vue | 69 ++++++--- 4 files changed, 240 insertions(+), 29 deletions(-) create mode 100644 src/components/FormElements/AirmassAltitudeInput.vue diff --git a/src/components/FormElements/AirmassAltitudeInput.vue b/src/components/FormElements/AirmassAltitudeInput.vue new file mode 100644 index 00000000..554cf327 --- /dev/null +++ b/src/components/FormElements/AirmassAltitudeInput.vue @@ -0,0 +1,135 @@ + + + + diff --git a/src/components/celestialmap/TheSkyChart.vue b/src/components/celestialmap/TheSkyChart.vue index bb424e36..4b9143e7 100644 --- a/src/components/celestialmap/TheSkyChart.vue +++ b/src/components/celestialmap/TheSkyChart.vue @@ -60,10 +60,6 @@ export default { type: Boolean, default: true }, - showDaylight: { - type: Boolean, - default: false - }, showMilkyWay: { type: Boolean, default: true @@ -114,6 +110,15 @@ export default { default: 0 }, + showAirmassCircle: { + type: Boolean, + default: true + }, + degAboveHorizon: { + type: Number, + default: 30 + }, + use_custom_date_location: { type: Boolean, default: false @@ -184,6 +189,10 @@ export default { show: this.showOpenClusters, minMagnitude: this.openClusterMagMin, maxMagnitude: this.openClusterMagMax + }, + airmassCircle: { + show: this.showAirmassCircle, + degAboveHorizon: this.degAboveHorizon } } @@ -374,9 +383,6 @@ export default { showPlanets () { Celestial.reload({ planets: { which: this.planetsList } }) }, - showDaylight () { - Celestial.apply({ daylight: { show: this.showDaylight } }) - }, showMilkyWay () { Celestial.apply({ mw: { show: this.showMilkyWay } }) }, @@ -420,7 +426,16 @@ export default { openClusterMagMax () { Celestial.customData.openClusters.maxMagnitude = this.openClusterMagMax Celestial.redraw() + }, + showAirmassCircle () { + Celestial.customData.airmassCircle.show = this.showAirmassCircle + Celestial.redraw() + }, + degAboveHorizon () { + Celestial.customData.airmassCircle.degAboveHorizon = this.degAboveHorizon + Celestial.redraw() } + }, computed: { diff --git a/src/components/celestialmap/add_custom_data.js b/src/components/celestialmap/add_custom_data.js index eebc26b8..49796f7e 100644 --- a/src/components/celestialmap/add_custom_data.js +++ b/src/components/celestialmap/add_custom_data.js @@ -49,6 +49,40 @@ const distance = (p1, p2) => { return Math.sqrt(d1 * d1 + d2 * d2) } +const drawAirmassCircle = Celestial => { + // Draw a circle centered at the zenith with a radius that extends to a certain airmass (altitude) above horizon + if (!Celestial.customData.airmassCircle?.show) return + + const color = 'yellow' + const lineWidth = 0.5 + + const degreesAboveHorizon = Celestial.customData.airmassCircle.degAboveHorizon + const degreesBelowZenith = 90 - degreesAboveHorizon + + const zenith = Celestial.zenith() // zenith ra/dec (degrees) + const zenithXY = Celestial.mapProjection(zenith) // convert to xy pixel coords + + // Don't show if zenith is [0,0] + // This is a simple fix to avoid rendering the circle before the map has positioned itself. + // Not worth worrying about the rare and brief moments when the zenith actually is [0,0]. + // Maybe there is a more elegant solution but this sky chart is impossible to figure out sometimes. + if (zenith[0] == 0 && zenith[1] == 0) return + + const horizon = [zenith[0], zenith[1] - degreesBelowZenith] // get a point on the horizon + const horizonXY = Celestial.mapProjection(horizon) // convert to xy pixel coords + + // the radius of our circle is the difference between the y coordinate for the horizon point and zenith + const radiusPix = Math.abs(zenithXY[1] - horizonXY[1]) + + // draw the circle + Celestial.context.strokeStyle = color + Celestial.context.lineWidth = lineWidth + Celestial.context.beginPath() + Celestial.context.arc(zenithXY[0], zenithXY[1], radiusPix, 0, 2 * Math.PI) + Celestial.context.closePath() + Celestial.context.stroke() +} + const draw_star = (Celestial, quadtree, styles, starbase, starexp, d) => { if (!Celestial.customData.stars.show) return if (d.properties.mag > Celestial.customData.stars.minMagnitude) return @@ -229,7 +263,6 @@ const add_custom_data = (Celestial, base_config, data_list) => { .data(sky_objects.features) .enter().append('path') .attr('class', 'custom_obj') - Celestial.redraw() }, redraw: () => { // The quadtree is used to avoid rendering object names that overlap @@ -255,6 +288,7 @@ const add_custom_data = (Celestial, base_config, data_list) => { } } }) + drawAirmassCircle(Celestial) } }) }; diff --git a/src/components/sitepages/SiteTargets.vue b/src/components/sitepages/SiteTargets.vue index 2c437262..074cc736 100644 --- a/src/components/sitepages/SiteTargets.vue +++ b/src/components/sitepages/SiteTargets.vue @@ -34,7 +34,6 @@ :show-moon="showMoon" :show-sun="showSun" - :show-daylight="showDaylight" :show-milky-way="showMilkyWay" :show-planets="showPlanets" @@ -49,6 +48,9 @@ :open-cluster-mag-min="openClusterMagMin" :open-cluster-mag-max="openClusterMagMax" + :show-airmass-circle="showAirmassCircle" + :deg-above-horizon="degAboveHorizon" + :use_custom_date_location="use_custom_date_location" :show_live_chart="isLiveSkyDisplay" :date="skychart_date" @@ -176,7 +178,7 @@ label="max mag" > -
- - + Horizon Circle + + + +
+
+ + + + +
+ +
+ +
Date: Mon, 30 Sep 2024 12:03:24 -0400 Subject: [PATCH 2/4] Improve error messages and conversion precision --- .../FormElements/AirmassAltitudeInput.vue | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/components/FormElements/AirmassAltitudeInput.vue b/src/components/FormElements/AirmassAltitudeInput.vue index 554cf327..c21d6b01 100644 --- a/src/components/FormElements/AirmassAltitudeInput.vue +++ b/src/components/FormElements/AirmassAltitudeInput.vue @@ -74,7 +74,7 @@ export default { }, altitudeToAirmass (alt) { // input altitude should be decimal degrees from horizon - const precision = 2 // how many decimal places to use for result + const precision = 3 // how many decimal places to use for result const altFromZenith = 90 - alt const altRad = altFromZenith * Math.PI / 180 @@ -84,19 +84,15 @@ export default { return airmass }, airmassToAltitude (airmass) { - // return altitude in decimal degrees from horizon - const precision = 1 // how many decimal places to use for result - // Try to use a cached result first if (airmass in this.airmassToAltitudeLookup) { - console.log('returning altitude from lookup: ', this.airmassToAltitudeLookup[airmass]) return this.airmassToAltitudeLookup[airmass] } // otherwise, do the computation const altFromZenithRad = Math.acos(1 / airmass) const altFromZenithDeg = altFromZenithRad * 180 / Math.PI - const altFromHorizonDeg = this.roundToPrecision(90 - altFromZenithDeg, precision) + const altFromHorizonDeg = 90 - altFromZenithDeg return altFromHorizonDeg }, convertToSelectedUnits (val) { @@ -118,9 +114,8 @@ export default { this.$emit('input', Number(value)) // Validate airmass input } else if (this.units === 'airmass') { - console.log('airmass: ', value) - if (isNaN(Number(value)) || Number(value) < 0) { - throw new RangeError('Must be a positive number') + if (isNaN(Number(value)) || Number(value) < 1) { + throw new RangeError('Airmass is a positive number >= 1') } // Since the component "speaks" only in altitude (degrees), convert the airmass before emitting this.$emit('input', this.airmassToAltitude(value)) From 8f9f10d29833c977a6a479ef1686314292e251fc Mon Sep 17 00:00:00 2001 From: Tim Beccue Date: Tue, 1 Oct 2024 14:24:42 -0400 Subject: [PATCH 3/4] Show label on hover --- .../FormElements/AirmassAltitudeInput.vue | 48 +++++++++++-------- src/components/celestialmap/TheSkyChart.vue | 17 ++++++- .../celestialmap/add_custom_data.js | 17 ++++++- src/components/sitepages/SiteTargets.vue | 2 +- 4 files changed, 61 insertions(+), 23 deletions(-) diff --git a/src/components/FormElements/AirmassAltitudeInput.vue b/src/components/FormElements/AirmassAltitudeInput.vue index c21d6b01..2be0cd08 100644 --- a/src/components/FormElements/AirmassAltitudeInput.vue +++ b/src/components/FormElements/AirmassAltitudeInput.vue @@ -7,7 +7,7 @@ :size="size" > 90) { + if (isNaN(Number(val)) || Number(val) < 0 || Number(val) > 90) { throw new RangeError('Must be between 0 and 90 degrees') } - this.$emit('input', Number(value)) + this.$emit('input', Number(val)) // Validate airmass input } else if (this.units === 'airmass') { - if (isNaN(Number(value)) || Number(value) < 1) { + if (isNaN(Number(val)) || Number(val) < 1) { throw new RangeError('Airmass is a positive number >= 1') } // Since the component "speaks" only in altitude (degrees), convert the airmass before emitting - this.$emit('input', this.airmassToAltitude(value)) + this.$emit('input', this.airmassToAltitude(val)) } } catch (e) { this.hasError = true diff --git a/src/components/celestialmap/TheSkyChart.vue b/src/components/celestialmap/TheSkyChart.vue index 4b9143e7..b89a4b85 100644 --- a/src/components/celestialmap/TheSkyChart.vue +++ b/src/components/celestialmap/TheSkyChart.vue @@ -151,6 +151,7 @@ export default { // Whether or not the mouse is hovering over the sky part of the map. mouse_in_sky: false, + airmassCircleIsHovered: false, resize_observer: '' } @@ -192,7 +193,8 @@ export default { }, airmassCircle: { show: this.showAirmassCircle, - degAboveHorizon: this.degAboveHorizon + degAboveHorizon: this.degAboveHorizon, + isHovered: this.airmassCircleIsHovered } } @@ -269,6 +271,15 @@ export default { handle_mouseover (e) { // Determine whether the mouse is inside the map or not const map_coords = Celestial.mapProjection.invert(e) this.mouse_in_sky = !!Celestial.clip(map_coords) // !! converts 0 or 1 to boolean + + // Check and store the state of whether the user is hovering over the airmass circle + const zenith = Celestial.zenith() + const zenithXY = Celestial.mapProjection(zenith) + const horizonXY = Celestial.mapProjection([zenith[0], zenith[1] - (90 - this.degAboveHorizon)]) // get a point on the horizon + const circleRadius = Math.abs(zenithXY[1] - horizonXY[1]) + const radiusToCenter = Math.sqrt((e[0] - zenithXY[0]) ** 2 + (e[1] - zenithXY[1]) ** 2) + const tolerance = 7 // how many pixels away should register as a hover event + this.airmassCircleIsHovered = (tolerance >= Math.abs(radiusToCenter - circleRadius)) }, rotate () { @@ -434,6 +445,10 @@ export default { degAboveHorizon () { Celestial.customData.airmassCircle.degAboveHorizon = this.degAboveHorizon Celestial.redraw() + }, + airmassCircleIsHovered () { + Celestial.customData.airmassCircle.isHovered = this.airmassCircleIsHovered + Celestial.redraw() } }, diff --git a/src/components/celestialmap/add_custom_data.js b/src/components/celestialmap/add_custom_data.js index 49796f7e..7f98fee5 100644 --- a/src/components/celestialmap/add_custom_data.js +++ b/src/components/celestialmap/add_custom_data.js @@ -49,10 +49,11 @@ const distance = (p1, p2) => { return Math.sqrt(d1 * d1 + d2 * d2) } -const drawAirmassCircle = Celestial => { +const drawAirmassCircle = (Celestial, quadtree) => { // Draw a circle centered at the zenith with a radius that extends to a certain airmass (altitude) above horizon if (!Celestial.customData.airmassCircle?.show) return + const mouseIsHovering = Celestial.customData.airmassCircle.isHovered const color = 'yellow' const lineWidth = 0.5 @@ -81,6 +82,18 @@ const drawAirmassCircle = Celestial => { Celestial.context.arc(zenithXY[0], zenithXY[1], radiusPix, 0, 2 * Math.PI) Celestial.context.closePath() Celestial.context.stroke() + + // Add object name if the user is hovering over the circle and if there is space + const nameStyle = { fill: color, font: '12px Helvetica, Arial, serif', align: 'center', baseline: 'top' } + const textPos = [zenithXY[0], zenithXY[1] + radiusPix + 5] + const label = `altitude: ${Math.round(degreesAboveHorizon)}°` + const nearest = quadtree.find(textPos) + const no_overlap = !nearest || distance(nearest, textPos) > PROXIMITY_LIMIT + if (no_overlap && mouseIsHovering) { + quadtree.add(textPos) + Celestial.setTextStyle(nameStyle) + Celestial.context.fillText(label, zenithXY[0], zenithXY[1] + radiusPix + 5) + } } const draw_star = (Celestial, quadtree, styles, starbase, starexp, d) => { @@ -269,6 +282,7 @@ const add_custom_data = (Celestial, base_config, data_list) => { const m = Celestial.metrics() const quadtree = d3.geom.quadtree().extent([[-1, -1], [m.width + 1, m.height + 1]])([]) + drawAirmassCircle(Celestial, quadtree) Celestial.container.selectAll('.custom_obj').each((d) => { if (Celestial.clip(d.geometry.coordinates)) { const type = d.properties.type @@ -288,7 +302,6 @@ const add_custom_data = (Celestial, base_config, data_list) => { } } }) - drawAirmassCircle(Celestial) } }) }; diff --git a/src/components/sitepages/SiteTargets.vue b/src/components/sitepages/SiteTargets.vue index 074cc736..d97f4357 100644 --- a/src/components/sitepages/SiteTargets.vue +++ b/src/components/sitepages/SiteTargets.vue @@ -363,7 +363,7 @@
- Horizon Circle + Airmass Boundary Date: Tue, 1 Oct 2024 14:36:46 -0400 Subject: [PATCH 4/4] Update labels --- src/components/sitepages/SiteTargets.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/sitepages/SiteTargets.vue b/src/components/sitepages/SiteTargets.vue index d97f4357..495bb990 100644 --- a/src/components/sitepages/SiteTargets.vue +++ b/src/components/sitepages/SiteTargets.vue @@ -363,11 +363,11 @@
- Airmass Boundary + Airmass/Altitude Boundary