Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Airmass / Altitude boundary display in skymap #163

Merged
merged 4 commits into from
Oct 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 140 additions & 0 deletions src/components/FormElements/AirmassAltitudeInput.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@

<template>
<div>
<b-field
:type="hasError ? 'is-danger' : 'is-dark'"
:message="errorMessage"
:size="size"
>
<b-input
v-model="localInputVal"
:size="size"
lazy
:disabled="disabled"
style="width: 100%;"
/>
<b-select
v-model="units"
:size="size"
:disabled="disabled"
>
<option value="alt">
altitude [deg]
</option>
<option value="airmass">
airmass
</option>
</b-select>
</b-field>
</div>
</template>

<script>
export default {
props: [
'value', // altitude (in degrees) fed from the parent component
'size',
'disabled'
],
data () {
return {
localInputVal: this.value, // the text input should match the altitude fed in from the parent component
airmassToAltitudeLookup: {},
units: 'airmass',
hasError: false,
errorMessage: ''
}
},
mounted () {
// initialize displayed value with the correct units
this.localInputVal = this.convertToSelectedUnits(this.value)
},
watch: {
value (newVal) {
// If the parent component updates the prop that is sent in to this component,
// update the local value in terms of the selected units
if (newVal == null || newVal === '') {
this.localInputVal = newVal
} else {
this.localInputVal = this.convertToSelectedUnits(newVal)
}
},
localInputVal (newVal) {
// When the value in the input field changes, validate it and emit it back to the parent component
if (newVal != null && newVal !== '') {
this.validateAndEmit(newVal)
}
},
units (newVal) {
// When the user switches between altitude and airmass units, recompute the equivalent value to display locally
if (this.value == null || this.value === '') return
this.localInputVal = this.convertToSelectedUnits(this.value)
}
},
methods: {
roundToPrecision (float, decimals) {
const decimalMover = 10 ** decimals
return Math.round(decimalMover * float) / decimalMover
},
altitudeToAirmass (alt) {
// Use the equation airmass = 1 / cos(zenithAngle), where zenith angle is 90 - altitude.
// note that this is not very precise at extremes.

// input altitude should be decimal degrees
const precision = 3 // how many decimal places to use for result

const zenithAngle = 90 - alt
const altRad = zenithAngle * Math.PI / 180
const airmass = this.roundToPrecision(1 / Math.cos(altRad), precision)
// Cache results for reverse calculation (avoid floating point equality errors)
this.airmassToAltitudeLookup[airmass] = alt
return airmass
},
airmassToAltitude (airmass) {
// Use the equation zenithAngle = arccos(1 / airmass), where zenith angle is 90 - altitude.
// note that this is not very precise at extremes.

// Try to use a cached result first
if (airmass in this.airmassToAltitudeLookup) {
return this.airmassToAltitudeLookup[airmass]
}

// otherwise, do the computation
const zenithAngleRad = Math.acos(1 / airmass)
const zenithAngleDeg = zenithAngleRad * 180 / Math.PI
const altitudeDeg = 90 - zenithAngleDeg
return altitudeDeg
},
convertToSelectedUnits (val) {
if (this.units == 'airmass') {
return this.altitudeToAirmass(val)
} else {
return val
}
},
validateAndEmit (val) {
this.hasError = false
this.errorMessage = ''
try {
// Validate altitude input
if (this.units === 'alt') {
if (isNaN(Number(val)) || Number(val) < 0 || Number(val) > 90) {
throw new RangeError('Must be between 0 and 90 degrees')
}
this.$emit('input', Number(val))
// Validate airmass input
} else if (this.units === 'airmass') {
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(val))
}
} catch (e) {
this.hasError = true
this.errorMessage = e.message
}
}
}
}
</script>
44 changes: 37 additions & 7 deletions src/components/celestialmap/TheSkyChart.vue
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,6 @@ export default {
type: Boolean,
default: true
},
showDaylight: {
type: Boolean,
default: false
},
showMilkyWay: {
type: Boolean,
default: true
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -146,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: ''
}
Expand Down Expand Up @@ -184,6 +190,11 @@ export default {
show: this.showOpenClusters,
minMagnitude: this.openClusterMagMin,
maxMagnitude: this.openClusterMagMax
},
airmassCircle: {
show: this.showAirmassCircle,
degAboveHorizon: this.degAboveHorizon,
isHovered: this.airmassCircleIsHovered
}
}

Expand Down Expand Up @@ -260,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 () {
Expand Down Expand Up @@ -374,9 +394,6 @@ export default {
showPlanets () {
Celestial.reload({ planets: { which: this.planetsList } })
},
showDaylight () {
Celestial.apply({ daylight: { show: this.showDaylight } })
},
showMilkyWay () {
Celestial.apply({ mw: { show: this.showMilkyWay } })
},
Expand Down Expand Up @@ -420,7 +437,20 @@ 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()
},
airmassCircleIsHovered () {
Celestial.customData.airmassCircle.isHovered = this.airmassCircleIsHovered
Celestial.redraw()
}

},

computed: {
Expand Down
49 changes: 48 additions & 1 deletion src/components/celestialmap/add_custom_data.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,53 @@ const distance = (p1, p2) => {
return Math.sqrt(d1 * d1 + d2 * d2)
}

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

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()

// 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) => {
if (!Celestial.customData.stars.show) return
if (d.properties.mag > Celestial.customData.stars.minMagnitude) return
Expand Down Expand Up @@ -229,13 +276,13 @@ 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
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
Expand Down
Loading
Loading