Skip to content
Open
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: 3 additions & 0 deletions settings/Config.Example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ db_events = true
; Leave blank for no password, otherwise choose a long and sufficiently random string
import_events_auth = ""

; This is the URL of the eventsapi, you can set to your development one if needed
events_api_url = "https://events.helioviewer.org"

[movie_params]
; FFmpeg location
ffmpeg = ffmpeg
Expand Down
10 changes: 9 additions & 1 deletion src/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,15 @@ public function __construct($file) {

if ( in_array('acao_url', array_keys($this->config)) ) {

if ( in_array('HTTP_ORIGIN', array_keys($_SERVER))
// In dev environments, allow CORS from all origins
if ( in_array('app_env', array_keys($this->config))
&& str_starts_with($this->config['app_env'], 'dev') ) {

header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: ".$this->config['acam']);
header("Access-Control-Allow-Headers: Content-Type");

} elseif ( in_array('HTTP_ORIGIN', array_keys($_SERVER))
&& in_array($_SERVER['HTTP_ORIGIN'], $this->config['acao_url']) ) {

header("Access-Control-Allow-Origin: ".$_SERVER['HTTP_ORIGIN']);
Expand Down
92 changes: 92 additions & 0 deletions src/Event/EventsApi.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php

namespace Helioviewer\Api\Event;

use DateTimeInterface;
use GuzzleHttp\Client;
use GuzzleHttp\ClientInterface;
use Helioviewer\Api\Sentry\Sentry;

class EventsApi {

private ClientInterface $client;

/**
* EventsApi constructor.
*
* @param ClientInterface|null $client Optional Guzzle client; if not provided, a new Client is created.
*/
public function __construct(ClientInterface $client = null)
{
$this->client = $client ?? new Client([
'timeout' => 4,
'headers' => [
'Accept' => 'application/json',
'User-Agent' => 'Helioviewer-API/2.0'
]
]);
}

/**
* Get events for a specific source
*
* @param DateTimeInterface $observationTime The observation time
* @param string $source The data source (e.g. "CCMC")
* @return array Array of event data
* @throws EventsApiException on API errors or unexpected responses
*/
public function getEventsForSource(DateTimeInterface $observationTime, string $source): array
{
// Build the API URL: /api/v1/events/{source}/observation/{datetime}
$formattedTime = $observationTime->format('Y-m-d H:i:s');
$encodedTime = urlencode($formattedTime);

$baseUrl = defined('HV_EVENTS_API_URL') ? HV_EVENTS_API_URL : 'https://events.helioviewer.org';
$url = $baseUrl . "/api/v1/events/{$source}/observation/{$encodedTime}";

Sentry::setContext('EventsApi', [
'url' => $url,
'source' => $source,
'observation_time' => $observationTime->format('Y-m-d\TH:i:s\Z')
]);

$response = $this->client->request('GET', $url);

return $this->parseResponse($response);
}

/**
* Parse the HTTP response and decode JSON
*
* @param \Psr\Http\Message\ResponseInterface $response
* @return array
* @throws EventsApiException if JSON decoding fails or response format is unexpected
*/
private function parseResponse($response): array
{
$body = (string)$response->getBody();
$data = json_decode($body, true);

if (json_last_error() !== JSON_ERROR_NONE) {
Sentry::setContext('EventsApi', [
'raw_response' => $body,
'json_error' => json_last_error_msg(),
'response_status' => $response->getStatusCode()
]);

throw new EventsApiException("Failed to decode JSON response: " . json_last_error_msg());
}

if (!is_array($data)) {
Sentry::setContext('EventsApi', [
'unexpected_response_type' => gettype($data),
'raw_response' => $body,
'response_status' => $response->getStatusCode()
]);

throw new EventsApiException("Unexpected response format: expected array, got " . gettype($data));
}

return $data;
}
}
8 changes: 8 additions & 0 deletions src/Event/EventsApiException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Helioviewer\Api\Event;

class EventsApiException extends \Exception
{

}
24 changes: 22 additions & 2 deletions src/Image/Composite/HelioviewerCompositeImage.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
require_once HV_ROOT_DIR.'/../src/Database/ImgIndex.php';
require_once HV_ROOT_DIR.'/../src/Module/SolarBodies.php';

use Helioviewer\Api\Sentry\Sentry;
use Helioviewer\Api\Event\EventsApi;
use Helioviewer\Api\Event\EventsApiException;

class Image_Composite_HelioviewerCompositeImage {

private $_composite;
Expand Down Expand Up @@ -587,16 +591,32 @@ private function _addEventLayer($imagickImage) {
require_once HV_ROOT_DIR . "/../src/Helper/EventInterface.php";

// Collect events from all data sources.
// Collect all HEK events
$hek = new Event_HEKAdapter();
$event_categories = $hek->getNormalizedEvents($this->date, Array());

$events_api_sources = ["CCMC", "RHESSI"];

$observationTime = new DateTimeImmutable($this->date);
$startDate = $observationTime->sub(new DateInterval("PT12H"));
$length = new DateInterval("P1D");
$event_categories = array_merge($event_categories, Helper_EventInterface::GetEvents($startDate, $length, $observationTime));

// Lay down all relevant event REGIONS first
// Collect CCMC events if any
try {

$eventsApi = new EventsApi();
$event_categories = array_merge($event_categories, $eventsApi->getEventsForSource($observationTime, "CCMC"));
// if there is no error only left is RHESSI to collect
$events_api_sources = ["RHESSI"];

} catch (EventsApiException $e) {
Sentry::capture($e);
}

// Collect RHESSI events
$event_categories = array_merge($event_categories, Helper_EventInterface::GetEvents($startDate, $length, $observationTime, $events_api_sources));

// Lay down all relevant event REGIONS first
$events_to_render = [];
$events_manager = $this->eventsManager;
$add_label_visibility_and_concept = function($events_data, $event_cat_pin, $event_group_name) use ($events_manager) {
Expand Down
19 changes: 19 additions & 0 deletions src/Module/SolarEvents.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
require_once HV_ROOT_DIR . "/../src/Helper/EventInterface.php";

use Helioviewer\Api\Sentry\Sentry;
use Helioviewer\Api\Event\EventsApi;
use Helioviewer\Api\Event\EventsApiException;

class Module_SolarEvents implements Module {

Expand Down Expand Up @@ -216,6 +218,7 @@ private function getHekEvents() {
public function events() {
// The given time is the observation time.
$observationTime = new DateTimeImmutable($this->_params['startTime']);

// The query start time is 12 hours earlier.
$start = $observationTime->sub(new DateInterval("PT12H"));

Expand All @@ -224,6 +227,22 @@ public function events() {
// at the center.
$length = new DateInterval('P1D');

// Handle CCMC source using new Events API
// This provides direct access to CCMC event data without going through the standard EventInterface
if (array_key_exists('sources', $this->_options) && $this->_options['sources'] === 'CCMC') {

try {
$eventsApi = new EventsApi();
$data = $eventsApi->getEventsForSource($observationTime, "CCMC");

header("Content-Type: application/json");
echo json_encode($data);
return;
} catch (EventsApiException $e) {
Sentry::capture($e);
}
}

// Check if any specific datasources were requested
if (array_key_exists('sources', $this->_options)) {
$sources = explode(',', $this->_options['sources']);
Expand Down
86 changes: 86 additions & 0 deletions tests/unit_tests/events/EventsApiTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php declare(strict_types=1);

/**
* @author Kasim Necdet Percinel <kasim.n.percinel@nasa.gov>
*/

use PHPUnit\Framework\TestCase;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Psr7\Response;
use Helioviewer\Api\Event\EventsApi;
use Helioviewer\Api\Event\EventsApiException;

final class EventsApiTest extends TestCase
{
private $mockClient;
private $eventsApi;

protected function setUp(): void
{
$this->mockClient = $this->createMock(ClientInterface::class);
$this->eventsApi = new EventsApi($this->mockClient);
}

public function testItShouldGetEventsSuccessfully(): void
{
$responseData = [
['id' => 1, 'type' => 'event'.rand()],
['id' => 2, 'type' => 'event'.rand()]
];

$this->mockClient->expects($this->once())
->method('request')
->with('GET', $this->stringContains('/api/v1/events/CCMC/observation/'))
->willReturn(new Response(200, [], json_encode($responseData)));

$result = $this->eventsApi->getEventsForSource(
new DateTimeImmutable('2024-01-15 12:00:00'),
'CCMC'
);

$this->assertEquals($responseData, $result);
}

public function testItShouldUrlEncodeObservationTime(): void
{
$this->mockClient->expects($this->once())
->method('request')
->with('GET', $this->stringContains('2024-01-15+12%3A30%3A45'))
->willReturn(new Response(200, [], json_encode([])));

$this->eventsApi->getEventsForSource(
new DateTimeImmutable('2024-01-15 12:30:45'),
'CCMC'
);
}

public function testItShouldThrowExceptionOnInvalidJson(): void
{
$this->mockClient->expects($this->once())
->method('request')
->willReturn(new Response(200, [], 'invalid json {'));

$this->expectException(EventsApiException::class);
$this->expectExceptionMessage('Failed to decode JSON response');

$this->eventsApi->getEventsForSource(
new DateTimeImmutable('2024-01-15 12:00:00'),
'CCMC'
);
}

public function testItShouldThrowExceptionWhenResponseIsNotArray(): void
{
$this->mockClient->expects($this->once())
->method('request')
->willReturn(new Response(200, [], '"just a string"'));

$this->expectException(EventsApiException::class);
$this->expectExceptionMessage('Unexpected response format: expected array, got string');

$this->eventsApi->getEventsForSource(
new DateTimeImmutable('2024-01-15 12:00:00'),
'CCMC'
);
}
}