EDITAR : ver https://gis.stackexchange.com/a/409403/93097 para obtener una respuesta actualizada con los datos de otro sitio web, tras el cierre del sitio web Eclipse de la NASA.
Aquí mi solución, enteramente en QGIS (versión >= 3.14 (para versiones >= 3.0 y < 3.14, tal vez sean necesarios algunos ajustes).
La solución es crear una rejilla de puntos en QGIS y recuperar los datos de las funciones de JavaScript y actualizar cada punto.
Los parámetros de entrada de la función son :
-
Interfaz QGIS iface
-
Los elementos del eclipse solar, los puedes encontrar en el código fuente de la página del eclipse solar de la NASA
por ejemplo ( fuente : Eclipse total de Sol de 2024 Abr 08 ):
/* Insert Eclipse Besselian Elements below */
//
// Eclipse Elements
//
// First line -
// (0) Julian date
// (1) t0
// (2) tmin
// (3) tmax
// (4) dT
// Second line -
// (5) X0, X1, X2, X3 - X elements
// Third line -
// (9) Y0, Y1, Y2, Y3 - Y elements
// Fourth line -
// (13) D0, D1, D2 - D elements
// Fifth line -
// (16) M0, M1, M2 - mu elements
// Sixth line -
// (19) L10, L11, L12 - L1 elements
// Seventh line -
// (22) L20, L21, L22 - L2 elements
// Eighth line -
// (25) tan f1
// (26) tan f2
//
var elements = new Array(
//*** #0U - Input Besselian Elements here
2460409.262835, 18.0, -4.0, 4.0, 70.6,
-0.31815711, 0.51171052, 0.00003265, -0.00000852,
0.21974689, 0.27095860, -0.00005943, -0.00000467,
7.58619928, 0.01484434, -0.00000168,
89.59121704, 15.00408363, -0.00000130,
0.53581262, 0.00006179, -0.00001275,
-0.01027351, 0.00006148, -0.00001269,
0.00466826, 0.00464501
);
-
Opcional : la fecha y la hora de la umbra que desea crear
-
Opcional : el espaciado de los puntos de la cuadrícula en grados, por defecto = 0.1°
Sólo tienes que copiar el código de abajo en un nuevo editor de la consola Python de QGIS, modificar los parámetros (últimas líneas del código) y ejecutarlo.
Aparecerá un práctico cuadro de diálogo :
Defina, en WGS 84 (EPSG: 4326) la extensión de cuadrícula deseada, puede recuperarla de una capa de proyecto de QGIS, de la extensión actual del lienzo de QGIS o simplemente dibujarla usted mismo.
Por último, haga clic en el botón Create Eclipse Layer y espere a que finalice el proceso.
El código :
#!/usr/bin/env python3
from datetime import datetime
from html.parser import HTMLParser
import processing
from PyQt5.QtCore import (
QCoreApplication,
QObject,
QRunnable,
QThreadPool,
QVariant,
pyqtSignal,
)
from PyQt5.QtWebKitWidgets import QWebPage, QWebView
from PyQt5.QtWidgets import QProgressBar, QPushButton, QVBoxLayout, QWidget
from qgis.core import (
QgsCoordinateReferenceSystem,
QgsField,
QgsProcessingFeatureSourceDefinition,
QgsProject,
)
from qgis.gui import QgsExtentGroupBox
class MarkerHTMLParser(HTMLParser):
"""Parse map marker html"""
def __init__(self):
super(MarkerHTMLParser, self).__init__()
self.loc_data = {}
self.loc_data["event"] = {}
self.data_name = ""
self.in_table = False
self.table_num = 0
self.table_line = 0
self.table_headers = []
self.current_header = 0
self.current_line = ""
def handle_starttag(self, tag, attrs):
if tag == "table":
self.in_table = True
self.table_num += 1
elif tag == "tr":
self.table_line += 1
self.current_header = 0
self.current_line = ""
else:
self.data_name = ""
def handle_endtag(self, tag):
if tag == "table":
self.in_table = False
self.table_line = 0
self.table_headers = []
self.current_header = 0
self.current_line = ""
def handle_data(self, data):
if self.in_table and self.table_num == 2:
data = data.replace("\xa0", " ")
if self.table_line == 1:
self.table_headers.append(data)
else:
if self.current_header == 0:
self.current_line = data.strip()
self.loc_data["event"][self.current_line] = {}
else:
self.loc_data["event"][self.current_line][
self.table_headers[self.current_header]
] = data
self.current_header += 1
else:
# \xa0 =
data_content = data.split("\xa0")
if "Eclipse" in data_content:
self.data_name = "type"
self.loc_data[self.data_name] = data.replace("\xa0", " ")
elif data_content[0] == "Duration":
self.data_name = "duration_of_totality"
self.loc_data[self.data_name] = data_content[3]
elif data_content[0] == "Magnitude:":
self.data_name = "magnitude"
self.loc_data[self.data_name] = data_content[1]
elif data_content[0] == "Obscuration:":
self.data_name = "obscuration"
self.loc_data[self.data_name] = data_content[1]
elif data == "Lat.":
self.data_name = "latitude"
elif data == "Long.":
self.data_name = "longitude"
elif self.data_name in ["latitude", "longitude"]:
self.loc_data[self.data_name] = data_content[1]
class CustomQWebPage(QWebPage):
"""Web page to send JS commands and get JS result"""
result = pyqtSignal(dict)
def __init__(self, parent=None):
super(CustomQWebPage, self).__init__(parent)
self.result_data = None
self.result.connect(lambda d: setattr(self, "result_data", d))
def javaScriptConsoleMessage(self, msg, line_number, source_id):
parser = MarkerHTMLParser()
parser.feed(msg)
self.result.emit(parser.loc_data)
def get_data(self, latitude: float, longitude: float):
self.mainFrame().evaluateJavaScript(
f"console.log(loc_circ({latitude}, {longitude}))"
)
while not self.result_data:
QCoreApplication.processEvents()
data = self.result_data
self.result_data = None
return data
class WorkerSignals(QObject):
progress = pyqtSignal(int)
result = pyqtSignal(object)
class Runnable(QRunnable):
def __init__(self, webpage, vl_pts):
super(Runnable, self).__init__()
self.webpage = webpage
self.vl_pts = vl_pts
# signals
self.signals = WorkerSignals()
def run(self) -> None:
total = self.vl_pts.featureCount()
# edit mode
self.vl_pts.startEditing()
for i, feat in enumerate(self.vl_pts.getFeatures()):
# send progress
self.signals.progress.emit(int(i / total * 100) + 1)
point = feat.geometry().asPoint()
lon = point.x()
lat = point.y()
data = self.webpage.get_data(lat, lon)
feat["type_eclip"] = data["type"]
feat["lat"] = data["latitude"]
feat["lon"] = data["longitude"]
if "Start of partial eclipse (C1) :" in data["event"]:
c_type = "Start of partial eclipse (C1) :"
c_field = "c1"
c_data = data["event"][c_type]
dt = datetime.strptime(
f'{c_data["Date"]} {c_data["Time (UT)"]}', "%Y/%m/%d %H:%M:%S.%f"
)
feat[c_field] = dt.strftime("%Y-%m-%d %H:%M %S")
if "Start of total eclipse (C2) :" in data["event"]:
c_type = "Start of total eclipse (C2) :"
c_field = "c2"
c_data = data["event"][c_type]
dt = datetime.strptime(
f'{c_data["Date"]} {c_data["Time (UT)"]}', "%Y/%m/%d %H:%M:%S.%f"
)
feat[c_field] = dt.strftime("%Y-%m-%d %H:%M %S")
if "Maximum eclipse :" in data["event"]:
c_type = "Maximum eclipse :"
c_field = "max_eclip"
c_data = data["event"][c_type]
dt = datetime.strptime(
f'{c_data["Date"]} {c_data["Time (UT)"]}', "%Y/%m/%d %H:%M:%S.%f"
)
feat[c_field] = dt.strftime("%Y-%m-%d %H:%M %S")
if "End of total eclipse (C3) :" in data["event"]:
c_type = "End of total eclipse (C3) :"
c_field = "c3"
c_data = data["event"][c_type]
dt = datetime.strptime(
f'{c_data["Date"]} {c_data["Time (UT)"]}', "%Y/%m/%d %H:%M:%S.%f"
)
feat[c_field] = dt.strftime("%Y-%m-%d %H:%M %S")
if "End of partial eclipse (C4) :" in data["event"]:
c_type = "End of partial eclipse (C4) :"
c_field = "c4"
c_data = data["event"][c_type]
dt = datetime.strptime(
f'{c_data["Date"]} {c_data["Time (UT)"]}', "%Y/%m/%d %H:%M:%S.%f"
)
feat[c_field] = dt.strftime("%Y-%m-%d %H:%M %S")
if "magnitude" in data:
feat["magnitude"] = data["magnitude"]
if "obscuration" in data:
obscuration = float(data["obscuration"].replace("%", ""))
feat["obscuration"] = obscuration
self.vl_pts.updateFeature(feat)
# end of edit mode
self.vl_pts.commitChanges()
# finish the process, emit a result
self.signals.result.emit(True)
class Total_Eclipse(QWidget):
def __init__(
self,
iface,
elements: str,
umbra_datetime: str = None,
spacing: float = 0.1,
parent=None,
):
super(Total_Eclipse, self).__init__(parent)
self.iface = iface
self._umbra_dt = umbra_datetime
self.spacing = spacing
self.lyr_pts = None
self.custom_page = CustomQWebPage(self)
self.webview = QWebView(self)
self.webview.setPage(self.custom_page)
self.layout = QVBoxLayout(self)
self.crs = QgsCoordinateReferenceSystem("EPSG:4326")
self.extent_box = QgsExtentGroupBox()
self.extent_box.setMapCanvas(self.iface.mapCanvas())
self.extent_box.setOutputCrs(self.crs)
self.progress_bar = QProgressBar(self)
self.progress_bar.setMinimum(0)
self.progress_bar.setMaximum(100)
self.launch_process = QPushButton("Create Eclipse Layer", self)
self.launch_process.setEnabled(False)
self.layout.addWidget(self.extent_box)
self.layout.addWidget(self.progress_bar)
self.layout.addWidget(self.launch_process)
# multithreading
self.pool = QThreadPool.globalInstance()
self.runnable = None
# signals
self.webview.loadFinished.connect(lambda: self.launch_process.setEnabled(True))
self.launch_process.clicked.connect(self.start_process)
# load JS scripts in the webview
html = (
'<!DOCTYPE html><html><head><meta charset="UTF-8">'
'<script type="text/javascript">'
"var elements = new Array({0});"
'</script><script type="text/javascript" '
'src="https://eclipse.gsfc.nasa.gov/SEgoogle/SEcirc.js">'
"</script></head></html>".format(elements.replace("\n", "").strip())
)
self.webview.setHtml(html)
@property
def umbra_datetime(self) -> str:
return self._umbra_dt
@umbra_datetime.setter
def umbra_datetime(self, value):
self._umbra_dt = value
def get_data(self, latitude: float, longitude: float):
return self.custom_page.get_data(latitude, longitude)
def start_process(self):
self.launch_process.setEnabled(False)
extent = self.extent_box.outputExtent()
extent_bounds = [
str(extent.xMinimum()),
str(extent.xMaximum()),
str(extent.yMinimum()),
str(extent.yMaximum()),
]
regular_points = processing.run(
"qgis:regularpoints",
{
"EXTENT": f'{",".join(extent_bounds)} [EPSG:4326]',
"SPACING": self.spacing,
"INSET": 0,
"RANDOMIZE": False,
"IS_SPACING": True,
"CRS": self.crs,
"OUTPUT": "TEMPORARY_OUTPUT",
},
)
self.lyr_pts = regular_points["OUTPUT"]
self.format_lyr_pts()
# multithreading
self.runnable = Runnable(self.custom_page, self.lyr_pts)
self.runnable.signals.result.connect(self.thread_result)
self.runnable.signals.progress.connect(self.update_progress)
self.pool.start(self.runnable)
def format_lyr_pts(self):
fields = [
("type_eclip", QVariant.String),
("lat", QVariant.String),
("lon", QVariant.String),
("magnitude", QVariant.Double),
("obscuration", QVariant.Double),
("c1", QVariant.DateTime),
("c2", QVariant.DateTime),
("max_eclip", QVariant.DateTime),
("c3", QVariant.DateTime),
("c4", QVariant.DateTime),
]
self.lyr_pts.startEditing()
for fld_name, fld_type in fields:
self.lyr_pts.addAttribute(QgsField(fld_name, fld_type))
self.lyr_pts.updateFields()
self.lyr_pts.commitChanges()
def update_progress(self, value):
self.progress_bar.setValue(value)
def thread_result(self, result):
if result:
self.lyr_pts.setName("eclipse_points")
QgsProject.instance().addMapLayer(self.lyr_pts)
if self._umbra_dt:
lyr_umbra = self.create_umbra(self._umbra_dt)
QgsProject.instance().addMapLayer(lyr_umbra)
self.progress_bar.setValue(0)
def create_umbra(self, umbra_date_time: str):
expression = (
"\"c2\" <= to_datetime('{0}', 'yyyy-MM-dd HH:mm:ss')"
" and \"c3\" >= to_datetime('{0}', 'yyyy-MM-dd HH:mm:ss')"
)
self.lyr_pts.selectByExpression(expression.format(umbra_date_time))
min_bounding = processing.run(
"qgis:minimumboundinggeometry",
{
"INPUT": QgsProcessingFeatureSourceDefinition(
self.lyr_pts.id(), selectedFeaturesOnly=True
),
"FIELD": "",
"TYPE": 3, # convex
"OUTPUT": "TEMPORARY_OUTPUT",
},
)
umbra = min_bounding["OUTPUT"]
umbra.setName("umbra")
return umbra
# Eclipse elements
eclipse_elements = """
2460409.262835, 18.0, -4.0, 4.0, 70.6,
-0.31815711, 0.51171052, 0.00003265, -0.00000852,
0.21974689, 0.27095860, -0.00005943, -0.00000467,
7.58619928, 0.01484434, -0.00000168,
89.59121704, 15.00408363, -0.00000130,
0.53581262, 0.00006179, -0.00001275,
-0.01027351, 0.00006148, -0.00001269,
0.00466826, 0.00464501
"""
umbra_datetime = "2024-04-08 19:14:32"
solar_te = Total_Eclipse(iface, eclipse_elements, umbra_datetime, spacing=0.1)
solar_te.show()
El resultado son dos capas :
- Una capa de puntos de la cuadrícula de la extensión del diálogo, con toda la información necesaria sobre el eclipse (busque en la tabla de atributos)
- Un polígono de la umbra solicitada (si se facilita la fecha/hora)