diff --git a/docroot/docs/index.php b/docroot/docs/index.php index b74496b3d..bcb7abecd 100755 --- a/docroot/docs/index.php +++ b/docroot/docs/index.php @@ -16,16 +16,15 @@ // Script was called directly. Redirect to an API-version specific URL. if (realpath(__FILE__) == $_SERVER['SCRIPT_FILENAME']) { require_once dirname(realpath(__FILE__)).'/../../src/Config.php'; - $config = new Config(dirname(realpath(__FILE__)) - . '/../../settings/Config.ini'); + $config = new Config(dirname(realpath(__FILE__)) . '/../../settings/Config.ini'); header('Location: '.HV_WEB_ROOT_URL.'/docs/'.$api_version); } function import_xml($api_version, &$api_xml_path, &$xml) { - $api_xml_path = dirname(realpath(__FILE__)) . '/' . $api_version - . '/api_definitions.xml'; - $xml = simplexml_load_file($api_xml_path); + $api_xml_url = sprintf("%s/docs/%s/api_definitions.xml", "https://api.helioviewer.org", $api_version); + $xml = simplexml_load_file($api_xml_url); + $api_xml_path = dirname(realpath(__FILE__)) . '/' . $api_version. '/api_definitions.xml'; } function output_html($api_version) { diff --git a/docroot/index.php b/docroot/index.php index 2bf6b9895..2e8207ef8 100644 --- a/docroot/index.php +++ b/docroot/index.php @@ -33,8 +33,10 @@ date_default_timezone_set('UTC'); register_shutdown_function('shutdownFunction'); -if ( array_key_exists('docs', $_GET) ) { - printAPIDocs(); +// Options requests are just for validating CORS +// Lets just pass them through +if ( array_key_exists('REQUEST_METHOD', $_SERVER) && $_SERVER['REQUEST_METHOD'] == 'OPTIONS' ) { + echo 'OK'; exit; } @@ -52,6 +54,7 @@ echo json_encode([ 'success' => false, 'message' => $re->getMessage(), + 'data' => [], ]); exit; } @@ -75,68 +78,65 @@ function loadModule($params) { $valid_actions = array( - 'downloadScreenshot' => 'WebClient', - 'getClosestImage' => 'WebClient', - 'getDataSources' => 'WebClient', - 'getJP2Header' => 'WebClient', - 'getNewsFeed' => 'WebClient', - 'getStatus' => 'WebClient', - 'getSciDataScript' => 'WebClient', - 'getTile' => 'WebClient', - 'downloadImage' => 'WebClient', - 'getUsageStatistics' => 'WebClient', - 'getDataCoverageTimeline' => 'WebClient', - 'getDataCoverage' => 'WebClient', - 'updateDataCoverage' => 'WebClient', // Deprecated, remove in V3, replaced by management scripts - 'shortenURL' => 'WebClient', - 'goto' => 'WebClient', - 'saveWebClientState' => 'WebClient', - 'getWebClientState' => 'WebClient', - 'takeScreenshot' => 'WebClient', - 'getRandomSeed' => 'WebClient', - 'getJP2Image' => 'JHelioviewer', - 'getJPX' => 'JHelioviewer', - 'getJPXClosestToMidPoint' => 'JHelioviewer', - 'launchJHelioviewer' => 'JHelioviewer', - 'downloadMovie' => 'Movies', - 'getMovieStatus' => 'Movies', - 'playMovie' => 'Movies', - 'queueMovie' => 'Movies', - 'reQueueMovie' => 'Movies', - 'uploadMovieToYouTube' => 'Movies', - 'checkYouTubeAuth' => 'Movies', - 'getYouTubeAuth' => 'Movies', - 'getUserVideos' => 'Movies', - 'getObservationDateVideos' => 'Movies', - 'events' => 'SolarEvents', - 'getEventFRMs' => 'SolarEvents', - 'getEvent' => 'SolarEvents', - 'getFRMs' => 'SolarEvents', - 'getDefaultEventTypes' => 'SolarEvents', - 'getEvents' => 'SolarEvents', - 'importEvents' => 'SolarEvents', // Deprecated, remove in V3, replaced by management scripts - 'getEventsByEventLayers' => 'SolarEvents', - 'getEventGlossary' => 'SolarEvents', - 'getSolarBodiesGlossary' => 'SolarBodies', - 'getSolarBodies' => 'SolarBodies', - 'getTrajectoryTime' => 'SolarBodies', - 'logNotificationStatistics' => 'WebClient', - 'getEclipseImage' => 'WebClient' + 'downloadScreenshot' => 'WebClient', + 'getClosestImage' => 'WebClient', + 'getDataSources' => 'WebClient', + 'getJP2Header' => 'WebClient', + 'getNewsFeed' => 'WebClient', + 'getStatus' => 'WebClient', + 'getSciDataScript' => 'WebClient', + 'getTile' => 'WebClient', + 'downloadImage' => 'WebClient', + 'getUsageStatistics' => 'WebClient', + 'getDataCoverageTimeline' => 'WebClient', + 'getDataCoverage' => 'WebClient', + 'updateDataCoverage' => 'WebClient', // Deprecated, remove in V3, replaced by management scripts + 'shortenURL' => 'WebClient', + 'goto' => 'WebClient', + 'saveWebClientState' => 'WebClient', + 'getWebClientState' => 'WebClient', + 'takeScreenshot' => 'WebClient', + 'postScreenshot' => 'WebClient', + 'getRandomSeed' => 'WebClient', + 'getJP2Image' => 'JHelioviewer', + 'getJPX' => 'JHelioviewer', + 'getJPXClosestToMidPoint' => 'JHelioviewer', + 'launchJHelioviewer' => 'JHelioviewer', + 'downloadMovie' => 'Movies', + 'getMovieStatus' => 'Movies', + 'playMovie' => 'Movies', + 'queueMovie' => 'Movies', + 'postMovie' => 'Movies', + 'reQueueMovie' => 'Movies', + 'uploadMovieToYouTube' => 'Movies', + 'checkYouTubeAuth' => 'Movies', + 'getYouTubeAuth' => 'Movies', + 'getUserVideos' => 'Movies', + 'getObservationDateVideos' => 'Movies', + 'events' => 'SolarEvents', + 'getEventFRMs' => 'SolarEvents', + 'getEvent' => 'SolarEvents', + 'getFRMs' => 'SolarEvents', + 'getDefaultEventTypes' => 'SolarEvents', + 'getEvents' => 'SolarEvents', + 'importEvents' => 'SolarEvents', // Deprecated, remove in V3, replaced by management scripts + 'getEventsByEventLayers' => 'SolarEvents', + 'getEventGlossary' => 'SolarEvents', + 'getSolarBodiesGlossary' => 'SolarBodies', + 'getSolarBodies' => 'SolarBodies', + 'getTrajectoryTime' => 'SolarBodies', + 'logNotificationStatistics' => 'WebClient', + 'getEclipseImage' => 'WebClient', + 'getClosestImageDatesForSources' => 'WebClient', ); include_once HV_ROOT_DIR.'/../src/Validation/InputValidator.php'; try { - if ( !array_key_exists('action', $params) || - !array_key_exists($params['action'], $valid_actions) ) { - - $url = HV_WEB_ROOT_URL.'/docs/'; - throw new Exception( - 'Invalid action specified.
Consult the ' . - 'API Documentation for a list of valid actions.', 26 - ); - } - else { + if ( !array_key_exists($params['action'], $valid_actions) ) { + throw new \InvalidArgumentException('Invalid action specified.
Consult the API Documentation for a list of valid actions.'); + } else { + //Set-up variables for rate-limiting $prefix = HV_RATE_LIMIT_PREFIX; //Use IP address as identifier. @@ -170,13 +170,19 @@ function loadModule($params) { $module->execute(); // Update usage stats - $actions_to_keep_stats_for = array('getClosestImage', - 'takeScreenshot', 'getJPX', 'getJPXClosestToMidPoint', 'uploadMovieToYouTube', 'getRandomSeed'); + $actions_to_keep_stats_for = [ + 'getClosestImage', + 'takeScreenshot', + 'postScreenshot', + 'getJPX', + 'getJPXClosestToMidPoint', + 'uploadMovieToYouTube', + 'getRandomSeed', + ]; // Note that in addition to the above, buildMovie requests and // addition to getTile when the tile was already in the cache. - if ( HV_ENABLE_STATISTICS_COLLECTION && - in_array($params['action'], $actions_to_keep_stats_for) ) { + if ( HV_ENABLE_STATISTICS_COLLECTION && in_array($params['action'], $actions_to_keep_stats_for) ) { include_once HV_ROOT_DIR.'/../src/Database/Statistics.php'; $statistics = new Database_Statistics(); @@ -198,9 +204,34 @@ function loadModule($params) { //limit exceeded } } - } - catch (Exception $e) { + } catch (\InvalidArgumentException $e) { + + // Proper response code + http_response_code(400); + + // Determine the content type of the request + $content_type = $_SERVER['CONTENT_TYPE'] ?? ''; + + // If the request is posting JSON + if('application/json' === $content_type) { + + // Set the content type to JSON + header('Content-Type: application/json'); + + echo json_encode([ + 'success' => false, + 'message' => $e->getMessage(), + 'data' => [], + ]); + exit; + } + + printHTMLErrorMsg($e->getMessage()); + + } catch (Exception $e) { + printHTMLErrorMsg($e->getMessage()); + } return true; @@ -263,8 +294,7 @@ function shutDownFunction() { $error = error_get_last(); if (!is_null($error) && $error['type'] == 1) { - handleError(sprintf("%s:%d - %s", $error['file'], $error['line'], - $error['message']), $error->getCode()); + handleError(sprintf("%s:%d - %s", $error['file'], $error['line'], $error['message'])); } } ?> diff --git a/docroot/statistics/statistics.js b/docroot/statistics/statistics.js index 12f16e889..57d482300 100644 --- a/docroot/statistics/statistics.js +++ b/docroot/statistics/statistics.js @@ -15,10 +15,10 @@ var colors = ["#D32F2F", "#9bd927", "#27d9be", "#6527d9", "#0091EA", "#FF6F00", var heirarchy = { "Total":["total","rate_limit_exceeded"], "Client Sites":["standard","embed","minimal"], - "Images":["takeScreenshot","getTile","getClosestImage","getJP2Image-web","getJP2Image-jpip","getJP2Image","downloadScreenshot","getJPX","getJPXClosestToMidPoint", "downloadImage"], - "Movies":["buildMovie","getMovieStatus","queueMovie","reQueueMovie","playMovie","downloadMovie","getUserVideos","getObservationDateVideos","uploadMovieToYouTube","checkYouTubeAuth","getYouTubeAuth"], + "Images":["takeScreenshot","postScreenshot","getTile","getClosestImage","getJP2Image-web","getJP2Image-jpip","getJP2Image","downloadScreenshot","getJPX","getJPXClosestToMidPoint", "downloadImage"], + "Movies":["buildMovie","getMovieStatus","queueMovie","postMovie","reQueueMovie","playMovie","downloadMovie","getUserVideos","getObservationDateVideos","uploadMovieToYouTube","checkYouTubeAuth","getYouTubeAuth"], "Events":["getEventGlossary", "events", "getEvents","getFRMs","getEvent","getEventFRMs","getDefaultEventTypes","getEventsByEventLayers","importEvents"], - "Data":["getRandomSeed","getDataSources","getJP2Header","getDataCoverage","getStatus","getNewsFeed","getDataCoverageTimeline","getClosestData","getSolarBodiesGlossary","getSolarBodies","getTrajectoryTime","sciScript-SSWIDL","sciScript-SunPy","getSciDataScript","updateDataCoverage","getEclipseImage"], + "Data":["getRandomSeed","getDataSources","getJP2Header","getDataCoverage","getStatus","getNewsFeed","getDataCoverageTimeline","getClosestData","getSolarBodiesGlossary","getSolarBodies","getTrajectoryTime","sciScript-SSWIDL","sciScript-SunPy","getSciDataScript","updateDataCoverage","getEclipseImage","getClosestImageDatesForSources"], "Other":["shortenURL", "goto", "getUsageStatistics","movie-notifications-granted","movie-notifications-denied","logNotificationStatistics","launchJHelioviewer", "saveWebClientState", "getWebClientState"], "WebGL":["getTexture","getGeometryServiceData"] }; diff --git a/docs/src/source/api/api_groups/movies.rst b/docs/src/source/api/api_groups/movies.rst index 911768a7a..8b8703c72 100644 --- a/docs/src/source/api/api_groups/movies.rst +++ b/docs/src/source/api/api_groups/movies.rst @@ -26,6 +26,7 @@ See the Coordinates Appendix for more infomration about working with the coordin used by Helioviewer.org. .. include:: movies/queueMovie.rst +.. include:: movies/postMovie.rst .. include:: movies/reQueueMovie.rst .. include:: movies/getMovieStatus.rst .. include:: movies/downloadMovie.rst diff --git a/docs/src/source/api/api_groups/movies/downloadMovie.rst b/docs/src/source/api/api_groups/movies/downloadMovie.rst index 003236fd0..74af88864 100644 --- a/docs/src/source/api/api_groups/movies/downloadMovie.rst +++ b/docs/src/source/api/api_groups/movies/downloadMovie.rst @@ -11,7 +11,7 @@ Download a custom movie in one of three file formats. +===========+==========+=========+=========+==================================================================================================+ | id | Required | string | VXvX5 | Unique movie identifier (provided by the response to a `queueMovie` request). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------------------------+ - | format | Required | string | mp4 | Movie Format (`mp4`, `webm`, or `flv`). | + | format | Required | string | mp4 | Movie Format (`mp4`, `webm`). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------------------------+ | hq | Optional | boolean | true | Optionally download a higher-quality movie file (valid for .mp4 movies only, ignored otherwise). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------------------------+ diff --git a/docs/src/source/api/api_groups/movies/getMovieStatus.rst b/docs/src/source/api/api_groups/movies/getMovieStatus.rst index 11e4c05b4..351a28c34 100644 --- a/docs/src/source/api/api_groups/movies/getMovieStatus.rst +++ b/docs/src/source/api/api_groups/movies/getMovieStatus.rst @@ -9,7 +9,7 @@ GET /v2/getMovieStatus/ +===========+==========+=========+=========+================================================================================+ | id | Required | string | VXvX5 | Unique movie identifier (provided by the response to a `queueMovie` request). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------+ - | format | Required | string | mp4 | Movie format (`mp4`, `webm`, or `flv`). | + | format | Required | string | mp4 | Movie format (`mp4`, `webm`). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------+ | verbose | Optional | boolean | true | Optionally include extra metadata in the response. | +-----------+----------+---------+---------+--------------------------------------------------------------------------------+ diff --git a/docs/src/source/api/api_groups/movies/playMovie.rst b/docs/src/source/api/api_groups/movies/playMovie.rst index a3c548598..d197500fe 100644 --- a/docs/src/source/api/api_groups/movies/playMovie.rst +++ b/docs/src/source/api/api_groups/movies/playMovie.rst @@ -11,7 +11,7 @@ Output an HTML web page with the requested movie embedded within. +===========+==========+=========+=========+==================================================================================================+ | id | Required | string | VXvX5 | Unique movie identifier (provided by the response to a `queueMovie` request). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------------------------+ - | format | Required | string | mp4 | Movie format (mp4, webm, or flv). | + | format | Required | string | mp4 | Movie format (mp4, webm). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------------------------+ | hq | Optional | boolean | true | Optionally download a higher-quality movie file (valid for .mp4 movies only, ignored otherwise). | +-----------+----------+---------+---------+--------------------------------------------------------------------------------------------------+ diff --git a/docs/src/source/api/api_groups/movies/postMovie.rst b/docs/src/source/api/api_groups/movies/postMovie.rst new file mode 100644 index 000000000..48565410f --- /dev/null +++ b/docs/src/source/api/api_groups/movies/postMovie.rst @@ -0,0 +1,265 @@ +postMovie +^^^^^^^^^ + +**URL:** ``/v2/postMovie/`` + +**Method:** ``POST`` + +**Content-Type:** ``application/json`` + +Create a custom movie with a POST request by submitting JSON to the movie generation queue. +The response returned will provide you with a unique Movie ID that can be used +to check the status of your movie (via `getMovieStatus <#getmoviestatus>`_) +and to download your movie (via `downloadMovie <#downloadmovie>`_). + +Request Format +~~~~~~~~~~~~~~ + +The request must be a JSON object with the following structure: + +.. code-block:: json + + { + "date" : "2014-01-01T23:59:59Z", + "imageScale" : 2.4204409, + "layers" : "[3,1,100]" + } + +Parameters +~~~~~~~~~~ + +Request JSON object consist of following parameters + +.. list-table:: JSON Request Parameters: + :header-rows: 1 + + * - Parameter + - Required + - Type + - Example + - Description + * - ``startTime`` + - Required + - string + - 2010-03-01T12:12:12Z + - Desired date and time of the first frame of the movie. ISO 8601 combined UTC date and time UTC format. + * - ``endTime`` + - Required + - string + - 2010-03-04T12:12:12Z + - Desired date and time of the final frame of the movie. ISO 8601 combined UTC date and time UTC format. + * - ``layers`` + - Required + - string + - | [3,1,100] + | or + | [3,1,100,2,60,1,2010-03-01T12:12:12.000Z] + - Image datasource layer(s) to include in the movie. + * - ``eventsState`` + - Optional + - object + - | { + | "tree_HEK": { + | "labels_visible": true, + | "layers": [ + | { + | "event_type": "flare", + | "frms": ["frm10", "frm20"], + | "event_instances": ["flare--frm1--event1", "flare--frm2--event2"] + | } + | ] + | }, + | .... + | } + - | Optional list of feature/event types to use to annotate the movie. + | To get more information about this structure, please see document : :ref:`events-state-page` + * - ``imageScale`` + - Required + - number + - 21.04 + - Image scale in arcseconds per pixel. + * - ``format`` + - Optional + - string + - mp4 + - Movie format (`mp4`, `webm`). Default value is `mp4`. + * - ``frameRate`` + - Optional + - string + - 15 + - Movie frames per second. 15 frames per second by default. + * - ``maxFrames`` + - Optional + - string + - 300 + - Maximum number of frames in the movie. May be capped by the server. + * - ``scale`` + - Optional + - boolean + - false + - Optionally overlay an image scale indicator. + * - ``scaleType`` + - Optional + - string + - earth + - Image scale indicator. + * - ``scaleX`` + - Optional + - number + - -1000 + - Horizontal offset of the image scale indicator in arcseconds with respect to the center of the Sun. + * - ``scaleY`` + - Optional + - number + - -500 + - Vertical offset of the image scale indicator in arcseconds with respect to the center of the Sun. + * - ``movieLength`` + - Optional + - number + - 4.3333 + - Movie length in seconds. + * - ``watermark`` + - Optional + - boolean + - true + - Optionally overlay a Helioviewer.org watermark image. Enabled by default. + * - ``width`` + - Optional + - string + - 1920 + - Width of the field of view in pixels. (Used in conjunction width `x0`,`y0`, and `height`). + * - ``height`` + - Optional + - string + - 1200 + - Height of the field of view in pixels. (Used in conjunction width `x0`,`y0`, and `width`). + * - ``x0`` + - Optional + - string + - 0 + - The horizontal offset of the center of the field of view from the center of the Sun. Used in conjunction with `y0`, `width`, and `height`. + * - ``y0`` + - Optional + - string + - 0 + - The vertical offset of the center of the field of view from the center of the Sun. Used in conjunction with `x0`, `width`, and `height`. + * - ``x1`` + - Optional + - string + - -5000 + - The horizontal offset of the top-left corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `y1`, `x2`, and `y2`. + * - ``y1`` + - Optional + - string + - -5000 + - The vertical offset of the top-left corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `x1`, `x2`, and `y2`. + * - ``x2`` + - Optional + - string + - 5000 + - The horizontal offset of the bottom-right corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `x1`, `y1`, and `y2`. + * - ``y2`` + - Optional + - string + - 5000 + - The vertical offset of the bottom-right corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `x1`, `y1`, and `x2`. + * - ``callback`` + - Optional + - string + - + - Wrap the response object in a function call of your choosing. + * - ``size`` + - Optional + - number + - 0 + - | Scale video to preset size + | 0 - Original size + | 1 - 720p (1280 x 720, HD Ready); + | 2 - 1080p (1920 x 1080, Full HD); + | 3 - 1440p (2560 x 1440, Quad HD); + | 4 - 2160p (3840 x 2160, 4K or Ultra HD). + * - ``movieIcons`` + - Optional + - number + - 0 + - Display other user generated movies on the video. + * - ``followViewport`` + - Optional + - number + - 0 + - Rotate field of view of movie with Sun. + * - ``reqObservationDate`` + - Optional + - string + - 2017-08-30T14:45:53.000Z + - Viewport time. Used when 'followViewport' enabled to shift viewport area to correct coordinates. + +Example: Queued Movie (JSON) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +JSON response to "postMovie" API requests. + +.. code-block:: http + :caption: Example Request: + + POST /v2/postMovie/ HTTP/1.1 + Host: api.helioviewer.org + + Content-Type: application/json + { + "startTime" : "2010-03-01T12:12:12Z", + "endTime" : "2010-03-04T12:12:12Z", + "imageScale" : 21.04, + "layers" : "[3,1,100]", + "eventsState" : { + "tree_HEK": { + "labels_visible": true, + "layers": [ + { + "event_type": "flare", + "frms": ["frm10", "frm20"], + "event_instances": ["flare--frm1--event1", "flare--frm2--event2"] + } + ] + }, + }, + "x1" : -5000, + "y1" : -5000, + "x2" : 5000, + "y2" : 5000, + } + +.. code-block:: json + :caption: Example Response: + + { + "id": "z6vX5", + "eta": 376, + "queue": 0, + "token": "50e0d98f645b42d159ec1c8a1e15de3e" + } + +.. list-table:: JSON Response Parameters: + :header-rows: 1 + + * - Parameter + - Required + - Type + - Description + * - ``id`` + - Required + - string + - Unique movie identifier (e.g. "z6vX5") + * - ``eta`` + - Required + - number + - Estimated time until movie generation will be completed in seconds + * - ``queue`` + - Required + - number + - Position in movie generation queue + * - ``token`` + - Required + - string + - Handle to job in the movie builder queue + diff --git a/docs/src/source/api/api_groups/movies/queueMovie.rst b/docs/src/source/api/api_groups/movies/queueMovie.rst index 741ff39be..cc39ec3ab 100644 --- a/docs/src/source/api/api_groups/movies/queueMovie.rst +++ b/docs/src/source/api/api_groups/movies/queueMovie.rst @@ -26,7 +26,7 @@ and to download your movie (via `downloadMovie <#downloadmovie>`_). +--------------------+----------+---------+------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | imageScale | Required | number | 21.04 | Image scale in arcseconds per pixel. | +--------------------+----------+---------+------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | format | Optional | string | mp4 | Movie format (`mp4`, `webm`, `flv`). Default value is `mp4`. | + | format | Optional | string | mp4 | Movie format (`mp4`, `webm`). Default value is `mp4`. | +--------------------+----------+---------+------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | frameRate | Optional | string | 15 | Movie frames per second. 15 frames per second by default. | +--------------------+----------+---------+------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ diff --git a/docs/src/source/api/api_groups/screenshots.rst b/docs/src/source/api/api_groups/screenshots.rst index ac0dc4b36..e9918f114 100644 --- a/docs/src/source/api/api_groups/screenshots.rst +++ b/docs/src/source/api/api_groups/screenshots.rst @@ -7,5 +7,6 @@ extended region polygons, associated text labels, and a size-of-earth scale indicator can optionally be overlayed onto a movie. .. include:: screenshots/takeScreenshot.rst +.. include:: screenshots/postScreenshot.rst .. include:: screenshots/downloadScreenshot.rst .. include:: screenshots/getEclipseImage.rst diff --git a/docs/src/source/api/api_groups/screenshots/postScreenshot.rst b/docs/src/source/api/api_groups/screenshots/postScreenshot.rst new file mode 100644 index 000000000..1bec34a03 --- /dev/null +++ b/docs/src/source/api/api_groups/screenshots/postScreenshot.rst @@ -0,0 +1,215 @@ +postScreenshot +^^^^^^^^^^^^^^ + +**URL:** ``/v2/postScreenshot/`` + +**Method:** ``POST`` + +**Content-Type:** ``application/json`` + +Generate a custom screenshot with JSON POST request. + +You must specify values for either `x1`, `y1`, `x2`, and `y2` +or `x0`, `y0`, `width` and `height` inside JSON. + +By default, the response is a JSON object containing a unique screenshot +identifier (`id`) that can be used to with the `downloadScreenshot` API endpoint. + +Set the `display` parameter to `true` to directly return the screenshot as +binary PNG image data in the response. + +Please note that each request causes the server to generate a screenshot from +scratch and is resource intensive. For performance reasons, you should cache the +response if you simply intend to serve exactly the same screenshot to multiple +users. + +Request Format +~~~~~~~~~~~~~~ + +The request must be a JSON object with the following structure: + +.. code-block:: json + + { + "date" : "2014-01-01T23:59:59Z", + "imageScale" : 2.4204409, + "layers" : "[3,1,100]" + } + +Parameters +~~~~~~~~~~ + +Request JSON object consist of following parameters + +.. list-table:: JSON Request Parameters: + :header-rows: 1 + + * - Parameter + - Required + - Type + - Example + - Description + * - ``date`` + - Required + - string + - 2014-01-01T23:59:59Z + - Desired date/time of the image. ISO 8601 combined UTC date and time UTC format. + * - ``imageScale`` + - Required + - number + - 2.4204409 + - Image scale in arcseconds per pixel. + * - ``layers`` + - Required + - string + - | [3,1,100] + | or + | [3,1,100,2,60,1,2010-03-01T12:12:12.000Z] + - Image datasource layer(s) to include in the screenshot. + * - ``eventsState`` + - Optional + - object + - | { + | "tree_HEK": { + | "labels_visible": true, + | "layers": [ + | { + | "event_type": "flare", + | "frms": ["frm10", "frm20"], + | "event_instances": ["flare--frm1--event1", "flare--frm2--event2"] + | } + | ] + | }, + | .... + | } + - | List feature/event types and FRMs to use to annotate the screenshot. Use the empty string to indicate that no feature/event annotations should be shown. + | To get more information about this structure, please see document : :ref:`events-state-page` + * - ``scale`` + - Optional + - boolean + - false + - Optionally overlay an image scale indicator. + * - ``scaleType`` + - Optional + - string + - earth + - Image scale indicator. + * - ``scaleX`` + - Optional + - number + - -1000 + - Horizontal offset of the image scale indicator in arcseconds with respect to the center of the Sun. + * - ``scaleY`` + - Optional + - number + - -500 + - Vertical offset of the image scale indicator in arcseconds with respect to the center of the Sun. + * - ``width`` + - Optional + - string + - 1920 + - Width of the field of view in pixels. (Used in conjunction width `x0`,`y0`, and `height`). + * - ``height`` + - Optional + - string + - 1200 + - Height of the field of view in pixels. (Used in conjunction width `x0`,`y0`, and `width`). + * - ``x0`` + - Optional + - string + - 0 + - The horizontal offset of the center of the field of view from the center of the Sun. Used in conjunction with `y0`, `width`, and `height`. + * - ``y0`` + - Optional + - string + - 0 + - The vertical offset of the center of the field of view from the center of the Sun. Used in conjunction with `x0`, `width`, and `height`. + * - ``x1`` + - Optional + - string + - -5000 + - The horizontal offset of the top-left corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `y1`, `x2`, and `y2`. + * - ``y1`` + - Optional + - string + - -5000 + - The vertical offset of the top-left corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `x1`, `x2`, and `y2`. + * - ``x2`` + - Optional + - string + - 5000 + - The horizontal offset of the bottom-right corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `x1`, `y1`, and `y2`. + * - ``y2`` + - Optional + - string + - 5000 + - The vertical offset of the bottom-right corner of the field of view with respect to the center of the Sun (in arcseconds). Used in conjunction with `x1`, `y1`, and `x2`. + * - ``display`` + - Optional + - boolean + - false + - Set to `true` to directly output binary PNG image data. Default is `false` (which outputs a JSON object). + * - ``watermark`` + - Optional + - boolean + - true + - Optionally overlay a watermark consisting of a Helioviewer logo and the datasource abbreviation(s) and timestamp(s) displayed in the screenshot. + * - ``callback`` + - Optional + - string + - + - Wrap the response object in a function call of your choosing. + +Example: Post Screenshot (JSON) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +JSON response to "postScreenshot" API requests. Assumes that the `display` +parameter was omitted or set to `false`. + +.. code-block:: http + :caption: Example Request: + + POST /v2/postScreenshot/ HTTP/1.1 + Host: api.helioviewer.org + + Content-Type: application/json + { + "date" : "2014-01-01T23:59:59Z", + "imageScale" : 2.4204409, + "layers" : "[3,1,100]" + } + +.. code-block:: json + :caption: Example Response: + + { + "id": 3285980 + } + +.. table:: Response Description + + +-----------+----------+--------+-----------------------------------------------+ + | Parameter | Required | Type | Description | + +===========+==========+========+===============================================+ + | id | Required | string | Unique screenshot identifier (e.g. "3285980") | + +-----------+----------+--------+-----------------------------------------------+ + +Example: binary (PNG image data) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Set the `display` parameter to `true` to directly return binary PNG image data +in the response. + +.. code-block:: http + :caption: Example Request: + + POST /v2/postScreenshot/ HTTP/1.1 + Host: api.Helioviewer.org + + Content-Type: application/json + { + "date" : "2014-01-01T23:59:59Z", + "imageScale" : "2.4204409", + "layers" : "[3,1,100]", + "display" : true + } diff --git a/docs/src/source/appendix.rst b/docs/src/source/appendix.rst index e57830fff..20d115ff3 100644 --- a/docs/src/source/appendix.rst +++ b/docs/src/source/appendix.rst @@ -9,3 +9,4 @@ The appendices below provide further context for Helioviewer API usage. appendix/data_sources.rst appendix/coordinates.rst appendix/helioviewer_event_format.rst + appendix/events_state.rst diff --git a/docs/src/source/appendix/events_state.rst b/docs/src/source/appendix/events_state.rst new file mode 100644 index 000000000..e30b562e0 --- /dev/null +++ b/docs/src/source/appendix/events_state.rst @@ -0,0 +1,135 @@ +.. _events-state-page: + +Events State +============ + +This document describes the structure of the events state JSON string, +which is used for filtering included event markers into the screenshots and movies, also for controlling the visibility of their labels. + +Structure +--------- + +The Event State data type is a JSON object with the following structure: + +.. code-block:: json + + { + "event_group_key": { + "labels_visible": true, + "layers": [ + { + "event_type": "flare", + "frms": ["frm10", "frm20"], + "event_instances": ["flare--frm1--event1", "flare--frm2--event2"] + } + ] + } + } + +Parameters +---------- + +The following table describes each parameter in the JSON object: + +.. list-table:: Event State Parameters + :header-rows: 1 + + * - Parameter + - Type + - Description + * - ``event_group_key`` + - object + - The root object representing an event layer grouping, such as CCMC or HEK. This contains the configuration for the grouping. + * - ``event_group_key.labels_visible`` + - boolean + - Controls the visibility of all event labels under this event layer grouping. + * - ``event_group_key.layers`` + - array + - An array of layer objects specifying which events to include in the generated screenshots and movies. + +Each layer object within the ``layers`` array contains the following fields: + +.. list-table:: Layer Parameters + :header-rows: 1 + + * - Parameter + - Type + - Description + * - ``event_type`` + - string + - The pin of the event (e.g., "FP") to be included into the screenshot and movies. Please see :ref:`helioviewer-event-format` for getting more information about event pin + * - ``frms`` + - array + - An array of strings representing for the event group names to be included into the screenshot and movies. Please see :ref:`helioviewer-event-format` for getting more information about event group title + * - ``event_instances`` + - array + - An array of strings representing the unique IDs of the event instances. Please see :ref:`event-instance-algorithm` for details on generating these IDs from events. + +Example +------- + +Below is an example of a Event State JSON object: + +.. code-block:: json + + { + "tree_HEK": { + "labels_visible": true, + "layers": [ + { + "event_type": "flare", + "frms": ["frm10", "frm20"], + "event_instances": ["flare--frm1--event1", "flare--frm2--event2"] + } + ] + } + } + +Description +----------- + +- **labels_visible**: This boolean field indicates whether the labels for all the event labels under this tree configuration should be visible. If set to `true`, labels are visible; if set to `false`, labels are hidden. +- **layers**: This array contains filtering configuration specifying which events should be included in to the generated screenshots and movies. Each layer provides different levels of filtering: + + - **event_type**: Includes all the events under this event pin. + - **frms**: Includes all of the events associated with the group names in this array. Please see :ref:`helioviewer-event-format` for getting more information about event group names. + - **event_instances**: Includes specific event instances identified by their unique IDs. Each ID follows the format `event_type--frm--event_id`. Please see :ref:`event-instance-algorithm` for details on generating these IDs. + +This structure allows you to filter which event markers are included in the generated screenshots and movies. + +.. _event-instance-algorithm: + +Individual Event IDs +-------------------- + +.. warning:: + This event ID generation is undergoing active development and may change without notice. + +Event IDs are generated from three components: + +- **event_pin**: The pin of the event. +- **event_group_name**: The name of the event group. +- **event_id**: The unique identifier of the event. + +Please see :ref:`helioviewer-event-format` for more information about these fields. After obtaining these fields, users should base64 encode ``event_id``, perform some cleaning, and join them with ``--``. + +Here is our implementation in PHP. + +.. code-block:: php + + + + +This method ensures that the event IDs are unique and suitable for use in filtering events. + + diff --git a/docs/src/source/conf.py b/docs/src/source/conf.py index b9e324479..2cf58c2c0 100644 --- a/docs/src/source/conf.py +++ b/docs/src/source/conf.py @@ -50,4 +50,6 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['appendix/images'] + +pygments_style = 'solarized-dark' diff --git a/install/database/2024_05_31_add_events_state_to_both_screenshots_and_movies_tables.sql b/install/database/2024_05_31_add_events_state_to_both_screenshots_and_movies_tables.sql new file mode 100644 index 000000000..4dea9ea79 --- /dev/null +++ b/install/database/2024_05_31_add_events_state_to_both_screenshots_and_movies_tables.sql @@ -0,0 +1,20 @@ +-- Add eventState JSON column with default JSON value +ALTER TABLE screenshots ADD COLUMN eventsState JSON NOT NULL DEFAULT '{}' AFTER eventsLabels; + +-- Update existing rows to have the eventsState as {} +UPDATE screenshots SET eventsState = '{}' WHERE eventsState IS NULL OR eventsState = ''; + +-- Add eventState JSON column with default JSON value +ALTER TABLE movies ADD COLUMN eventsState JSON NOT NULL DEFAULT '{}' AFTER eventsLabels; + +-- Update existing rows to have the eventsState as {} +UPDATE movies SET eventsState = '{}' WHERE eventsState IS NULL OR eventsState = ''; + + + + + + + + + diff --git a/install/helioviewer/db.py b/install/helioviewer/db.py index c0da0bee8..af7dce02e 100644 --- a/install/helioviewer/db.py +++ b/install/helioviewer/db.py @@ -942,6 +942,7 @@ def create_movies_table(cursor): `dataSourceBitMask` BIGINT UNSIGNED, `eventSourceString` VARCHAR(1024) DEFAULT NULL, `eventsLabels` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', + `eventsState` JSON NOT NULL DEFAULT '{}', `movieIcons` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', `followViewport` tinyint(1) DEFAULT '0', `scale` TINYINT(1) UNSIGNED NOT NULL DEFAULT '0', @@ -1148,6 +1149,7 @@ def create_screenshots_table(cursor): `dataSourceBitMask` BIGINT UNSIGNED, `eventSourceString` VARCHAR(1024) DEFAULT NULL, `eventsLabels` TINYINT(1) UNSIGNED NOT NULL, + `eventsState` JSON NOT NULL DEFAULT '{}', `movieIcons` tinyint(1) UNSIGNED NOT NULL DEFAULT '0', `scale` TINYINT(1) unsigned NOT NULL DEFAULT '0', `scaleType` VARCHAR(12) DEFAULT 'earth', diff --git a/scripts/hv_stats/hv_stats.py b/scripts/hv_stats/hv_stats.py index 957878a81..4948a945b 100644 --- a/scripts/hv_stats/hv_stats.py +++ b/scripts/hv_stats/hv_stats.py @@ -2271,10 +2271,10 @@ def add_harea_if_timestamp_in_range(start_time, end_time, color, alpha, label): heirarchy = { "Total":["total","rate_limit_exceeded"], "Client Sites":["standard","embed","minimal"], - "Images":["takeScreenshot","getTile","getClosestImage","getJP2Image-web","getJP2Image-jpip","getJP2Image","downloadScreenshot","getJPX","getJPXClosestToMidPoint"], - "Movies":["buildMovie","getMovieStatus","queueMovie","reQueueMovie","playMovie","downloadMovie","getUserVideos","getObservationDateVideos","uploadMovieToYouTube","checkYouTubeAuth","getYouTubeAuth"], + "Images":["takeScreenshot","postScreenshot","getTile","getClosestImage","getJP2Image-web","getJP2Image-jpip","getJP2Image","downloadScreenshot","getJPX","getJPXClosestToMidPoint"], + "Movies":["buildMovie","postMovie","getMovieStatus","queueMovie","postMovie","reQueueMovie","playMovie","downloadMovie","getUserVideos","getObservationDateVideos","uploadMovieToYouTube","checkYouTubeAuth","getYouTubeAuth"], "Events":["getEventGlossary","getEvents","getFRMs","getEvent","getEventFRMs","getDefaultEventTypes","getEventsByEventLayers","importEvents"], - "Data":["getRandomSeed","getDataSources","getJP2Header","getDataCoverage","getStatus","getNewsFeed","getDataCoverageTimeline","getClosestData","getSolarBodiesGlossary","getSolarBodies","getTrajectoryTime","sciScript-SSWIDL","sciScript-SunPy","getSciDataScript","updateDataCoverage","getEclipseImage"], + "Data":["getRandomSeed","getDataSources","getJP2Header","getDataCoverage","getStatus","getNewsFeed","getDataCoverageTimeline","getClosestData","getSolarBodiesGlossary","getSolarBodies","getTrajectoryTime","sciScript-SSWIDL","sciScript-SunPy","getSciDataScript","updateDataCoverage","getEclipseImage", "getClosestImageDatesForSources"], "Other":["shortenURL","getUsageStatistics","movie-notifications-granted","movie-notifications-denied","logNotificationStatistics","launchJHelioviewer", "saveWebClientState", "getWebClientState"], "WebGL":["getTexture","getGeometryServiceData"] }; diff --git a/scripts/resque.php b/scripts/resque.php index 436ead29c..4232bc033 100644 --- a/scripts/resque.php +++ b/scripts/resque.php @@ -6,6 +6,7 @@ require_once __DIR__.'/../lib/Resque.php'; require_once __DIR__.'/../lib/Resque/Worker.php'; +require_once __DIR__.'/../vendor/autoload.php'; $REDIS_BACKEND = getenv('REDIS_BACKEND'); if(!empty($REDIS_BACKEND)) { diff --git a/src/Database/ImgIndex.php b/src/Database/ImgIndex.php index 672d2f212..d23e13bf5 100644 --- a/src/Database/ImgIndex.php +++ b/src/Database/ImgIndex.php @@ -43,14 +43,24 @@ protected function _dbConnect() { * * @return int Identifier in the `screenshots` table */ - public function insertScreenshot($date, $imageScale, $roi, $watermark, - $layers, $bitmask, $events, $eventsLabels, $movieIcons, $scale, $scaleType, - $scaleX, $scaleY, $numLayers, $switchSources, $celestialLabels, $celestialTrajectories) { + public function insertScreenshot($date, $imageScale, $roi, $watermark, $layers, $bitmask, $eventsStateString, $movieIcons, $scale, $scaleType,$scaleX, $scaleY, $numLayers, $switchSources, $celestialLabels, $celestialTrajectories) { include_once HV_ROOT_DIR.'/../src/Helper/DateTimeConversions.php'; $this->_dbConnect(); + // ATTENTION! These two fields eventsLabels and eventSourceString needs to be kept in DB schema + // We are keeping them to support old takeScreenshot , queueMovie requests + + // old implementation removed for events strings + // used to be $this->events->serialize(); + $old_events_layer_string = ""; + + // old if events labels are shown switch , removed for new implementation + // used to be $this->eventsLabels; + $old_events_labels_bool = false; + + $sql = sprintf( "INSERT INTO screenshots " . "SET " @@ -64,7 +74,8 @@ public function insertScreenshot($date, $imageScale, $roi, $watermark, . "dataSourceBitMask " . " = %d, " . "eventSourceString " . " ='%s', " . "eventsLabels " . " = %b, " - . "movieIcons " . " = %b, " + . "eventsState " . " = '%s', " + . "movieIcons " . " = %b, " . "scale " . " = %b, " . "scaleType " . " ='%s', " . "scaleX " . " = %f, " @@ -74,23 +85,18 @@ public function insertScreenshot($date, $imageScale, $roi, $watermark, . "celestialBodiesLabels" . " = '%s', " . "celestialBodiesTrajectories" . " = '%s';", - $this->_dbConnection->link->real_escape_string( - isoDateToMySQL($date) ), + $this->_dbConnection->link->real_escape_string(isoDateToMySQL($date)), (float)$imageScale, - $this->_dbConnection->link->real_escape_string( - $roi ), + $this->_dbConnection->link->real_escape_string($roi), (bool)$watermark, - $this->_dbConnection->link->real_escape_string( - $layers ), - bindec($this->_dbConnection->link->real_escape_string( - (binary)$bitmask ) ), - $this->_dbConnection->link->real_escape_string( - $events ), - (bool)$eventsLabels, + $this->_dbConnection->link->real_escape_string($layers), + bindec($this->_dbConnection->link->real_escape_string((binary)$bitmask)), + $old_events_layer_string, // eventsSourceString is always empty not used any more + $old_events_labels_bool, // eventLabels is not used anymore + $this->_dbConnection->link->real_escape_string($eventsStateString), (bool)$movieIcons, (bool)$scale, - $this->_dbConnection->link->real_escape_string( - $scaleType ), + $this->_dbConnection->link->real_escape_string($scaleType), (float)$scaleX, (float)$scaleY, (int)$numLayers, @@ -98,11 +104,11 @@ public function insertScreenshot($date, $imageScale, $roi, $watermark, $celestialLabels, $celestialTrajectories ); + try { $result = $this->_dbConnection->query($sql); - } - catch (Exception $e) { - return false; + } catch (Exception $e) { + throw new \Exception("Could not create screenshot in our database", 2, $e); } return $this->_dbConnection->getInsertId(); @@ -502,6 +508,74 @@ protected function _getGroupForSourceId($sourceId) { return ""; } + /** + * Return the closest matches from the `data` table whose time is just before and just after of the given time + * there can be null dates, if there is before or after image + * @param string $date UTC date string like "2003-10-05T00:00:00Z" + * @param int $sourceId The data source identifier in the database + * + * @return array Array containing 1 next image and 1 prev date image + */ + public function getClosestDataBeforeAndAfter($date, $sourceId) + { + include_once HV_ROOT_DIR.'/../src/Helper/DateTimeConversions.php'; + + $this->_dbConnect(); + + $datestr = isoDateToMySQL($date); + + // Before date first image + $sql_before = sprintf( + "SELECT date " + . "FROM data " + . "WHERE " + . "sourceId " . " = %d AND " + . "date " . "< '%s' " + . "ORDER BY date DESC " + . "LIMIT 1;", + (int)$sourceId, + $this->_dbConnection->link->real_escape_string($datestr) + ); + + try { + $result = $this->_dbConnection->query($sql_before); + } catch (Exception $e) { + throw new \Exception("Unable to find before image for ".$date." and sourceId:".$sourceId, 2, $e); + } + + $before_date = $result->fetch_array(MYSQLI_ASSOC); + + // After date first image + $sql_after = sprintf( + "SELECT date " + . "FROM data " + . "WHERE " + . "sourceId " . " = %d AND " + . "date " . "> '%s' " + . "ORDER BY date ASC " + . "LIMIT 1;", + (int)$sourceId, + $this->_dbConnection->link->real_escape_string($datestr) + ); + + try { + $result = $this->_dbConnection->query($sql_after); + } catch (Exception $e) { + throw new \Exception("Unable to find before image for ".$date." and sourceId:".$sourceId, 2, $e); + } + + // pre($result); + $after_date = $result->fetch_array(MYSQLI_ASSOC); + + return [ + 'prev_date' => $before_date ? $before_date['date']: null, + 'next_date' => $after_date ? $after_date['date'] : null, + ]; + + } + + + /** * Return the closest match from the `data` table whose time is on * or before the specified time. diff --git a/src/Database/MovieDatabase.php b/src/Database/MovieDatabase.php index 9ec90858f..3d680da1a 100644 --- a/src/Database/MovieDatabase.php +++ b/src/Database/MovieDatabase.php @@ -48,20 +48,32 @@ private function _dbConnect() { * @return int Identifier in the `movies` table or boolean false */ public function insertMovie($startTime, $endTime, $reqObservationDate, $imageScale, $roi, - $maxFrames, $watermark, $layerString, $layerBitMask, $eventString, - $eventsLabels, $movieIcons, $followViewport, $scale, $scaleType, $scaleX, $scaleY, $numLayers, + $maxFrames, $watermark, $layerString, $layerBitMask, $eventsStateString, + $movieIcons, $followViewport, $scale, $scaleType, $scaleX, $scaleY, $numLayers, $queueNum, $frameRate, $movieLength, $size, $switchSources, $celestialBodies) { $this->_dbConnect(); $startTime = isoDateToMySQL($startTime); $endTime = isoDateToMySQL($endTime); + if($reqObservationDate != false){ - $reqObservationDate = '"'.$this->_dbConnection->link->real_escape_string(isoDateToMySQL($reqObservationDate)).'"'; + $reqObservationDate = '"'.$this->_dbConnection->link->real_escape_string(isoDateToMySQL($reqObservationDate)).'"'; }else{ - $reqObservationDate = "NULL"; + $reqObservationDate = "NULL"; } + // ATTENTION! These two fields eventsLabels and eventSourceString needs to be kept in DB schema + // We are keeping them to support old takeScreenshot , queueMovie requests + + // old implementation removed for events strings + // used to be $this->events->serialize(); + $old_events_layer_string = ""; + + // old if events labels are shown switch , removed for new implementation + // used to be $this->eventsLabels; + $old_events_labels_bool = false; + $sql = sprintf( 'INSERT INTO movies ' . 'SET ' @@ -78,6 +90,7 @@ public function insertMovie($startTime, $endTime, $reqObservationDate, $imageSca . 'dataSourceBitMask ' . ' = %d, ' . 'eventSourceString ' . ' ="%s", ' . 'eventsLabels ' . ' = %b, ' + . 'eventsState ' . ' = "%s", ' . 'movieIcons ' . ' = %b, ' . 'followViewport ' . ' = %b, ' . 'scale ' . ' = %b, ' @@ -107,8 +120,9 @@ public function insertMovie($startTime, $endTime, $reqObservationDate, $imageSca (bool)$watermark, $this->_dbConnection->link->real_escape_string($layerString), $layerBitMask, - $this->_dbConnection->link->real_escape_string($eventString), - (bool)$eventsLabels, + $old_events_layer_string, // eventsSourceString is always empty not used any more + $old_events_labels_bool, // eventLabels is not used anymore + $this->_dbConnection->link->real_escape_string($eventsStateString), (bool)$movieIcons, (bool)$followViewport, (bool)$scale, @@ -130,7 +144,7 @@ public function insertMovie($startTime, $endTime, $reqObservationDate, $imageSca } catch (Exception $e) { error_log("Failed to insert movie: " . $e->getMessage()); - return false; + throw new \Exception("Could create movie in our database", 2, $e); } return $this->_dbConnection->getInsertId(); diff --git a/src/Database/Statistics.php b/src/Database/Statistics.php index 2017d981d..eb20ef56d 100644 --- a/src/Database/Statistics.php +++ b/src/Database/Statistics.php @@ -424,11 +424,14 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu // Array to keep track of counts for each action $counts = array( "buildMovie" => array(), + "postMovie" => array(), "getClosestData" => array(), "getClosestImage" => array(), + "getClosestImageDatesForSources" => array(), "getJPX" => array(), "getJPXClosestToMidPoint" => array(), "takeScreenshot" => array(), + "postScreenshot" => array(), "uploadMovieToYouTube" => array(), "embed" => array(), "minimal" => array(), @@ -446,11 +449,14 @@ public function getUsageStatistics($resolution, $dateStart = null, $dateEnd = nu // Summary array $summary = array( "buildMovie" => 0, + "postMovie" => 0, "getClosestData" => 0, "getClosestImage" => 0, "getJPX" => 0, "getJPXClosestToMidPoint" => 0, + "getClosestImageDatesForSources" => 0, "takeScreenshot" => 0, + "postScreenshot" => 0, "uploadMovieToYouTube" => 0, "embed" => 0, "minimal" => 0, @@ -830,6 +836,7 @@ private function _createCountsArray(){ 'getWebClientState' => array(), 'goto' => array(), 'takeScreenshot' => array(), + 'postScreenshot' => array(), 'getRandomSeed' => array(), 'getJP2Image' => array(), 'getJPX' => array(), @@ -839,6 +846,7 @@ private function _createCountsArray(){ 'getMovieStatus' => array(), 'playMovie' => array(), 'queueMovie' => array(), + 'postMovie' => array(), 'reQueueMovie' => array(), 'uploadMovieToYouTube' => array(), 'checkYouTubeAuth' => array(), @@ -862,6 +870,7 @@ private function _createCountsArray(){ 'getGeometryServiceData' => array(), 'buildMovie' => array(),//this one happens in HelioviewerMovie.php "getClosestData" => array(), + "getClosestImageDatesForSources" => array(), "embed" => array(), "minimal" => array(), "standard" => array(), @@ -880,6 +889,7 @@ private function _createSummaryArray(){ "total" => 0, 'downloadScreenshot' => 0, 'getClosestImage' => 0, + "getClosestImageDatesForSources" => 0, 'getDataSources' => 0, 'getJP2Header' => 0, 'getNewsFeed' => 0, @@ -896,6 +906,7 @@ private function _createSummaryArray(){ 'getWebClientState' => 0, 'goto' => 0, 'takeScreenshot' => 0, + 'postScreenshot' => 0, 'getRandomSeed' => 0, 'getJP2Image' => 0, 'getJPX' => 0, @@ -906,6 +917,7 @@ private function _createSummaryArray(){ 'playMovie' => 0, 'queueMovie' => 0, 'reQueueMovie' => 0, + 'postMovie' => 0, 'uploadMovieToYouTube' => 0, 'checkYouTubeAuth' => 0, 'getYouTubeAuth' => 0, diff --git a/src/Event/EventsStateManager.php b/src/Event/EventsStateManager.php new file mode 100644 index 000000000..a6357f2eb --- /dev/null +++ b/src/Event/EventsStateManager.php @@ -0,0 +1,313 @@ + + * @license http://www.mozilla.org/MPL/MPL-1.1.html Mozilla Public License 1.1 + * @link https://github.com/Helioviewer-Project + */ + +namespace Helioviewer\Api\Event; + +class EventsStateManager +{ + // internal events state original + public array $events_state; + + // internal structure to process random events, if they are OK to be in above events_state + private array $events_tree; + + // internal structure to process random events, if their labels are visible + private array $events_tree_label_visibility; + + /** + * Creates a new EventsStateManager + * @param array $events_state, events state posted from frontend + * @return void + */ + private function __construct(array $events_state) + { + $this->events_state = $events_state; + $this->events_tree = []; + $this->events_tree_label_visibility = []; + + foreach($events_state as $eventHelioGroupName => $eventHelioGroupState) { // CCMC or HEK state + + // If we don't have visible markers for CCMC or HEK then no need to handle them + if (array_key_exists('markers_visible', $eventHelioGroupState) && $eventHelioGroupState['markers_visible'] != true) { + continue; + } + + + foreach($eventHelioGroupState['layers'] as $eventHelioGroupLayer) { + + $layer_event_type = $eventHelioGroupLayer['event_type']; + + if (!array_key_exists($layer_event_type, $this->events_tree)) { + $this->events_tree[$layer_event_type] = []; + $this->events_tree_label_visibility[$layer_event_type] = $eventHelioGroupState['labels_visible']; + } + + // This damn all fix + if (in_array("all",$eventHelioGroupLayer['frms'])) { + $this->events_tree[$layer_event_type] = "all_frms"; + } else { + + foreach($eventHelioGroupLayer['frms'] as $eventLayerFrm) { + if (!array_key_exists($eventLayerFrm, $this->events_tree[$layer_event_type])) { + $this->events_tree[$layer_event_type][$eventLayerFrm] = 'all_event_instances'; + } + } + + foreach($eventHelioGroupLayer['event_instances'] as $eventLayerEventInstance) { + + $event_instance_frm_pieces = explode('--',$eventLayerEventInstance); + $event_instance_frm = $event_instance_frm_pieces[1]; + + // if we have frms all included like "frm1" and in event instance "flare--frm1--event1" + // we just ignore those since they are all included into the tree with frm1 anyways + // this is also indicates, eventsState is invalid somehow + if (in_array($event_instance_frm, $eventHelioGroupLayer['frms'])) { + continue; + } + + if (!array_key_exists($event_instance_frm, $this->events_tree[$layer_event_type])) { + $this->events_tree[$layer_event_type][$event_instance_frm] = []; + } + + $this->events_tree[$layer_event_type][$event_instance_frm][] = $eventLayerEventInstance; + } + } + + } + } + + } + + /** + * Creates a new EventsStateManager from events_state + * @param array $events_state, events state posted from frontend + * @return EventsStateManager + */ + public static function buildFromEventsState(array $events_state) : EventsStateManager + { + return new self($events_state); + } + + /** + * Creates a new EventsStateManager from events_state + * @param array $events_state, events state posted from frontend + * @return EventsStateManager + */ + public static function buildFromLegacyEventStrings(string $events_state_string, bool $events_label) : EventsStateManager + { + $events_layers = []; + + // Prevent possible bugs + $events_state_string = trim($events_state_string); + + // this is one of the bloody cases + if(!empty($events_state_string)) { + $event_strings = explode("],[", trim(stripslashes($events_state_string), '][')); + + // Process individual events in string + foreach ($event_strings as $es) { + + $event_pieces = explode(",", $es); + + // just don't take risks in this environment + // there should be 3 element + if (count($event_pieces) < 3) { + continue; + } + + list($event_type, $combined_frms, $visible) = $event_pieces; + + $frms = explode(";", $combined_frms); + if (!empty($combined_frms) && !empty($frms)) { + // if frms not empty + if (!empty($frms)) { + // if this event type defined earlier + if(array_key_exists($event_type, $events_layers)) { + $event_layers[$event_type]['frms'] = array_unique(array_merge($events_layers[$event_type]['frms'], $frms)); + } else { + $events_layers[$event_type] = [ + 'event_type' => $event_type, + 'frms' => $frms, + 'event_instances' => [], + 'open' => true, // do not know this fields function better to keep it + ]; + } + } + } + } + } + + $events_state = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => true, + 'labels_visible' => $events_label, + 'layers' => array_values($events_layers), + ] + ]; + + return new self($events_state); + } + + + /** + * Export events state to stream into database tables + * @return string + */ + public function export() : string + { + return json_encode($this->events_state); + } + + /** + * Tells if there is events in this manager + * @return bool + */ + public function hasEvents() : bool + { + return count($this->events_tree) > 0; + } + + /** + * Lets you to access to events_state + * @return array + */ + public function getState(): array + { + return $this->events_state; + } + + /** + * Lets you to access to events_tree + * @return array + */ + public function getStateTree(): array + { + return $this->events_tree; + } + + /** + * Lets you to access to events_tree_label_visibility + * @return array + */ + public function getStateTreeLabelVisibility(): array + { + return $this->events_tree_label_visibility; + } + + /** + * Checks if this event_category has events in this state + * @param string event_category_pin , given event_type + * @return bool + */ + public function hasEventsForEventType(string $event_category_pin): bool + { + return array_key_exists($event_category_pin, $this->events_tree); + } + + /** + * Checks if this event state allows all events for this event_type + * @param string event_category_pin , given event_type + * @return bool + */ + public function appliesAllEventsForEventType(string $event_category_pin): bool + { + return $this->hasEventsForEventType($event_category_pin) && $this->events_tree[$event_category_pin] == "all_frms"; + } + + /** + * Checks if this event_category and frm_name has events in this state + * @param string event_category_pin , given event_type + * @param string frm_name , given frm_name + * @return bool + */ + public function appliesFrmForEventType(string $event_category_pin, string $frm_name): bool + { + // We keep IDs with underscores to reduce bugs + $frm_underscored_name = str_replace(' ', '_', $frm_name); + + // Check if we want the events for this group + return in_array($frm_underscored_name, array_keys($this->events_tree[$event_category_pin])); + } + + /** + * Checks if this event state allows all events for frm of this event_type + * @param string event_category_pin , given event_type + * @param string frm_name , given frm_name + * @return bool + */ + public function appliesAllEventInstancesForFrm(string $event_category_pin, string $frm_name): bool + { + // We keep IDs with underscores to reduce bugs + $frm_underscored_name = str_replace(' ', '_', $frm_name); + + // All event instances works for this frm + $all_event_instances_work_for_frm = $this->events_tree[$event_category_pin][$frm_underscored_name] == "all_event_instances"; + + return $this->appliesFrmForEventType($event_category_pin, $frm_name) && $all_event_instances_work_for_frm; + } + + + /** + * Checks if this event state allows this particular event_instance + * @param string event_category_pin , given event_type + * @param string frm_name , given frm_name + * @param array event , given event to check + * @return bool + */ + public function appliesEventInstance(string $event_category_pin, string $frm_name, array $event): bool + { + $event_instance_id = self::makeEventId($event_category_pin, $frm_name, $event); + + // We keep IDs with underscores to reduce bugs + $frm_underscored_name = str_replace(' ', '_', $frm_name); + + return in_array($event_instance_id, $this->events_tree[$event_category_pin][$frm_underscored_name]); + } + + /** + * Checks if this event state allows events labels are visiblle for given event_category + * @param string event_category_pin , given event_type + * @return bool + */ + public function isEventTypeLabelVisible(string $event_category_pin): bool + { + $is_defined_visiblity = array_key_exists($event_category_pin, $this->events_tree_label_visibility); + return $is_defined_visiblity && $this->events_tree_label_visibility[$event_category_pin]; + } + + + /** + * Makes event id from given event and its belonging event_type and frm_name + * @param string event_category_pin , given event_type + * @param string frm_name , given frm_name + * @param array event , given event to check + * @return string + */ + public static function makeEventId(string $event_category_pin, string $frm_name, array $event): string + { + $event_id_pieces = [ + $event_category_pin, + $frm_name, + base64_encode($event['id']), + ]; + + $cleaned_event_id_pieces = array_map(function($p) { + return str_replace([' ','=','+','.','(',')'], ['_','_','\+','\.','\(','\)'], $p); + }, $event_id_pieces); + + return join('--', $cleaned_event_id_pieces); + } + +} + +?> diff --git a/src/Event/HEKEventNormalizer.php b/src/Event/HEKEventNormalizer.php index 26ff49fb3..c8c0d2730 100644 --- a/src/Event/HEKEventNormalizer.php +++ b/src/Event/HEKEventNormalizer.php @@ -13,11 +13,25 @@ static public function Normalize(array &$event_types, array &$events): array { // Normalizes event FRMs into the main data container. // FRMs make up everything in the Helioviewer Event Format except for the data itself. $event_container = self::NormalizeFRMs($event_types); + + // Count if any label in our events is more than once + $all_event_labels = array_map(fn($e): string => self::CreateEventLabel($e), $events); + $all_event_labels_counts = array_count_values($all_event_labels); + $dublicate_labels = array_filter($all_event_labels_counts, fn($v, $k): bool => $v > 1, ARRAY_FILTER_USE_BOTH); + $dublicate_labels = array_keys($dublicate_labels); + foreach ($events as &$event) { // Operates in-place to assign eve $event['hv_hpc_x'] = $event['hv_hpc_x_final']; $event['hv_hpc_y'] = $event['hv_hpc_y_final']; + $event['label'] = self::CreateEventLabel($event); + + if(in_array($event['label'], $dublicate_labels)) { + $event['label'] = self::CreateEventLabel($event) . " ". number_format($event['event_coord1'],2).",".number_format($event['event_coord2'],2); + $event['short_label'] = self::CreateEventLabel($event) ." ". number_format($event['event_coord1'],2).",".number_format($event['event_coord2'],2); + } + $event['version'] = $event['frm_specificid']; $event['id'] = $event['kb_archivid']; $event['type'] = $event['event_type']; @@ -25,6 +39,7 @@ static public function Normalize(array &$event_types, array &$events): array { $event['end'] = $event['event_endtime']; self::AssignEventToFRM($event_container, $event); } + return $event_container; } diff --git a/src/Image/Composite/HelioviewerCompositeImage.php b/src/Image/Composite/HelioviewerCompositeImage.php index a9dceedbe..d2ebe304b 100644 --- a/src/Image/Composite/HelioviewerCompositeImage.php +++ b/src/Image/Composite/HelioviewerCompositeImage.php @@ -35,8 +35,7 @@ class Image_Composite_HelioviewerCompositeImage { protected $height; protected $interlace; protected $layers; - protected $events; - protected $eventsLabels; + protected $eventsManager; protected $movieIcons; protected $scale; protected $scaleType; @@ -75,8 +74,7 @@ class Image_Composite_HelioviewerCompositeImage { * * @return void */ - public function __construct($layers, $events, $eventLabels, $movieIcons, $celestialBodies, $scale, - $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options) { + public function __construct($layers, $eventsManager, $movieIcons, $celestialBodies, $scale, $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options) { set_time_limit(90); // Extend time limit to avoid timeouts @@ -107,8 +105,7 @@ public function __construct($layers, $events, $eventLabels, $movieIcons, $celest $this->db = $options['database'] ? $options['database'] : new Database_ImgIndex(); $this->layers = $layers; - $this->events = $events; - $this->eventsLabels = $eventLabels; + $this->eventsManager = $eventsManager; $this->movieIcons = $movieIcons; $this->scale = $scale; $this->scaleType = $scaleType; @@ -202,7 +199,8 @@ private function _buildCompositeImageLayers() { * * @return object A HelioviewerImage instance (e.g. AIAImage or LASCOImage) */ - private function _buildImageLayer($layer) { + private function _buildImageLayer($layer) + { $image = $this->db->getClosestData($this->date, $layer['sourceId']); // Instantiate a JP2Image @@ -418,11 +416,9 @@ private function _buildCompositeImage() { // For single layer images the composite image is simply the first // image layer $image = $this->_imageLayers[0]->getIMagickImage(); - } - - if ( count($this->events) > 0 && - $this->date != '2999-01-01T00:00:00.000Z') { + } + if ( $this->eventsManager->hasEvents() && $this->date != '2999-01-01T00:00:00.000Z') { $this->_addEventLayer($image); } @@ -579,6 +575,7 @@ private function _compressImage($imagickImage) { * @return void */ private function _addEventLayer($imagickImage) { + if ( $this->width < 200 || $this->height < 200 ) { return; } @@ -599,30 +596,74 @@ private function _addEventLayer($imagickImage) { $event_categories = array_merge($event_categories, Helper_EventInterface::GetEvents($startDate, $length, $observationTime)); // Lay down all relevant event REGIONS first - $allowedFRMs = $this->events->toArray(); + $events_to_render = []; - foreach($event_categories as $event_category) { - foreach( $allowedFRMs as $j => $frm ) { - // Match the 2 letter abbreviation to all the event groups. - if ($event_category['pin'] == $frm['event_type']) { - // Find the specific FRM within the group - foreach ($event_category['groups'] as $group) { - // Match the group name to the frm name - $underscored_name = str_replace(' ', '_', $group['name']); - if ($frm['frm_name'] == 'all' || - strpos($frm['frm_name'], $underscored_name) !== false) { - // This group of events was selected to show up in the image. - // Merge them into the final list of events. - foreach ($group['data'] as &$event) { - $event['concept'] = $event_category['name']; + $events_manager = $this->eventsManager; + $add_label_visibility_and_concept = function($events_data, $event_cat_pin, $event_group_name) use ($events_manager) { + return array_map(function($ed) use ($events_manager, $event_cat_pin, $event_group_name) { + $ed['concept'] = $event_group_name; + $ed['label_visibility'] = $events_manager->isEventTypeLabelVisible($event_cat_pin) ? true : false; + return $ed; + }, $events_data); + }; + + + foreach($event_categories as $event_cat) { + + $event_cat_pin = $event_cat['pin']; + + // if we dont have any configuration for this event_type + if (!$this->eventsManager->hasEventsForEventType($event_cat_pin)) { + continue; + } + + // Are we going to go for all children of this event type + if ($this->eventsManager->appliesAllEventsForEventType($event_cat_pin)) { + + foreach($event_cat['groups'] as $ecg) { + $events_to_render = array_merge( + $events_to_render, + $add_label_visibility_and_concept($ecg['data'], $event_cat_pin, $ecg['name']) + ); + } + + continue; + + } + + // Check each group now + foreach($event_cat['groups'] as $event_cat_group) { + + // Applies for event type + if($this->eventsManager->appliesFrmForEventType($event_cat_pin, $event_cat_group['name'])) { + + // applies all events for this group + if($this->eventsManager->appliesAllEventInstancesForFrm($event_cat_pin, $event_cat_group['name'])) { + $events_to_render = array_merge( + $events_to_render, + $add_label_visibility_and_concept($event_cat_group['data'], $event_cat_pin, $event_cat_group['name']) + ); + } else { + + // applies some events for this group + $events_filtered_for_event_instances = []; + + foreach($event_cat_group['data'] as $ev) { + + if ($this->eventsManager->appliesEventInstance($event_cat_pin, $event_cat_group['name'], $ev)) { + $events_filtered_for_event_instances[] = $ev; } - // The variable $event is re-used later so it must be unset so that it is not left pointing to the last member of the group array. - // See the warning here https://www.php.net/manual/en/control-structures.foreach.php - unset($event); - $events_to_render = array_merge($events_to_render, $group['data']); + } + + $events_to_render = array_merge( + $events_to_render, + $add_label_visibility_and_concept($events_filtered_for_event_instances, $event_cat_pin, $event_cat_group['name']) + ); + } } + } } @@ -637,16 +678,15 @@ private function _addEventLayer($imagickImage) { if ( $width >= 1 && $height >= 1 ) { - $region_polygon = new IMagick( - HV_ROOT_DIR.'/'.urldecode($event['hv_poly_url']) ); + $region_polygon = new IMagick(HV_ROOT_DIR.'/'.urldecode($event['hv_poly_url']) ); $x = (( $event['hv_poly_hpc_x_final'] - $this->roi->left()) / $this->roi->imageScale()); $y = (( $event['hv_poly_hpc_y_final'] - $this->roi->top() ) / $this->roi->imageScale()); - $x = $x - $this->_timeOffsetX; - $y = $y - $this->_timeOffsetY; + $x = $x - $this->_timeOffsetX; + $y = $y - $this->_timeOffsetY; $region_polygon->resizeImage( $width, $height, Imagick::FILTER_LANCZOS,1); @@ -688,8 +728,7 @@ private function _addEventLayer($imagickImage) { $y = $y - $this->_timeOffsetY; $imagickImage->compositeImage($marker, IMagick::COMPOSITE_DISSOLVE, $x - $markerPinPixelOffsetX, $y - $markerPinPixelOffsetY); - - if ( $this->eventsLabels == true ) { + if ($event['label_visibility']) { $x = $x + 11; $y = $y - 24; @@ -1562,8 +1601,7 @@ public function build($filepath) { public function display() { $fileinfo = new finfo(FILEINFO_MIME); $mimetype = $fileinfo->file($this->_filepath); - header("Content-Disposition: inline; filename=\"" . - $this->_filename . "\""); + header("Content-Disposition: inline; filename=\"". $this->_filename ."\""); header('Content-type: ' . $mimetype); $this->_composite->setImageFormat('png32'); echo $this->_composite; diff --git a/src/Image/Composite/HelioviewerMovieFrame.php b/src/Image/Composite/HelioviewerMovieFrame.php index 5e3724376..3a680565e 100644 --- a/src/Image/Composite/HelioviewerMovieFrame.php +++ b/src/Image/Composite/HelioviewerMovieFrame.php @@ -13,19 +13,16 @@ */ require_once HV_ROOT_DIR.'/../src/Image/Composite/HelioviewerCompositeImage.php'; -class Image_Composite_HelioviewerMovieFrame - extends Image_Composite_HelioviewerCompositeImage { +class Image_Composite_HelioviewerMovieFrame extends Image_Composite_HelioviewerCompositeImage { /** * Helioviewer movie frame */ - public function __construct($filepath, $layers, $events, $eventsLabels, $movieIcons, $celestialBodies, - $scale, $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options) { - - parent::__construct($layers, $events, $eventsLabels, $movieIcons, $celestialBodies, - $scale, $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options); + public function __construct($filepath, $layers, $eventsManager, $movieIcons, $celestialBodies, $scale, $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options) + { + parent::__construct($layers, $eventsManager, $movieIcons, $celestialBodies, $scale, $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options); $this->build($filepath); } } -?> \ No newline at end of file +?> diff --git a/src/Image/Composite/HelioviewerScreenshot.php b/src/Image/Composite/HelioviewerScreenshot.php index c1921f051..f26019c62 100644 --- a/src/Image/Composite/HelioviewerScreenshot.php +++ b/src/Image/Composite/HelioviewerScreenshot.php @@ -13,8 +13,8 @@ */ require_once HV_ROOT_DIR.'/../src/Image/Composite/HelioviewerCompositeImage.php'; -class Image_Composite_HelioviewerScreenshot - extends Image_Composite_HelioviewerCompositeImage { +class Image_Composite_HelioviewerScreenshot extends Image_Composite_HelioviewerCompositeImage +{ public $id; public $timestamp; @@ -22,23 +22,23 @@ class Image_Composite_HelioviewerScreenshot /** * Creates a new screenshot */ - public function __construct($layers, $events, $eventLabels, $movieIcons, $celestialBodies, $scale, - $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options) { + public function __construct($layers, $eventsManager, $movieIcons, $celestialBodies, $scale, $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options) { - parent::__construct($layers, $events, $eventLabels, $movieIcons, $celestialBodies, $scale, - $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options); + parent::__construct($layers, $eventsManager, $movieIcons, $celestialBodies, $scale, $scaleType, $scaleX, $scaleY, $obsDate, $roi, $options); - if ( array_key_exists('action', $options) && - $options['action'] == 'downloadScreenshot' ) { + if ( array_key_exists('action', $options) && $options['action'] == 'downloadScreenshot' ) { $this->id = $options['id']; $this->timestamp = $options['timestamp']; $this->date = $options['observationDate']; - } - else { + + } else { + $this->id = $this->_getScreenshotId(); $this->timestamp = date('Y-m-d'); + } + $this->build($this->_buildFilepath()); //TODO: Either include a status field in db, or remove entry if @@ -57,12 +57,7 @@ private function _buildFilepath() { HV_CACHE_DIR, date('Y/m/d', $created->getTimestamp()), $this->id, - substr( - str_replace( - array(':', '-', 'T', 'Z', ' '), - '_', - $this->date), - 0, 19), + substr(str_replace(array(':', '-', 'T', 'Z', ' '), '_', $this->date), 0, 19), $this->layers->toString() ); } @@ -72,7 +67,8 @@ private function _buildFilepath() { * * @return int Screenshot id */ - private function _getScreenshotId() { + private function _getScreenshotId() + { return $this->db->insertScreenshot( $this->date, $this->imageScale, @@ -80,8 +76,7 @@ private function _getScreenshotId() { $this->watermark, $this->layers->serialize(), $this->layers->getBitMask(), - $this->events->serialize(), - $this->eventsLabels, + $this->eventsManager->export(), $this->movieIcons, $this->scale, $this->scaleType, @@ -94,4 +89,4 @@ private function _getScreenshotId() { ); } } -?> \ No newline at end of file +?> diff --git a/src/Module/Movies.php b/src/Module/Movies.php index 14c8932e9..3bfacc95f 100644 --- a/src/Module/Movies.php +++ b/src/Module/Movies.php @@ -1,5 +1,4 @@ validate()) { try { $this->{$this->_params['action']}(); - } - catch (Exception $e) { + } catch (Exception $e) { handleError($e->getMessage(), $e->getCode()); } } } + /** * Queues a request for a Helioviewer.org movie + * Does it with HTTP POST request */ - public function queueMovie() { + public function postMovie() { + include_once HV_ROOT_DIR.'/../lib/alphaID/alphaID.php'; include_once HV_ROOT_DIR.'/../lib/Resque.php'; include_once HV_ROOT_DIR.'/../lib/Redisent/Redisent.php'; @@ -59,6 +62,181 @@ public function queueMovie() { include_once HV_ROOT_DIR.'/../src/Database/MovieDatabase.php'; include_once HV_ROOT_DIR.'/../src/Database/ImgIndex.php'; + $json_params = $this->_params['json']; + // Connect to redis + $redis = new Redisent(HV_REDIS_HOST, HV_REDIS_PORT); + + // If the queue is currently full, don't process the request + $queueSize = Resque::size(HV_MOVIE_QUEUE); + if ( $queueSize >= MOVIE_QUEUE_MAX_SIZE ) { + throw new Exception( + 'Sorry, due to current high demand, we are currently unable ' . + 'to process your request. Please try again later.', 40); + } + + // Get current number of HV_MOVIE_QUEUE workers + $workers = Resque::redis()->smembers('workers'); + $movieWorkers = array_filter($workers, function ($elem) { + return strpos($elem, HV_MOVIE_QUEUE) !== false; + }); + + if( isset($json_params['celestialBodiesLabels']) && isset($json_params['celestialBodiesTrajectories']) ){ + $celestialBodies = array( "labels" => $json_params['celestialBodiesLabels'], + "trajectories" => $json_params['celestialBodiesTrajectories']); + }else{ + $celestialBodies = array( "labels" => "", + "trajectories" => ""); + } + + // Default options + $defaults = array( + "format" => 'mp4', + "frameRate" => null, + "movieLength" => null, + "maxFrames" => HV_MAX_MOVIE_FRAMES, + "watermark" => true, + "scale" => false, + "scaleType" => 'earth', + "scaleX" => 0, + "scaleY" => 0, + "size" => 0, + "movieIcons" => 0, + "followViewport" => 0, + "reqStartTime" => $json_params['startTime'], + "reqEndTime" => $json_params['endTime'], + "reqObservationDate" => null, + "switchSources" => false, + "celestialBodies" => $celestialBodies + ); + + $options = array_replace($defaults, $json_params); + + // Default to 15fps if no frame-rate or length was specified + if ( is_null($options['frameRate']) && is_null($options['movieLength'])) { + $options['frameRate'] = 15; + } + + // Limit movies to three layers + $layers = new Helper_HelioviewerLayers($json_params['layers']); + if ( $layers->length() < 1 || $layers->length() > 3 ) { + throw new Exception('Invalid layer choices! You must specify 1-3 comma-separated layer names.', 22); + } + + $events_manager = EventsStateManager::buildFromEventsState($json_params['eventsState']); + + // TODO 2012/04/11 + // Discard any layers which do not share an overlap with the roi to + // avoid generating kdu_expand errors later. Check is performed already + // on front-end, but should also be done before queuing a request. + + // Determine the ROI + $roi = $this->_getMovieROI($options, $json_params); + $roiString = $roi->getPolygonString(); + + $numPixels = $roi->getPixelWidth() * $roi->getPixelHeight(); + + // Use reduce image scale if necessary + $imageScale = $roi->imageScale(); + + // Max number of frames + $maxFrames = min($this->_getMaxFrames($queueSize), $options['maxFrames']); + + // Create a connection to the database + $db = new Database_ImgIndex(); + $movieDb = new Database_MovieDatabase(); + + // Estimate the number of frames + $numFrames = $this->_estimateNumFrames($db, $layers, $json_params['startTime'], $json_params['endTime']); + $numFrames = min($numFrames, $maxFrames); + + // Estimate the time to create movie frames + // @TODO 06/2012: Factor in total number of workers and number of + // workers that are currently available? + $estBuildTime = $this->_estimateMovieBuildTime($movieDb, $numFrames, $numPixels, $options['format']); + + // If all workers are in use, increment and use estimated wait counter + if ( $queueSize +1 >= sizeOf($movieWorkers) ) { + $eta = $redis->incrby('helioviewer:movie_queue_wait', $estBuildTime); + $updateCounter = true; + } + else { + // Otherwise simply use the time estimated for the single movie + $eta = $estBuildTime; + $updateCounter = false; + } + + // Get datasource bitmask + $bitmask = bindec($layers->getBitMask()); + + // Create entry in the movies table in MySQL + $dbId = $movieDb->insertMovie( + $json_params['startTime'], + $json_params['endTime'], + (isset($json_params['reqObservationDate']) ? $json_params['reqObservationDate'] : false), + $imageScale, + $roiString, + $maxFrames, + $options['watermark'], + $json_params['layers'], + $bitmask, + $events_manager->export(), + (isset($json_params['movieIcons']) ? $json_params['movieIcons'] : false), + (isset($json_params['followViewport']) ? $json_params['followViewport'] : false), + $options['scale'], + $options['scaleType'], + $options['scaleX'], + $options['scaleY'], + $layers->length(), + $queueSize, + $options['frameRate'], + $options['movieLength'], + $options['size'], + $options['switchSources'], + $options['celestialBodies'] + ); + + // Convert id + $publicId = alphaID($dbId, false, 5, HV_MOVIE_ID_PASS); + + // Queue movie request + $args = array( + 'movieId' => $publicId, + 'eta' => $estBuildTime, + 'format' => $options['format'], + 'counter' => $updateCounter + ); + + // Create entries for each version of the movie in the movieFormats + // table + foreach(array('mp4', 'webm') as $format) { + $movieDb->insertMovieFormat($dbId, $format); + } + + $token = Resque::enqueue(HV_MOVIE_QUEUE, 'Job_MovieBuilder', $args, true); + + // Print response + $response = array( + 'id' => $publicId, + 'eta' => $eta, + 'queue' => max(0, $queueSize + 1 - sizeOf($movieWorkers)), + 'token' => $token + ); + + $this->_printJSON(json_encode($response)); + } + + /** + * Queues a request for a Helioviewer.org movie + */ + public function queueMovie() { + + include_once HV_ROOT_DIR.'/../lib/alphaID/alphaID.php'; + include_once HV_ROOT_DIR.'/../lib/Resque.php'; + include_once HV_ROOT_DIR.'/../lib/Redisent/Redisent.php'; + include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerLayers.php'; + include_once HV_ROOT_DIR.'/../src/Database/MovieDatabase.php'; + include_once HV_ROOT_DIR.'/../src/Database/ImgIndex.php'; + // Connect to redis $redis = new Redisent(HV_REDIS_HOST, HV_REDIS_PORT); @@ -114,12 +292,26 @@ public function queueMovie() { // Limit movies to three layers $layers = new Helper_HelioviewerLayers($this->_params['layers']); if ( $layers->length() < 1 || $layers->length() > 3 ) { - throw new Exception( - 'Invalid layer choices! You must specify 1-3 comma-separated '. - 'layer names.', 22); + throw new Exception('Invalid layer choices! You must specify 1-3 comma-separated layer names.', 22); + } + + // Event legacy string + $events_legacy_string = ""; + if ( array_key_exists('events', $this->_params) ) { + $events_legacy_string = $this->_params['events']; + } + + // Event legacy labels switch + $event_labels = false; + if ( array_key_exists('eventLabels', $this->_params) ) { + $event_labels = (bool)$this->_params['eventLabels']; } - $events = new Helper_HelioviewerEvents($this->_params['events']); + // ATTENTION! These two fields eventsLabels and eventSourceString needs to be kept in DB schema + // We are keeping them to support old takeScreenshot , queueMovie requests + + // Events manager built from old logic + $events_manager = EventsStateManager::buildFromLegacyEventStrings($events_legacy_string, $event_labels); // TODO 2012/04/11 // Discard any layers which do not share an overlap with the roi to @@ -127,7 +319,7 @@ public function queueMovie() { // on front-end, but should also be done before queuing a request. // Determine the ROI - $roi = $this->_getMovieROI($options); + $roi = $this->_getMovieROI($options, $this->_params); $roiString = $roi->getPolygonString(); $numPixels = $roi->getPixelWidth() * $roi->getPixelHeight(); @@ -176,8 +368,7 @@ public function queueMovie() { $options['watermark'], $this->_params['layers'], $bitmask, - $this->_params['events'], - $this->_params['eventsLabels'], + $events_manager->export(), (isset($this->_params['movieIcons']) ? $this->_params['movieIcons'] : false), (isset($this->_params['followViewport']) ? $this->_params['followViewport'] : false), $options['scale'], @@ -231,7 +422,6 @@ public function reQueueMovie($silent=false) { include_once HV_ROOT_DIR.'/../lib/Resque.php'; include_once HV_ROOT_DIR.'/../lib/Redisent/Redisent.php'; include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerLayers.php'; - include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerEvents.php'; include_once HV_ROOT_DIR.'/../src/Database/MovieDatabase.php'; include_once HV_ROOT_DIR.'/../src/Database/ImgIndex.php'; include_once HV_ROOT_DIR.'/../src/Movie/HelioviewerMovie.php'; @@ -270,8 +460,8 @@ public function reQueueMovie($silent=false) { // Check if movie exists on disk before re-queueing if ( $options['force'] === false ) { - $helioviewerMovie = new Movie_HelioviewerMovie( - $this->_params['id'], $options['format']); + $helioviewerMovie = new Movie_HelioviewerMovie($this->_params['id'], $options['format']); + $filepath = $helioviewerMovie->getFilepath(); $path_parts = pathinfo($filepath); @@ -290,6 +480,11 @@ public function reQueueMovie($silent=false) { $movieDatabase = new Database_MovieDatabase(); $movie = $movieDatabase->getMovieMetadata($movieId); + // Movie not found + if(!$movie) { + return $this->_sendResponse(404, 'NOT FOUND', 'Movie could not be found'); + } + // Check if movie is already in the queue (status=0) // or is already being processed (status=1) before re-queueing. @@ -304,11 +499,9 @@ public function reQueueMovie($silent=false) { foreach ( $movieFormats as $movieFormat ) { if ($movieFormat['status'] == 0) { throw new Exception('Movie is already in queue', 47); - return; } if ( $movieFormat['status'] == 1) { throw new Exception('Movie is currently being processed', 47); - return; } } @@ -529,7 +722,8 @@ private function _getDefaultROIOptions($scale) { * Returns the region of interest for the movie request or throws an error * if one was not properly specified. */ - private function _getMovieROI($options) { + private function _getMovieROI($options, $params) { + include_once HV_ROOT_DIR.'/../src/Helper/RegionOfInterest.php'; // Region of interest (center in arcseconds and dimensions in pixels) @@ -539,7 +733,7 @@ private function _getMovieROI($options) { !isset($options['x2']) && !isset($options['y2']) && !isset($options['x0']) && !isset($options['y0']) && !isset($options['width']) && !isset($options['height']) ) { - $defaultOptions = $this->_getDefaultROIOptions($this->_params['imageScale']); + $defaultOptions = $this->_getDefaultROIOptions($params['imageScale']); $x1 = $defaultOptions['x1']; $y1 = $defaultOptions['y1']; $x2 = $defaultOptions['x2']; @@ -559,14 +753,14 @@ private function _getMovieROI($options) { // Region of interest (top-left and bottom-right coords in // arcseconds) $x1 = $options['x0'] - 0.5 * $options['width'] - * $this->_params['imageScale']; + * $params['imageScale']; $y1 = $options['y0'] - 0.5 * $options['height'] - * $this->_params['imageScale']; + * $params['imageScale']; $x2 = $options['x0'] + 0.5 * $options['width'] - * $this->_params['imageScale']; + * $params['imageScale']; $y2 = $options['y0'] + 0.5 * $options['height'] - * $this->_params['imageScale']; + * $params['imageScale']; } else { throw new Exception( @@ -574,7 +768,7 @@ private function _getMovieROI($options) { } $roi = new Helper_RegionOfInterest($x1, $y1, $x2, $y2, - $this->_params['imageScale']); + $params['imageScale']); return $roi; } @@ -591,26 +785,21 @@ private function _getMovieROI($options) { * use of prior actual movie generation times and will not require use of * manually-selected system-dependent coefficients */ - private function _estimateNumFrames($db, $layers, $startTime, $endTime) { + private function _estimateNumFrames($db, $layers, $startTime, $endTime) + { $numFrames = 0; - $sql = 'SELECT COUNT(*) FROM images WHERE DATE BETWEEN "%s" ' . - 'AND "%s" AND sourceId=%d;'; // Estimate number of movies frames for each layer foreach ( $layers->toArray() as $layer ) { - $numFrames += $db->getDataCount($startTime, $endTime, - $layer['sourceId']); + $numFrames += $db->getDataCount($startTime, $endTime, $layer['sourceId']); } // Raise an error if few or no frames were found for the request range // and data sources if ($numFrames == 0) { - throw new Exception('No images found for requested time range. '. - 'Please try a different time.', 12); - } - else if ($numFrames <= 3) { - throw new Exception('Insufficient data was found for the '. - 'requested time range. Please try a different time.', 16); + throw new Exception('No images found for requested time range. Please try a different time.', 12); + } else if ($numFrames <= 3) { + throw new Exception('Insufficient data was found for the requested time range. Please try a different time.', 16); } return $numFrames; @@ -1346,6 +1535,11 @@ public function validate() { 'ints' => array('maxFrames', 'width', 'height', 'size') ); break; + case 'postMovie': + $expected = [ + 'required' => ['json'], + ]; + break; case 'reQueueMovie': $expected = array( 'required' => array('id'), @@ -1405,5 +1599,25 @@ public function validate() { return true; } + + /** + * Helper function to handle response code and response message with + * output result as either JSON or JSONP + * + * @param int $code HTTP response code to return + * @param string $message Message for the response code, + * @param mixed $data Data can be anything + * + * @return void + */ + private function _sendResponse(int $code, string $message, mixed $data) : void + { + http_response_code($code); + $this->_printJSON(json_encode([ + 'status_code' => $code, + 'status_txt' => $message, + 'data' => $data, + ])); + } } ?> diff --git a/src/Module/WebClient.php b/src/Module/WebClient.php index c552e77c7..e3e541117 100644 --- a/src/Module/WebClient.php +++ b/src/Module/WebClient.php @@ -17,6 +17,8 @@ require_once HV_ROOT_DIR.'/../src/Validation/InputValidator.php'; require_once HV_ROOT_DIR.'/../src/Helper/ErrorHandler.php'; +use Helioviewer\Api\Event\EventsStateManager; + class Module_WebClient implements Module { private $_params; @@ -69,6 +71,10 @@ public function downloadScreenshot() { $info = $imgIndex->getScreenshot($this->_params['id']); + if(!$info) { + return $this->_sendResponse(404, "NOT FOUND", "Screenshot not found"); + } + $layers = new Helper_HelioviewerLayers($info['dataSourceString']); $dir = sprintf('%s/screenshots/%s/%s/', @@ -110,6 +116,31 @@ public function downloadScreenshot() { echo file_get_contents($filepath); } + /** + * Finds the closest image available for a given time and datasource + * + * @return JSON meta information for matching image + * + * TODO: Combine with getJP2Image? (e.g. "&display=true") + */ + public function getClosestImageDatesForSources() { + + include_once HV_ROOT_DIR.'/../src/Database/ImgIndex.php'; + + $imgIndex = new Database_ImgIndex(); + + $results = []; + + foreach($this->_params['sources'] as $sid) { + $closestImages = $imgIndex->getClosestDataBeforeAndAfter($this->_params['date'], $sid); + $results[$sid]['prev_date'] = $closestImages['prev_date']; + $results[$sid]['next_date'] = $closestImages['next_date']; + } + + // Print result + $this->_printJSON(json_encode($results)); + } + /** * Finds the closest image available for a given time and datasource * @@ -383,6 +414,88 @@ public function getTile() { $tile->display(); } + /** + * Obtains layer information, ranges of pixels visible, and the date being + * looked at and creates a composite image (a Screenshot) of all the + * layers. Does it with HTTP POST request + * + * See the API webpage for example usage. + * + * @return image/jpeg or JSON + */ + public function postScreenshot() + { + include_once HV_ROOT_DIR.'/../src/Image/Composite/HelioviewerScreenshot.php'; + include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerLayers.php'; + + $json_params = $this->_params['json']; + + // Data Layers + $layers = new Helper_HelioviewerLayers($json_params['layers']); + + // Event Labels + $movieIcons = false; + if ( array_key_exists('movieIcons', $json_params) ) { + $movieIcons = $json_params['movieIcons']; + } + + // Scale + $scale = false; + $scaleType = 'earth'; + $scaleX = 0; + $scaleY = 0; + if ( array_key_exists('scale', $json_params) ) { + $scale = (isset($json_params['scale']) ? $json_params['scale'] : $scale); + $scaleType = (isset($json_params['scaleType']) ? $json_params['scaleType'] : $scaleType); + $scaleX = (isset($json_params['scaleX']) ? $json_params['scaleX'] : $scaleX); + $scaleY = (isset($json_params['scaleY']) ? $json_params['scaleY'] : $scaleY); + } + + // Region of interest + $roi = $this->_getRegionOfInterest($json_params, $json_params); + + // Celestial Bodies + if( isset($json_params['celestialBodiesLabels']) && isset($json_params['celestialBodiesTrajectories']) ){ + + $celestialBodiesLabels = $json_params['celestialBodiesLabels']; + $celestialBodiesTrajectories = $json_params['celestialBodiesTrajectories']; + $celestialBodies = array( + 'labels' => $celestialBodiesLabels, + 'trajectories' => $celestialBodiesTrajectories + ); + + } else { + + $celestialBodies = array( "labels" => "", "trajectories" => ""); + + } + + $events_manager = EventsStateManager::buildFromEventsState($json_params['eventsState']); + + // Create the screenshot + $screenshot = new Image_Composite_HelioviewerScreenshot( + $layers, + $events_manager, + $movieIcons, + $celestialBodies, + $scale, + $scaleType, + $scaleX, + $scaleY, + $json_params['date'], + $roi, + $json_params + ); + + // Display screenshot + if (isset($json_params['display']) && $json_params['display']) { + $screenshot->display(); + } else { + // Print JSON + $this->_printJSON(json_encode(array('id' => $screenshot->id))); + } + } + /** * Obtains layer information, ranges of pixels visible, and the date being * looked at and creates a composite image (a Screenshot) of all the @@ -398,24 +511,10 @@ public function getTile() { public function takeScreenshot() { include_once HV_ROOT_DIR.'/../src/Image/Composite/HelioviewerScreenshot.php'; include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerLayers.php'; - include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerEvents.php'; // Data Layers $layers = new Helper_HelioviewerLayers($this->_params['layers']); - // Event Layers - $events = Array(); - if ( !array_key_exists('events', $this->_params) ) { - $this->_params['events'] = ''; - } - $events = new Helper_HelioviewerEvents($this->_params['events']); - - // Event Labels - $eventLabels = false; - if ( array_key_exists('eventLabels', $this->_params) ) { - $eventLabels = $this->_params['eventLabels']; - } - // Event Labels $movieIcons = false; if ( array_key_exists('movieIcons', $this->_params) ) { @@ -435,7 +534,7 @@ public function takeScreenshot() { } // Region of interest - $roi = $this->_getRegionOfInterest(); + $roi = $this->_getRegionOfInterest($this->_options, $this->_params); // Celestial Bodies if( isset($this->_params['celestialBodiesLabels']) && isset($this->_params['celestialBodiesTrajectories']) ){ @@ -445,22 +544,51 @@ public function takeScreenshot() { 'labels' => $celestialBodiesLabels, 'trajectories' => $celestialBodiesTrajectories ); - }else{ - $celestialBodies = array( "labels" => "", - "trajectories" => ""); + } else { + $celestialBodies = array( + "labels" => "", + "trajectories" => "" + ); } + // Event legacy string + $events_legacy_string = ""; + if ( array_key_exists('events', $this->_params) ) { + $events_legacy_string = $this->_params['events']; + } + + // Event legacy labels switch + $event_labels = false; + if ( array_key_exists('eventLabels', $this->_params) ) { + $event_labels = (bool)$this->_params['eventLabels']; + } + + + // ATTENTION! These two fields eventsLabels and eventSourceString needs to be kept in DB schema + // We are keeping them to support old takeScreenshot , queueMovie requests + + // Events manager built from old logic + $events_manager = EventsStateManager::buildFromLegacyEventStrings($events_legacy_string, $event_labels); + // Create the screenshot $screenshot = new Image_Composite_HelioviewerScreenshot( - $layers, $events, $eventLabels, $movieIcons, $celestialBodies, $scale, $scaleType, $scaleX, - $scaleY, $this->_params['date'], $roi, $this->_options + $layers, + $events_manager, + $movieIcons, + $celestialBodies, + $scale, + $scaleType, + $scaleX, + $scaleY, + $this->_params['date'], + $roi, + $this->_options ); // Display screenshot if (isset($this->_options['display']) && $this->_options['display']) { $screenshot->display(); - } - else { + } else { // Print JSON $this->_printJSON(json_encode(array('id' => $screenshot->id))); } @@ -476,7 +604,6 @@ public function reTakeScreenshot($screenshotId) { include_once HV_ROOT_DIR.'/../src/Database/ImgIndex.php'; include_once HV_ROOT_DIR.'/../src/Image/Composite/HelioviewerScreenshot.php'; include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerLayers.php'; - include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerEvents.php'; include_once HV_ROOT_DIR.'/../src/Helper/RegionOfInterest.php'; // Default options @@ -538,19 +665,35 @@ public function reTakeScreenshot($screenshotId) { } // Event Layers - $events = new Helper_HelioviewerEvents( - $metaData['eventSourceString']); + $events_state_from_metadata = json_decode($metaData['eventsState'], true); + $events_manager; + + // ATTENTION! These two fields eventsLabels and eventSourceString needs to be kept in DB schema + // We are keeping them to support old takeScreenshot , queueMovie requests + + if(!empty($events_state_from_metadata)) { + $events_manager = EventsStateManager::buildFromEventsState($events_state_from_metadata); + } else { + $events_manager = EventsStateManager::buildFromLegacyEventStrings($metaData['eventSourceString'], (bool)$metaData['eventsLabels']); + } + $celestialBodies = array( "labels" => $metaData['celestialBodiesLabels'], "trajectories" => $metaData['celestialBodiesTrajectories']); // Create the screenshot $screenshot = new Image_Composite_HelioviewerScreenshot( - $layers, $events, (bool)$metaData['eventsLabels'], (bool)$metaData['movieIcons'], + $layers, + $events_manager, + (bool)$metaData['movieIcons'], $celestialBodies, - (bool)$metaData['scale'], $metaData['scaleType'], - $metaData['scaleX'], $metaData['scaleY'], - $metaData['observationDate'], $roi, $options + (bool)$metaData['scale'], + $metaData['scaleType'], + $metaData['scaleX'], + $metaData['scaleY'], + $metaData['observationDate'], + $roi, + $options ); } @@ -623,7 +766,6 @@ public function saveWebClientState() { $client_state = new ClientState(); try { - $state_key = $client_state->upsert($this->_params['json']); return $this->_sendResponse(200, 'OK', $state_key); @@ -743,17 +885,14 @@ public function getUsageStatistics() { */ public function getSciDataScript() { - if ( strtolower($this->_params['lang']) == 'sswidl' ) { + if (strtolower($this->_params['lang']) == 'sswidl') { include_once HV_ROOT_DIR.'/../src/Helper/SSWIDL.php'; $script = new Helper_SSWIDL($this->_params); - } - else if ( strtolower($this->_params['lang']) == 'sunpy' ) { + } else if (strtolower($this->_params['lang']) == 'sunpy') { include_once HV_ROOT_DIR.'/../src/Helper/SunPy.php'; $script = new Helper_SunPy($this->_params); - } - else { - handleError( - 'Invalid value specified for request parameter "lang".', 25); + } else { + handleError('Invalid value specified for request parameter "lang".', 25); } $script->buildScript(); @@ -781,7 +920,6 @@ public function getDataCoverage() { } - $start = @$this->_options['startDate']; if ($start && !preg_match('/^[0-9]+$/', $start)) { die("Invalid start parameter: $start"); @@ -1197,9 +1335,11 @@ public function getEclipseImage() { $range = 6000; $roi = new Helper_RegionOfInterest(-$range, -$range, $range, $range, 15); + // ATTENTION! These two fields eventsLabels and eventSourceString needs to be kept in DB schema + // We are keeping them to support old takeScreenshot , queueMovie requests + // Create empty events object required for screenshots. - include_once HV_ROOT_DIR.'/../src/Helper/HelioviewerEvents.php'; - $events = new Helper_HelioviewerEvents(''); + $events_manager = EventStateManager::buildFromLegacyEventStrings('', false); // Create empty celestial bodies list $celestialBodies = array( "labels" => "", @@ -1208,8 +1348,21 @@ public function getEclipseImage() { // Create the base screenshot image include_once HV_ROOT_DIR.'/../src/Image/Composite/HelioviewerScreenshot.php'; $screenshot = new Image_Composite_HelioviewerScreenshot( - $layers, $events, false, false, $celestialBodies, false, 'earth', 0, 0, $now_str, $roi, - ['grayscale' => true, 'eclipse' => true, 'moon' => $this->_options['moon']] + $layers, + $events_manager, + false, + $celestialBodies, + false, + 'earth', + 0, + 0, + $now_str, + $roi, + [ + 'grayscale' => true, + 'eclipse' => true, + 'moon' => $this->_options['moon'] + ] ); $screenshot->display(); } @@ -1281,42 +1434,50 @@ private function _computeStatusLevel($elapsed, $inst) { * 1) x1, y1, x2, y2, OR * 2) x0, y0, width, height */ - private function _getRegionOfInterest() { + private function _getRegionOfInterest($options, $params) + { include_once HV_ROOT_DIR.'/../src/Helper/RegionOfInterest.php'; + $isset_x0 = isset($options['x0']); + $isset_x1 = isset($options['x1']); + $isset_x2 = isset($options['x2']); + $isset_y0 = isset($options['y0']); + $isset_y1 = isset($options['y1']); + $isset_y2 = isset($options['y2']); + $isset_height = isset($options['height']); + $isset_width = isset($options['width']); + + // Region of interest: x1, x2, y1, y2 - if (isset($this->_options['x1']) && isset($this->_options['y1']) && - isset($this->_options['x2']) && isset($this->_options['y2'])) { + if ($isset_x1 && $isset_y1 && $isset_x2 && $isset_y2) { - $x1 = $this->_options['x1']; - $y1 = $this->_options['y1']; - $x2 = $this->_options['x2']; - $y2 = $this->_options['y2']; - } - else if ( isset($this->_options['x0']) && - isset($this->_options['y0']) && - isset($this->_options['width']) && - isset($this->_options['height']) ) { + $x1 = $options['x1']; + $y1 = $options['y1']; + $x2 = $options['x2']; + $y2 = $options['y2']; + + } else if ( $isset_x0 && $isset_y0 && $isset_width && $isset_height ) { // Region of interest: x0, y0, width, height - $x1 = $this->_options['x0'] - 0.5 * $this->_options['width'] * $this->_params['imageScale']; - $y1 = $this->_options['y0'] - 0.5 * $this->_options['height'] * $this->_params['imageScale']; + $x1 = $options['x0'] - 0.5 * $options['width'] * $params['imageScale']; + $y1 = $options['y0'] - 0.5 * $options['height'] * $params['imageScale']; + + $x2 = $options['x0'] + 0.5 * $options['width'] * $params['imageScale']; + $y2 = $options['y0'] + 0.5 * $options['height'] * $params['imageScale']; + + } else { - $x2 = $this->_options['x0'] + 0.5 * $this->_options['width'] * $this->_params['imageScale']; - $y2 = $this->_options['y0'] + 0.5 * $this->_options['height'] * $this->_params['imageScale']; - } - else { throw new Exception( 'Region of interest not specified: you must specify values ' . 'for imageScale and either x1, x2, y1, and y2 or x0, y0, ' . 'width and height.', 23 ); + } // Create RegionOfInterest helper object - return new Helper_RegionOfInterest($x1, $y1, $x2, $y2, - $this->_params['imageScale']); + return new Helper_RegionOfInterest($x1, $y1, $x2, $y2, $params['imageScale']); } @@ -1507,6 +1668,14 @@ public function validate() { 'ints' => array('sourceId') ); break; + + case 'getClosestImageDatesForSources': + $expected = array( + 'required' => array('date', 'sources'), + 'dates' => array('date'), + 'array_ints' => array('sources'), + ); + break; case 'getDataSources': $expected = array( 'optional' => array('verbose', 'callback', 'enable'), @@ -1594,6 +1763,12 @@ public function validate() { 'layer' => array('layers') ); break; + + case 'postScreenshot': + $expected = [ + 'required' => ['json'], + ]; + break; case 'getStatus': $expected = array( 'optional' => array('key'), diff --git a/src/Movie/HelioviewerMovie.php b/src/Movie/HelioviewerMovie.php index a831de3b5..d4e2dc5f5 100644 --- a/src/Movie/HelioviewerMovie.php +++ b/src/Movie/HelioviewerMovie.php @@ -31,11 +31,12 @@ require_once HV_ROOT_DIR . '/../lib/alphaID/alphaID.php'; require_once HV_ROOT_DIR . '/../src/Database/ImgIndex.php'; require_once HV_ROOT_DIR . '/../src/Helper/DateTimeConversions.php'; -require_once HV_ROOT_DIR . '/../src/Helper/HelioviewerEvents.php'; require_once HV_ROOT_DIR . '/../src/Helper/HelioviewerLayers.php'; require_once HV_ROOT_DIR . '/../src/Helper/RegionOfInterest.php'; require_once HV_ROOT_DIR . '/../src/Helper/Serialize.php'; +use Helioviewer\Api\Event\EventsStateManager; + class Movie_HelioviewerMovie { const STATUS_QUEUED = 0; const STATUS_PROCESSING = 1; @@ -62,7 +63,6 @@ class Movie_HelioviewerMovie { public $timestamp; public $modified; public $watermark; - public $eventsLabels; public $movieIcons; public $celestialBodies; public $followViewport; @@ -77,7 +77,7 @@ class Movie_HelioviewerMovie { private $_db; private $_layers; - private $_events; + private $_eventsManager; private $_roi; private $_timestamps = array(); private $_frames = array(); @@ -139,7 +139,6 @@ public function __construct($publicId, $format='mp4') { $this->width = (int)$info['width']; $this->height = (int)$info['height']; $this->watermark = (bool)$info['watermark']; - $this->eventsLabels = (bool)$info['eventsLabels']; $this->movieIcons = (bool)$info['movieIcons']; $this->celestialBodies = array( 'labels' => $info['celestialBodiesLabels'], @@ -156,7 +155,18 @@ public function __construct($publicId, $format='mp4') { // Data Layers $this->_layers = new Helper_HelioviewerLayers($info['dataSourceString']); - $this->_events = new Helper_HelioviewerEvents($info['eventSourceString']); + + // ATTENTION! These two fields eventsLabels and eventSourceString needs to be kept in DB schema + // We are keeping them to support old takeScreenshot , queueMovie requests + + // Events Manager + $events_state_from_info = json_decode($info['eventsState'], true); + + if(!empty($events_state_from_info)) { + $this->_eventsManager = EventsStateManager::buildFromEventsState($events_state_from_info); + } else { + $this->_eventsManager = EventsStateManager::buildFromLegacyEventStrings($info['eventSourceString'], (bool)$info['eventsLabels']); + } // Regon of interest $this->_roi = Helper_RegionOfInterest::parsePolygonString($info['roi'], $info['imageScale']); @@ -249,7 +259,7 @@ public function build() { $statistics = new Database_Statistics(); $statistics->log('buildMovie'); - $statistics->logRedis('buildMovie'); + $statistics->logRedis('buildMovie'); } $this->_cleanUp(); @@ -281,7 +291,7 @@ public function getCompletedMovieInformation($verbose=false) { 'duration' => $this->getDuration(), 'imageScale' => $this->imageScale, 'layers' => $this->_layers->serialize(), - 'events' => $this->_events->serialize(), + 'events' => $this->_eventsManager->getState(), 'x1' => $this->_roi->left(), 'y1' => $this->_roi->top(), 'x2' => $this->_roi->right(), @@ -351,6 +361,12 @@ public function getCurrentFrame() { } } } + + // Do not call closedir boolean if we can not open directory + if (false === $handle) { + throw new \Exception("Could not find requested movie frames"); + } + @closedir($handle); return ($newest_frame>0) ? (int)$newest_frame : null; @@ -421,8 +437,8 @@ private function _buildMovieFrames($watermark) { 'compress' => false, 'interlace' => false, 'watermark' => $watermark, - 'movie' => true, - 'size' => $this->size, + 'movie' => true, + 'size' => $this->size, 'followViewport' => $this->followViewport, 'startDate' => $this->startDate, 'reqStartDate' => $this->reqStartDate, @@ -444,8 +460,8 @@ private function _buildMovieFrames($watermark) { try { $screenshot = new Image_Composite_HelioviewerMovieFrame( - $filepath, $this->_layers, $this->_events, - $this->eventsLabels, $this->movieIcons, $this->celestialBodies, + $filepath, $this->_layers, $this->_eventsManager, + $this->movieIcons, $this->celestialBodies, $this->scale, $this->scaleType, $this->scaleX, $this->scaleY, $time, $this->_roi, $options); @@ -581,18 +597,18 @@ private function _encodeMovie() { // https://bugs.launchpad.net/helioviewer.org/+bug/979231 $frameRate = round($this->frameRate, 1); - if($this->size == 1){ - $this->width = 1280; - $this->height = 720; + if($this->size == 1){ + $this->width = 1280; + $this->height = 720; }else if($this->size == 2){ - $this->width = 1920; - $this->height = 1080; + $this->width = 1920; + $this->height = 1080; }else if($this->size == 3){ - $this->width = 2560; - $this->height = 1440; + $this->width = 2560; + $this->height = 1440; }else if($this->size == 4){ - $this->width = 3840; - $this->height = 2160; + $this->width = 3840; + $this->height = 2160; } // Create and FFmpeg encoder instance @@ -761,21 +777,21 @@ private function _setMovieProperties() { $this->_setMovieDimensions(); - if($this->size == 1){ - $width = 1280; - $height = 720; + if($this->size == 1){ + $width = 1280; + $height = 720; }else if($this->size == 2){ - $width = 1920; - $height = 1080; + $width = 1920; + $height = 1080; }else if($this->size == 3){ - $width = 2560; - $height = 1440; + $width = 2560; + $height = 1440; }else if($this->size == 4){ - $width = 3840; - $height = 2160; + $width = 3840; + $height = 2160; }else{ - $width = $this->width; - $height = $this->height; + $width = $this->width; + $height = $this->height; } // Update movie entry in database with new details diff --git a/src/Validation/InputValidator.php b/src/Validation/InputValidator.php index a0b9c1c08..bdb5643bb 100644 --- a/src/Validation/InputValidator.php +++ b/src/Validation/InputValidator.php @@ -34,18 +34,19 @@ public static function checkInput(&$expected, &$input, &$optional) { // Validation checks $checks = array( - "required" => "checkForMissingParams", - "alphanum" => "checkAlphaNumericStrings", - "ints" => "checkInts", - "floats" => "checkFloats", - "bools" => "checkBools", - "dates" => "checkDates", - "encoded" => "checkURLEncodedStrings", - "urls" => "checkURLs", - "files" => "checkFilePaths", - "uuids" => "checkUUIDs", - "layer" => "checkLayerValidity", - "choices" => "checkChoices" + "required" => "checkForMissingParams", + "alphanum" => "checkAlphaNumericStrings", + "ints" => "checkInts", + "array_ints" => "checkOfArrayInts", + "floats" => "checkFloats", + "bools" => "checkBools", + "dates" => "checkDates", + "encoded" => "checkURLEncodedStrings", + "urls" => "checkURLs", + "files" => "checkFilePaths", + "uuids" => "checkUUIDs", + "layer" => "checkLayerValidity", + "choices" => "checkChoices" ); // Run validation checks @@ -211,6 +212,36 @@ public static function checkInts($ints, &$params) } } + + /** + * Typecasts validates and fixes types for array integer parameters + * + * @param array $ints A list of integer array parameters which are used by an action. + * @param array &$params The parameters that were passed in + * + * @return void + */ + public static function checkOfArrayInts($ints, &$params) + { + foreach ($ints as $int) { + if (isset($params[$int])) { + + + $integers_to_check = explode(',',$params[$int]); + $validated_ints = []; + + foreach($integers_to_check as $itc) { + if (filter_var(trim($itc), FILTER_VALIDATE_INT) === false) { + throw new InvalidArgumentException("Invalid value for $int. Please specify an integer array value, as ex:1,2,3", 25); + } + $validated_ints[] = (int) trim($itc); + } + + $params[$int] = $validated_ints; + } + } + } + /** * Typecasts validates and fixes types for float parameters * diff --git a/tests/unit_tests/events/EventsStateManagerTest.php b/tests/unit_tests/events/EventsStateManagerTest.php new file mode 100644 index 000000000..960abf59e --- /dev/null +++ b/tests/unit_tests/events/EventsStateManagerTest.php @@ -0,0 +1,458 @@ + [ + 'id' => 'HEK', + 'markers_visible' => true, + 'labels_visible' => true, + 'layers' => [ + [ + 'event_type' => 'flare', + 'frms' => ['frm10', 'frm20'], + 'event_instances' => ['flare--frm1--event1', 'flare--frm2--event2'], + 'open' => true, + ] + ] + ], + 'tree_CCMC' => [ + 'id' => 'CCMC', + 'markers_visible' => true, + 'labels_visible' => false, + 'layers' => [ + [ + 'event_type' => 'storm', + 'frms' => ['frm30', 'frm40'], + 'event_instances' => ['storm--frm3--event3', 'storm--frm4--event4'], + 'open' => true, + ] + ] + ] + ]; + + + // Test building EventsStateManager from events state with correct tree + public function testItShouldBuildFromEventStateArrayAsExpectedWithAllFrms() + { + $events_state_with_all = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => true, + 'labels_visible' => false, + 'layers' => [ + [ + 'event_type' => 'flare', + 'frms' => ['all'], + 'event_instances' => ['flare--frm1--event1', 'flare--frm2--event2'], + 'open' => true, + ] + ] + ], + ]; + $manager = EventsStateManager::buildFromEventsState($events_state_with_all); + $this->assertInstanceOf(EventsStateManager::class, $manager); + $this->assertEquals($manager->getStateTree(), [ + 'flare' => 'all_frms', + ]); + $this->assertEquals($manager->getStateTreeLabelVisibility(), [ + 'flare' => false, + ]); + + } + + // Test building EventsStateManager from events state with correct tree + public function testItShouldBuildFromEventStateArrayAsExpected() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $this->assertInstanceOf(EventsStateManager::class, $manager); + $this->assertEquals($manager->getStateTree(), [ + 'flare' => [ + 'frm10' => 'all_event_instances', + 'frm20' => 'all_event_instances', + 'frm1' => [ + 'flare--frm1--event1' + ], + 'frm2' => [ + 'flare--frm2--event2' + ], + ], + 'storm' => [ + 'frm30' => 'all_event_instances', + 'frm40' => 'all_event_instances', + 'frm3' => [ + 'storm--frm3--event3' + ], + 'frm4' => [ + 'storm--frm4--event4' + ], + ], + ]); + $this->assertEquals($manager->getStateTreeLabelVisibility(), [ + 'storm' => false, + 'flare' => true, + ]); + + } + + // Test building EventsStateManager from events state with correct tree + public function testItShouldCorrectlyIgnoreLayersWithNotVisibleMarkersAndBuildFromEventStateArrayAsExpected() + { + $event_state_markers_not_visible = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => false, + 'labels_visible' => true, + 'layers' => [ + [ + 'event_type' => 'flare', + 'frms' => ['frm10', 'frm20'], + 'event_instances' => ['flare--frm1--event1', 'flare--frm2--event2'], + 'open' => true, + ] + ] + ], + 'tree_CCMC' => [ + 'id' => 'CCMC', + 'markers_visible' => true, + 'labels_visible' => false, + 'layers' => [ + [ + 'event_type' => 'storm', + 'frms' => ['frm30', 'frm40'], + 'event_instances' => ['storm--frm3--event3', 'storm--frm4--event4'], + 'open' => true, + ] + ] + ] + ]; + $manager = EventsStateManager::buildFromEventsState($event_state_markers_not_visible); + $this->assertInstanceOf(EventsStateManager::class, $manager); + $this->assertEquals($manager->getStateTree(), [ + 'storm' => [ + 'frm30' => 'all_event_instances', + 'frm40' => 'all_event_instances', + 'frm3' => [ + 'storm--frm3--event3' + ], + 'frm4' => [ + 'storm--frm4--event4' + ], + ], + ]); + $this->assertEquals($manager->getStateTreeLabelVisibility(), [ + 'storm' => false, + ]); + + } + + public function testItShouldBuildFromInvalidEventStateArrayWithInvalidEventInstancesAsExpected() + { + $invalid_event_state = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => true, + 'labels_visible' => true, + 'layers' => [ + [ + 'event_type' => 'flare', + 'frms' => ['frm10', 'frm20'], + 'event_instances' => ['flare--frm10--event1', 'flare--frm20--event2'], + 'open' => true, + ] + ] + ], + 'tree_CCMC' => [ + 'id' => 'CCMC', + 'markers_visible' => true, + 'labels_visible' => false, + 'layers' => [ + [ + 'event_type' => 'storm', + 'frms' => ['frm30', 'frm40'], + 'event_instances' => ['storm--frm30--event3', 'storm--frm40--event4'], + 'open' => true, + ] + ] + ] + ]; + $manager = EventsStateManager::buildFromEventsState($invalid_event_state); + $this->assertInstanceOf(EventsStateManager::class, $manager); + $this->assertEquals($manager->getStateTree(), [ + 'flare' => [ + 'frm10' => 'all_event_instances', + 'frm20' => 'all_event_instances', + ], + 'storm' => [ + 'frm30' => 'all_event_instances', + 'frm40' => 'all_event_instances', + ], + ]); + $this->assertEquals($manager->getStateTreeLabelVisibility(), [ + 'storm' => false, + 'flare' => true, + ]); + } + + // Test building EventsStateManager from legacy event strings + public function testItShouldBuildFromLegacyEventStrings1() + { + $legacyString = '[flare,frm1;frm2,1],[storm,frm3;frm4,0]'; + $manager = EventsStateManager::buildFromLegacyEventStrings($legacyString, true); + $this->assertInstanceOf(EventsStateManager::class, $manager); + $this->assertEquals($manager->getStateTree(), [ + 'flare' => [ + 'frm1' => 'all_event_instances', + 'frm2' => 'all_event_instances', + ], + 'storm' => [ + 'frm3' => 'all_event_instances', + 'frm4' => 'all_event_instances', + ] + ]); + $this->assertEquals($manager->getStateTreeLabelVisibility(), [ + 'storm' => true, + 'flare' => true, + ]); + } + + public function testItShouldBuildFromLegacyEventStrings2() + { + $legacyString = '[flare,frm1,1],[storm,frm3;frm4,0]'; + $manager = EventsStateManager::buildFromLegacyEventStrings($legacyString, false); + $this->assertInstanceOf(EventsStateManager::class, $manager); + $this->assertEquals($manager->getStateTree(), [ + 'flare' => [ + 'frm1' => 'all_event_instances', + ], + 'storm' => [ + 'frm3' => 'all_event_instances', + 'frm4' => 'all_event_instances', + ] + ]); + $this->assertEquals($manager->getStateTreeLabelVisibility(), [ + 'storm' => false, + 'flare' => false, + ]); + } + + public function testItShouldBuildFromLegacyEventStrings3() + { + $legacyString = '[flare,frm1,1],[storm,,0]'; + $manager = EventsStateManager::buildFromLegacyEventStrings($legacyString, false); + $this->assertInstanceOf(EventsStateManager::class, $manager); + $this->assertEquals($manager->getStateTree(), [ + 'flare' => [ + 'frm1' => 'all_event_instances', + ], + ]); + $this->assertEquals($manager->getStateTreeLabelVisibility(), [ + 'flare' => false, + ]); + } + + // Test if the manager correctly identifies no events + public function testItShouldCorrectlySayIfNoEvents() + { + $emptyState = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => false, + 'labels_visible' => false, + 'layers' => [] + ], + 'tree_CCMC' => [ + 'id' => 'CCMC', + 'markers_visible' => false, + 'labels_visible' => false, + 'layers' => [] + ] + ]; + $manager = EventsStateManager::buildFromEventsState($emptyState); + $this->assertFalse($manager->hasEvents()); + } + + public function testItShouldCorrectlySayIfNoEventsWithLegacyStrings() + { + $legacyString = '[flare,,1],[storm,,0]'; + $manager = EventsStateManager::buildFromLegacyEventStrings($legacyString, false); + $this->assertFalse($manager->hasEvents()); + } + + // Test checking for events of a specific type + public function testItShouldTellIfHasEventsForEventType() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $this->assertTrue($manager->hasEventsForEventType('flare')); + $this->assertTrue($manager->hasEventsForEventType('storm')); + } + + // Test checking for events of an unknown type + public function testItShouldTellIfHasNoEventsForEventType() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $this->assertFalse($manager->hasEventsForEventType('unknown_event_type')); + } + + // Test if the manager applies all events for a specific type + public function testItShouldTellIfStateAppliesAllEventsForEventType() + { + $events_state = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => true, + 'labels_visible' => true, + 'layers' => [ + [ + 'event_type' => 'flare', + 'frms' => ['all'], + 'event_instances' => [], + 'open' => true, + ] + ] + ], + 'tree_CCMC' => [ + 'id' => 'CCMC', + 'markers_visible' => true, + 'labels_visible' => true, + 'layers' => [ + [ + 'event_type' => 'foo', + 'frms' => ['hede'], + 'event_instances' => [], + 'open' => true, + ] + ] + ] + ]; + $manager = EventsStateManager::buildFromEventsState($events_state); + $this->assertTrue($manager->appliesAllEventsForEventType('flare')); + $this->assertFalse($manager->appliesAllEventsForEventType('foo')); + } + + + // Test if a specific frm is applied for an event type + public function testItShouldAppliesFrmForEventType() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $this->assertTrue($manager->appliesFrmForEventType('flare', 'frm1')); + $this->assertTrue($manager->appliesFrmForEventType('flare', 'frm10')); + $this->assertFalse($manager->appliesFrmForEventType('flare', 'frm3')); + $this->assertTrue($manager->appliesFrmForEventType('storm', 'frm3')); + $this->assertTrue($manager->appliesFrmForEventType('storm', 'frm30')); + $this->assertFalse($manager->appliesFrmForEventType('storm', 'frm1')); + } + + // Test if a non-existent frm is applied for an event type + public function testItShouldNotApplyFrmForEventTypeNoFrm() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $this->assertFalse($manager->appliesFrmForEventType('flare', 'unknown_frm')); + $this->assertFalse($manager->appliesFrmForEventType('storm', 'unknown_frm')); + } + + // Test if all event instances for a frm are applied + public function testItShouldApplyAllEventInstancesForFrm() + { + $eventsState = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => true, + 'labels_visible' => true, + 'layers' => [ + [ + 'event_type' => 'flare', + 'frms' => ['frm1'], + 'event_instances' => ['flare--frm2--all_event_instances'], + 'open' => true, + ] + ] + ] + ]; + $manager = EventsStateManager::buildFromEventsState($eventsState); + $this->assertTrue($manager->appliesAllEventInstancesForFrm('flare', 'frm1')); + $this->assertFalse($manager->appliesAllEventInstancesForFrm('flare', 'frm2')); + } + + + // Test if a specific event instance is applied + public function testItShouldApplyEventInstanceWhenMatch() + { + $event_1 = ['id' => 'event1']; + $event_id_1 = EventsStateManager::makeEventId('flare', 'frm1', $event_1); + $event_2 = ['id' => 'event2']; + $event_id_2 = EventsStateManager::makeEventId('flare', 'frm2', $event_2); + + $event_3 = ['id' => 'event3']; + $event_id_3 = EventsStateManager::makeEventId('storm', 'frm3', $event_3); + $event_4 = ['id' => 'event4']; + $event_id_4 = EventsStateManager::makeEventId('storm', 'frm4', $event_4); + + $event_5 = ['id' => 'event5']; + $event_id_5 = EventsStateManager::makeEventId('storm', 'frm4', $event_5); + + $event_state = [ + 'tree_HEK' => [ + 'id' => 'HEK', + 'markers_visible' => true, + 'labels_visible' => true, + 'layers' => [ + [ + 'event_type' => 'flare', + 'frms' => ['frm10', 'frm20'], + 'event_instances' => [$event_id_1, $event_id_2], + 'open' => true, + ] + ] + ], + 'tree_CCMC' => [ + 'id' => 'CCMC', + 'markers_visible' => true, + 'labels_visible' => false, + 'layers' => [ + [ + 'event_type' => 'storm', + 'frms' => ['frm30', 'frm40'], + 'event_instances' => [$event_id_3, $event_id_4], + 'open' => true, + ] + ] + ] + ]; + + $manager = EventsStateManager::buildFromEventsState($event_state); + $this->assertTrue($manager->appliesEventInstance('flare', 'frm1', $event_1)); + $this->assertTrue($manager->appliesEventInstance('flare', 'frm2', $event_2)); + $this->assertTrue($manager->appliesEventInstance('storm', 'frm3', $event_3)); + $this->assertTrue($manager->appliesEventInstance('storm', 'frm4', $event_4)); + $this->assertFalse($manager->appliesEventInstance('storm', 'frm3', $event_5)); + } + + // Test if a non-existent event instance is applied + public function testItShouldNotApplyEventInstanceNoInstance() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $event = ['id' => 'unknown_event']; + $this->assertFalse($manager->appliesEventInstance('flare', 'frm1', $event)); + } + + // Test if event type labels are visible + public function testItShouldCorrectlyKeepEventTypesLabelVisible() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $this->assertTrue($manager->isEventTypeLabelVisible('flare')); + $this->assertFalse($manager->isEventTypeLabelVisible('storm')); + } + + // Test if event type labels are visible for an unknown event type + public function testItShouldCorrectlyReportNonExistantEventTypesLabelVisible() + { + $manager = EventsStateManager::buildFromEventsState($this->eventsState); + $this->assertFalse($manager->isEventTypeLabelVisible('unknown_event_type')); + } +} + diff --git a/tests/unit_tests/movies/reQueueMovieTest.php b/tests/unit_tests/movies/reQueueMovieTest.php index e575448c8..d96472d3b 100644 --- a/tests/unit_tests/movies/reQueueMovieTest.php +++ b/tests/unit_tests/movies/reQueueMovieTest.php @@ -70,7 +70,6 @@ private function _markMovieAsProcessing($movie) { * the movie is already there */ public function testRequeueMovie_MovieExists() { - $this->markTestSkipped("Integration Tests to be handled later"); // Queue the test movie $result = $this->_queueTestMovie(); // Build the test movie @@ -93,7 +92,6 @@ public function testRequeueMovie_MovieExists() { * @runInSeparateProcess */ public function testRequeueMovie_Force() { - $this->markTestSkipped("Integration Tests to be handled later"); // Queue the test movie $result = $this->_queueTestMovie(); // Build the test movie @@ -115,7 +113,6 @@ public function testRequeueMovie_Force() { * Helper function so the test can be run with both force = true & false */ public function _testRequeueMovie_MovieProcessing($force) { - $this->markTestSkipped("Integration Tests to be handled later"); // Queue the test movie $result = $this->_queueTestMovie(); // Build the test movie @@ -146,7 +143,6 @@ public function _testRequeueMovie_MovieProcessing($force) { * @runInSeparateProcess */ public function testRequeueMovie_MovieProcessing() { - $this->markTestSkipped("Integration Tests to be handled later"); $this->_testRequeueMovie_MovieProcessing(false); } @@ -156,7 +152,6 @@ public function testRequeueMovie_MovieProcessing() { * @runInSeparateProcess */ public function testRequeueMovie_MovieProcessing_Force() { - $this->markTestSkipped("Integration Tests to be handled later"); $this->_testRequeueMovie_MovieProcessing(true); } @@ -164,7 +159,6 @@ public function testRequeueMovie_MovieProcessing_Force() { * Issue #262 - When mp4 creation fails, movie can't be requeued */ public function testRequeueMovie_262() { - $this->markTestSkipped("Integration Tests to be handled later"); // Queue the test movie $result = $this->_queueTestMovie(); // Get the movie ID as an integer diff --git a/tests/unit_tests/validation/ValidatorTest.php b/tests/unit_tests/validation/ValidatorTest.php index 193899236..e7f3d2ff9 100644 --- a/tests/unit_tests/validation/ValidatorTest.php +++ b/tests/unit_tests/validation/ValidatorTest.php @@ -61,4 +61,60 @@ public function test_ValidateLayerArray_MultipleLayers(): void // that no exception was raised. $this->assertTrue(true); } + + public function test_ValidateArrayIntegersProblem1(): void + { + // The expected layer string to be given + $input = array( + 'sources' => '' + ); + + $rules = array( + 'array_ints' => array('sources') + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid value for sources. Please specify an integer array value, as ex:1,2,3'); + Validation_InputValidator::checkInput($rules, $input, $input) ; + // checkInput will raise an exception if it fails, so assertTrue means + // that no exception was raised. + } + + public function test_ValidateArrayIntegersProblem2(): void + { + // The expected layer string to be given + $input = array( + 'sources' => 'a,1,2' + ); + + $rules = array( + 'array_ints' => array('sources') + ); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid value for sources. Please specify an integer array value, as ex:1,2,3'); + Validation_InputValidator::checkInput($rules, $input, $input) ; + // checkInput will raise an exception if it fails, so assertTrue means + // that no exception was raised. + } + + public function test_ValidateArrayIntegersCorrectly(): void + { + // The expected layer string to be given + $input = array( + 'sources' => '4,1,2' + ); + + $rules = array( + 'array_ints' => array('sources') + ); + + Validation_InputValidator::checkInput($rules, $input, $input) ; + + $this->assertEquals($input, [ + 'sources' => [4,1,2] + ]); + // checkInput will raise an exception if it fails, so assertTrue means + // that no exception was raised. + } }