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

feat: planetary name resolvers (openstreetmap and gazetteer of planetary features) #103

Merged
merged 17 commits into from
Sep 16, 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
The method accepts `astropy.io.fits.HDUList`, `pathlib.Path`, or `string` representing paths (#86)
- New way to make a selection on the view with `selection` method (#100)
- Add selected sources export as `astropy.Table` list with property `selected_objects` (#100)
- Add function `get_view_as_fits` to export the view as a `astropy.io.fits.HDUList` (#86)
- Add function `get_view_as_fits` to export the view as a `astropy.io.fits.HDUList` (#98)
- Add function `save_view_as_image` to save the view as an image file (#108)
- Support for planetary objects for Aladin Lite target (#103)

### Deprecated

Expand Down
244 changes: 244 additions & 0 deletions examples/12_Planetary_surveys.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "8b1a9d152f4e1c7f",
"metadata": {},
"source": [
"# Planetary surveys\n",
"\n",
"With `ipyaladin`, you can also display planetary surfaces."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "initial_id",
"metadata": {},
"outputs": [],
"source": [
"from ipyaladin import Aladin\n",
"from astropy.table import Table"
]
},
{
"cell_type": "markdown",
"id": "60b133f13ce59125",
"metadata": {},
"source": [
"First, let's create an Aladin Lite widget with the Mars Viking MDIM21 survey."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "85d284fd3f5cc7bd",
"metadata": {},
"outputs": [],
"source": [
"mars = Aladin(\n",
" target=\"159.2135528 -58.6241989\",\n",
" survey=\"https://alasky.u-strasbg.fr/Planets/Mars_Viking_MDIM21\",\n",
" fov=10,\n",
")\n",
"mars"
]
},
{
"cell_type": "markdown",
"id": "35ea9256",
"metadata": {},
"source": [
"The target does not return a `SkyCoord` object anymore, since we don't represent the sky here. It is a couple of `Longitude` and `Latitude`."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "43c34b52",
"metadata": {},
"outputs": [],
"source": [
"mars.target"
]
},
{
"cell_type": "markdown",
"id": "e6f17ff7",
"metadata": {},
"source": [
"The WCS are also defined on planetary surfaces (in [Marmo *et.al.* 2018](https://doi.org/10.1029/2018EA000388)). This is why you see a CTYPE starting with `MA` for Mars."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "59bcf84c",
"metadata": {},
"outputs": [],
"source": [
"mars.wcs"
]
},
{
"cell_type": "markdown",
"id": "34747eb38e0db450",
"metadata": {},
"source": [
"Let's change the center of the view to Olympus Mons."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "dbe012440a30b6fe",
"metadata": {},
"outputs": [],
"source": [
"mars.target = \"Olympus Mons\""
]
},
{
"cell_type": "markdown",
"id": "879f484a",
"metadata": {},
"source": [
"Any name recognized by the [Gazetter of Planetary Nomenclature](https://planetarynames.wr.usgs.gov/Nomenclature) will work. \n",
"\n",
"We can also add tables:"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5461264f0485d880",
"metadata": {},
"outputs": [],
"source": [
"longitudes = [\n",
" 226.2,\n",
" 70.5,\n",
" 250.4,\n",
" -59.2,\n",
" 147.21,\n",
" 316.0,\n",
" 32.53,\n",
" -112.58,\n",
" 298.0,\n",
" 30.0,\n",
" 70.5,\n",
" 280.0,\n",
" 87.0,\n",
" 117.5,\n",
" 350.0,\n",
"]\n",
"latitudes = [\n",
" 18.65,\n",
" -42.4,\n",
" 40.5,\n",
" -13.9,\n",
" 25.02,\n",
" -49.7,\n",
" 70.0,\n",
" 1.57,\n",
" 25.0,\n",
" 19.79,\n",
" -42.4,\n",
" 45.0,\n",
" 12.9,\n",
" 46.7,\n",
" -45.0,\n",
"]\n",
"names = [\n",
" \"Olympus Mons\",\n",
" \"Hellas Planitia\",\n",
" \"Alba Mons\",\n",
" \"Valles Marineris\",\n",
" \"Elysium Mons\",\n",
" \"Argyre Basin\",\n",
" \"Vastitas Borealis\",\n",
" \"Tharsis Montes\",\n",
" \"Outflow channels\",\n",
" \"Arabia Terra\",\n",
" \"Hellas Basin\",\n",
" \"Tempe Terra\",\n",
" \"Isidis Basin\",\n",
" \"Utopia Basin\",\n",
" \"Noachis Terra\",\n",
"]\n",
"\n",
"table = Table([longitudes, latitudes, names], names=(\"Longitude\", \"Latitude\", \"Name\"))\n",
"mars.add_table(\n",
" table, color=\"#67d38d\", source_size=15, shape=\"cross\", name=\"Mars_features\"\n",
")"
]
},
{
"cell_type": "markdown",
"id": "4af1422b",
"metadata": {},
"source": [
"## Surveys of Earth"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "479ddc87",
"metadata": {},
"outputs": [],
"source": [
"earth = Aladin(\n",
" survey=\"CDS/P/Earth/DEM/elevation\",\n",
" fov=100,\n",
")\n",
"earth"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "983e6c40",
"metadata": {},
"outputs": [],
"source": [
"earth.target"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4609cf6b",
"metadata": {},
"outputs": [],
"source": [
"earth.wcs"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3b887792",
"metadata": {},
"outputs": [],
"source": [
"earth.target = \"Strasbourg\" # ipyaladin's home"
]
},
{
"cell_type": "markdown",
"id": "d042bb57",
"metadata": {},
"source": [
"Any name recognized by OpenStreetMaps will work."
]
}
],
"metadata": {
"nbsphinx": {
"execute": "never"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
16 changes: 9 additions & 7 deletions js/models/event_handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,13 +129,6 @@ export default class EventHandler {
this.aladin.setFoV(fov);
});

this.aladin.on("layerChanged", (imageLayer, layerName, state) => {
if (layerName !== "base" || state !== "ADDED") return;
this.updateWCS();
this.model.set("_base_layer_last_view", imageLayer.id);
this.model.save_changes();
});

/* Div control */
this.model.on("change:_height", () => {
let height = this.model.get("_height");
Expand All @@ -158,6 +151,15 @@ export default class EventHandler {
this.model.save_changes();
});

this.aladin.on("layerChanged", (imageLayer, layerName, state) => {
if (layerName === "base")
this.model.set("_survey_body", imageLayer.hipsBody || "sky");
if (layerName !== "base" || state !== "ADDED") return;
this.updateWCS();
this.model.set("_base_layer_last_view", imageLayer.id);
this.model.save_changes();
});

this.aladin.on("resizeChanged", (width, height) => {
// Skip resize event when the div is hidden
if (width === 1 && height === 1) {
Expand Down
2 changes: 1 addition & 1 deletion src/ipyaladin/__about__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "0.4.0"
__aladin_lite_version__ = "3.4.4-beta"
__aladin_lite_version__ = "3.5.1-alpha"
82 changes: 71 additions & 11 deletions src/ipyaladin/utils/_coordinate_parser.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import warnings
from typing import Tuple

from astropy.coordinates import SkyCoord, Angle
import requests
from astropy.coordinates import SkyCoord, Angle, Longitude, Latitude
import re

from ipyaladin.utils.exceptions import NameResolverWarning

def parse_coordinate_string(string: str) -> SkyCoord:

def _parse_coordinate_string(
string: str, body: str = "sky"
) -> Tuple[Longitude, Latitude]:
"""Parse a string containing coordinates.

Parameters
----------
string : str
The string containing the coordinates.
body : str
The planetary body to use for the coordinates. Default
is "sky" when there is no planetary body.

Returns
-------
Expand All @@ -19,20 +28,71 @@ def parse_coordinate_string(string: str) -> SkyCoord:

"""
if not _is_coordinate_string(string):
return SkyCoord.from_name(string)
coordinates: Tuple[str, str] = _split_coordinate_string(string)
if body == "sky" or body is None:
sesame = SkyCoord.from_name(string)
return sesame.icrs.ra.deg, sesame.icrs.dec.deg
return _from_name_on_planet(string, body)
# coordinates should be parsed from the string
coordinates = _split_coordinate_string(string)
# Parse ra and dec to astropy Angle objects
dec: Angle = Angle(coordinates[1], unit="deg")
lat: Angle = Angle(coordinates[1], unit="deg")
if _is_hour_angle_string(coordinates[0]):
ra = Angle(coordinates[0], unit="hour")
lon = Angle(coordinates[0], unit="hour")
else:
ra = Angle(coordinates[0], unit="deg")
lon = Angle(coordinates[0], unit="deg")
# Create SkyCoord object
if string[0] == "B":
return SkyCoord(ra=ra, dec=dec, equinox="B1950", frame="fk4")
if string[0] == "G":
return SkyCoord(l=ra, b=dec, frame="galactic")
return SkyCoord(ra=ra, dec=dec, frame="icrs")
coo = SkyCoord(ra=lon, dec=lat, equinox="B1950", frame="fk4")
elif string[0] == "G":
coo = SkyCoord(l=lon, b=lat, frame="galactic")
else:
coo = SkyCoord(ra=lon, dec=lat, frame="icrs")
return coo.icrs.ra.deg, coo.icrs.dec.deg


def _from_name_on_planet(string: str, body: str) -> SkyCoord:
"""Get coordinates from a name on a planetary body.

Parameters
----------
string : str
The name of the feature.
body : str
The planetary body to use for the coordinates.

Returns
-------
SkyCoord
An `astropy.coordinates.SkyCoord` object representing the coordinates.
"""
url = (
f"https://alasky.cds.unistra.fr/planetary-features/resolve"
f"?identifier={string.replace(' ', '+')}&body={body}&threshold=0.7&format=json"
)
request = requests.get(url)
if request.status_code != requests.codes.ok:
raise ValueError(
"No coordinates found for this requested planetary feature: " f"'{string}'"
)
data = request.json()["data"]
# response is different for earth
if body == "earth":
return data[0][0], data[0][1]
# case of every other planetary bodies
identifier = data[0][1]
lat = data[0][5] # inverted lon and lat in response
lon = data[0][6]
system = data[0][11]
if identifier != string:
warnings.warn(
f"Nothing found for '{string}' on {body}. However, a {identifier} exists. "
f"Moving to {identifier}.",
NameResolverWarning,
stacklevel=2,
)
if "+West" in system:
lon = 360 - lon
return lon, lat


def _is_coordinate_string(string: str) -> bool:
Expand Down
Loading
Loading