From 353434609fb188ecb191cc8099b3c1db3a562852 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 9 Apr 2024 19:51:22 +0200 Subject: [PATCH 01/43] fix build --- apps/fishing-map/features/vessel/insights/InsightFishing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/features/vessel/insights/InsightFishing.tsx b/apps/fishing-map/features/vessel/insights/InsightFishing.tsx index af66d15f8f..74ed2711d7 100644 --- a/apps/fishing-map/features/vessel/insights/InsightFishing.tsx +++ b/apps/fishing-map/features/vessel/insights/InsightFishing.tsx @@ -6,7 +6,7 @@ import { ParsedAPIError } from '@globalfishingwatch/api-client' import InsightError from 'features/vessel/insights/InsightErrorMessage' import DataTerminology from 'features/vessel/identity/DataTerminology' import InsightEventDetails from 'features/vessel/insights/InsightEventsDetails' -import { selectVesselEventsDataWithVoyages } from '../vessel.selectors' +import { selectVesselEventsDataWithVoyages } from '../selectors/vessel.resources.selectors' import styles from './Insights.module.css' const InsightFishing = ({ From 27cfa99f1c46d463c3fe847af3e8478d64b5cdd1 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 15:42:20 +0200 Subject: [PATCH 02/43] New translations translations.json (English) --- apps/fishing-map/public/locales/en/translations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/public/locales/en/translations.json b/apps/fishing-map/public/locales/en/translations.json index d5bab5584e..c9f96464fd 100644 --- a/apps/fishing-map/public/locales/en/translations.json +++ b/apps/fishing-map/public/locales/en/translations.json @@ -812,7 +812,7 @@ "flagChangesCount": "{{count}} flag changes", "flagChangesEmpty": "No flag changes", "MOULists": "Tokyo and Paris MOU Lists", - "MOUListsEmpty": "Flying under a flag/flags not present on the Tokio or Paris MOU black or grey lists", + "MOUListsEmpty": "Flying under a flag/flags not present on the Tokyo or Paris MOU black or grey lists", "MOUParisListsCount": "Flag present on the Paris MOU black or grey list ({{flags}})", "MOUTokyoListsCount": "Flag present on the Tokyo MOU black or grey list ({{flags}})", "MOUTokyoListsPreviousAppearance": "Previously flew under another flag on the Tokyo MOU black or grey lists", From 832f45aa1d47ffe022d83131a2d3d8ba8c192c1b Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 16:28:47 +0200 Subject: [PATCH 03/43] New translations data-terminology.json (English) --- apps/fishing-map/public/locales/en/data-terminology.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/public/locales/en/data-terminology.json b/apps/fishing-map/public/locales/en/data-terminology.json index 94cca4fff1..aa8e0d6591 100644 --- a/apps/fishing-map/public/locales/en/data-terminology.json +++ b/apps/fishing-map/public/locales/en/data-terminology.json @@ -9,7 +9,7 @@ "encounter": "

Overview

\n
    \n
  • Encounter events describe when AIS data shows two vessels that appear to be meeting at sea. Encounter events can be indicative of potential transshipment events.
  • \n
  • Global Fishing Watch records an event as an encounter when two vessels are detected within 500 meters of each other for at least 2 hours, travelling at a median speed of less than 2 knots, whilst at least 10 kilometers (5.4 nautical miles) from a coastal anchorage.
  • \n
  • Currently, encounter events are shown between carrier and fishing vessels and support and fishing vessels. In the future, encounters between other vessel types may be displayed.
  • \n
  • View details on the encounter, such as location of the event and details of the encountered vessel, by clicking on the ‘i’ icon associated with the encounter.
  • \n
\n

Caveats

\n
    \n
  • There may be many other reasons (i.e. not transhipment activity) why two vessels encounter each other at sea. Vessels may have encounters to exchange equipment or for crew safety.
  • \n
  • Given the different reasons that vessels may meet at sea, these events are intended to support further review of activity and should not be used in isolation.
  • \n
  • Sometimes transhipment events fall below the criteria used to define an encounter event, and therefore may not appear in our data. It is important to corroborate encounter events with other sources of information, such as RFMO transhipment records.
  • \n
  • If transmission data is poor, the average location can be inconsistent with the vessel track and the event may appear slightly alongside the track.
  • \n
  • Not all encounters are shown in vessel viewer. Currently, encounter events are shown between carrier and fishing vessels and support and fishing vessels. In the future, encounters between other vessel types may be displayed.
  • \n
\n

Learn more

\n
    \n

    You can read more about transshipment behaviour from our report or scientific publication.

    ", "loitering": "

    Overview

    \n
      \n
    • Loitering events are recorded when one vessel shows signs of potential encounters, or meeting another vessel at sea, but there is no second vessel detected.
    • \n
    • Loitering events are based on speed and distance from shore.
    • \n
    • A loitering event is recorded when a vessel travels at an average speed of less than 2 knots over 20 nautical miles (37.04 kilometers) from shore.
    • \n
    • There may be many reasons a loitering event is recorded, so results should be interpreted with caution.
    • \n
    \n

    Caveats

    \n
      \n
    • There may be many reasons a vessel slows down away from shore.
    • \n
    • Loitering events are indicative, and the vessel may not be meeting any other vessels at the time of the event.
    • \n
    • Other events in which a vessel may remain fairly stationary or moving slowly include maintenance, poor weather or waiting for owner instruction.
    • \n
    • Loitering events for fishing vessels may be associated with normal fishing behavior, as fishing vessels often move slowly during fishing operations.
    • \n
    • Due to the individual definitions of loitering events and encounter events, it is possible for a loitering event to overlap with an encounter event, representing the same activity, or the loitering event may encompass one or more encounter events.
    • \n
    • If transmission data is poor, the average location can be inconsistent with the vessel track and the event may appear slightly alongside the track.
    • \n
    ", "fishing": "

    Overview

    \n
      \n
    • Global Fishing Watch analyzes AIS data collected from vessels that our research has identified as known or possible fishing vessels, and applies a fishing detection algorithm to determine “apparent fishing activity” based on changes in vessel speed and direction.
    • \n
    • Our machine learning model classifies each AIS broadcast data point for these vessels as either apparently fishing or not fishing.
    • \n
    • Fishing events use those data points as input and summarize them into one event for easier analysis.
    • \n
    • A fishing event is defined where:
        \n
      • Fishing positions appear consecutively and are separated by less than 10 kilometers or 2 hours; and,
      • \n
      • Fishing positions within 1 hour and 2 kilometers of another fishing event are grouped together into a single event.
      • \n
      \n
    • \n
    • The dataset is further restricted by removing fishing events that are brief and fast, as these are less likely to indicate a realistic fishing event. The following short fishing events are removed:
        \n
      • Events less than 20 minutes in duration;
      • \n
      • Events comprised of five or fewer fishing positions;
      • \n
      • Events that cover distance of less than 0.5 km (for all gears except estimated squid gear);
      • \n
      • Events that cover distance of less than 50m (for estimated squid gear); and,
      • \n
      • Abnormally fast moving vessel events with an average vessel speed of 10 knots or greater.
      • \n
      \n
    • \n
    \n

    Caveats

    \n
      \n
    • False positives may appear in the dataset where vessel’s slow down and change direction, but aren’t engaged in fishing activity.
    • \n
    • Our machine learning model is better at predicting some types of fishing (e.g. trawling and longlining) compared to other types of fishing that may not have been as present in data used to train the machine learning model.
    • \n
    • Most AIS devices fall into one of three “classes”: Class A, Class B, and Class B+. Class A devices broadcast at a stronger power and they broadcast a vessel’s position more frequently. Lower wattage Class B and B+ devices are detected less frequently by satellites, and therefore it may seem there is less fishing activity in areas where they are predominantly used.
    • \n
    • In addition to areas with low satellite coverage, areas with a high density of vessel traffic can also limit the number of signals processed, particularly related to Class B (or B+) AIS systems. This may lead to reduced or underestimated vessel activity in the Global Fishing Watch map or data in such areas.
    • \n
    \n

    Learn more

    Learn more about our estimation of apparent fishing effort.", - "insights": "

    Overview

    The Vessel Insights tab offers a set of indicators that bring together important information on a vessel’s known activity (based on AIS) and authorizations sourced from public regional and national registries. The objective is to support users in due diligence review, risk-based assessment and inform decision making and operational planning. Insights offer key characteristic information of a vessel and indicate potential or opportunity of a vessel engaged in illegal, unreported and unregulated (IUU) fishing, thus requiring further investigation. Vessel insights shall be treated with caution. All references to activity events, including fishing, encounters, loitering, AIS gap and port visit should be understood in the context of Global Fishing Watch's algorithms, which are best efforts to determine apparent vessel activity events based on AIS data collected via satellites and terrestrial receivers.

    Caveats

    • The insights presented are from over 40 public registries with more consistent data inputs and calculation from January 1, 2017. We recommend adjusting the time range from 2017 onwards.
    • AIS data and generally public registries vary in completeness, accuracy and quality. It is possible that some events are not identified. 
    • It is also possible that some events are identified but are incorrect or do not indicate actual activity. 
    • Global Fishing Watch qualifies all designations of events, including synonyms of event terms such as \"fishing effort,\" \"fishing\" or \"fishing activity,\" as apparent rather than certain. 
    • Any/all Global Fishing Watch information about apparent events should be considered an estimate and must be relied upon solely at your own risk.
    • Global Fishing Watch is constantly improving processes to make sure event algorithms and designations are as accurate as possible.
    ", + "insights": "

    Overview

    The Vessel Insights tab offers a set of indicators that bring together important information on a vessel’s known activity (based on AIS) and authorizations sourced from public regional and national registries. The objective is to support users in due diligence review, risk-based assessment and inform decision making and operational planning. Insights offer key characteristic information of a vessel and indicate potential or opportunity of a vessel engaged in illegal, unreported and unregulated (IUU) fishing, thus requiring further investigation. Vessel insights shall be treated with caution. All references to activity events, including fishing, encounters, loitering, AIS gap and port visit should be understood in the context of Global Fishing Watch's algorithms, which are best efforts to determine apparent vessel activity events based on AIS data collected via satellites and terrestrial receivers.

    Caveats

    • The insights presented are from over 40 public registries with more consistent data inputs and calculation from January 1, 2020. We recommend adjusting the time range from 2020 onwards.
    • AIS data and generally public registries vary in completeness, accuracy and quality. It is possible that some events are not identified. 
    • It is also possible that some events are identified but are incorrect or do not indicate actual activity. 
    • Global Fishing Watch qualifies all designations of events, including synonyms of event terms such as \"fishing effort,\" \"fishing\" or \"fishing activity,\" as apparent rather than certain. 
    • Any/all Global Fishing Watch information about apparent events should be considered an estimate and must be relied upon solely at your own risk.
    • Global Fishing Watch is constantly improving processes to make sure event algorithms and designations are as accurate as possible.
    ", "insightsCoverage": "

    Overview

    The coverage metric is an estimate of how well a vessel’s activities outside of port, i.e. where it travelled and what it did, can be captured by the vessel’s AIS data for the time range of interest. A vessel’s AIS coverage metric is critical to interpreting vessel activity information. The higher the coverage percentage, the greater confidence on the accuracy of events listed to represent the vessel’s actual activities.

    To calculate the coverage metric, all voyages linked to a vessel in the selected time range are segmented into one hour blocks, and the proportion of each hour block a vessel in a voyage has at least one AIS transmission. An ‘N/A’ value could be due to no reported activity for the vessel in the selected time range. This could be of poor coverage, but may also be the result of inactivity e.g. the vessel undergoing maintenance. In these cases, we recommend you check additional information sources and request supporting records from the vessel.

    Caveats

    • As the AIS coverage metric is calculated based on voyages, eg. the vessel’s activity between port visits, coverage is reflective of the time and AIS reception quality while a vessel is active at sea rather than at port.
    • Voyage is defined based on port visits (exit and entry). Any issue detecting port visits for a vessel may result in lower accuracy of the AIS coverage calculation.
    • The coverage metric does not distinguish between a lack of activity (non-event) and poor transmission resulting in disability to detect a true event.

    Learn more

    Learn more about our work on transmission gaps.

    ", "insightsFishing": "

    Overview

    Global Fishing Watch (GFW) analyzes and applies a fishing detection algorithm based on AIS data collected from vessels. Each AIS broadcast data point is classified as either “apparently fishing” or not fishing, and further reviewed to determine individual “fishing events”. 

    Based on the time range of interest, the following two insights are reviewed: 

    • Any apparent fishing events detected in areas with no known RFMO authorization 
    • Any apparent fishing events detected in no-take MPAs

    Fishing in areas with no known RFMO authorization

    GFW indicates any apparent fishing events in areas with no known RFMO authorization compiled from 7 Regional Fisheries Management Organisation (RFMO) lists*. 

    Caveats

    • The insight covers the listed RFMO areas only*. 
    • The insight does not include national registration or licensing lists. 

    Fishing in no-take Marine Protected Areas (MPAs)

    This insight is presented by cross-referencing any apparent fishing events in the boundaries of no-take MPAs (Source: World Database on Protected Areas ).  

    Caveats

    • Events close to boundary lines may be reported as being inside an MPA boundary. We recommend that you check the vessel track positions on the map alongside adding the MPA layer to confirm exact operation. 
    • There may be variability in no-take MPA permissions and restrictions based on seasonal closures and gear restrictions. 

    General caveats

    • Due to limited access to national databases, the insights do not consider other unknown authorizations e.g. seasonal closures, fishing authorisation within EEZs. 
    • All references to activity events (e.g. fishing), should be understood in the context of GFW's algorithms, which are best efforts to determine apparent fishing activity events based on AIS data collected.
    • While the insights provide potential events, GFW recommends to visually inspect vessel tracks, always refer to additional information sources, and request records from a vessel to confirm any findings.

    * The 7 RFMO lists considered for the authorization insight include: 

    • Commission for the Conservation of Southern Bluefin Tuna (CCSBT)
    • Inter-American Tropical Tuna Commission (IATTC)
    • International Commission for the Conservation of Atlantic Tunas (ICCAT)
    • Indian Ocean Tuna Commission (IOTC)
    • North Pacific Fisheries Commission (NPFC)
    • South Pacific Regional Fisheries Management Organisation (SPRFMO)
    • Western and Central Pacific Fisheries Commission (WCPFC)

    Learn more

    Learn more about how apparent fishing is estimated through Global Fishing Watch’s technology , and the difference between apparent fishing effort and fishing events. ", "insightsFlagsChanges": "

    Overview

    This insight tracks and details when the registry information available on a vessel indicates the vessel has been flagged to more than one unique flag state during the time range of interest, indicating a potential flag change. Adjust the end date of the time bar to present date to show the current vessel flag; Extend time range to track historical changes.

    Caveats

    • Global Fishing Watch matches vessel identity data with over 40 public registries to validate an individual vessel and enable tracking of identity and activity changes throughout the vessel’s lifetime from construction to scrapping.
    • The vessel identity insights rely solely on registry data. The insights do not consider AIS based vessel flag identity.
    • The quantity and quality of available registry data vary by flag state, thereby introducing uneven degrees of vessel identity information.
    • Conflicts between information sources can occur leading to incorrect identification of vessel flags. This is more common for vessels not listed on public registries and vessels operating under multiple flags within a short time range.
    • While every attempt is made to ensure the insight provided is accurate and up-to-date, this cannot be guaranteed, particularly on any changes outside of the monthly data update cycle.

    Learn more

    Global Fishing Watch works to offset the lack of public registry data by complementing with other data sources and advocating for transparency in regional and national fora. Learn more about:

    ", From 61dd2544a815f9e4b49fba05b10f3da745760b70 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 16:32:27 +0200 Subject: [PATCH 04/43] update map popup content --- .../workspace/highlight-panel/highlight-panel.content.ts | 4 ++-- apps/fishing-map/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/fishing-map/features/workspace/highlight-panel/highlight-panel.content.ts b/apps/fishing-map/features/workspace/highlight-panel/highlight-panel.content.ts index de6591ffb9..77af5ec1d7 100644 --- a/apps/fishing-map/features/workspace/highlight-panel/highlight-panel.content.ts +++ b/apps/fishing-map/features/workspace/highlight-panel/highlight-panel.content.ts @@ -51,9 +51,9 @@ const HIGHLIGHT_CONFIGS: HighlightPanelConfig[] = [ learnMoreUrl: 'https://globalfishingwatch.org/faqs/2024-april-data-error/', en: { title: - 'IMPORTANT - Global Fishing Watch is working on an underlying AIS data error in the platform.', + 'Resolved - Global Fishing Watch has resolved an underlying AIS data error in the platform.', description: - "This is why you haven't been able to access the data since January 26th. If you accessed Global Fishing Watch AIS or VMS data since 26 January, there may be errors in the fishing effort and fishing event data. We are working on fixing the issue as soon as possible. Our advice is to wait for the fix and repeat any analysis with the correct data. This will be fixed by this Friday, April 12, at the latest. We're sorry for any inconvenience caused.", + 'If you accessed Global Fishing Watch AIS or VMS data between 26 January - 10 April 2024, there may be errors in the fishing effort and fishing event data. The AIS error has been resolved, and we are working on the VMS issue now. Our advice is to repeat any analysis completed with the correct data. We’re sorry for any inconvenience caused.', }, }, { diff --git a/apps/fishing-map/package.json b/apps/fishing-map/package.json index 9a9a845dfd..66a7f48af7 100644 --- a/apps/fishing-map/package.json +++ b/apps/fishing-map/package.json @@ -1,6 +1,6 @@ { "name": "@globalfishingwatchapp/fishing-map", - "version": "2.1.30", + "version": "2.2.0", "private": true, "scripts": { "dev": "next", From f878de0f05709173a3ce6a1f0443aa9c8f8df72d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 10 Apr 2024 14:34:22 +0000 Subject: [PATCH 05/43] Bump tar from 6.2.0 to 6.2.1 Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1. - [Release notes](https://github.com/isaacs/node-tar/releases) - [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md) - [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1) --- updated-dependencies: - dependency-name: tar dependency-type: indirect ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index eb7e21d556..7f4979bbbb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22782,8 +22782,8 @@ __metadata: linkType: hard "tar@npm:^6.1.11, tar@npm:^6.1.2": - version: 6.2.0 - resolution: "tar@npm:6.2.0" + version: 6.2.1 + resolution: "tar@npm:6.2.1" dependencies: chownr: "npm:^2.0.0" fs-minipass: "npm:^2.0.0" @@ -22791,7 +22791,7 @@ __metadata: minizlib: "npm:^2.1.1" mkdirp: "npm:^1.0.3" yallist: "npm:^4.0.0" - checksum: 10/2042bbb14830b5cd0d584007db0eb0a7e933e66d1397e72a4293768d2332449bc3e312c266a0887ec20156dea388d8965e53b4fc5097f42d78593549016da089 + checksum: 10/bfbfbb2861888077fc1130b84029cdc2721efb93d1d1fb80f22a7ac3a98ec6f8972f29e564103bbebf5e97be67ebc356d37fa48dbc4960600a1eb7230fbd1ea0 languageName: node linkType: hard From c1a9d5d448f8721092b6119c1b31a6450d770430 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 17:01:25 +0200 Subject: [PATCH 06/43] New Crowdin updates (#2602) New translations data-terminology --- apps/fishing-map/public/locales/en/data-terminology.json | 2 +- apps/fishing-map/public/locales/val/data-terminology.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/fishing-map/public/locales/en/data-terminology.json b/apps/fishing-map/public/locales/en/data-terminology.json index aa8e0d6591..89af204cb6 100644 --- a/apps/fishing-map/public/locales/en/data-terminology.json +++ b/apps/fishing-map/public/locales/en/data-terminology.json @@ -9,7 +9,7 @@ "encounter": "

    Overview

    \n
      \n
    • Encounter events describe when AIS data shows two vessels that appear to be meeting at sea. Encounter events can be indicative of potential transshipment events.
    • \n
    • Global Fishing Watch records an event as an encounter when two vessels are detected within 500 meters of each other for at least 2 hours, travelling at a median speed of less than 2 knots, whilst at least 10 kilometers (5.4 nautical miles) from a coastal anchorage.
    • \n
    • Currently, encounter events are shown between carrier and fishing vessels and support and fishing vessels. In the future, encounters between other vessel types may be displayed.
    • \n
    • View details on the encounter, such as location of the event and details of the encountered vessel, by clicking on the ‘i’ icon associated with the encounter.
    • \n
    \n

    Caveats

    \n
      \n
    • There may be many other reasons (i.e. not transhipment activity) why two vessels encounter each other at sea. Vessels may have encounters to exchange equipment or for crew safety.
    • \n
    • Given the different reasons that vessels may meet at sea, these events are intended to support further review of activity and should not be used in isolation.
    • \n
    • Sometimes transhipment events fall below the criteria used to define an encounter event, and therefore may not appear in our data. It is important to corroborate encounter events with other sources of information, such as RFMO transhipment records.
    • \n
    • If transmission data is poor, the average location can be inconsistent with the vessel track and the event may appear slightly alongside the track.
    • \n
    • Not all encounters are shown in vessel viewer. Currently, encounter events are shown between carrier and fishing vessels and support and fishing vessels. In the future, encounters between other vessel types may be displayed.
    • \n
    \n

    Learn more

    \n
      \n

      You can read more about transshipment behaviour from our report or scientific publication.

      ", "loitering": "

      Overview

      \n
        \n
      • Loitering events are recorded when one vessel shows signs of potential encounters, or meeting another vessel at sea, but there is no second vessel detected.
      • \n
      • Loitering events are based on speed and distance from shore.
      • \n
      • A loitering event is recorded when a vessel travels at an average speed of less than 2 knots over 20 nautical miles (37.04 kilometers) from shore.
      • \n
      • There may be many reasons a loitering event is recorded, so results should be interpreted with caution.
      • \n
      \n

      Caveats

      \n
        \n
      • There may be many reasons a vessel slows down away from shore.
      • \n
      • Loitering events are indicative, and the vessel may not be meeting any other vessels at the time of the event.
      • \n
      • Other events in which a vessel may remain fairly stationary or moving slowly include maintenance, poor weather or waiting for owner instruction.
      • \n
      • Loitering events for fishing vessels may be associated with normal fishing behavior, as fishing vessels often move slowly during fishing operations.
      • \n
      • Due to the individual definitions of loitering events and encounter events, it is possible for a loitering event to overlap with an encounter event, representing the same activity, or the loitering event may encompass one or more encounter events.
      • \n
      • If transmission data is poor, the average location can be inconsistent with the vessel track and the event may appear slightly alongside the track.
      • \n
      ", "fishing": "

      Overview

      \n
        \n
      • Global Fishing Watch analyzes AIS data collected from vessels that our research has identified as known or possible fishing vessels, and applies a fishing detection algorithm to determine “apparent fishing activity” based on changes in vessel speed and direction.
      • \n
      • Our machine learning model classifies each AIS broadcast data point for these vessels as either apparently fishing or not fishing.
      • \n
      • Fishing events use those data points as input and summarize them into one event for easier analysis.
      • \n
      • A fishing event is defined where:
          \n
        • Fishing positions appear consecutively and are separated by less than 10 kilometers or 2 hours; and,
        • \n
        • Fishing positions within 1 hour and 2 kilometers of another fishing event are grouped together into a single event.
        • \n
        \n
      • \n
      • The dataset is further restricted by removing fishing events that are brief and fast, as these are less likely to indicate a realistic fishing event. The following short fishing events are removed:
          \n
        • Events less than 20 minutes in duration;
        • \n
        • Events comprised of five or fewer fishing positions;
        • \n
        • Events that cover distance of less than 0.5 km (for all gears except estimated squid gear);
        • \n
        • Events that cover distance of less than 50m (for estimated squid gear); and,
        • \n
        • Abnormally fast moving vessel events with an average vessel speed of 10 knots or greater.
        • \n
        \n
      • \n
      \n

      Caveats

      \n
        \n
      • False positives may appear in the dataset where vessel’s slow down and change direction, but aren’t engaged in fishing activity.
      • \n
      • Our machine learning model is better at predicting some types of fishing (e.g. trawling and longlining) compared to other types of fishing that may not have been as present in data used to train the machine learning model.
      • \n
      • Most AIS devices fall into one of three “classes”: Class A, Class B, and Class B+. Class A devices broadcast at a stronger power and they broadcast a vessel’s position more frequently. Lower wattage Class B and B+ devices are detected less frequently by satellites, and therefore it may seem there is less fishing activity in areas where they are predominantly used.
      • \n
      • In addition to areas with low satellite coverage, areas with a high density of vessel traffic can also limit the number of signals processed, particularly related to Class B (or B+) AIS systems. This may lead to reduced or underestimated vessel activity in the Global Fishing Watch map or data in such areas.
      • \n
      \n

      Learn more

      Learn more about our estimation of apparent fishing effort.", - "insights": "

      Overview

      The Vessel Insights tab offers a set of indicators that bring together important information on a vessel’s known activity (based on AIS) and authorizations sourced from public regional and national registries. The objective is to support users in due diligence review, risk-based assessment and inform decision making and operational planning. Insights offer key characteristic information of a vessel and indicate potential or opportunity of a vessel engaged in illegal, unreported and unregulated (IUU) fishing, thus requiring further investigation. Vessel insights shall be treated with caution. All references to activity events, including fishing, encounters, loitering, AIS gap and port visit should be understood in the context of Global Fishing Watch's algorithms, which are best efforts to determine apparent vessel activity events based on AIS data collected via satellites and terrestrial receivers.

      Caveats

      • The insights presented are from over 40 public registries with more consistent data inputs and calculation from January 1, 2020. We recommend adjusting the time range from 2020 onwards.
      • AIS data and generally public registries vary in completeness, accuracy and quality. It is possible that some events are not identified. 
      • It is also possible that some events are identified but are incorrect or do not indicate actual activity. 
      • Global Fishing Watch qualifies all designations of events, including synonyms of event terms such as \"fishing effort,\" \"fishing\" or \"fishing activity,\" as apparent rather than certain. 
      • Any/all Global Fishing Watch information about apparent events should be considered an estimate and must be relied upon solely at your own risk.
      • Global Fishing Watch is constantly improving processes to make sure event algorithms and designations are as accurate as possible.
      ", + "insights": "

      Overview

      The Vessel Insights tab offers a set of indicators that bring together important information on a vessel’s known activity (based on AIS) and authorizations sourced from public regional and national registries. The objective is to support users in due diligence review, risk-based assessment and inform decision making and operational planning. Insights offer key characteristic information of a vessel and indicate potential or opportunity of a vessel engaged in illegal, unreported and unregulated (IUU) fishing, thus requiring further investigation. Vessel insights shall be treated with caution. All references to activity events, including fishing, encounters, loitering, AIS gap and port visit should be understood in the context of Global Fishing Watch's algorithms, which are best efforts to determine apparent vessel activity events based on AIS data collected via satellites and terrestrial receivers.

      Caveats

      • The insights presented are from over 40 public registries with more consistent data inputs and calculation from January 1, 2020. We recommend adjusting the time range from 2020 onwards.
      • AIS data and generally public registries vary in completeness, accuracy and quality. It is possible that some events are not identified. 
      • It is also possible that some events are identified but are incorrect or do not indicate actual activity. 
      • Global Fishing Watch qualifies all designations of events, including synonyms of event terms such as \\\"fishing effort,\\\" \\\"fishing\\\" or \\\"fishing activity,\\\" as apparent rather than certain. 
      • Any/all Global Fishing Watch information about apparent events should be considered an estimate and must be relied upon solely at your own risk.
      • Global Fishing Watch is constantly improving processes to make sure event algorithms and designations are as accurate as possible.
      ", "insightsCoverage": "

      Overview

      The coverage metric is an estimate of how well a vessel’s activities outside of port, i.e. where it travelled and what it did, can be captured by the vessel’s AIS data for the time range of interest. A vessel’s AIS coverage metric is critical to interpreting vessel activity information. The higher the coverage percentage, the greater confidence on the accuracy of events listed to represent the vessel’s actual activities.

      To calculate the coverage metric, all voyages linked to a vessel in the selected time range are segmented into one hour blocks, and the proportion of each hour block a vessel in a voyage has at least one AIS transmission. An ‘N/A’ value could be due to no reported activity for the vessel in the selected time range. This could be of poor coverage, but may also be the result of inactivity e.g. the vessel undergoing maintenance. In these cases, we recommend you check additional information sources and request supporting records from the vessel.

      Caveats

      • As the AIS coverage metric is calculated based on voyages, eg. the vessel’s activity between port visits, coverage is reflective of the time and AIS reception quality while a vessel is active at sea rather than at port.
      • Voyage is defined based on port visits (exit and entry). Any issue detecting port visits for a vessel may result in lower accuracy of the AIS coverage calculation.
      • The coverage metric does not distinguish between a lack of activity (non-event) and poor transmission resulting in disability to detect a true event.

      Learn more

      Learn more about our work on transmission gaps.

      ", "insightsFishing": "

      Overview

      Global Fishing Watch (GFW) analyzes and applies a fishing detection algorithm based on AIS data collected from vessels. Each AIS broadcast data point is classified as either “apparently fishing” or not fishing, and further reviewed to determine individual “fishing events”. 

      Based on the time range of interest, the following two insights are reviewed: 

      • Any apparent fishing events detected in areas with no known RFMO authorization 
      • Any apparent fishing events detected in no-take MPAs

      Fishing in areas with no known RFMO authorization

      GFW indicates any apparent fishing events in areas with no known RFMO authorization compiled from 7 Regional Fisheries Management Organisation (RFMO) lists*. 

      Caveats

      • The insight covers the listed RFMO areas only*. 
      • The insight does not include national registration or licensing lists. 

      Fishing in no-take Marine Protected Areas (MPAs)

      This insight is presented by cross-referencing any apparent fishing events in the boundaries of no-take MPAs (Source: World Database on Protected Areas ).  

      Caveats

      • Events close to boundary lines may be reported as being inside an MPA boundary. We recommend that you check the vessel track positions on the map alongside adding the MPA layer to confirm exact operation. 
      • There may be variability in no-take MPA permissions and restrictions based on seasonal closures and gear restrictions. 

      General caveats

      • Due to limited access to national databases, the insights do not consider other unknown authorizations e.g. seasonal closures, fishing authorisation within EEZs. 
      • All references to activity events (e.g. fishing), should be understood in the context of GFW's algorithms, which are best efforts to determine apparent fishing activity events based on AIS data collected.
      • While the insights provide potential events, GFW recommends to visually inspect vessel tracks, always refer to additional information sources, and request records from a vessel to confirm any findings.

      * The 7 RFMO lists considered for the authorization insight include: 

      • Commission for the Conservation of Southern Bluefin Tuna (CCSBT)
      • Inter-American Tropical Tuna Commission (IATTC)
      • International Commission for the Conservation of Atlantic Tunas (ICCAT)
      • Indian Ocean Tuna Commission (IOTC)
      • North Pacific Fisheries Commission (NPFC)
      • South Pacific Regional Fisheries Management Organisation (SPRFMO)
      • Western and Central Pacific Fisheries Commission (WCPFC)

      Learn more

      Learn more about how apparent fishing is estimated through Global Fishing Watch’s technology , and the difference between apparent fishing effort and fishing events. ", "insightsFlagsChanges": "

      Overview

      This insight tracks and details when the registry information available on a vessel indicates the vessel has been flagged to more than one unique flag state during the time range of interest, indicating a potential flag change. Adjust the end date of the time bar to present date to show the current vessel flag; Extend time range to track historical changes.

      Caveats

      • Global Fishing Watch matches vessel identity data with over 40 public registries to validate an individual vessel and enable tracking of identity and activity changes throughout the vessel’s lifetime from construction to scrapping.
      • The vessel identity insights rely solely on registry data. The insights do not consider AIS based vessel flag identity.
      • The quantity and quality of available registry data vary by flag state, thereby introducing uneven degrees of vessel identity information.
      • Conflicts between information sources can occur leading to incorrect identification of vessel flags. This is more common for vessels not listed on public registries and vessels operating under multiple flags within a short time range.
      • While every attempt is made to ensure the insight provided is accurate and up-to-date, this cannot be guaranteed, particularly on any changes outside of the monthly data update cycle.

      Learn more

      Global Fishing Watch works to offset the lack of public registry data by complementing with other data sources and advocating for transparency in regional and national fora. Learn more about:

      ", diff --git a/apps/fishing-map/public/locales/val/data-terminology.json b/apps/fishing-map/public/locales/val/data-terminology.json index a2f834d9a4..159cb89acb 100644 --- a/apps/fishing-map/public/locales/val/data-terminology.json +++ b/apps/fishing-map/public/locales/val/data-terminology.json @@ -9,7 +9,7 @@ "encounter": "crwdns68717:0crwdne68717:0", "loitering": "crwdns68719:0crwdne68719:0", "fishing": "crwdns68721:0crwdne68721:0", - "insights": "crwdns83142:0crwdne83142:0", + "insights": "crwdns83146:0crwdne83146:0", "insightsCoverage": "crwdns83106:0crwdne83106:0", "insightsFishing": "crwdns83108:0crwdne83108:0", "insightsFlagsChanges": "crwdns83110:0crwdne83110:0", From 2192c1b7d7765948e2e677f903c2d969ac932d7b Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 4 Apr 2024 19:45:17 +0200 Subject: [PATCH 07/43] wip: run fetch in cell interaction click --- .../features/map/map-interactions.hooks.ts | 91 +++++++++++-------- .../map/overlays/rulers/rulers.hooks.ts | 10 +- .../features/map/popups/MapPopups.tsx | 1 + .../hooks/deck-layers-interaction.hooks.ts | 41 ++++++++- package.json | 1 + yarn.lock | 10 ++ 6 files changed, 112 insertions(+), 42 deletions(-) diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index ce2f7795e6..fbf1e05a82 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -16,6 +16,7 @@ import { useSetMapHoverInteraction, useMapClickInteraction, useSetMapClickInteraction, + DeckLayerInteraction, } from '@globalfishingwatch/deck-layer-composer' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' @@ -115,18 +116,59 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { } } -export const useMapMouseClick = (style?: ExtendedStyle) => { - // const map = useMapInstance() - const map = useDeckMap() +export const useHandleMapClickFeatures = () => { const { isMapDrawing } = useMapDrawConnect() - const setMapClickFeatures = useSetMapClickInteraction() const { isMapAnnotating, addMapAnnotation } = useMapAnnotation() const { isErrorNotificationEditing, addErrorNotification } = useMapErrorNotification() + const { onRulerMapClick, rulersEditing } = useRulers() const isMarineManagerLocation = useSelector(selectIsMarineManagerLocation) + const handleMapClickInteraction = useCallback((interaction: DeckLayerInteraction) => { + const { latitude, longitude, features } = interaction + const position = [longitude, latitude] as Position + if (isMapAnnotating) { + return addMapAnnotation(position) + } + if (isErrorNotificationEditing) { + return addErrorNotification(position) + } + if (rulersEditing) { + return onRulerMapClick(position) + } + if (!features || !features.length) { + return + } + // // get temporal grid clicked features and order them by sublayerindex + // const fishingActivityFeatures = features + // .filter((feature) => { + // if (!feature.temporalgrid?.visible) { + // return false + // } + // return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes( + // feature.temporalgrid.sublayerInteractionType + // ) + // }) + // .sort((feature) => feature.temporalgrid?.sublayerIndex ?? 0) + + // if (fishingActivityFeatures?.length) { + // dispatch(setHintDismissed('clickingOnAGridCellToShowVessels')) + // const activityProperties = fishingActivityFeatures.map((feature) => + // feature.temporalgrid?.sublayerInteractionType === 'detections' ? 'detections' : 'hours' + // ) + // fishingPromiseRef.current = dispatch( + // fetchFishingActivityInteractionThunk({ fishingActivityFeatures, activityProperties }) + // ) + // } + }, []) + return handleMapClickInteraction +} +export const useMapMouseClick = (style?: ExtendedStyle) => { + // const map = useMapInstance() + const map = useDeckMap() + const handleMapClickFeatures = useHandleMapClickFeatures() + const setMapClickFeatures = useSetMapClickInteraction() const dataviews = useSelector(selectCurrentDataviewInstancesResolved) const temporalgridDataviews = useSelector(selectActiveTemporalgridDataviews) - const { onRulerMapClick, rulersEditing } = useRulers() - const { clickedEvent, dispatchClickedEvent } = useClickedEventConnect() + const { clickedEvent } = useClickedEventConnect() // const onClick = useMapClick(dispatchClickedEvent, style?.metadata as ExtendedStyleMeta, map) @@ -193,38 +235,15 @@ export const useMapMouseClick = (style?: ExtendedStyle) => { } catch (e) { console.warn(e) } - setMapClickFeatures({ longitude: info.coordinate[0], latitude: info.coordinate[1], features }) - const fourWingsValues = features?.map( - (f: PickingInfo) => - f.sourceLayer?.getPickingInfo({ info, mode: 'click', sourceLayer: f.sourceLayer }).object - ?.values - )[0] - if (fourWingsValues) { - console.log('fourWingsValues', fourWingsValues) - } - - if (isMapAnnotating) { - return addMapAnnotation(info.coordinate as Position) - } - if (isErrorNotificationEditing) { - return addErrorNotification(info.coordinate as Position) - } - if (rulersEditing) { - return onRulerMapClick(info) + const mapClickInteraction: DeckLayerInteraction = { + longitude: info.coordinate[0], + latitude: info.coordinate[1], + features, } - // onClick(event) + setMapClickFeatures(mapClickInteraction) + handleMapClickFeatures(mapClickInteraction) }, - [ - clickedCellLayers, - map, - isMapAnnotating, - isErrorNotificationEditing, - rulersEditing, - addMapAnnotation, - addErrorNotification, - setMapClickFeatures, - onRulerMapClick, - ] + [map, clickedCellLayers, setMapClickFeatures, handleMapClickFeatures] ) return { onMapClick, clickedTooltipEvent } diff --git a/apps/fishing-map/features/map/overlays/rulers/rulers.hooks.ts b/apps/fishing-map/features/map/overlays/rulers/rulers.hooks.ts index 9f2372b560..64500987f1 100644 --- a/apps/fishing-map/features/map/overlays/rulers/rulers.hooks.ts +++ b/apps/fishing-map/features/map/overlays/rulers/rulers.hooks.ts @@ -1,11 +1,12 @@ import { useCallback } from 'react' import { useSelector } from 'react-redux' import { throttle } from 'lodash' -import { PickingInfo } from '@deck.gl/core' +import { PickingInfo, Position } from '@deck.gl/core' import { RulerData } from '@globalfishingwatch/deck-layers' import { useLocationConnect } from 'routes/routes.hook' import { selectAreMapRulersVisible, selectMapRulers } from 'features/app/selectors/app.selectors' import { useMapControl } from 'features/map/controls/map-controls.hooks' + const useRulers = () => { const rulers = useSelector(selectMapRulers) const rulersVisible = useSelector(selectAreMapRulersVisible) @@ -63,11 +64,10 @@ const useRulers = () => { ) const onRulerMapClick = useCallback( - (info: PickingInfo) => { - const [longitude, latitude] = info.coordinate as number[] + (position: Position) => { const point = { - longitude, - latitude, + longitude: position[0], + latitude: position[1], } if (!value) { setRuleStart(point) diff --git a/apps/fishing-map/features/map/popups/MapPopups.tsx b/apps/fishing-map/features/map/popups/MapPopups.tsx index 097c51cd10..7af9b39629 100644 --- a/apps/fishing-map/features/map/popups/MapPopups.tsx +++ b/apps/fishing-map/features/map/popups/MapPopups.tsx @@ -8,6 +8,7 @@ import PopupWrapper from 'features/map/popups/PopupWrapper' function MapPopups() { const hoverInteraction = useMapHoverInteraction() const clickInteraction = useMapClickInteraction() + console.log('🚀 ~ MapPopups ~ clickInteraction:', clickInteraction) return ( diff --git a/libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts b/libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts index 9cd4fb1794..8eaeb25439 100644 --- a/libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts +++ b/libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts @@ -1,4 +1,5 @@ import { atom, useAtomValue, useSetAtom } from 'jotai' +import { atomEffect } from 'jotai-effect' import { useCallback } from 'react' import { ContextPickingInfo, FourwingsPickingInfo } from '@globalfishingwatch/deck-layers' @@ -10,8 +11,45 @@ export type DeckLayerInteraction = { } export const deckHoverInteractionAtom = atom({} as DeckLayerInteraction) +export const basedeckClickInteractionAtom = atom({} as DeckLayerInteraction) export const deckClickInteractionAtom = atom({} as DeckLayerInteraction) +export async function fetchData(url: string, signal: AbortSignal) { + const res = await fetch(url, { signal }) + if (signal.aborted) return + const data = await res.json() + if (signal.aborted) return + return data +} + +let index = 1 +const interactionClickEffect = atomEffect((get, set) => { + const interactionAtom = get(basedeckClickInteractionAtom) + const controller = new AbortController() + ;(async () => { + const fourwingsFeatures = interactionAtom.features?.filter( + (f) => f.object?.category === 'activity' + ) + console.log('🚀 ~ interactionClickEffect ~ interactionAtom.features:', interactionAtom.features) + if (fourwingsFeatures?.length) { + const cellId = fourwingsFeatures[0].object?.properties.cellId + console.log('🚀 ~ ; ~ cellId:', cellId) + const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${index++}`, { + signal: controller.signal, + }) + if (controller.signal.aborted) return + console.log('🚀 ~ ; ~ res:', res) + const data = await res.json() + if (controller.signal.aborted) return + set(deckClickInteractionAtom, { ...interactionAtom, cellId, data: data }) + } + })() + return () => { + console.log('aborting') + controller.abort() + } +}) + export const useMapHoverInteraction = () => { return useAtomValue(deckHoverInteractionAtom) } @@ -25,6 +63,7 @@ export const useSetMapHoverInteraction = () => { } export const useSetMapClickInteraction = () => { - const setDeckInteraction = useSetAtom(deckClickInteractionAtom) + const setDeckInteraction = useSetAtom(basedeckClickInteractionAtom) + useAtomValue(interactionClickEffect) return useCallback(setDeckInteraction, [setDeckInteraction]) } diff --git a/package.json b/package.json index d4456a621e..8264f73f0f 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "i18next-http-backend": "2.4.2", "intersection-observer": "0.12.2", "jotai": "^2.7.0", + "jotai-effect": "^0.6.0", "json2csv": "^6.0.0-alpha.2", "jszip": "^3.10.1", "lodash": "^4.17.21", diff --git a/yarn.lock b/yarn.lock index 7ee3a8d921..13d9a3c273 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2559,6 +2559,7 @@ __metadata: jest: "npm:^29.4.1" jest-environment-jsdom: "npm:^29.4.1" jotai: "npm:^2.7.0" + jotai-effect: "npm:^0.6.0" json2csv: "npm:^6.0.0-alpha.2" jszip: "npm:^3.10.1" lint-staged: "npm:^15.2.2" @@ -16782,6 +16783,15 @@ __metadata: languageName: node linkType: hard +"jotai-effect@npm:^0.6.0": + version: 0.6.0 + resolution: "jotai-effect@npm:0.6.0" + peerDependencies: + jotai: ">=2.5.0" + checksum: 10/d7e8ecf9acc6bcdcaaad2f80f51fc3064078c4ffd557076e434dcf058d7e3b98537560460e58b3ab3ae075449703e5791f0c4feced7392767285d71b8ec311e1 + languageName: node + linkType: hard + "jotai@npm:^2.7.0": version: 2.7.0 resolution: "jotai@npm:2.7.0" From df1b8af4ceade99f51302c59fef764134e1b9948 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 8 Apr 2024 16:24:13 +0200 Subject: [PATCH 08/43] see vessels in cell click interaction --- .../features/map/map-interactions.hooks.ts | 105 +++---- apps/fishing-map/features/map/map.slice.ts | 24 +- .../features/map/popups/ActivityLayers.tsx | 4 +- .../features/map/popups/MapPopups.tsx | 12 +- .../features/map/popups/PopupWrapper.tsx | 2 +- .../features/map/popups/VesselsTable.tsx | 13 +- .../src/hooks/deck-layers-composer.hooks.ts | 2 +- .../hooks/deck-layers-interaction.hooks.ts | 69 ----- .../src/hooks/deck-layers-legends.hooks.ts | 2 +- libs/deck-layer-composer/src/hooks/index.ts | 1 - libs/deck-layer-composer/src/index.ts | 1 + .../deck-layers-interaction.hooks.ts | 20 ++ .../src/interactions/index.ts | 1 + libs/deck-layer-composer/src/types.ts | 9 + .../src/layers/context/context.types.ts | 4 +- .../layers/fourwings/FourwingsHeatmapLayer.ts | 4 +- .../src/layers/fourwings/fourwings.types.ts | 1 + .../src/fourwings/helpers/cells.ts | 8 +- .../src/fourwings/lib/parse-fourwings.ts | 5 +- libs/deck-loaders/src/fourwings/lib/types.ts | 2 + .../src/use-map-interaction/index.ts | 9 +- .../use-map-interaction.ts | 258 +++++++----------- 22 files changed, 215 insertions(+), 341 deletions(-) delete mode 100644 libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts create mode 100644 libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts create mode 100644 libs/deck-layer-composer/src/interactions/index.ts diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index fbf1e05a82..98c15af675 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -116,62 +116,51 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { } } -export const useHandleMapClickFeatures = () => { +// Hook to wrap the custom tools click interactions with the map that has more priority +// returning undefined when not handled so we can continue with the propagation +export const useHandleMapToolsClick = () => { const { isMapDrawing } = useMapDrawConnect() const { isMapAnnotating, addMapAnnotation } = useMapAnnotation() const { isErrorNotificationEditing, addErrorNotification } = useMapErrorNotification() const { onRulerMapClick, rulersEditing } = useRulers() const isMarineManagerLocation = useSelector(selectIsMarineManagerLocation) - const handleMapClickInteraction = useCallback((interaction: DeckLayerInteraction) => { - const { latitude, longitude, features } = interaction - const position = [longitude, latitude] as Position - if (isMapAnnotating) { - return addMapAnnotation(position) - } - if (isErrorNotificationEditing) { - return addErrorNotification(position) - } - if (rulersEditing) { - return onRulerMapClick(position) - } - if (!features || !features.length) { - return - } - // // get temporal grid clicked features and order them by sublayerindex - // const fishingActivityFeatures = features - // .filter((feature) => { - // if (!feature.temporalgrid?.visible) { - // return false - // } - // return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes( - // feature.temporalgrid.sublayerInteractionType - // ) - // }) - // .sort((feature) => feature.temporalgrid?.sublayerIndex ?? 0) - - // if (fishingActivityFeatures?.length) { - // dispatch(setHintDismissed('clickingOnAGridCellToShowVessels')) - // const activityProperties = fishingActivityFeatures.map((feature) => - // feature.temporalgrid?.sublayerInteractionType === 'detections' ? 'detections' : 'hours' - // ) - // fishingPromiseRef.current = dispatch( - // fetchFishingActivityInteractionThunk({ fishingActivityFeatures, activityProperties }) - // ) - // } - }, []) + const handleMapClickInteraction = useCallback( + (interaction: DeckLayerInteraction) => { + const { latitude, longitude, features } = interaction + const position = [longitude, latitude] as Position + if (isMapAnnotating) { + return addMapAnnotation(position) + } + if (isErrorNotificationEditing) { + return addErrorNotification(position) + } + if (rulersEditing) { + return onRulerMapClick(position) + } + return undefined + }, + [ + addErrorNotification, + addMapAnnotation, + isErrorNotificationEditing, + isMapAnnotating, + onRulerMapClick, + rulersEditing, + ] + ) return handleMapClickInteraction } -export const useMapMouseClick = (style?: ExtendedStyle) => { + +export const useMapMouseClick = () => { // const map = useMapInstance() const map = useDeckMap() - const handleMapClickFeatures = useHandleMapClickFeatures() - const setMapClickFeatures = useSetMapClickInteraction() + const handleMapToolsClick = useHandleMapToolsClick() + // const setMapClickFeatures = useSetMapClickInteraction() const dataviews = useSelector(selectCurrentDataviewInstancesResolved) const temporalgridDataviews = useSelector(selectActiveTemporalgridDataviews) - const { clickedEvent } = useClickedEventConnect() - - // const onClick = useMapClick(dispatchClickedEvent, style?.metadata as ExtendedStyleMeta, map) + const { clickedEvent, dispatchClickedEvent } = useClickedEventConnect() + const onClick = useMapClick(dispatchClickedEvent) const clickedTooltipEvent = parseMapTooltipEvent(clickedEvent, dataviews, temporalgridDataviews) const clickedCellLayers = useMemo(() => { @@ -203,28 +192,11 @@ export const useMapMouseClick = (style?: ExtendedStyle) => { // this is needed to allow interacting with overlay elements content return true } - // const features = deckRef?.current?.pickMultipleObjects({ - // x: info.x, - // y: info.y, - // }) trackEvent({ category: TrackCategory.EnvironmentalData, action: `Click in grid cell`, label: getEventLabel(clickedCellLayers ?? []), }) - // const hasWorkspacesFeatures = - // event?.features?.find( - // (feature: any) => feature.properties.type === WORKSPACES_POINTS_TYPE - // ) !== undefined - // if (isMapDrawing || (isMarineManagerLocation && !hasWorkspacesFeatures)) { - // return undefined - // } - - // const hasRulerFeature = - // event.features?.find((f) => f.source === RULERS_LAYER_ID) !== undefined - // if (rulersEditing && !hasRulerFeature) { - // return onRulerMapClick(event) - // } let features = defaultEmptyFeatures try { features = map?.pickMultipleObjects({ @@ -235,15 +207,20 @@ export const useMapMouseClick = (style?: ExtendedStyle) => { } catch (e) { console.warn(e) } + const mapClickInteraction: DeckLayerInteraction = { longitude: info.coordinate[0], latitude: info.coordinate[1], + point: { x: info.x, y: info.y }, features, } - setMapClickFeatures(mapClickInteraction) - handleMapClickFeatures(mapClickInteraction) + + const toolsClickedHandled = handleMapToolsClick(mapClickInteraction) !== undefined + if (!toolsClickedHandled) { + onClick(mapClickInteraction) + } }, - [map, clickedCellLayers, setMapClickFeatures, handleMapClickFeatures] + [map, clickedCellLayers, handleMapToolsClick, onClick] ) return { onMapClick, clickedTooltipEvent } diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index 7cb2385582..d721fd955b 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -1,4 +1,5 @@ import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit' +import { current } from 'immer' import { uniqBy } from 'lodash' import { InteractionEvent, ExtendedFeature } from '@globalfishingwatch/react-hooks' import { GFWAPI } from '@globalfishingwatch/api-client' @@ -16,6 +17,8 @@ import { APIPagination, } from '@globalfishingwatch/api-types' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' +import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layer-composer' +import { FourwingsPickingInfo, FourwingsPickingObject } from '@globalfishingwatch/deck-layers' import { AsyncReducerStatus } from 'utils/async-slice' import { AppDispatch } from 'store' import { @@ -46,7 +49,7 @@ export type ExtendedEventVessel = EventVessel & { dataset?: string } export type ExtendedFeatureEvent = ApiEvent & { dataset: Dataset } -export type SliceExtendedFeature = ExtendedFeature & { +export type SliceExtendedFeature = DeckLayerInteractionFeature & { event?: ExtendedFeatureEvent vessels?: ExtendedFeatureVessel[] } @@ -476,15 +479,16 @@ const slice = createSlice({ state.fishingStatus = AsyncReducerStatus.Finished state.currentFishingRequestId = '' if (!state.clicked || !state.clicked.features || !action.payload) return - - action.payload.vessels.forEach((sublayerVessels) => { - const sublayer = state.clicked?.features?.find( - (feature) => - feature.temporalgrid && feature.temporalgrid.sublayerId === sublayerVessels.sublayerId - ) - if (!sublayer) return - sublayer.vessels = sublayerVessels.vessels - }) + if (state?.clicked?.features?.length && action.payload?.vessels?.length) { + state.clicked.features = state.clicked.features.map((feature: any) => { + const sublayers = (feature as FourwingsPickingObject).sublayers.map((sublayer) => { + const vessels = + action.payload?.vessels.find((v) => v.sublayerId === sublayer.id)?.vessels || [] + return { ...sublayer, vessels } + }) + return { ...feature, sublayers } + }) + } }) builder.addCase(fetchFishingActivityInteractionThunk.rejected, (state, action) => { if (action.error.message === 'Aborted') { diff --git a/apps/fishing-map/features/map/popups/ActivityLayers.tsx b/apps/fishing-map/features/map/popups/ActivityLayers.tsx index 6f5afa0c9b..845bb772f5 100644 --- a/apps/fishing-map/features/map/popups/ActivityLayers.tsx +++ b/apps/fishing-map/features/map/popups/ActivityLayers.tsx @@ -34,10 +34,10 @@ function ActivityTooltipRow({ feature, showFeaturesDetails }: ActivityTooltipRow })} - {/* // TODO:deck add subcategory info + {/* // TODO:deck add subcategory info */} {showFeaturesDetails && ( - )} */} + )} diff --git a/apps/fishing-map/features/map/popups/MapPopups.tsx b/apps/fishing-map/features/map/popups/MapPopups.tsx index 7af9b39629..1280e0b480 100644 --- a/apps/fishing-map/features/map/popups/MapPopups.tsx +++ b/apps/fishing-map/features/map/popups/MapPopups.tsx @@ -1,14 +1,12 @@ -import { Fragment, useEffect } from 'react' -import { - useMapClickInteraction, - useMapHoverInteraction, -} from '@globalfishingwatch/deck-layer-composer' +import { Fragment } from 'react' +import { useSelector } from 'react-redux' +import { useMapHoverInteraction } from '@globalfishingwatch/deck-layer-composer' import PopupWrapper from 'features/map/popups/PopupWrapper' +import { selectClickedEvent } from '../map.slice' function MapPopups() { const hoverInteraction = useMapHoverInteraction() - const clickInteraction = useMapClickInteraction() - console.log('🚀 ~ MapPopups ~ clickInteraction:', clickInteraction) + const clickInteraction = useSelector(selectClickedEvent) return ( diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index ffd3d770de..5b48fb7836 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -68,7 +68,7 @@ function PopupWrapper({ const featureByCategory = groupBy( interaction.features - .map((feature) => feature.object) + // .map((feature) => feature.object) .sort( (a, b) => POPUP_CATEGORY_ORDER.indexOf(a?.category as DataviewCategory) - diff --git a/apps/fishing-map/features/map/popups/VesselsTable.tsx b/apps/fishing-map/features/map/popups/VesselsTable.tsx index 26f9385f99..adc7f8ef59 100644 --- a/apps/fishing-map/features/map/popups/VesselsTable.tsx +++ b/apps/fishing-map/features/map/popups/VesselsTable.tsx @@ -99,15 +99,16 @@ function VesselsTable({ }) { const { t } = useTranslation() - const interactionAllowed = [...SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION].includes( - feature.temporalgrid?.sublayerInteractionType || '' - ) - - const vessels = feature.vesselsInfo?.vessels?.slice(0, MAX_TOOLTIP_LIST) + // const interactionAllowed = [...SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION].includes( + // feature?.sublayerInteractionType || '' + // ) + // TODO:deck fix this + const interactionAllowed = true + const vessels = feature?.vessels?.slice(0, MAX_TOOLTIP_LIST) const hasPinColumn = interactionAllowed && - feature?.vesselsInfo?.vessels?.some((vessel) => { + feature?.vessels?.some((vessel) => { const hasDatasets = vessel.infoDataset !== undefined || vessel.trackDataset !== undefined return hasDatasets }) diff --git a/libs/deck-layer-composer/src/hooks/deck-layers-composer.hooks.ts b/libs/deck-layer-composer/src/hooks/deck-layers-composer.hooks.ts index 8f4ec826bb..9984c39fb3 100644 --- a/libs/deck-layer-composer/src/hooks/deck-layers-composer.hooks.ts +++ b/libs/deck-layer-composer/src/hooks/deck-layers-composer.hooks.ts @@ -4,7 +4,7 @@ import { AnyDeckLayer } from '@globalfishingwatch/deck-layers' import { DataviewInstance } from '@globalfishingwatch/api-types' import { getDataviewsResolved } from '../resolvers' import { dataviewToDeckLayer, ResolverGlobalConfig } from '../resolvers' -import { useMapHoverInteraction } from './deck-layers-interaction.hooks' +import { useMapHoverInteraction } from '../interactions' // Atom used to have all deck instances available export const deckLayerInstancesAtom = atom([]) diff --git a/libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts b/libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts deleted file mode 100644 index 8eaeb25439..0000000000 --- a/libs/deck-layer-composer/src/hooks/deck-layers-interaction.hooks.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { atom, useAtomValue, useSetAtom } from 'jotai' -import { atomEffect } from 'jotai-effect' -import { useCallback } from 'react' -import { ContextPickingInfo, FourwingsPickingInfo } from '@globalfishingwatch/deck-layers' - -// Atom used to have all the layer instances loading state available -export type DeckLayerInteraction = { - latitude: number - longitude: number - features: (FourwingsPickingInfo | ContextPickingInfo)[] -} - -export const deckHoverInteractionAtom = atom({} as DeckLayerInteraction) -export const basedeckClickInteractionAtom = atom({} as DeckLayerInteraction) -export const deckClickInteractionAtom = atom({} as DeckLayerInteraction) - -export async function fetchData(url: string, signal: AbortSignal) { - const res = await fetch(url, { signal }) - if (signal.aborted) return - const data = await res.json() - if (signal.aborted) return - return data -} - -let index = 1 -const interactionClickEffect = atomEffect((get, set) => { - const interactionAtom = get(basedeckClickInteractionAtom) - const controller = new AbortController() - ;(async () => { - const fourwingsFeatures = interactionAtom.features?.filter( - (f) => f.object?.category === 'activity' - ) - console.log('🚀 ~ interactionClickEffect ~ interactionAtom.features:', interactionAtom.features) - if (fourwingsFeatures?.length) { - const cellId = fourwingsFeatures[0].object?.properties.cellId - console.log('🚀 ~ ; ~ cellId:', cellId) - const res = await fetch(`https://jsonplaceholder.typicode.com/posts/${index++}`, { - signal: controller.signal, - }) - if (controller.signal.aborted) return - console.log('🚀 ~ ; ~ res:', res) - const data = await res.json() - if (controller.signal.aborted) return - set(deckClickInteractionAtom, { ...interactionAtom, cellId, data: data }) - } - })() - return () => { - console.log('aborting') - controller.abort() - } -}) - -export const useMapHoverInteraction = () => { - return useAtomValue(deckHoverInteractionAtom) -} -export const useMapClickInteraction = () => { - return useAtomValue(deckClickInteractionAtom) -} - -export const useSetMapHoverInteraction = () => { - const setDeckInteraction = useSetAtom(deckHoverInteractionAtom) - return useCallback(setDeckInteraction, [setDeckInteraction]) -} - -export const useSetMapClickInteraction = () => { - const setDeckInteraction = useSetAtom(basedeckClickInteractionAtom) - useAtomValue(interactionClickEffect) - return useCallback(setDeckInteraction, [setDeckInteraction]) -} diff --git a/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts b/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts index 78d8ac5f3d..60ccfe9250 100644 --- a/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts +++ b/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts @@ -8,7 +8,7 @@ import { } from '@globalfishingwatch/deck-layers' import { GRID_AREA_BY_ZOOM_LEVEL, HEATMAP_DEFAULT_MAX_ZOOM } from '../config' import { DeckLegend, LegendType } from '../types' -import { deckHoverInteractionAtom } from './deck-layers-interaction.hooks' +import { deckHoverInteractionAtom } from '../interactions' import { deckLayersAtom } from './deck-layers.hooks' export const deckLayersLegendsAtom = atom((get) => { diff --git a/libs/deck-layer-composer/src/hooks/index.ts b/libs/deck-layer-composer/src/hooks/index.ts index d49779bc7b..35995755b8 100644 --- a/libs/deck-layer-composer/src/hooks/index.ts +++ b/libs/deck-layer-composer/src/hooks/index.ts @@ -1,5 +1,4 @@ export * from './deck-layers.hooks' export * from './deck-layers-composer.hooks' -export * from './deck-layers-interaction.hooks' export * from './deck-layers-legends.hooks' export * from './deck-layers-state.hooks' diff --git a/libs/deck-layer-composer/src/index.ts b/libs/deck-layer-composer/src/index.ts index ba8696f604..f1b1c1129f 100644 --- a/libs/deck-layer-composer/src/index.ts +++ b/libs/deck-layer-composer/src/index.ts @@ -1,4 +1,5 @@ export * from './config' export * from './hooks' +export * from './interactions' export * from './resolvers' export * from './types' diff --git a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts new file mode 100644 index 0000000000..3566a800bf --- /dev/null +++ b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts @@ -0,0 +1,20 @@ +import { atom, useAtomValue, useSetAtom } from 'jotai' +import { useCallback } from 'react' +import { DeckLayerInteractionFeature } from '../types' + +// Atom used to have all the layer instances loading state available +export type DeckLayerInteraction = { + latitude: number + longitude: number + features: DeckLayerInteractionFeature[] +} + +export const deckHoverInteractionAtom = atom({} as DeckLayerInteraction) + +export const useMapHoverInteraction = () => { + return useAtomValue(deckHoverInteractionAtom) +} +export const useSetMapHoverInteraction = () => { + const setDeckInteraction = useSetAtom(deckHoverInteractionAtom) + return useCallback(setDeckInteraction, [setDeckInteraction]) +} diff --git a/libs/deck-layer-composer/src/interactions/index.ts b/libs/deck-layer-composer/src/interactions/index.ts new file mode 100644 index 0000000000..cba44543e5 --- /dev/null +++ b/libs/deck-layer-composer/src/interactions/index.ts @@ -0,0 +1 @@ +export * from './deck-layers-interaction.hooks' diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 3bf6a631c5..93a0de8e02 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -1,5 +1,10 @@ import { + ContextLayer, + ContextPickingInfo, FourwingsDeckSublayer, + FourwingsLayer, + FourwingsPickingInfo, + FourwingsPickingObject, FourwingsTileLayerColorDomain, FourwingsTileLayerColorRange, } from '@globalfishingwatch/deck-layers' @@ -43,3 +48,7 @@ export interface DeckLegendBivariate extends DeckLegend { sublayersBreaks: [number[], number[]] bivariateRamp: string[] } + +export type DeckLayerInteractionFeature = + | (FourwingsPickingInfo & { layer: FourwingsLayer }) + | (ContextPickingInfo & { layer: ContextLayer }) diff --git a/libs/deck-layers/src/layers/context/context.types.ts b/libs/deck-layers/src/layers/context/context.types.ts index 47d6525f56..b43a060768 100644 --- a/libs/deck-layers/src/layers/context/context.types.ts +++ b/libs/deck-layers/src/layers/context/context.types.ts @@ -50,4 +50,6 @@ export type ContextFeature = Feature // TODO:deck create this type in the proper deck class layer export type UserContextFeature = Feature> & ContextFeatureProperties -export type ContextPickingInfo = PickingInfo +export type ContextPickingObject = ContextFeature | UserContextFeature + +export type ContextPickingInfo = PickingInfo diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts index cc8fbda45a..013a5f50f6 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts @@ -25,11 +25,13 @@ export class FourwingsHeatmapLayer extends CompositeLayer }): FourwingsPickingInfo => { - const { id, startTime, endTime, availableIntervals, category, sublayers, tilesCache } = + const { id, tile, startTime, endTime, availableIntervals, category, sublayers, tilesCache } = this.props + const object: FourwingsPickingObject = { ...(info.object || ({} as FourwingsFeature)), title: id, + tile: tile.index, category, sublayers, } diff --git a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts index 9de943cbf8..07592f0488 100644 --- a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts +++ b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts @@ -61,6 +61,7 @@ export type FourwingsPickingObject = FourwingsFeature< FourwingsFeatureProperties & Partial > & { title: string + tile: { x: number; y: number; z: number } category: string sublayers: FourwingsDeckSublayer[] } diff --git a/libs/deck-loaders/src/fourwings/helpers/cells.ts b/libs/deck-loaders/src/fourwings/helpers/cells.ts index 597fc2e163..348d8c8345 100644 --- a/libs/deck-loaders/src/fourwings/helpers/cells.ts +++ b/libs/deck-loaders/src/fourwings/helpers/cells.ts @@ -3,6 +3,7 @@ import { Position } from 'geojson' export type BBox = [number, number, number, number] export const getCellProperties = (tileBBox: BBox, cellIndex: number, numCols: number) => { + // TODO:deck review calculation here because interaction is returning a wrong col const col = cellIndex % numCols const row = Math.floor(cellIndex / numCols) const [minX, minY, maxX, maxY] = tileBBox @@ -30,11 +31,8 @@ export const getCellCoordinates = ({ cols, rows, }: GetCellCoordinatesParams): Position[] => { - const col = cellIndex % cols - const row = Math.floor(cellIndex / cols) - const [minX, minY, maxX, maxY] = tileBBox - const width = maxX - minX - const height = maxY - minY + const { col, row, width, height } = getCellProperties(tileBBox, cellIndex, cols) + const [minX, minY] = tileBBox const squareMinX = minX + (col / cols) * width const squareMinY = minY + (row / rows) * height const squareMaxX = minX + ((col + 1) / cols) * width diff --git a/libs/deck-loaders/src/fourwings/lib/parse-fourwings.ts b/libs/deck-loaders/src/fourwings/lib/parse-fourwings.ts index 61d05df878..3d5af8dbc9 100644 --- a/libs/deck-loaders/src/fourwings/lib/parse-fourwings.ts +++ b/libs/deck-loaders/src/fourwings/lib/parse-fourwings.ts @@ -1,7 +1,7 @@ import Pbf from 'pbf' import { GeoBoundingBox } from '@deck.gl/geo-layers/dist/tileset-2d' import { CONFIG_BY_INTERVAL, getTimeRangeKey } from '../helpers/time' -import { BBox, generateUniqueId, getCellCoordinates } from '../helpers/cells' +import { BBox, generateUniqueId, getCellCoordinates, getCellProperties } from '../helpers/cells' import type { FourwingsFeature, FourwingsLoaderOptions, @@ -78,6 +78,7 @@ export const getCellTimeseries = ( // add the feature if previous sublayers didn't contain data for it if (!features[cellNum]) { + const { col, row } = getCellProperties(tileBBox, cellNum, cols) features[cellNum] = { type: 'Feature', geometry: { @@ -92,6 +93,8 @@ export const getCellTimeseries = ( ], }, properties: { + col, + row, values: new Array(sublayersLength), dates: new Array(sublayersLength), cellId: generateUniqueId(tile.index.x, tile.index.y, cellNum), diff --git a/libs/deck-loaders/src/fourwings/lib/types.ts b/libs/deck-loaders/src/fourwings/lib/types.ts index a65d1ccff1..49de760291 100644 --- a/libs/deck-loaders/src/fourwings/lib/types.ts +++ b/libs/deck-loaders/src/fourwings/lib/types.ts @@ -49,6 +49,8 @@ export type FourwingsFeatureProperties = { values: number[][] cellId: number cellNum: number + col: number + row: number } export type FourwingsStaticFeatureProperties = { count: number diff --git a/libs/react-hooks/src/use-map-interaction/index.ts b/libs/react-hooks/src/use-map-interaction/index.ts index bf43a89d0d..565ca9af80 100644 --- a/libs/react-hooks/src/use-map-interaction/index.ts +++ b/libs/react-hooks/src/use-map-interaction/index.ts @@ -1,9 +1,6 @@ -import { - Interval, - ContextLayerType, - HeatmapAnimatedInteractionType, -} from '@globalfishingwatch/layer-composer' +import { Interval, HeatmapAnimatedInteractionType } from '@globalfishingwatch/layer-composer' import { SublayerCombinationMode } from '@globalfishingwatch/fourwings-aggregate' +import { ContextLayerId } from '@globalfishingwatch/deck-layers' export * from './use-map-interaction' @@ -28,7 +25,7 @@ export type ExtendedFeature = { sourceLayer: string generatorId: string | number | null generatorType: string | null - generatorContextLayer?: ContextLayerType | null + generatorContextLayer?: ContextLayerId | null datasetId?: string promoteId?: string id: string diff --git a/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts b/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts index d6fc51ccdd..cfe3cedfd5 100644 --- a/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts +++ b/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts @@ -8,6 +8,17 @@ import { ExtendedLayer, Group, } from '@globalfishingwatch/layer-composer' +import { + ContextLayer, + FourwingsLayer, + FourwingsPickingObject, + ContextPickingInfo, + ContextPickingObject, +} from '@globalfishingwatch/deck-layers' +import { + DeckLayerInteraction, + DeckLayerInteractionFeature, +} from '@globalfishingwatch/deck-layer-composer' import { aggregateCell, SublayerCombinationMode, @@ -15,6 +26,7 @@ import { } from '@globalfishingwatch/fourwings-aggregate' import type { Map, GeoJSONFeature, MapLayerMouseEvent } from '@globalfishingwatch/maplibre-gl' import { DataviewType } from '@globalfishingwatch/api-types' +import { getUTCDate } from '@globalfishingwatch/data-transforms' import { ExtendedFeature, InteractionEventCallback, InteractionEvent } from '.' export type MaplibreGeoJSONFeature = GeoJSONFeature & { @@ -48,176 +60,99 @@ export const filterUniqueFeatureInteraction = (features: ExtendedFeature[]) => { return filtered } -const getId = (feature: MaplibreGeoJSONFeature) => { - const promoteIdValue = - feature.layer.metadata?.promoteId && feature.properties[feature.layer.metadata?.promoteId] - if (feature.id !== undefined) { - return feature.id - } else if (feature.properties?.gfw_id !== undefined) { - return feature.properties?.gfw_id - } else if (promoteIdValue !== undefined) { - return promoteIdValue - } -} - -const getFeatureTile = (feature: MaplibreGeoJSONFeature) => { - return { - x: (feature as any)._vectorTileFeature._x, - y: (feature as any)._vectorTileFeature._y, - z: (feature as any)._vectorTileFeature._z, - } -} - -const getExtendedFeature = ( - feature: MaplibreGeoJSONFeature, - metadata?: ExtendedStyleMeta, - debug = false -): ExtendedFeature[] => { - const generatorType = feature.layer.metadata?.generatorType ?? null - const generatorId = feature.layer.metadata?.generatorId ?? null +const getExtendedFeature = (feature: DeckLayerInteractionFeature): ExtendedFeature[] => { + // const generatorType = feature.layer.metadata?.generatorType ?? null + // const generatorId = feature.layer.metadata?.generatorId ?? null // TODO: if no generatorMetadata is found, fallback to feature.layer.metadata, but the former should be prefered - let generatorMetadata: any - if (generatorId && metadata?.generatorsMetadata && metadata?.generatorsMetadata[generatorId]) { - generatorMetadata = metadata?.generatorsMetadata[generatorId] - } else { - generatorMetadata = feature.layer.metadata - } - - const uniqueFeatureInteraction = feature.layer?.metadata?.uniqueFeatureInteraction ?? false - const stopPropagation = feature.layer?.metadata?.stopPropagation ?? false - const properties = feature.properties || {} - let value = properties.value || properties.name || properties.id || properties?.count - const { valueProperties } = feature.layer.metadata || {} - if (valueProperties?.length) { - value = - valueProperties.length === 1 - ? properties[valueProperties[0]] - : valueProperties - .flatMap((prop) => (properties[prop] ? `${prop}: ${properties[prop]}` : [])) - .join('
      ') - } - - const extendedFeature: ExtendedFeature | null = { - properties, - generatorType, - generatorId, + // let generatorMetadata: any + // if (generatorId && metadata?.generatorsMetadata && metadata?.generatorsMetadata[generatorId]) { + // generatorMetadata = metadata?.generatorsMetadata[generatorId] + // } else { + // generatorMetadata = feature.layer.metadata + // } + + // TODO:deck implement the uniqueFeatureInteraction feature inside the getPickingInfo + // const uniqueFeatureInteraction = feature?.metadata?.uniqueFeatureInteraction ?? false + // TODO:deck implement the stopPropagation feature + // const stopPropagation = feature.layer?.metadata?.stopPropagation ?? false + + const extendedFeature: ExtendedFeature = { + ...feature.object, layerId: feature.layer.id, - source: feature.source, - sourceLayer: feature.sourceLayer, - uniqueFeatureInteraction, - stopPropagation, - value, - id: getId(feature), - tile: getFeatureTile(feature), + // uniqueFeatureInteraction, + // stopPropagation, } - switch (generatorType) { - case DataviewType.HeatmapAnimated: - const timeChunks = generatorMetadata?.timeChunks - const frame = timeChunks?.activeChunkFrame - const activeTimeChunk = pickActiveTimeChunk(timeChunks) + + if (feature.layer instanceof FourwingsLayer) { + const object = feature.object as FourwingsPickingObject + if (feature.layer?.props.static) { + return [ + { + ...extendedFeature, + value: extendedFeature.value / VALUE_MULTIPLIER, + unit: object.sublayers[0].unit, + }, + ] + } else { + // const values = object.sublayers.map((sublayer) => sublayer.value!) // This is used when querying the interaction endpoint, so that start begins at the start of the frame (ie start of a 10days interval) // This avoids querying a cell visible on the map, when its actual timerange is not included in the app-overall time range - const getDate = CONFIG_BY_INTERVAL[timeChunks.interval as Interval].getDate - const visibleStartDate = getDate(timeChunks.visibleStartFrame).toISOString() - const visibleEndDate = getDate(timeChunks.visibleEndFrame).toISOString() - const numSublayers = generatorMetadata?.numSublayers - const values = aggregateCell({ - rawValues: properties.rawValues, - frame, - delta: Math.max(1, timeChunks.deltaInIntervalUnits), - quantizeOffset: activeTimeChunk.quantizeOffset, - sublayerCount: - generatorMetadata?.sublayerCombinationMode === SublayerCombinationMode.TimeCompare - ? 2 - : numSublayers, - aggregationOperation: generatorMetadata?.aggregationOperation, - sublayerCombinationMode: generatorMetadata?.sublayerCombinationMode, - multiplier: generatorMetadata?.multiplier, - }) - - if (debug) { - console.log(properties.rawValues) - } - // Clean values with 0 for sum aggregation and with NaN for avg aggregation layers - if ( - !values || - !values.filter((v: number) => { - const matchesMin = - generatorMetadata?.minVisibleValue !== undefined - ? v >= generatorMetadata?.minVisibleValue - : true - const matchesMax = - generatorMetadata?.maxVisibleValue !== undefined - ? v <= generatorMetadata?.maxVisibleValue - : true - return v !== 0 && !isNaN(v) && matchesMin && matchesMax - }).length - ) { - return [] - } - const visibleSublayers = generatorMetadata?.visibleSublayers as boolean[] - const sublayers = generatorMetadata?.sublayers - return values.flatMap((value: any, i: number) => { - if (value === 0) return [] + // const getDate = CONFIG_BY_INTERVAL[timeChunks.interval as Interval].getDate + const layer = feature.layer as FourwingsLayer + const visibleStartDate = getUTCDate(layer?.props?.startTime).toISOString() + const visibleEndDate = getUTCDate(layer?.props?.endTime).toISOString() + return object.sublayers.flatMap((sublayer, i) => { + if (sublayer.value === 0) return [] const temporalGridExtendedFeature: ExtendedFeature = { ...extendedFeature, temporalgrid: { sublayerIndex: i, - sublayerId: sublayers[i].id, - sublayerInteractionType: sublayers[i].interactionType, - sublayerCombinationMode: generatorMetadata?.sublayerCombinationMode, - visible: visibleSublayers[i] === true, - col: properties._col as number, - row: properties._row as number, - interval: timeChunks.interval, + sublayerId: sublayer.id, + sublayerInteractionType: object.category, + sublayerCombinationMode: layer.props.comparisonMode, + visible: true, + col: object.properties.col as number, + row: object.properties.row as number, + interval: layer.getInterval(), visibleStartDate, visibleEndDate, - unit: sublayers[i].legend.unit, + unit: sublayer.unit, }, - value, + value: sublayer.value, } return [temporalGridExtendedFeature] }) - case DataviewType.HeatmapStatic: { - return [ - { - ...extendedFeature, - value: extendedFeature.value / VALUE_MULTIPLIER, - unit: generatorMetadata.legends[0]?.unit, - }, - ] - } - case DataviewType.Context: - case DataviewType.UserPoints: - case DataviewType.UserContext: { - return [ - { - ...extendedFeature, - datasetId: feature.layer.metadata?.datasetId, - promoteId: feature.layer.metadata?.promoteId, - generatorContextLayer: feature.layer.metadata?.layer, - geometry: feature.geometry, - }, - ] } - default: - return [extendedFeature] + } else if (feature.layer instanceof ContextLayer) { + // TODO: deck add support for these layers + // case DataviewType.Context: + // case DataviewType.UserPoints: + // case DataviewType.UserContext: + const object = feature.object as ContextPickingObject + return [ + { + ...extendedFeature, + datasetId: object.datasetId, + promoteId: object.promoteId, + generatorContextLayer: object?.layerId, + geometry: object.geometry, + }, + ] } + + return [extendedFeature] } -const getExtendedFeatures = ( - features: MaplibreGeoJSONFeature[], - metadata?: ExtendedStyleMeta, - debug = false -): ExtendedFeature[] => { - const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) - if (stopPropagationFeature) { - return getExtendedFeature(stopPropagationFeature, metadata, debug) - } +const getExtendedFeatures = (features: DeckLayerInteractionFeature[]): ExtendedFeature[] => { + // TODO: deck implement the stopPropagation feature + // const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) + // if (stopPropagationFeature) { + // return getExtendedFeature(stopPropagationFeature, metadata, debug) + // } const extendedFeatures: ExtendedFeature[] = features.flatMap((feature) => { - return getExtendedFeature(feature, metadata, debug) || [] + return getExtendedFeature(feature) || [] }) return extendedFeatures } @@ -284,38 +219,31 @@ export const useFeatureState = (map?: Map) => { return featureState } -export const useMapClick = ( - clickCallback: InteractionEventCallback, - metadata: ExtendedStyleMeta, - map?: Map -) => { - const { updateFeatureState, cleanFeatureState } = useFeatureState(map) +export const useMapClick = (clickCallback: InteractionEventCallback) => { + // const { updateFeatureState, cleanFeatureState } = useFeatureState(map) const onMapClick = useCallback( - (event: MapLayerMouseEvent) => { - cleanFeatureState('click') + (event: DeckLayerInteraction) => { + // cleanFeatureState('click') if (!clickCallback) return + const interactionEvent: InteractionEvent = { type: 'click', - longitude: event.lngLat.lng, - latitude: event.lngLat.lat, + longitude: event.longitude, + latitude: event.latitude, point: event.point, } if (event.features?.length) { - const extendedFeatures: ExtendedFeature[] = getExtendedFeatures( - event.features as MaplibreGeoJSONFeature[], - metadata, - false - ) + const extendedFeatures: ExtendedFeature[] = getExtendedFeatures(event.features) const extendedFeaturesLimit = filterUniqueFeatureInteraction(extendedFeatures) if (extendedFeaturesLimit.length) { interactionEvent.features = extendedFeaturesLimit - updateFeatureState(extendedFeaturesLimit, 'click') + // updateFeatureState(extendedFeaturesLimit, 'click') } } clickCallback(interactionEvent) }, - [cleanFeatureState, clickCallback, metadata, updateFeatureState] + [clickCallback] ) return onMapClick From f8fe40c60e0c1cc9caf0ac59243d33673e960b6e Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 8 Apr 2024 16:24:22 +0200 Subject: [PATCH 09/43] fix replace url params bug --- libs/datasets-client/src/resolve-endpoint.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libs/datasets-client/src/resolve-endpoint.ts b/libs/datasets-client/src/resolve-endpoint.ts index 6d1a7fb9a1..fb93720330 100644 --- a/libs/datasets-client/src/resolve-endpoint.ts +++ b/libs/datasets-client/src/resolve-endpoint.ts @@ -20,16 +20,13 @@ export const resolveEndpoint = ( if (!endpoint) return null - const template = endpoint.pathTemplate - .replace('{{x}}', '{x}') - .replace('{{y}}', '{y}') - .replace('{{z}}', '{z}') - - let url = template + let url = endpoint.pathTemplate datasetConfig.params?.forEach((param) => { url = url.replace(`{{${param.id}}}`, param.value as string) }) + url = url.replace('{{x}}', '{x}').replace('{{y}}', '{y}').replace('{{z}}', '{z}') + if (datasetConfig.query) { const resolvedQuery = new URLSearchParams() datasetConfig.query.forEach((query) => { From 3fecc72d3bf9215ba7bab448cb87ca0500b795ce Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 8 Apr 2024 18:14:56 +0200 Subject: [PATCH 10/43] fix missing dataview --- apps/fishing-map/data/workspaces.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/data/workspaces.ts b/apps/fishing-map/data/workspaces.ts index 0a30092971..56ad964419 100644 --- a/apps/fishing-map/data/workspaces.ts +++ b/apps/fishing-map/data/workspaces.ts @@ -65,7 +65,7 @@ export const TEMPLATE_CLUSTERS_DATAVIEW_SLUG = 'template-for-bigquery-cluster-ev export const TEMPLATE_DATAVIEW_SLUGS = [ TEMPLATE_USER_TRACK_SLUG, - // TEMPLATE_VESSEL_DATAVIEW_SLUG, + TEMPLATE_VESSEL_DATAVIEW_SLUG, TEMPLATE_CONTEXT_DATAVIEW_SLUG, TEMPLATE_ENVIRONMENT_DATAVIEW_SLUG, TEMPLATE_GFW_ENVIRONMENT_DATAVIEW_SLUG, From 931e490352f68e87ef5709fbd5d4c3c3b694314e Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 8 Apr 2024 18:43:11 +0200 Subject: [PATCH 11/43] connect debug mode with secret menu --- apps/fishing-map/features/map/map.hooks.ts | 8 ++++++-- libs/deck-layer-composer/src/resolvers/fourwings.ts | 4 ++-- libs/deck-layer-composer/src/resolvers/types.ts | 1 + 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/fishing-map/features/map/map.hooks.ts b/apps/fishing-map/features/map/map.hooks.ts index 8af58c2869..6f300cde00 100644 --- a/apps/fishing-map/features/map/map.hooks.ts +++ b/apps/fishing-map/features/map/map.hooks.ts @@ -44,6 +44,7 @@ import { selectMapResolution, } from 'features/app/selectors/app.selectors' import { selectWorkspaceVisibleEventsArray } from 'features/workspace/workspace.selectors' +import { selectDebugOptions } from 'features/debug/debug.slice' import { WORKSPACES_POINTS_TYPE, WORKSPACE_GENERATOR_ID, @@ -88,12 +89,14 @@ export const useGlobalConfigConnect = () => { const detectionsVisualizationMode = useSelector(selectDetectionsVisualizationMode) const visibleEvents = useSelector(selectWorkspaceVisibleEventsArray) const mapResolution = useSelector(selectMapResolution) + const debug = useSelector(selectDebugOptions)?.debug return useMemo(() => { let globalConfig: ResolverGlobalConfig = { zoom: viewState.zoom, start, end, + debug, token: GFWAPI.getToken(), locale: i18n.language as Locale, bivariateDataviews, @@ -113,14 +116,15 @@ export const useGlobalConfigConnect = () => { viewState.zoom, start, end, + debug, i18n.language, bivariateDataviews, - mapResolution, activityVisualizationMode, detectionsVisualizationMode, + mapResolution, + visibleEvents, showTimeComparison, timeComparisonValues, - visibleEvents, ]) } diff --git a/libs/deck-layer-composer/src/resolvers/fourwings.ts b/libs/deck-layer-composer/src/resolvers/fourwings.ts index 2456cfa529..47862f9080 100644 --- a/libs/deck-layer-composer/src/resolvers/fourwings.ts +++ b/libs/deck-layer-composer/src/resolvers/fourwings.ts @@ -24,7 +24,7 @@ import { ResolverGlobalConfig } from './types' // TODO: decide if include static here or create a new one export const resolveDeckFourwingsLayerProps = ( dataview: UrlDataviewInstance, - { start, end, resolution }: ResolverGlobalConfig, + { start, end, resolution, debug }: ResolverGlobalConfig, interactions: PickingInfo[] ): FourwingsLayerProps => { const startTime = start ? getUTCDateTime(start).toMillis() : 0 @@ -110,7 +110,7 @@ export const resolveDeckFourwingsLayerProps = ( hoveredFeatures: interactions, minVisibleValue: dataview.config?.minVisibleValue, maxVisibleValue: dataview.config?.maxVisibleValue, - debug: dataview.config?.debug ?? false, + debug: debug ?? false, visible: dataview.config?.visible ?? true, colorRampWhiteEnd: dataview.config?.colorRampWhiteEnd ?? false, ...(tilesUrl && { tilesUrl }), diff --git a/libs/deck-layer-composer/src/resolvers/types.ts b/libs/deck-layer-composer/src/resolvers/types.ts index ed3a7a267c..7cf720f159 100644 --- a/libs/deck-layer-composer/src/resolvers/types.ts +++ b/libs/deck-layer-composer/src/resolvers/types.ts @@ -6,6 +6,7 @@ export type ResolverGlobalConfig = { end: string zoom?: number token?: string + debug?: boolean bivariateDataviews?: [string, string] resolution?: FourwingsResolution activityVisualizationMode?: FourwingsVisualizationMode From e015111bc4eb3c4fc0527015790948380dc78943 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 8 Apr 2024 19:14:41 +0200 Subject: [PATCH 12/43] move deck-map-interaction to deck libray to maintain compatibility --- .../features/map/map-interactions.hooks.ts | 4 +- apps/fishing-map/features/map/map.hooks.ts | 6 +- .../features/dataviews/dataviews.utils.ts | 5 +- libs/api-types/src/dataviews.ts | 11 +- .../deck-layers-interaction.hooks.ts | 31 +++ .../src/interactions/index.ts | 2 + .../interaction-features.utils.ts | 122 +++++++++ .../src/interactions/types.ts | 35 +++ .../src/use-map-interaction/index.ts | 9 +- .../use-map-interaction.ts | 258 +++++++++++------- 10 files changed, 377 insertions(+), 106 deletions(-) create mode 100644 libs/deck-layer-composer/src/interactions/interaction-features.utils.ts create mode 100644 libs/deck-layer-composer/src/interactions/types.ts diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 98c15af675..227817115a 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -4,7 +4,6 @@ import { DeckProps, PickingInfo, Position, Deck } from '@deck.gl/core' import { InteractionEventCallback, useFeatureState, - useMapClick, useMapHover, useSimpleMapHover, } from '@globalfishingwatch/react-hooks' @@ -14,9 +13,8 @@ import { MapLayerMouseEvent } from '@globalfishingwatch/maplibre-gl' import { useMapHoverInteraction, useSetMapHoverInteraction, - useMapClickInteraction, - useSetMapClickInteraction, DeckLayerInteraction, + useMapClick, } from '@globalfishingwatch/deck-layer-composer' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' diff --git a/apps/fishing-map/features/map/map.hooks.ts b/apps/fishing-map/features/map/map.hooks.ts index 6f300cde00..40675b54c2 100644 --- a/apps/fishing-map/features/map/map.hooks.ts +++ b/apps/fishing-map/features/map/map.hooks.ts @@ -240,12 +240,10 @@ export const useClickedEventConnect = () => { // get temporal grid clicked features and order them by sublayerindex const fishingActivityFeatures = event.features .filter((feature) => { - if (!feature.temporalgrid?.visible) { + if (feature?.sublayers.every((sublayer) => !sublayer.visible)) { return false } - return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes( - feature.temporalgrid.sublayerInteractionType - ) + return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes(feature.category) }) .sort((feature) => feature.temporalgrid?.sublayerIndex ?? 0) diff --git a/apps/vessel-history/features/dataviews/dataviews.utils.ts b/apps/vessel-history/features/dataviews/dataviews.utils.ts index 4b68bba839..5f38febcb3 100644 --- a/apps/vessel-history/features/dataviews/dataviews.utils.ts +++ b/apps/vessel-history/features/dataviews/dataviews.utils.ts @@ -1,6 +1,7 @@ import { uniq } from 'lodash' import { Dataview, + DataviewConfig, DataviewDatasetConfig, DataviewDatasetConfigParam, DataviewInstance, @@ -69,7 +70,7 @@ export const getVesselDataviewInstanceFactory = }) }) } - const vesselDataviewInstance = { + const vesselDataviewInstance: DataviewInstance = { id: getVesselDataviewInstanceId(vessel.id), dataviewId: defaultVesselDataviewId, config: { @@ -84,7 +85,7 @@ export const getVesselDataviewInstanceFactory = inactiveIconsSize: 2, }, showIcons: true, - }, + } as DataviewConfig, datasetsConfig, } return vesselDataviewInstance diff --git a/libs/api-types/src/dataviews.ts b/libs/api-types/src/dataviews.ts index bfce6ddf78..0745798669 100644 --- a/libs/api-types/src/dataviews.ts +++ b/libs/api-types/src/dataviews.ts @@ -99,7 +99,16 @@ export interface DataviewConfig { /** Used to store the vessel name */ name?: string - event?: string + event?: + | string + // Used in VV + | { + activeIconsSize?: number + activeStrokeColor?: string + strokeColor?: string + iconsPrefix?: string + inactiveIconsSize?: number + } pointsToSegmentsSwitchLevel?: number showIcons?: boolean showAuthorizationStatus?: boolean diff --git a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts index 3566a800bf..09a5fa7725 100644 --- a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts +++ b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts @@ -1,6 +1,8 @@ import { atom, useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' import { DeckLayerInteractionFeature } from '../types' +import { ExtendedFeature, InteractionEvent, InteractionEventCallback } from './types' +import { filterUniqueFeatureInteraction, getExtendedFeatures } from './interaction-features.utils' // Atom used to have all the layer instances loading state available export type DeckLayerInteraction = { @@ -18,3 +20,32 @@ export const useSetMapHoverInteraction = () => { const setDeckInteraction = useSetAtom(deckHoverInteractionAtom) return useCallback(setDeckInteraction, [setDeckInteraction]) } + +export const useMapClick = (clickCallback: InteractionEventCallback) => { + // const { updateFeatureState, cleanFeatureState } = useFeatureState(map) + const onMapClick = useCallback( + (event: DeckLayerInteraction) => { + if (!clickCallback) return + + const interactionEvent: InteractionEvent = { + type: 'click', + longitude: event.longitude, + latitude: event.latitude, + point: event.point, + } + if (event.features?.length) { + const extendedFeatures: ExtendedFeature[] = getExtendedFeatures(event.features) + const extendedFeaturesLimit = filterUniqueFeatureInteraction(extendedFeatures) + + if (extendedFeaturesLimit.length) { + interactionEvent.features = extendedFeaturesLimit + // updateFeatureState(extendedFeaturesLimit, 'click') + } + } + clickCallback(interactionEvent) + }, + [clickCallback] + ) + + return onMapClick +} diff --git a/libs/deck-layer-composer/src/interactions/index.ts b/libs/deck-layer-composer/src/interactions/index.ts index cba44543e5..b62f43c929 100644 --- a/libs/deck-layer-composer/src/interactions/index.ts +++ b/libs/deck-layer-composer/src/interactions/index.ts @@ -1 +1,3 @@ export * from './deck-layers-interaction.hooks' +// export * from './interaction-features.utils' +export * from './types' diff --git a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts new file mode 100644 index 0000000000..c59616c1db --- /dev/null +++ b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts @@ -0,0 +1,122 @@ +import { + ContextLayer, + ContextPickingObject, + FourwingsLayer, + FourwingsPickingObject, +} from '@globalfishingwatch/deck-layers' +import { getUTCDate } from '@globalfishingwatch/data-transforms' +import { VALUE_MULTIPLIER } from '@globalfishingwatch/fourwings-aggregate' +import { DeckLayerInteractionFeature } from '../types' +import { ExtendedFeature } from './types' + +export const filterUniqueFeatureInteraction = (features: ExtendedFeature[]) => { + const uniqueLayerIdFeatures: Record = {} + const filtered = features?.filter(({ layerId, id, uniqueFeatureInteraction }) => { + if (!uniqueFeatureInteraction) { + return true + } + if (uniqueLayerIdFeatures[layerId] === undefined) { + uniqueLayerIdFeatures[layerId] = id + return true + } + return uniqueLayerIdFeatures[layerId] === id + }) + return filtered +} + +const getExtendedFeature = (feature: DeckLayerInteractionFeature): ExtendedFeature[] => { + // const generatorType = feature.layer.metadata?.generatorType ?? null + // const generatorId = feature.layer.metadata?.generatorId ?? null + + // TODO: if no generatorMetadata is found, fallback to feature.layer.metadata, but the former should be prefered + // let generatorMetadata: any + // if (generatorId && metadata?.generatorsMetadata && metadata?.generatorsMetadata[generatorId]) { + // generatorMetadata = metadata?.generatorsMetadata[generatorId] + // } else { + // generatorMetadata = feature.layer.metadata + // } + + // TODO:deck implement the uniqueFeatureInteraction feature inside the getPickingInfo + // const uniqueFeatureInteraction = feature?.metadata?.uniqueFeatureInteraction ?? false + // TODO:deck implement the stopPropagation feature + // const stopPropagation = feature.layer?.metadata?.stopPropagation ?? false + + const extendedFeature: ExtendedFeature = { + ...feature.object, + layerId: feature.layer.id, + // uniqueFeatureInteraction, + // stopPropagation, + } + + if (feature.layer instanceof FourwingsLayer) { + const object = feature.object as FourwingsPickingObject + if (feature.layer?.props.static) { + return [ + { + ...extendedFeature, + value: extendedFeature.value / VALUE_MULTIPLIER, + unit: object.sublayers[0].unit, + }, + ] + } else { + // const values = object.sublayers.map((sublayer) => sublayer.value!) + + // This is used when querying the interaction endpoint, so that start begins at the start of the frame (ie start of a 10days interval) + // This avoids querying a cell visible on the map, when its actual timerange is not included in the app-overall time range + // const getDate = CONFIG_BY_INTERVAL[timeChunks.interval as Interval].getDate + const layer = feature.layer as FourwingsLayer + const visibleStartDate = getUTCDate(layer?.props?.startTime).toISOString() + const visibleEndDate = getUTCDate(layer?.props?.endTime).toISOString() + return object.sublayers.flatMap((sublayer, i) => { + if (sublayer.value === 0) return [] + const temporalGridExtendedFeature: ExtendedFeature = { + ...extendedFeature, + temporalgrid: { + sublayerIndex: i, + sublayerId: sublayer.id, + sublayerInteractionType: object.category, + sublayerCombinationMode: layer.props.comparisonMode, + visible: true, + col: object.properties.col as number, + row: object.properties.row as number, + interval: layer.getInterval(), + visibleStartDate, + visibleEndDate, + unit: sublayer.unit, + }, + value: sublayer.value, + } + return [temporalGridExtendedFeature] + }) + } + } else if (feature.layer instanceof ContextLayer) { + // TODO: deck add support for these layers + // case DataviewType.Context: + // case DataviewType.UserPoints: + // case DataviewType.UserContext: + const object = feature.object as ContextPickingObject + return [ + { + ...extendedFeature, + datasetId: object.datasetId, + promoteId: object.promoteId, + generatorContextLayer: object?.layerId, + geometry: object.geometry, + }, + ] + } + + return [extendedFeature] +} + +export const getExtendedFeatures = (features: DeckLayerInteractionFeature[]): ExtendedFeature[] => { + // TODO: deck implement the stopPropagation feature + // const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) + // if (stopPropagationFeature) { + // return getExtendedFeature(stopPropagationFeature, metadata, debug) + // } + const extendedFeatures: ExtendedFeature[] = features.flatMap((feature) => { + return getExtendedFeature(feature) || [] + }) + return extendedFeatures +} diff --git a/libs/deck-layer-composer/src/interactions/types.ts b/libs/deck-layer-composer/src/interactions/types.ts new file mode 100644 index 0000000000..6d7731c87c --- /dev/null +++ b/libs/deck-layer-composer/src/interactions/types.ts @@ -0,0 +1,35 @@ +import { + ContextLayerId, + ContextPickingObject, + FourwingsPickingObject, +} from '@globalfishingwatch/deck-layers' + +export type InteractionEventCallback = (event: InteractionEvent | null) => void + +export type ExtendedFeature = FourwingsPickingObject & + ContextPickingObject & { + layerId: string + generatorContextLayer?: ContextLayerId | null + datasetId?: string + promoteId?: string + id: string + value: any + geometry?: any + stopPropagation?: boolean + uniqueFeatureInteraction?: boolean + unit?: string + // TODO:deck review if this is needed anywhere else + // tile: { + // x: number + // y: number + // z: number + // } + } + +export type InteractionEvent = { + type: 'click' | 'hover' + features?: ExtendedFeature[] + latitude: number + longitude: number + point: { x: number; y: number } +} diff --git a/libs/react-hooks/src/use-map-interaction/index.ts b/libs/react-hooks/src/use-map-interaction/index.ts index 565ca9af80..bf43a89d0d 100644 --- a/libs/react-hooks/src/use-map-interaction/index.ts +++ b/libs/react-hooks/src/use-map-interaction/index.ts @@ -1,6 +1,9 @@ -import { Interval, HeatmapAnimatedInteractionType } from '@globalfishingwatch/layer-composer' +import { + Interval, + ContextLayerType, + HeatmapAnimatedInteractionType, +} from '@globalfishingwatch/layer-composer' import { SublayerCombinationMode } from '@globalfishingwatch/fourwings-aggregate' -import { ContextLayerId } from '@globalfishingwatch/deck-layers' export * from './use-map-interaction' @@ -25,7 +28,7 @@ export type ExtendedFeature = { sourceLayer: string generatorId: string | number | null generatorType: string | null - generatorContextLayer?: ContextLayerId | null + generatorContextLayer?: ContextLayerType | null datasetId?: string promoteId?: string id: string diff --git a/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts b/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts index cfe3cedfd5..d6fc51ccdd 100644 --- a/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts +++ b/libs/react-hooks/src/use-map-interaction/use-map-interaction.ts @@ -8,17 +8,6 @@ import { ExtendedLayer, Group, } from '@globalfishingwatch/layer-composer' -import { - ContextLayer, - FourwingsLayer, - FourwingsPickingObject, - ContextPickingInfo, - ContextPickingObject, -} from '@globalfishingwatch/deck-layers' -import { - DeckLayerInteraction, - DeckLayerInteractionFeature, -} from '@globalfishingwatch/deck-layer-composer' import { aggregateCell, SublayerCombinationMode, @@ -26,7 +15,6 @@ import { } from '@globalfishingwatch/fourwings-aggregate' import type { Map, GeoJSONFeature, MapLayerMouseEvent } from '@globalfishingwatch/maplibre-gl' import { DataviewType } from '@globalfishingwatch/api-types' -import { getUTCDate } from '@globalfishingwatch/data-transforms' import { ExtendedFeature, InteractionEventCallback, InteractionEvent } from '.' export type MaplibreGeoJSONFeature = GeoJSONFeature & { @@ -60,99 +48,176 @@ export const filterUniqueFeatureInteraction = (features: ExtendedFeature[]) => { return filtered } -const getExtendedFeature = (feature: DeckLayerInteractionFeature): ExtendedFeature[] => { - // const generatorType = feature.layer.metadata?.generatorType ?? null - // const generatorId = feature.layer.metadata?.generatorId ?? null +const getId = (feature: MaplibreGeoJSONFeature) => { + const promoteIdValue = + feature.layer.metadata?.promoteId && feature.properties[feature.layer.metadata?.promoteId] + if (feature.id !== undefined) { + return feature.id + } else if (feature.properties?.gfw_id !== undefined) { + return feature.properties?.gfw_id + } else if (promoteIdValue !== undefined) { + return promoteIdValue + } +} + +const getFeatureTile = (feature: MaplibreGeoJSONFeature) => { + return { + x: (feature as any)._vectorTileFeature._x, + y: (feature as any)._vectorTileFeature._y, + z: (feature as any)._vectorTileFeature._z, + } +} + +const getExtendedFeature = ( + feature: MaplibreGeoJSONFeature, + metadata?: ExtendedStyleMeta, + debug = false +): ExtendedFeature[] => { + const generatorType = feature.layer.metadata?.generatorType ?? null + const generatorId = feature.layer.metadata?.generatorId ?? null // TODO: if no generatorMetadata is found, fallback to feature.layer.metadata, but the former should be prefered - // let generatorMetadata: any - // if (generatorId && metadata?.generatorsMetadata && metadata?.generatorsMetadata[generatorId]) { - // generatorMetadata = metadata?.generatorsMetadata[generatorId] - // } else { - // generatorMetadata = feature.layer.metadata - // } - - // TODO:deck implement the uniqueFeatureInteraction feature inside the getPickingInfo - // const uniqueFeatureInteraction = feature?.metadata?.uniqueFeatureInteraction ?? false - // TODO:deck implement the stopPropagation feature - // const stopPropagation = feature.layer?.metadata?.stopPropagation ?? false - - const extendedFeature: ExtendedFeature = { - ...feature.object, - layerId: feature.layer.id, - // uniqueFeatureInteraction, - // stopPropagation, + let generatorMetadata: any + if (generatorId && metadata?.generatorsMetadata && metadata?.generatorsMetadata[generatorId]) { + generatorMetadata = metadata?.generatorsMetadata[generatorId] + } else { + generatorMetadata = feature.layer.metadata } - if (feature.layer instanceof FourwingsLayer) { - const object = feature.object as FourwingsPickingObject - if (feature.layer?.props.static) { - return [ - { - ...extendedFeature, - value: extendedFeature.value / VALUE_MULTIPLIER, - unit: object.sublayers[0].unit, - }, - ] - } else { - // const values = object.sublayers.map((sublayer) => sublayer.value!) + const uniqueFeatureInteraction = feature.layer?.metadata?.uniqueFeatureInteraction ?? false + const stopPropagation = feature.layer?.metadata?.stopPropagation ?? false + const properties = feature.properties || {} + let value = properties.value || properties.name || properties.id || properties?.count + const { valueProperties } = feature.layer.metadata || {} + if (valueProperties?.length) { + value = + valueProperties.length === 1 + ? properties[valueProperties[0]] + : valueProperties + .flatMap((prop) => (properties[prop] ? `${prop}: ${properties[prop]}` : [])) + .join('
      ') + } + + const extendedFeature: ExtendedFeature | null = { + properties, + generatorType, + generatorId, + layerId: feature.layer.id, + source: feature.source, + sourceLayer: feature.sourceLayer, + uniqueFeatureInteraction, + stopPropagation, + value, + id: getId(feature), + tile: getFeatureTile(feature), + } + switch (generatorType) { + case DataviewType.HeatmapAnimated: + const timeChunks = generatorMetadata?.timeChunks + const frame = timeChunks?.activeChunkFrame + const activeTimeChunk = pickActiveTimeChunk(timeChunks) // This is used when querying the interaction endpoint, so that start begins at the start of the frame (ie start of a 10days interval) // This avoids querying a cell visible on the map, when its actual timerange is not included in the app-overall time range - // const getDate = CONFIG_BY_INTERVAL[timeChunks.interval as Interval].getDate - const layer = feature.layer as FourwingsLayer - const visibleStartDate = getUTCDate(layer?.props?.startTime).toISOString() - const visibleEndDate = getUTCDate(layer?.props?.endTime).toISOString() - return object.sublayers.flatMap((sublayer, i) => { - if (sublayer.value === 0) return [] + const getDate = CONFIG_BY_INTERVAL[timeChunks.interval as Interval].getDate + const visibleStartDate = getDate(timeChunks.visibleStartFrame).toISOString() + const visibleEndDate = getDate(timeChunks.visibleEndFrame).toISOString() + const numSublayers = generatorMetadata?.numSublayers + const values = aggregateCell({ + rawValues: properties.rawValues, + frame, + delta: Math.max(1, timeChunks.deltaInIntervalUnits), + quantizeOffset: activeTimeChunk.quantizeOffset, + sublayerCount: + generatorMetadata?.sublayerCombinationMode === SublayerCombinationMode.TimeCompare + ? 2 + : numSublayers, + aggregationOperation: generatorMetadata?.aggregationOperation, + sublayerCombinationMode: generatorMetadata?.sublayerCombinationMode, + multiplier: generatorMetadata?.multiplier, + }) + + if (debug) { + console.log(properties.rawValues) + } + // Clean values with 0 for sum aggregation and with NaN for avg aggregation layers + if ( + !values || + !values.filter((v: number) => { + const matchesMin = + generatorMetadata?.minVisibleValue !== undefined + ? v >= generatorMetadata?.minVisibleValue + : true + const matchesMax = + generatorMetadata?.maxVisibleValue !== undefined + ? v <= generatorMetadata?.maxVisibleValue + : true + return v !== 0 && !isNaN(v) && matchesMin && matchesMax + }).length + ) { + return [] + } + const visibleSublayers = generatorMetadata?.visibleSublayers as boolean[] + const sublayers = generatorMetadata?.sublayers + return values.flatMap((value: any, i: number) => { + if (value === 0) return [] const temporalGridExtendedFeature: ExtendedFeature = { ...extendedFeature, temporalgrid: { sublayerIndex: i, - sublayerId: sublayer.id, - sublayerInteractionType: object.category, - sublayerCombinationMode: layer.props.comparisonMode, - visible: true, - col: object.properties.col as number, - row: object.properties.row as number, - interval: layer.getInterval(), + sublayerId: sublayers[i].id, + sublayerInteractionType: sublayers[i].interactionType, + sublayerCombinationMode: generatorMetadata?.sublayerCombinationMode, + visible: visibleSublayers[i] === true, + col: properties._col as number, + row: properties._row as number, + interval: timeChunks.interval, visibleStartDate, visibleEndDate, - unit: sublayer.unit, + unit: sublayers[i].legend.unit, }, - value: sublayer.value, + value, } return [temporalGridExtendedFeature] }) + case DataviewType.HeatmapStatic: { + return [ + { + ...extendedFeature, + value: extendedFeature.value / VALUE_MULTIPLIER, + unit: generatorMetadata.legends[0]?.unit, + }, + ] + } + case DataviewType.Context: + case DataviewType.UserPoints: + case DataviewType.UserContext: { + return [ + { + ...extendedFeature, + datasetId: feature.layer.metadata?.datasetId, + promoteId: feature.layer.metadata?.promoteId, + generatorContextLayer: feature.layer.metadata?.layer, + geometry: feature.geometry, + }, + ] } - } else if (feature.layer instanceof ContextLayer) { - // TODO: deck add support for these layers - // case DataviewType.Context: - // case DataviewType.UserPoints: - // case DataviewType.UserContext: - const object = feature.object as ContextPickingObject - return [ - { - ...extendedFeature, - datasetId: object.datasetId, - promoteId: object.promoteId, - generatorContextLayer: object?.layerId, - geometry: object.geometry, - }, - ] + default: + return [extendedFeature] } - - return [extendedFeature] } -const getExtendedFeatures = (features: DeckLayerInteractionFeature[]): ExtendedFeature[] => { - // TODO: deck implement the stopPropagation feature - // const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) - // if (stopPropagationFeature) { - // return getExtendedFeature(stopPropagationFeature, metadata, debug) - // } +const getExtendedFeatures = ( + features: MaplibreGeoJSONFeature[], + metadata?: ExtendedStyleMeta, + debug = false +): ExtendedFeature[] => { + const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) + if (stopPropagationFeature) { + return getExtendedFeature(stopPropagationFeature, metadata, debug) + } const extendedFeatures: ExtendedFeature[] = features.flatMap((feature) => { - return getExtendedFeature(feature) || [] + return getExtendedFeature(feature, metadata, debug) || [] }) return extendedFeatures } @@ -219,31 +284,38 @@ export const useFeatureState = (map?: Map) => { return featureState } -export const useMapClick = (clickCallback: InteractionEventCallback) => { - // const { updateFeatureState, cleanFeatureState } = useFeatureState(map) +export const useMapClick = ( + clickCallback: InteractionEventCallback, + metadata: ExtendedStyleMeta, + map?: Map +) => { + const { updateFeatureState, cleanFeatureState } = useFeatureState(map) const onMapClick = useCallback( - (event: DeckLayerInteraction) => { - // cleanFeatureState('click') + (event: MapLayerMouseEvent) => { + cleanFeatureState('click') if (!clickCallback) return - const interactionEvent: InteractionEvent = { type: 'click', - longitude: event.longitude, - latitude: event.latitude, + longitude: event.lngLat.lng, + latitude: event.lngLat.lat, point: event.point, } if (event.features?.length) { - const extendedFeatures: ExtendedFeature[] = getExtendedFeatures(event.features) + const extendedFeatures: ExtendedFeature[] = getExtendedFeatures( + event.features as MaplibreGeoJSONFeature[], + metadata, + false + ) const extendedFeaturesLimit = filterUniqueFeatureInteraction(extendedFeatures) if (extendedFeaturesLimit.length) { interactionEvent.features = extendedFeaturesLimit - // updateFeatureState(extendedFeaturesLimit, 'click') + updateFeatureState(extendedFeaturesLimit, 'click') } } clickCallback(interactionEvent) }, - [clickCallback] + [cleanFeatureState, clickCallback, metadata, updateFeatureState] ) return onMapClick From bb2fda19f03626b15d5ab0e231faf3fe4adc9917 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 8 Apr 2024 20:29:17 +0200 Subject: [PATCH 13/43] cleanup interactions --- apps/fishing-map/features/map/Map.tsx | 2 - .../features/map/map-interactions.hooks.ts | 200 ++++++++++++++++-- apps/fishing-map/features/map/map.hooks.ts | 175 +-------------- .../features/map/popups/PopupWrapper.tsx | 4 +- .../features/sidebar/CategoryTabs.tsx | 2 +- apps/fishing-map/features/vessel/Vessel.tsx | 2 +- .../environmental/environmental.hooks.ts | 181 ---------------- .../features/workspace/events/events.hooks.ts | 52 ----- .../deck-layers-interaction.hooks.ts | 11 +- .../interaction-features.utils.ts | 7 +- .../src/interactions/types.ts | 37 ++-- libs/deck-layer-composer/src/types.ts | 9 +- .../src/layers/context/ContextLayer.ts | 10 +- .../src/layers/context/context.types.ts | 8 +- package.json | 12 +- yarn.lock | 94 ++++---- 16 files changed, 281 insertions(+), 525 deletions(-) delete mode 100644 apps/fishing-map/features/workspace/environmental/environmental.hooks.ts delete mode 100644 apps/fishing-map/features/workspace/events/events.hooks.ts diff --git a/apps/fishing-map/features/map/Map.tsx b/apps/fishing-map/features/map/Map.tsx index 38259fa78c..34fc3eb99b 100644 --- a/apps/fishing-map/features/map/Map.tsx +++ b/apps/fishing-map/features/map/Map.tsx @@ -44,7 +44,6 @@ import { selectIsWorkspaceLocation, } from 'routes/routes.selectors' import { useMapLoaded, useSetMapIdleAtom } from 'features/map/map-state.hooks' -import { useEnvironmentalBreaksUpdate } from 'features/workspace/environmental/environmental.hooks' import { mapReadyAtom } from 'features/map/map-state.atom' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { selectHighlightedTime } from 'features/timebar/timebar.slice' @@ -142,7 +141,6 @@ const MapWrapper = () => { // Used it only once here to attach the listener only once useSetMapIdleAtom() // useMapSourceTilesLoadedAtom() - useEnvironmentalBreaksUpdate() useMapRulersDrag() const { rulers, editingRuler, rulersVisible } = useRulers() // const map = useMapInstance() diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 227817115a..cc3a58c0be 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -1,27 +1,22 @@ -import { useCallback, useMemo, useState } from 'react' +import { useCallback, useMemo, useRef, useState } from 'react' import { useSelector } from 'react-redux' -import { DeckProps, PickingInfo, Position, Deck } from '@deck.gl/core' -import { - InteractionEventCallback, - useFeatureState, - useMapHover, - useSimpleMapHover, -} from '@globalfishingwatch/react-hooks' -import { ExtendedStyle, ExtendedStyleMeta } from '@globalfishingwatch/layer-composer' +import { DeckProps, PickingInfo, Position } from '@deck.gl/core' +import { InteractionEventCallback, useSimpleMapHover } from '@globalfishingwatch/react-hooks' +import { ExtendedStyle } from '@globalfishingwatch/layer-composer' import { DataviewCategory, DataviewType } from '@globalfishingwatch/api-types' -import { MapLayerMouseEvent } from '@globalfishingwatch/maplibre-gl' import { useMapHoverInteraction, useSetMapHoverInteraction, - DeckLayerInteraction, useMapClick, + InteractionEvent, } from '@globalfishingwatch/deck-layer-composer' +import { ClusterLayer, ClusterPickingObject } from '@globalfishingwatch/deck-layers' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' import { + SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION, TooltipEventFeature, parseMapTooltipEvent, - useClickedEventConnect, useMapHighlightedEvent, } from 'features/map/map.hooks' import useRulers from 'features/map/overlays/rulers/rulers.hooks' @@ -30,19 +25,180 @@ import { selectActiveTemporalgridDataviews } from 'features/dataviews/selectors/ import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import { getEventLabel } from 'utils/analytics' import { POPUP_CATEGORY_ORDER } from 'data/config' -import { selectIsMarineManagerLocation } from 'routes/routes.selectors' +import { selectIsMarineManagerLocation, selectLocationType } from 'routes/routes.selectors' import { useMapClusterTilesLoaded } from 'features/map/map-sources.hooks' -import { - ANNOTATIONS_GENERATOR_ID, - RULERS_LAYER_ID, - WORKSPACES_POINTS_TYPE, -} from 'features/map/map.config' +import { WORKSPACES_POINTS_TYPE } from 'features/map/map.config' import { useMapErrorNotification } from 'features/map/overlays/error-notification/error-notification.hooks' import { selectIsGFWUser } from 'features/user/selectors/user.selectors' import { selectCurrentDataviewInstancesResolved } from 'features/dataviews/selectors/dataviews.instances.selectors' -import { SliceInteractionEvent } from './map.slice' -import { isRulerLayerPoint } from './map-interaction.utils' +import { DEFAULT_WORKSPACE_ID, DEFAULT_WORKSPACE_CATEGORY } from 'data/workspaces' +import { useAppDispatch } from 'features/app/app.hooks' +import { ENCOUNTER_EVENTS_SOURCE_ID } from 'features/dataviews/dataviews.utils' +import { setHintDismissed } from 'features/help/hints.slice' +import { USER, WORKSPACES_LIST, HOME, WORKSPACE } from 'routes/routes' +import { useLocationConnect } from 'routes/routes.hook' import { useMapRulersDrag } from './overlays/rulers/rulers-drag.hooks' +import { isRulerLayerPoint } from './map-interaction.utils' +import { + SliceInteractionEvent, + fetchBQEventThunk, + fetchEncounterEventThunk, + fetchFishingActivityInteractionThunk, + selectApiEventStatus, + selectClickedEvent, + selectFishingInteractionStatus, + setClickedEvent, +} from './map.slice' +import { useSetViewState } from './map-viewport.hooks' + +function cleanFeatureState(state: any) { + console.warn('TODO: handle this in deck') +} + +export const useClickedEventConnect = () => { + const map = useMapInstance() + const dispatch = useAppDispatch() + const clickedEvent = useSelector(selectClickedEvent) + const locationType = useSelector(selectLocationType) + const fishingInteractionStatus = useSelector(selectFishingInteractionStatus) + const apiEventStatus = useSelector(selectApiEventStatus) + const { dispatchLocation } = useLocationConnect() + // const { cleanFeatureState } = useFeatureState(map) + const setViewState = useSetViewState() + const { setMapAnnotation } = useMapAnnotation() + const tilesClusterLoaded = useMapClusterTilesLoaded() + const fishingPromiseRef = useRef() + const presencePromiseRef = useRef() + const eventsPromiseRef = useRef() + + const cancelPendingInteractionRequests = useCallback(() => { + const promisesRef = [fishingPromiseRef, presencePromiseRef, eventsPromiseRef] + promisesRef.forEach((ref) => { + if (ref.current) { + ref.current.abort() + } + }) + }, []) + + const dispatchClickedEvent = (event: InteractionEvent | null) => { + if (event === null) { + dispatch(setClickedEvent(null)) + return + } + + // Used on workspaces-list or user panel to go to the workspace detail page + if (locationType === USER || locationType === WORKSPACES_LIST) { + const workspace = event?.features?.find( + (feature: any) => feature.properties.type === WORKSPACES_POINTS_TYPE + ) + if (workspace) { + const isDefaultWorkspace = workspace.properties.id === DEFAULT_WORKSPACE_ID + dispatchLocation( + isDefaultWorkspace ? HOME : WORKSPACE, + isDefaultWorkspace + ? {} + : { + payload: { + category: + workspace.properties?.category && workspace.properties.category !== 'null' + ? workspace.properties.category + : DEFAULT_WORKSPACE_CATEGORY, + workspaceId: workspace.properties.id, + }, + }, + { replaceQuery: true } + ) + const { latitude, longitude, zoom } = workspace.properties + if (latitude && longitude && zoom) { + setViewState({ latitude, longitude, zoom }) + } + return + } + } + + const clusterFeature = event?.features?.find( + (f) => f.layer instanceof ClusterLayer + ) as ClusterPickingObject + if (clusterFeature?.properties?.expansionZoom) { + const { count, expansionZoom, lat, lon } = clusterFeature.properties + if (count > 1) { + if (tilesClusterLoaded && lat && lon) { + setViewState({ + latitude: lat, + longitude: lon, + zoom: expansionZoom, + }) + cleanFeatureState('click') + } + return + } + } + + const annotatedFeature = event?.features?.find( + (f) => f.generatorType === DataviewType.Annotation + ) + if (annotatedFeature?.properties?.id) { + setMapAnnotation(annotatedFeature.properties) + return + } + + // Cancel all pending promises + cancelPendingInteractionRequests() + + if (!event || !event.features) { + if (clickedEvent) { + dispatch(setClickedEvent(null)) + } + return + } + + // When hovering in a vessel event we don't want to have clicked events + const areAllFeaturesVesselEvents = event.features.every( + (f) => f.generatorType === DataviewType.VesselEvents + ) + + if (areAllFeaturesVesselEvents) { + return + } + + dispatch(setClickedEvent(event as SliceInteractionEvent)) + + // get temporal grid clicked features and order them by sublayerindex + const fishingActivityFeatures = event.features.filter((feature) => { + if (feature?.sublayers.every((sublayer) => !sublayer.visible)) { + return false + } + return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes(feature.category) + }) + + if (fishingActivityFeatures?.length) { + dispatch(setHintDismissed('clickingOnAGridCellToShowVessels')) + const activityProperties = fishingActivityFeatures.map((feature) => + feature.temporalgrid?.sublayerInteractionType === 'detections' ? 'detections' : 'hours' + ) + fishingPromiseRef.current = dispatch( + fetchFishingActivityInteractionThunk({ fishingActivityFeatures, activityProperties }) + ) + } + + const tileClusterFeature = event.features.find( + (f) => f.generatorType === DataviewType.TileCluster + ) + if (tileClusterFeature) { + const bqPocQuery = tileClusterFeature.source !== ENCOUNTER_EVENTS_SOURCE_ID + const fetchFn = bqPocQuery ? fetchBQEventThunk : fetchEncounterEventThunk + eventsPromiseRef.current = dispatch(fetchFn(tileClusterFeature)) + } + } + + return { + clickedEvent, + fishingInteractionStatus, + apiEventStatus, + dispatchClickedEvent, + cancelPendingInteractionRequests, + } +} const defaultEmptyFeatures = [] as PickingInfo[] export const useMapMouseHover = (style?: ExtendedStyle) => { @@ -123,7 +279,7 @@ export const useHandleMapToolsClick = () => { const { onRulerMapClick, rulersEditing } = useRulers() const isMarineManagerLocation = useSelector(selectIsMarineManagerLocation) const handleMapClickInteraction = useCallback( - (interaction: DeckLayerInteraction) => { + (interaction: InteractionEvent) => { const { latitude, longitude, features } = interaction const position = [longitude, latitude] as Position if (isMapAnnotating) { @@ -206,7 +362,7 @@ export const useMapMouseClick = () => { console.warn(e) } - const mapClickInteraction: DeckLayerInteraction = { + const mapClickInteraction: InteractionEvent = { longitude: info.coordinate[0], latitude: info.coordinate[1], point: { x: info.x, y: info.y }, diff --git a/apps/fishing-map/features/map/map.hooks.ts b/apps/fishing-map/features/map/map.hooks.ts index 40675b54c2..0d2c2cbe00 100644 --- a/apps/fishing-map/features/map/map.hooks.ts +++ b/apps/fishing-map/features/map/map.hooks.ts @@ -1,8 +1,7 @@ import { useSelector } from 'react-redux' -import { useCallback, useEffect, useMemo, useRef } from 'react' +import { useCallback, useEffect, useMemo } from 'react' import { debounce } from 'lodash' import { useTranslation } from 'react-i18next' -import { InteractionEvent, useFeatureState } from '@globalfishingwatch/react-hooks' import { UrlDataviewInstance, MULTILAYER_SEPARATOR, @@ -17,26 +16,17 @@ import { import { GFWAPI } from '@globalfishingwatch/api-client' import { SublayerCombinationMode } from '@globalfishingwatch/fourwings-aggregate' import { ResolverGlobalConfig } from '@globalfishingwatch/deck-layer-composer' -import { selectLocationType } from 'routes/routes.selectors' -import { HOME, USER, WORKSPACE, WORKSPACES_LIST } from 'routes/routes' -import { useLocationConnect } from 'routes/routes.hook' -import { DEFAULT_WORKSPACE_CATEGORY, DEFAULT_WORKSPACE_ID } from 'data/workspaces' -import useMapInstance from 'features/map/map-context.hooks' import { getActiveDatasetsInActivityDataviews, getDatasetTitleByDataview, } from 'features/datasets/datasets.utils' import { useTimerangeConnect } from 'features/timebar/timebar.hooks' import { selectHighlightedEvents, setHighlightedEvents } from 'features/timebar/timebar.slice' -import { setHintDismissed } from 'features/help/hints.slice' -import { useMapClusterTilesLoaded } from 'features/map/map-sources.hooks' -import { ENCOUNTER_EVENTS_SOURCE_ID } from 'features/dataviews/dataviews.utils' import { useAppDispatch } from 'features/app/app.hooks' import { selectShowTimeComparison, selectTimeComparisonValues, } from 'features/reports/reports.selectors' -import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' import { selectActivityVisualizationMode, selectBivariateDataviews, @@ -45,25 +35,14 @@ import { } from 'features/app/selectors/app.selectors' import { selectWorkspaceVisibleEventsArray } from 'features/workspace/workspace.selectors' import { selectDebugOptions } from 'features/debug/debug.slice' +import { WORKSPACE_GENERATOR_ID, REPORT_BUFFER_GENERATOR_ID } from './map.config' import { - WORKSPACES_POINTS_TYPE, - WORKSPACE_GENERATOR_ID, - REPORT_BUFFER_GENERATOR_ID, -} from './map.config' -import { - setClickedEvent, - selectClickedEvent, MAX_TOOLTIP_LIST, - fetchEncounterEventThunk, SliceInteractionEvent, - selectFishingInteractionStatus, - selectApiEventStatus, ExtendedFeatureVessel, - fetchFishingActivityInteractionThunk, - fetchBQEventThunk, SliceExtendedFeature, } from './map.slice' -import { useSetViewState, useViewStateAtom } from './map-viewport.hooks' +import { useViewStateAtom } from './map-viewport.hooks' export const SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION = ['activity', 'detections'] @@ -128,154 +107,6 @@ export const useGlobalConfigConnect = () => { ]) } -export const useClickedEventConnect = () => { - const map = useMapInstance() - const dispatch = useAppDispatch() - const clickedEvent = useSelector(selectClickedEvent) - const locationType = useSelector(selectLocationType) - const fishingInteractionStatus = useSelector(selectFishingInteractionStatus) - const apiEventStatus = useSelector(selectApiEventStatus) - const { dispatchLocation } = useLocationConnect() - const { cleanFeatureState } = useFeatureState(map) - const setViewState = useSetViewState() - const { setMapAnnotation } = useMapAnnotation() - const tilesClusterLoaded = useMapClusterTilesLoaded() - const fishingPromiseRef = useRef() - const presencePromiseRef = useRef() - const eventsPromiseRef = useRef() - - const cancelPendingInteractionRequests = useCallback(() => { - const promisesRef = [fishingPromiseRef, presencePromiseRef, eventsPromiseRef] - promisesRef.forEach((ref) => { - if (ref.current) { - ref.current.abort() - } - }) - }, []) - - const dispatchClickedEvent = (event: InteractionEvent | null) => { - if (event === null) { - dispatch(setClickedEvent(null)) - return - } - - // Used on workspaces-list or user panel to go to the workspace detail page - if (locationType === USER || locationType === WORKSPACES_LIST) { - const workspace = event?.features?.find( - (feature: any) => feature.properties.type === WORKSPACES_POINTS_TYPE - ) - if (workspace) { - const isDefaultWorkspace = workspace.properties.id === DEFAULT_WORKSPACE_ID - dispatchLocation( - isDefaultWorkspace ? HOME : WORKSPACE, - isDefaultWorkspace - ? {} - : { - payload: { - category: - workspace.properties?.category && workspace.properties.category !== 'null' - ? workspace.properties.category - : DEFAULT_WORKSPACE_CATEGORY, - workspaceId: workspace.properties.id, - }, - }, - { replaceQuery: true } - ) - const { latitude, longitude, zoom } = workspace.properties - if (latitude && longitude && zoom) { - setViewState({ latitude, longitude, zoom }) - } - return - } - } - - const clusterFeature = event?.features?.find( - (f) => f.generatorType === DataviewType.TileCluster - ) - if (clusterFeature?.properties?.expansionZoom) { - const { count, expansionZoom, lat, lng, lon } = clusterFeature.properties - const longitude = lng || lon - if (count > 1) { - if (tilesClusterLoaded && lat && longitude) { - setViewState({ - latitude: lat, - longitude, - zoom: expansionZoom, - }) - cleanFeatureState('click') - } - return - } - } - - const annotatedFeature = event?.features?.find( - (f) => f.generatorType === DataviewType.Annotation - ) - if (annotatedFeature?.properties?.id) { - setMapAnnotation(annotatedFeature.properties) - return - } - - // Cancel all pending promises - cancelPendingInteractionRequests() - - if (!event || !event.features) { - if (clickedEvent) { - dispatch(setClickedEvent(null)) - } - return - } - - // When hovering in a vessel event we don't want to have clicked events - const areAllFeaturesVesselEvents = event.features.every( - (f) => f.generatorType === DataviewType.VesselEvents - ) - - if (areAllFeaturesVesselEvents) { - return - } - - dispatch(setClickedEvent(event as SliceInteractionEvent)) - - // get temporal grid clicked features and order them by sublayerindex - const fishingActivityFeatures = event.features - .filter((feature) => { - if (feature?.sublayers.every((sublayer) => !sublayer.visible)) { - return false - } - return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes(feature.category) - }) - .sort((feature) => feature.temporalgrid?.sublayerIndex ?? 0) - - if (fishingActivityFeatures?.length) { - dispatch(setHintDismissed('clickingOnAGridCellToShowVessels')) - const activityProperties = fishingActivityFeatures.map((feature) => - feature.temporalgrid?.sublayerInteractionType === 'detections' ? 'detections' : 'hours' - ) - fishingPromiseRef.current = dispatch( - fetchFishingActivityInteractionThunk({ fishingActivityFeatures, activityProperties }) - ) - } - - const tileClusterFeature = event.features.find( - (f) => f.generatorType === DataviewType.TileCluster - ) - if (tileClusterFeature) { - const bqPocQuery = tileClusterFeature.source !== ENCOUNTER_EVENTS_SOURCE_ID - const fetchFn = bqPocQuery ? fetchBQEventThunk : fetchEncounterEventThunk - eventsPromiseRef.current = dispatch(fetchFn(tileClusterFeature)) - } - } - - return { - clickedEvent, - fishingInteractionStatus, - apiEventStatus, - dispatchClickedEvent, - cancelPendingInteractionRequests, - } -} - // TODO:deck fuerte // hack to allow building the app wihtout migrating the rest of the interactions // needs to be updated with the new deck-layers diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index 5b48fb7836..be383d7102 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -6,7 +6,7 @@ import type { Anchor } from 'react-map-gl' import { useSelector } from 'react-redux' import { DataviewCategory, DataviewType } from '@globalfishingwatch/api-types' import { IconButton, Spinner } from '@globalfishingwatch/ui-components' -import { DeckLayerInteraction } from '@globalfishingwatch/deck-layer-composer' +import { InteractionEvent } from '@globalfishingwatch/deck-layer-composer' import { ContextFeature, FourwingsPickingObject } from '@globalfishingwatch/deck-layers' import { TooltipEvent } from 'features/map/map.hooks' import { POPUP_CATEGORY_ORDER } from 'data/config' @@ -32,7 +32,7 @@ import ComparisonRow from './ComparisonRow' import ReportBufferTooltip from './ReportBufferLayers' type PopupWrapperProps = { - interaction: DeckLayerInteraction | null + interaction: InteractionEvent | null closeButton?: boolean closeOnClick?: boolean className?: string diff --git a/apps/fishing-map/features/sidebar/CategoryTabs.tsx b/apps/fishing-map/features/sidebar/CategoryTabs.tsx index df0f2dd244..22dff85815 100644 --- a/apps/fishing-map/features/sidebar/CategoryTabs.tsx +++ b/apps/fishing-map/features/sidebar/CategoryTabs.tsx @@ -18,7 +18,7 @@ import { selectLocationType, } from 'routes/routes.selectors' import { selectUserData } from 'features/user/selectors/user.selectors' -import { useClickedEventConnect } from 'features/map/map.hooks' +import { useClickedEventConnect } from 'features/map/map-interactions.hooks' import useMapInstance from 'features/map/map-context.hooks' import { selectAvailableWorkspacesCategories } from 'features/workspaces-list/workspaces-list.selectors' import { useSetViewState } from 'features/map/map-viewport.hooks' diff --git a/apps/fishing-map/features/vessel/Vessel.tsx b/apps/fishing-map/features/vessel/Vessel.tsx index eba6405b98..b6826b99ee 100644 --- a/apps/fishing-map/features/vessel/Vessel.tsx +++ b/apps/fishing-map/features/vessel/Vessel.tsx @@ -26,7 +26,7 @@ import { import { fetchWorkspaceThunk } from 'features/workspace/workspace.slice' import { useUpdateVesselEventsVisibility } from 'features/vessel/vessel.hooks' import useMapInstance from 'features/map/map-context.hooks' -import { useClickedEventConnect } from 'features/map/map.hooks' +import { useClickedEventConnect } from 'features/map/map-interactions.hooks' import VesselAreas from 'features/vessel/areas/VesselAreas' import RelatedVessels from 'features/vessel/related-vessels/RelatedVessels' import { useLocationConnect } from 'routes/routes.hook' diff --git a/apps/fishing-map/features/workspace/environmental/environmental.hooks.ts b/apps/fishing-map/features/workspace/environmental/environmental.hooks.ts deleted file mode 100644 index 26511e7b9f..0000000000 --- a/apps/fishing-map/features/workspace/environmental/environmental.hooks.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { useCallback, useEffect } from 'react' -import { ckmeans, sample, mean, standardDeviation, min, max } from 'simple-statistics' -import { useSelector } from 'react-redux' -import { Feature, Geometry, GeoJsonProperties } from 'geojson' -import { - COLOR_RAMP_DEFAULT_NUM_STEPS, - HEATMAP_STATIC_PROPERTY_ID, - HeatmapLayerMeta, -} from '@globalfishingwatch/layer-composer' -import { MiniglobeBounds } from '@globalfishingwatch/ui-components' -import { filterFeaturesByBounds } from '@globalfishingwatch/data-transforms' -import { ChunkFeature, aggregateFeatures } from '@globalfishingwatch/features-aggregate' -import { DataviewConfig, DataviewType } from '@globalfishingwatch/api-types' -import { GeoJSONFeature } from '@globalfishingwatch/maplibre-gl' -import { useDataviewInstancesConnect } from 'features/workspace/workspace.hook' -import { selectActiveHeatmapEnvironmentalDataviews } from 'features/dataviews/selectors/dataviews.selectors' -import { - DataviewFeature, - areDataviewsFeatureLoaded, - useMapDataviewFeatures, -} from 'features/map/map-sources.hooks' -import { selectReportArea } from 'features/reports/reports.selectors' -import { useMapBounds } from 'features/map/map-bounds.hooks' -import { AreaGeometry } from 'features/areas/areas.slice' -import { filterByPolygon } from 'features/reports/reports-geo.utils' - -const filterVisibleValues = ( - rawData: number[], - config: DataviewConfig | undefined -) => { - if (!config?.minVisibleValue && !config?.maxVisibleValue) return rawData - return rawData.filter((d) => { - const matchesMin = config?.minVisibleValue !== undefined ? d >= config?.minVisibleValue : true - const matchesMax = config?.maxVisibleValue !== undefined ? d <= config?.maxVisibleValue : true - return matchesMin && matchesMax - }) -} -const getValues = ( - features: Feature[], - metadata: HeatmapLayerMeta | undefined -) => { - return metadata?.static - ? features.map((f) => f.properties?.[HEATMAP_STATIC_PROPERTY_ID] as number) - : aggregateFeatures( - features as GeoJSONFeature>[], - metadata as HeatmapLayerMeta - ) -} - -export const useEnvironmentalBreaksUpdate = () => { - const dataviews = useSelector(selectActiveHeatmapEnvironmentalDataviews) - const area = useSelector(selectReportArea) - const { bounds } = useMapBounds() - const dataviewFeatures = useMapDataviewFeatures(dataviews) - const sourcesLoaded = areDataviewsFeatureLoaded(dataviewFeatures) - const layersFilterHash = dataviews - .flatMap(({ config }) => `${config?.minVisibleValue}-${config?.maxVisibleValue}`) - .join(',') - const { upsertDataviewInstance } = useDataviewInstancesConnect() - const hasDataviewStats = dataviews.every((d) => d.config?.stats) - - const updateBreaksByViewportValues = useCallback( - (dataviewFeatures: DataviewFeature[], bounds: MiniglobeBounds) => { - const dataviewInstances = dataviewFeatures?.flatMap( - ({ features, chunksFeatures, dataviewsId, metadata }) => { - const resolvedFeatures = chunksFeatures?.[0]?.features || features || ({} as ChunkFeature) - if (resolvedFeatures && resolvedFeatures.length) { - const config = dataviews.find(({ id }) => dataviewsId.includes(id))?.config - const filteredFeatures = filterFeaturesByBounds(resolvedFeatures, bounds) - const rawData = getValues(filteredFeatures, metadata) - const data = filterVisibleValues(rawData, config) - if (data && data.length) { - const dataSampled = data.length > 1000 ? sample(data, 1000, Math.random) : data - // filter data to 2 standard deviations from mean to remove outliers - const meanValue = mean(dataSampled) - const standardDeviationValue = standardDeviation(dataSampled) - const upperCut = meanValue + standardDeviationValue * 2 - const lowerCut = meanValue - standardDeviationValue * 2 - const dataFiltered = dataSampled.filter((a) => a >= lowerCut && a <= upperCut) - const steps = Math.min(dataFiltered.length, COLOR_RAMP_DEFAULT_NUM_STEPS - 1) - // using ckmeans as jenks - const ck = ckmeans(dataFiltered, steps).map(([clusterFirst]) => - parseFloat(clusterFirst.toFixed(3)) - ) - // Needed to ensure there is the correct num of steps in areas where - // ck returns less than COLOR_RAMP_DEFAULT_NUM_STEPS - const ckWithAllSteps = [...new Array(COLOR_RAMP_DEFAULT_NUM_STEPS)].map((_, i) => { - return ck[i] || 0 - }) - let cleanBreaks = [] as number[] - ckWithAllSteps.forEach((k, i) => { - if (i >= 1) { - const cleanBreak = - k === 0 || k <= cleanBreaks?.[i - 1] ? cleanBreaks[i - 1] + 0.01 : k - cleanBreaks.push(cleanBreak) - } else { - cleanBreaks.push(k) - } - }) - return { - id: dataviewsId[0], - config: { - breaks: cleanBreaks, - }, - } - } - return [] - } - return [] - } - ) - if (dataviewInstances) { - upsertDataviewInstance(dataviewInstances) - } - }, - [dataviews, upsertDataviewInstance] - ) - - const updateStatsByArea = useCallback( - (dataviewFeatures: DataviewFeature[], geometry: AreaGeometry) => { - const dataviewInstances = dataviewFeatures?.flatMap( - ({ features, chunksFeatures, dataviewsId, metadata }) => { - const resolvedFeatures = chunksFeatures?.[0]?.features || features || ({} as ChunkFeature) - if (resolvedFeatures && resolvedFeatures.length && geometry) { - const config = dataviews.find(({ id }) => dataviewsId.includes(id))?.config - // TODO:deck review why this any typing is needed - const featuresInReportArea = filterByPolygon( - [resolvedFeatures] as any, - geometry, - 'point' - )[0] - const allFeaturesInReportArea = [ - ...(featuresInReportArea?.contained || []), - ...(featuresInReportArea?.overlapping || []), - ] - const values = getValues(allFeaturesInReportArea, metadata) - const visibleValues = filterVisibleValues(values, config) - if (!visibleValues.length) return [] - const areaStats = { - min: min(visibleValues), - mean: mean(visibleValues), - max: max(visibleValues), - } - return { - id: dataviewsId[0], - config: { - stats: areaStats, - }, - } - } - return [] - } - ) - if (dataviewInstances) { - upsertDataviewInstance(dataviewInstances) - } - }, - [dataviews, upsertDataviewInstance] - ) - - useEffect(() => { - if (sourcesLoaded) { - updateBreaksByViewportValues(dataviewFeatures, bounds) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sourcesLoaded, layersFilterHash]) - - useEffect(() => { - if (hasDataviewStats) { - upsertDataviewInstance(dataviews.map(({ id }) => ({ id, config: { stats: undefined } }))) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [area?.geometry]) - - useEffect(() => { - if (sourcesLoaded && area?.geometry && !hasDataviewStats) { - updateStatsByArea(dataviewFeatures, area.geometry) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [area?.geometry, sourcesLoaded, layersFilterHash, hasDataviewStats]) -} diff --git a/apps/fishing-map/features/workspace/events/events.hooks.ts b/apps/fishing-map/features/workspace/events/events.hooks.ts deleted file mode 100644 index c73ff4d76b..0000000000 --- a/apps/fishing-map/features/workspace/events/events.hooks.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { useCallback, useEffect } from 'react' -import { ckmeans } from 'simple-statistics' -import { MiniglobeBounds } from '@globalfishingwatch/ui-components' -import { filterFeaturesByBounds } from '@globalfishingwatch/data-transforms' -import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' -import { MAX_ZOOM_TO_CLUSTER_POINTS } from '@globalfishingwatch/layer-composer' -import { useDataviewInstancesConnect } from 'features/workspace/workspace.hook' -import { - DataviewFeature, - areDataviewsFeatureLoaded, - useMapDataviewFeatures, -} from 'features/map/map-sources.hooks' -import { useViewStateAtom } from 'features/map/map-viewport.hooks' -import { useMapBounds } from 'features/map/map-bounds.hooks' -import type { GeoJSONFeature } from '@globalfishingwatch/maplibre-gl' - -export const useEventsDynamicRamp = (dataview: UrlDataviewInstance) => { - const { bounds } = useMapBounds() - const { viewState } = useViewStateAtom() - const dataviewFeatures = useMapDataviewFeatures(dataview) - const sourcesLoaded = areDataviewsFeatureLoaded(dataviewFeatures) - const { upsertDataviewInstance } = useDataviewInstancesConnect() - - const updateBreaksByViewportValues = useCallback( - ({ features, dataviewsId } = {} as DataviewFeature, bounds: MiniglobeBounds) => { - const filteredFeatures = filterFeaturesByBounds(features, bounds) as GeoJSONFeature[] - if (filteredFeatures?.length > 0) { - const data = filteredFeatures.map((feature) => feature.properties?.count) - const steps = Math.min(data.length, 3) - // using ckmeans as jenks - const ck = ckmeans(data, steps).map(([clusterFirst]) => parseInt(clusterFirst)) - const max = data.reduce((acc, data) => (data > acc ? data : acc), 0) - upsertDataviewInstance({ - id: dataviewsId[0], - config: { - breaks: [ck[0], ck[0] === ck[1] ? ck[1] + 1 : ck[1], ck[1] === max ? max + 1 : max], - }, - }) - } - }, - [upsertDataviewInstance] - ) - - const roundZoom = Math.floor(viewState.zoom) - useEffect(() => { - const maxZoomCluster = dataview.config?.maxZoomCluster || MAX_ZOOM_TO_CLUSTER_POINTS - if (sourcesLoaded && roundZoom < maxZoomCluster) { - updateBreaksByViewportValues(dataviewFeatures[0], bounds) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [sourcesLoaded, roundZoom]) -} diff --git a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts index 09a5fa7725..f6c8e49c6d 100644 --- a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts +++ b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts @@ -4,14 +4,7 @@ import { DeckLayerInteractionFeature } from '../types' import { ExtendedFeature, InteractionEvent, InteractionEventCallback } from './types' import { filterUniqueFeatureInteraction, getExtendedFeatures } from './interaction-features.utils' -// Atom used to have all the layer instances loading state available -export type DeckLayerInteraction = { - latitude: number - longitude: number - features: DeckLayerInteractionFeature[] -} - -export const deckHoverInteractionAtom = atom({} as DeckLayerInteraction) +export const deckHoverInteractionAtom = atom({} as InteractionEvent) export const useMapHoverInteraction = () => { return useAtomValue(deckHoverInteractionAtom) @@ -24,7 +17,7 @@ export const useSetMapHoverInteraction = () => { export const useMapClick = (clickCallback: InteractionEventCallback) => { // const { updateFeatureState, cleanFeatureState } = useFeatureState(map) const onMapClick = useCallback( - (event: DeckLayerInteraction) => { + (event: InteractionEvent) => { if (!clickCallback) return const interactionEvent: InteractionEvent = { diff --git a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts index c59616c1db..55694fa728 100644 --- a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts +++ b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts @@ -6,7 +6,6 @@ import { } from '@globalfishingwatch/deck-layers' import { getUTCDate } from '@globalfishingwatch/data-transforms' import { VALUE_MULTIPLIER } from '@globalfishingwatch/fourwings-aggregate' -import { DeckLayerInteractionFeature } from '../types' import { ExtendedFeature } from './types' export const filterUniqueFeatureInteraction = (features: ExtendedFeature[]) => { @@ -24,7 +23,7 @@ export const filterUniqueFeatureInteraction = (features: ExtendedFeature[]) => { return filtered } -const getExtendedFeature = (feature: DeckLayerInteractionFeature): ExtendedFeature[] => { +const getExtendedFeature = (feature: ExtendedFeature): ExtendedFeature[] => { // const generatorType = feature.layer.metadata?.generatorType ?? null // const generatorId = feature.layer.metadata?.generatorId ?? null @@ -99,8 +98,6 @@ const getExtendedFeature = (feature: DeckLayerInteractionFeature): ExtendedFeatu { ...extendedFeature, datasetId: object.datasetId, - promoteId: object.promoteId, - generatorContextLayer: object?.layerId, geometry: object.geometry, }, ] @@ -109,7 +106,7 @@ const getExtendedFeature = (feature: DeckLayerInteractionFeature): ExtendedFeatu return [extendedFeature] } -export const getExtendedFeatures = (features: DeckLayerInteractionFeature[]): ExtendedFeature[] => { +export const getExtendedFeatures = (features: ExtendedFeature[]): ExtendedFeature[] => { // TODO: deck implement the stopPropagation feature // const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) // if (stopPropagationFeature) { diff --git a/libs/deck-layer-composer/src/interactions/types.ts b/libs/deck-layer-composer/src/interactions/types.ts index 6d7731c87c..6f9133a81d 100644 --- a/libs/deck-layer-composer/src/interactions/types.ts +++ b/libs/deck-layer-composer/src/interactions/types.ts @@ -3,28 +3,27 @@ import { ContextPickingObject, FourwingsPickingObject, } from '@globalfishingwatch/deck-layers' +import { DeckLayerInteractionFeature } from '../types' export type InteractionEventCallback = (event: InteractionEvent | null) => void -export type ExtendedFeature = FourwingsPickingObject & - ContextPickingObject & { - layerId: string - generatorContextLayer?: ContextLayerId | null - datasetId?: string - promoteId?: string - id: string - value: any - geometry?: any - stopPropagation?: boolean - uniqueFeatureInteraction?: boolean - unit?: string - // TODO:deck review if this is needed anywhere else - // tile: { - // x: number - // y: number - // z: number - // } - } +export type ExtendedFeature = DeckLayerInteractionFeature & { + layerId: string + datasetId?: string + promoteId?: string + id: string + value: any + geometry?: any + stopPropagation?: boolean + uniqueFeatureInteraction?: boolean + unit?: string + // TODO:deck review if this is needed anywhere else + // tile: { + // x: number + // y: number + // z: number + // } +} export type InteractionEvent = { type: 'click' | 'hover' diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 93a0de8e02..6ed29233b0 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -1,12 +1,16 @@ import { + ClusterLayer, + ClusterPickingObject, ContextLayer, ContextPickingInfo, + ContextPickingObject, FourwingsDeckSublayer, FourwingsLayer, FourwingsPickingInfo, FourwingsPickingObject, FourwingsTileLayerColorDomain, FourwingsTileLayerColorRange, + UserContextPickingObject, } from '@globalfishingwatch/deck-layers' export const DECK_LAYER_LIFECYCLE = { @@ -50,5 +54,6 @@ export interface DeckLegendBivariate extends DeckLegend { } export type DeckLayerInteractionFeature = - | (FourwingsPickingInfo & { layer: FourwingsLayer }) - | (ContextPickingInfo & { layer: ContextLayer }) + | (FourwingsPickingObject & { layer: FourwingsLayer }) + | ((ContextPickingObject | UserContextPickingObject) & { layer: ContextLayer }) + | (ClusterPickingObject & { layer: ClusterLayer }) diff --git a/libs/deck-layers/src/layers/context/ContextLayer.ts b/libs/deck-layers/src/layers/context/ContextLayer.ts index cac2cc5ffa..394ec00655 100644 --- a/libs/deck-layers/src/layers/context/ContextLayer.ts +++ b/libs/deck-layers/src/layers/context/ContextLayer.ts @@ -1,8 +1,9 @@ -import { CompositeLayer, Color, DefaultProps } from '@deck.gl/core' +import { CompositeLayer, Color, DefaultProps, PickingInfo } from '@deck.gl/core' import { GeoBoundingBox, TileLayer, TileLayerProps } from '@deck.gl/geo-layers' import { GeoJsonLayer } from '@deck.gl/layers' import { GeoJsonProperties } from 'geojson' import { PathStyleExtension } from '@deck.gl/extensions' +import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' import { COLOR_HIGHLIGHT_FILL, COLOR_HIGHLIGHT_LINE, @@ -21,6 +22,7 @@ import { ContextPickingInfo, ContextFeature, ContextLayerId, + ContextPickingObject, } from './context.types' import { getContextId, getContextLink, getContextValue } from './context.utils' @@ -56,7 +58,11 @@ export class ContextLayer extends CompositeLayer<_ContextLayerProps return EEZ_SETTLED_BOUNDARIES.includes(d.properties?.LINE_TYPE) ? [0, 0] : [8, 8] } - getPickingInfo = ({ info }: { info: ContextPickingInfo }): ContextPickingInfo => { + getPickingInfo = ({ + info, + }: { + info: PickingInfo + }): ContextPickingInfo => { const { idProperty, valueProperties } = this.props info.object = transformTileCoordsToWGS84( info.object as ContextFeature, diff --git a/libs/deck-layers/src/layers/context/context.types.ts b/libs/deck-layers/src/layers/context/context.types.ts index b43a060768..d499f2dee7 100644 --- a/libs/deck-layers/src/layers/context/context.types.ts +++ b/libs/deck-layers/src/layers/context/context.types.ts @@ -50,6 +50,10 @@ export type ContextFeature = Feature // TODO:deck create this type in the proper deck class layer export type UserContextFeature = Feature> & ContextFeatureProperties -export type ContextPickingObject = ContextFeature | UserContextFeature +export type ContextPickingObject = ContextFeature +export type UserContextPickingObject = UserContextFeature -export type ContextPickingInfo = PickingInfo +export type ContextPickingInfo = PickingInfo< + ContextPickingObject | UserContextPickingObject, + { tile?: Tile2DHeader } +> diff --git a/package.json b/package.json index 8264f73f0f..7ddbef8361 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "postinstall": "husky" }, "dependencies": { - "@deck.gl/core": "^9.0.3", - "@deck.gl/extensions": "^9.0.3", - "@deck.gl/geo-layers": "^9.0.3", - "@deck.gl/layers": "^9.0.3", - "@deck.gl/mesh-layers": "^9.0.3", - "@deck.gl/react": "^9.0.3", + "@deck.gl/core": "^9.0.6", + "@deck.gl/extensions": "^9.0.6", + "@deck.gl/geo-layers": "^9.0.6", + "@deck.gl/layers": "^9.0.6", + "@deck.gl/mesh-layers": "^9.0.6", + "@deck.gl/react": "^9.0.6", "@dnd-kit/core": "6.1.0", "@dnd-kit/modifiers": "7.0.0", "@dnd-kit/sortable": "8.0.0", diff --git a/yarn.lock b/yarn.lock index 13d9a3c273..f967f0f495 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1751,17 +1751,17 @@ __metadata: languageName: node linkType: hard -"@deck.gl/core@npm:^9.0.3": - version: 9.0.3 - resolution: "@deck.gl/core@npm:9.0.3" +"@deck.gl/core@npm:^9.0.6": + version: 9.0.6 + resolution: "@deck.gl/core@npm:9.0.6" dependencies: "@loaders.gl/core": "npm:^4.1.4" "@loaders.gl/images": "npm:^4.1.4" - "@luma.gl/constants": "npm:^9.0.8" - "@luma.gl/core": "npm:^9.0.8" - "@luma.gl/engine": "npm:^9.0.8" - "@luma.gl/shadertools": "npm:^9.0.8" - "@luma.gl/webgl": "npm:^9.0.8" + "@luma.gl/constants": "npm:^9.0.9" + "@luma.gl/core": "npm:^9.0.9" + "@luma.gl/engine": "npm:^9.0.9" + "@luma.gl/shadertools": "npm:^9.0.9" + "@luma.gl/webgl": "npm:^9.0.9" "@math.gl/core": "npm:^4.0.0" "@math.gl/sun": "npm:^4.0.0" "@math.gl/web-mercator": "npm:^4.0.0" @@ -1771,28 +1771,28 @@ __metadata: "@types/offscreencanvas": "npm:^2019.6.4" gl-matrix: "npm:^3.0.0" mjolnir.js: "npm:^2.7.0" - checksum: 10/7194c300e438bbfbbf1c1e8197f49d2b6e2fc0bef44106809fc8ecaf0511a866f7447a633f6d95bf6553b68d676a0876a27f87e9da7b8a3859511d48dca08c6e + checksum: 10/a47561940301d29cf9f0723904db44d8d486b07cc043ca0b6589e1f9fb7b8ea882a7eb57bd9799503df322368d09bc86725d5e66e9e946801e0a42eb80b1b2dc languageName: node linkType: hard -"@deck.gl/extensions@npm:^9.0.3": - version: 9.0.3 - resolution: "@deck.gl/extensions@npm:9.0.3" +"@deck.gl/extensions@npm:^9.0.6": + version: 9.0.6 + resolution: "@deck.gl/extensions@npm:9.0.6" dependencies: - "@luma.gl/constants": "npm:^9.0.8" - "@luma.gl/shadertools": "npm:^9.0.8" + "@luma.gl/constants": "npm:^9.0.9" + "@luma.gl/shadertools": "npm:^9.0.9" "@math.gl/core": "npm:^4.0.0" peerDependencies: "@deck.gl/core": ^9.0.0 "@luma.gl/core": ^9.0.0 "@luma.gl/engine": ^9.0.0 - checksum: 10/15afd2aa3ae4e2aa7937d316d469b9a84080915bfa9acb498322d251fb935e080d7f35436cb28315b993082769d56a9f76058ea3b15b7372ba3a0be53f3e7106 + checksum: 10/13c1baa76df19c5d5949b0669293f827108d02ad15ae2ccdcff00fb7118f9bc62bcc850bb20e4766d4e024da53fa12bb47bc2c6587d63ce9f9c99a881e9b8da2 languageName: node linkType: hard -"@deck.gl/geo-layers@npm:^9.0.3": - version: 9.0.3 - resolution: "@deck.gl/geo-layers@npm:9.0.3" +"@deck.gl/geo-layers@npm:^9.0.6": + version: 9.0.6 + resolution: "@deck.gl/geo-layers@npm:9.0.6" dependencies: "@loaders.gl/3d-tiles": "npm:^4.1.4" "@loaders.gl/gis": "npm:^4.1.4" @@ -1802,8 +1802,8 @@ __metadata: "@loaders.gl/terrain": "npm:^4.1.4" "@loaders.gl/tiles": "npm:^4.1.4" "@loaders.gl/wms": "npm:^4.1.4" - "@luma.gl/gltf": "npm:^9.0.8" - "@luma.gl/shadertools": "npm:^9.0.8" + "@luma.gl/gltf": "npm:^9.0.9" + "@luma.gl/shadertools": "npm:^9.0.9" "@math.gl/core": "npm:^4.0.0" "@math.gl/culling": "npm:^4.0.0" "@math.gl/web-mercator": "npm:^4.0.0" @@ -1818,13 +1818,13 @@ __metadata: "@loaders.gl/core": ^4.1.0 "@luma.gl/core": ^9.0.0 "@luma.gl/engine": ^9.0.0 - checksum: 10/17833ea330b965c0af858482f0503326610073b81d66f2a97567330dbbb56d21db4a17cf047b070d3df25d03cadcf7211d3c25ed8964f1ef45101207b54cc4da + checksum: 10/eb7fb20c5bce9e3bdde62123ac20911dfc94242ee5429d0ddef5ba5d82ef6c23529ca0f4428a900f050431c84faef69b51c6301fe2a5288a2831825ff9687814 languageName: node linkType: hard -"@deck.gl/layers@npm:^9.0.3": - version: 9.0.3 - resolution: "@deck.gl/layers@npm:9.0.3" +"@deck.gl/layers@npm:^9.0.6": + version: 9.0.6 + resolution: "@deck.gl/layers@npm:9.0.6" dependencies: "@loaders.gl/images": "npm:^4.1.4" "@loaders.gl/schema": "npm:^4.1.4" @@ -1838,33 +1838,33 @@ __metadata: "@loaders.gl/core": ^4.1.0 "@luma.gl/core": ^9.0.0 "@luma.gl/engine": ^9.0.0 - checksum: 10/2ad0507f90fdac2ff2bf7e9afc60801278e1b40febc8dd6301e0601be22fdf379d72e15cd8d5f1dca5a6e5020e6588044f2a071cfe7d4646face9b12588584eb + checksum: 10/430d042f80a4d00b2e484efd09bd5a74888c56a39aaf1fc15a6d952e930740e73c04146916d3657aa75dff0230c51d32eff77de5c95f909b73180ebd0a2ea542 languageName: node linkType: hard -"@deck.gl/mesh-layers@npm:^9.0.3": - version: 9.0.3 - resolution: "@deck.gl/mesh-layers@npm:9.0.3" +"@deck.gl/mesh-layers@npm:^9.0.6": + version: 9.0.6 + resolution: "@deck.gl/mesh-layers@npm:9.0.6" dependencies: "@loaders.gl/gltf": "npm:^4.1.4" - "@luma.gl/gltf": "npm:^9.0.8" - "@luma.gl/shadertools": "npm:^9.0.8" + "@luma.gl/gltf": "npm:^9.0.9" + "@luma.gl/shadertools": "npm:^9.0.9" peerDependencies: "@deck.gl/core": ^9.0.0 "@luma.gl/core": ^9.0.0 "@luma.gl/engine": ^9.0.0 - checksum: 10/8256ac8feff044e93bcdee6aba4fef6aefe45cec36cef0550b100821265b0e96b32bf1ee0e695a1fd630fc0536a38094d0dd3cff429ef638a0bc6f978c8b8664 + checksum: 10/45a4f7cdff8ad73a88e34aa664e03c3d81a97431874f263d97286ca9d93ecb30f346c0e93dca07d59c0fe6798f433b374bfc8c79b83ba8dcfef36828fa54e9f3 languageName: node linkType: hard -"@deck.gl/react@npm:^9.0.3": - version: 9.0.3 - resolution: "@deck.gl/react@npm:9.0.3" +"@deck.gl/react@npm:^9.0.6": + version: 9.0.6 + resolution: "@deck.gl/react@npm:9.0.6" peerDependencies: "@deck.gl/core": ^9.0.0 react: ">=16.3.0" react-dom: ">=16.3.0" - checksum: 10/bcbb294cf728358a4be9676bed1e9fd922974a974a7fbdfe727ac2a22cbec697f275d5c0a8ae1f79e16d0dbc81a4b33eedb10ccb2350bf0a78704b9bbf0c03c6 + checksum: 10/b8a15ba25fa1b490498948f7554c61fc05aa3028c44222b7928d0209b8a5df1e76561f3408041c74e7c9d09fca031a99e3d3bf5001d37142e5e6dfa39c284cb1 languageName: node linkType: hard @@ -2410,12 +2410,12 @@ __metadata: version: 0.0.0-use.local resolution: "@globalfishingwatch/root@workspace:." dependencies: - "@deck.gl/core": "npm:^9.0.3" - "@deck.gl/extensions": "npm:^9.0.3" - "@deck.gl/geo-layers": "npm:^9.0.3" - "@deck.gl/layers": "npm:^9.0.3" - "@deck.gl/mesh-layers": "npm:^9.0.3" - "@deck.gl/react": "npm:^9.0.3" + "@deck.gl/core": "npm:^9.0.6" + "@deck.gl/extensions": "npm:^9.0.6" + "@deck.gl/geo-layers": "npm:^9.0.6" + "@deck.gl/layers": "npm:^9.0.6" + "@deck.gl/mesh-layers": "npm:^9.0.6" + "@deck.gl/react": "npm:^9.0.6" "@dnd-kit/core": "npm:6.1.0" "@dnd-kit/modifiers": "npm:7.0.0" "@dnd-kit/sortable": "npm:8.0.0" @@ -3284,14 +3284,14 @@ __metadata: languageName: node linkType: hard -"@luma.gl/constants@npm:9.0.9, @luma.gl/constants@npm:^9.0.8": +"@luma.gl/constants@npm:9.0.9, @luma.gl/constants@npm:^9.0.9": version: 9.0.9 resolution: "@luma.gl/constants@npm:9.0.9" checksum: 10/42d263929be89e97520631b502722e537d7a85092b03f4d061f0def3474a9f6d26d9a298012ab382f1cebec900b6bd17ed755be9124069deb9192f83f2ed5915 languageName: node linkType: hard -"@luma.gl/core@npm:^9.0.8": +"@luma.gl/core@npm:^9.0.9": version: 9.0.9 resolution: "@luma.gl/core@npm:9.0.9" dependencies: @@ -3304,7 +3304,7 @@ __metadata: languageName: node linkType: hard -"@luma.gl/engine@npm:^9.0.8": +"@luma.gl/engine@npm:^9.0.9": version: 9.0.9 resolution: "@luma.gl/engine@npm:9.0.9" dependencies: @@ -3318,7 +3318,7 @@ __metadata: languageName: node linkType: hard -"@luma.gl/gltf@npm:^9.0.8": +"@luma.gl/gltf@npm:^9.0.9": version: 9.0.9 resolution: "@luma.gl/gltf@npm:9.0.9" dependencies: @@ -3332,7 +3332,7 @@ __metadata: languageName: node linkType: hard -"@luma.gl/shadertools@npm:9.0.9, @luma.gl/shadertools@npm:^9.0.8": +"@luma.gl/shadertools@npm:9.0.9, @luma.gl/shadertools@npm:^9.0.9": version: 9.0.9 resolution: "@luma.gl/shadertools@npm:9.0.9" dependencies: @@ -3344,7 +3344,7 @@ __metadata: languageName: node linkType: hard -"@luma.gl/webgl@npm:^9.0.8": +"@luma.gl/webgl@npm:^9.0.9": version: 9.0.9 resolution: "@luma.gl/webgl@npm:9.0.9" dependencies: From a33122b00f02a06691228ea3972b6d1c5ad82dad Mon Sep 17 00:00:00 2001 From: j8seangel Date: Mon, 8 Apr 2024 20:29:31 +0200 Subject: [PATCH 14/43] cluster layers interaction --- libs/deck-layers/src/index.ts | 2 +- .../src/layers/cluster/ClusterLayer.ts | 43 ++++++++----------- .../src/layers/cluster/cluster.types.ts | 33 ++++++++++++++ libs/deck-layers/src/layers/cluster/index.ts | 2 + 4 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 libs/deck-layers/src/layers/cluster/cluster.types.ts create mode 100644 libs/deck-layers/src/layers/cluster/index.ts diff --git a/libs/deck-layers/src/index.ts b/libs/deck-layers/src/index.ts index de6abea69e..12fb344232 100644 --- a/libs/deck-layers/src/index.ts +++ b/libs/deck-layers/src/index.ts @@ -1,5 +1,5 @@ export * from './layers/basemap/BasemapLayer' -export * from './layers/cluster/ClusterLayer' +export * from './layers/cluster' export * from './layers/context' export * from './layers/fourwings' export * from './layers/rulers/RulersLayer' diff --git a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts index 09f66ce83e..7a2a1cace4 100644 --- a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts +++ b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts @@ -1,38 +1,16 @@ -import { CompositeLayer, DefaultProps, LayerProps } from '@deck.gl/core' +import { CompositeLayer, DefaultProps, GetPickingInfoParams, LayerProps } from '@deck.gl/core' import { MVTLayer, TileLayerProps } from '@deck.gl/geo-layers' -import { Feature, Point } from 'geojson' import { stringify } from 'qs' import { GFWAPI } from '@globalfishingwatch/api-client' import { LayerGroup, getLayerGroupOffset, hexToDeckColor } from '../../utils' - -type EventType = 'encounter' | 'gap' | 'port_visit' - -export type ClusterLayerProps = { - color: string - datasetId: string - end: string - eventType?: EventType - id: string - maxClusterZoom?: number - start: string - tilesUrl: string - visible: boolean -} - -type ClusterFeatureProps = { - count: number - event_id: string - expansionZoom: number -} - -type ClusterFeature = Feature +import { ClusterEventType, ClusterLayerProps, ClusterPickingObject } from './cluster.types' const defaultProps: DefaultProps = { eventType: 'encounter', maxClusterZoom: 4, } -const ICON_MAPPING: Record = { +const ICON_MAPPING: Record = { encounter: { x: 0, y: 0, width: 36, height: 36, mask: true }, gap: { x: 40, y: 0, width: 36, height: 36, mask: true }, port_visit: { x: 80, y: 0, width: 36, height: 36, mask: true }, @@ -42,6 +20,20 @@ export class ClusterLayer extends CompositeLayer) => { + // const { object } = info + // if (object) { + // return { + // ...info, + // object: { + // ...object, + // id: object.properties.event_id, + // }, + // } + // } + return info + } + renderLayers() { const baseUrl = GFWAPI.generateUrl(this.props.tilesUrl as string, { absolute: true }) const params = { @@ -55,6 +47,7 @@ export class ClusterLayer extends CompositeLayer getLayerGroupOffset(LayerGroup.Cluster, params), getFillColor: color, getIconColor: color, diff --git a/libs/deck-layers/src/layers/cluster/cluster.types.ts b/libs/deck-layers/src/layers/cluster/cluster.types.ts new file mode 100644 index 0000000000..811f512f17 --- /dev/null +++ b/libs/deck-layers/src/layers/cluster/cluster.types.ts @@ -0,0 +1,33 @@ +import { PickingInfo } from '@deck.gl/core' +import { Feature, Polygon, MultiPolygon, Geometry, Point } from 'geojson' +import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' +import { EventTypes } from '@globalfishingwatch/api-types' + +export type ClusterEventType = + | `${EventTypes.Encounter}` + | `${EventTypes.Gap}` + | `${EventTypes.Port}` + +export type ClusterLayerProps = { + color: string + datasetId: string + end: string + eventType?: ClusterEventType + id: string + maxClusterZoom?: number + start: string + tilesUrl: string + visible: boolean +} + +type ClusterFeatureProps = { + count: number + event_id: string + expansionZoom: number + lat: number + lon: number +} + +export type ClusterPickingObject = Feature + +export type ClusterPickingInfo = PickingInfo diff --git a/libs/deck-layers/src/layers/cluster/index.ts b/libs/deck-layers/src/layers/cluster/index.ts new file mode 100644 index 0000000000..c21daaddb3 --- /dev/null +++ b/libs/deck-layers/src/layers/cluster/index.ts @@ -0,0 +1,2 @@ +export * from './ClusterLayer' +export * from './cluster.types' From bd50446534047107dfb4dd637711d436f045e2ae Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 9 Apr 2024 09:39:09 +0200 Subject: [PATCH 15/43] add category in picking info --- .../src/resolvers/basemap.ts | 1 + .../src/resolvers/clusters.ts | 1 + libs/deck-layers/src/index.ts | 4 +-- .../src/layers/basemap/BasemapLayer.ts | 16 +++------- .../src/layers/basemap/basemap.types.ts | 9 ++++++ libs/deck-layers/src/layers/basemap/index.ts | 2 ++ .../src/layers/cluster/ClusterLayer.ts | 25 ++++++++-------- .../src/layers/cluster/cluster.types.ts | 10 +++++-- .../src/layers/context/context.types.ts | 2 +- .../src/layers/rulers/RulersLayer.ts | 21 ++++++++------ libs/deck-layers/src/layers/rulers/index.ts | 1 + .../src/layers/rulers/rulers.config.ts | 3 -- .../src/layers/rulers/rulers.types.ts | 27 +++++++++++++++++ .../src/layers/rulers/rulers.utils.ts | 2 +- libs/deck-layers/src/types.ts | 29 +++++-------------- 15 files changed, 87 insertions(+), 66 deletions(-) create mode 100644 libs/deck-layers/src/layers/basemap/basemap.types.ts create mode 100644 libs/deck-layers/src/layers/basemap/index.ts create mode 100644 libs/deck-layers/src/layers/rulers/index.ts delete mode 100644 libs/deck-layers/src/layers/rulers/rulers.config.ts create mode 100644 libs/deck-layers/src/layers/rulers/rulers.types.ts diff --git a/libs/deck-layer-composer/src/resolvers/basemap.ts b/libs/deck-layer-composer/src/resolvers/basemap.ts index 28c596ab73..161cc6e01c 100644 --- a/libs/deck-layer-composer/src/resolvers/basemap.ts +++ b/libs/deck-layer-composer/src/resolvers/basemap.ts @@ -4,6 +4,7 @@ import { BaseMapLayerProps, BasemapType } from '@globalfishingwatch/deck-layers' export function resolveDeckBasemapLayerProps(dataview: DataviewInstance): BaseMapLayerProps { return { id: dataview.id, + category: dataview.category!, visible: dataview.config?.visible || true, basemap: (dataview.config?.basemap as BasemapType) || BasemapType.Default, } diff --git a/libs/deck-layer-composer/src/resolvers/clusters.ts b/libs/deck-layer-composer/src/resolvers/clusters.ts index 194fa725c8..594cfdf7ef 100644 --- a/libs/deck-layer-composer/src/resolvers/clusters.ts +++ b/libs/deck-layer-composer/src/resolvers/clusters.ts @@ -13,6 +13,7 @@ export const resolveDeckClusterLayerProps = ( return { id: dataview.id, + category: dataview.category!, datasetId: dataset?.id || '', color: dataview.config?.color || '', start: start, diff --git a/libs/deck-layers/src/index.ts b/libs/deck-layers/src/index.ts index 12fb344232..acddfb812d 100644 --- a/libs/deck-layers/src/index.ts +++ b/libs/deck-layers/src/index.ts @@ -1,8 +1,8 @@ -export * from './layers/basemap/BasemapLayer' +export * from './layers/basemap' export * from './layers/cluster' export * from './layers/context' export * from './layers/fourwings' -export * from './layers/rulers/RulersLayer' +export * from './layers/rulers' export * from './layers/vessel/VesselLayer' export * from './types' export * from './utils' diff --git a/libs/deck-layers/src/layers/basemap/BasemapLayer.ts b/libs/deck-layers/src/layers/basemap/BasemapLayer.ts index 92855c3faf..0706c32452 100644 --- a/libs/deck-layers/src/layers/basemap/BasemapLayer.ts +++ b/libs/deck-layers/src/layers/basemap/BasemapLayer.ts @@ -1,27 +1,20 @@ import { BitmapLayer } from '@deck.gl/layers' import { CompositeLayer, LayerContext } from '@deck.gl/core' -import { TileLayer as TileLayerWrongTyping } from '@deck.gl/geo-layers' +import { TileLayer } from '@deck.gl/geo-layers' import { MVTLayer, MVTLayerProps } from '@deck.gl/geo-layers' import { LayerGroup, getLayerGroupOffset } from '../../utils' -import { BasemapType } from '../../types' +import { _BasemapLayerProps, BasemapType } from './basemap.types' -const TileLayer = TileLayerWrongTyping as any +export type BaseMapLayerProps = Omit & _BasemapLayerProps -export type BasemapLayerOwnProps = { basemap: BasemapType } -export type BaseMapLayerProps = Omit & BasemapLayerOwnProps export class BaseMapLayer extends CompositeLayer { static layerName = 'ContextLayer' static defaultProps = { basemap: BasemapType.Default, } - layers: (typeof TileLayer | MVTLayer)[] = [] - initializeState(context: LayerContext): void { super.initializeState(context) - this.state = { - loaded: false, - } } _getBathimetryLayer() { @@ -93,7 +86,6 @@ export class BaseMapLayer extends CompositeLayer { } renderLayers() { - this.layers = this._getBasemap() - return this.layers + return this._getBasemap() } } diff --git a/libs/deck-layers/src/layers/basemap/basemap.types.ts b/libs/deck-layers/src/layers/basemap/basemap.types.ts new file mode 100644 index 0000000000..84d8ab5d42 --- /dev/null +++ b/libs/deck-layers/src/layers/basemap/basemap.types.ts @@ -0,0 +1,9 @@ +import { BaseLayerProps } from '../../types' + +export enum BasemapType { + Satellite = 'satellite', + Default = 'basemap_default', + Labels = 'basemap_labels', +} + +export type _BasemapLayerProps = BaseLayerProps & { basemap: BasemapType } diff --git a/libs/deck-layers/src/layers/basemap/index.ts b/libs/deck-layers/src/layers/basemap/index.ts new file mode 100644 index 0000000000..397019fd08 --- /dev/null +++ b/libs/deck-layers/src/layers/basemap/index.ts @@ -0,0 +1,2 @@ +export * from './basemap.types' +export * from './BasemapLayer' diff --git a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts index 7a2a1cace4..1135f5ae9f 100644 --- a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts +++ b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts @@ -1,9 +1,14 @@ -import { CompositeLayer, DefaultProps, GetPickingInfoParams, LayerProps } from '@deck.gl/core' +import { CompositeLayer, DefaultProps, LayerProps } from '@deck.gl/core' import { MVTLayer, TileLayerProps } from '@deck.gl/geo-layers' import { stringify } from 'qs' import { GFWAPI } from '@globalfishingwatch/api-client' import { LayerGroup, getLayerGroupOffset, hexToDeckColor } from '../../utils' -import { ClusterEventType, ClusterLayerProps, ClusterPickingObject } from './cluster.types' +import { + ClusterEventType, + ClusterFeature, + ClusterLayerProps, + ClusterPickingInfo, +} from './cluster.types' const defaultProps: DefaultProps = { eventType: 'encounter', @@ -20,17 +25,11 @@ export class ClusterLayer extends CompositeLayer) => { - // const { object } = info - // if (object) { - // return { - // ...info, - // object: { - // ...object, - // id: object.properties.event_id, - // }, - // } - // } + getPickingInfo = ({ info }: { info: ClusterPickingInfo }) => { + let { object } = info + if (object) { + object.category = this.props.category + } return info } diff --git a/libs/deck-layers/src/layers/cluster/cluster.types.ts b/libs/deck-layers/src/layers/cluster/cluster.types.ts index 811f512f17..cfc8ed1e49 100644 --- a/libs/deck-layers/src/layers/cluster/cluster.types.ts +++ b/libs/deck-layers/src/layers/cluster/cluster.types.ts @@ -2,13 +2,14 @@ import { PickingInfo } from '@deck.gl/core' import { Feature, Polygon, MultiPolygon, Geometry, Point } from 'geojson' import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' import { EventTypes } from '@globalfishingwatch/api-types' +import { BaseLayerProps, BasePickingInfo } from '../../types' export type ClusterEventType = | `${EventTypes.Encounter}` | `${EventTypes.Gap}` | `${EventTypes.Port}` -export type ClusterLayerProps = { +export type ClusterLayerProps = BaseLayerProps & { color: string datasetId: string end: string @@ -28,6 +29,9 @@ type ClusterFeatureProps = { lon: number } -export type ClusterPickingObject = Feature +export type ClusterFeature = Feature -export type ClusterPickingInfo = PickingInfo +export type ClusterPickingInfo = PickingInfo< + ClusterFeature & BasePickingInfo, + { tile?: Tile2DHeader } +> diff --git a/libs/deck-layers/src/layers/context/context.types.ts b/libs/deck-layers/src/layers/context/context.types.ts index d499f2dee7..44ebb04a30 100644 --- a/libs/deck-layers/src/layers/context/context.types.ts +++ b/libs/deck-layers/src/layers/context/context.types.ts @@ -41,7 +41,7 @@ export type ContextFeatureProperties = { value: string | number layerId: ContextLayerId datasetId: string - category: string + category: DataviewCategory link?: string } export type ContextFeature = Feature> & diff --git a/libs/deck-layers/src/layers/rulers/RulersLayer.ts b/libs/deck-layers/src/layers/rulers/RulersLayer.ts index 233e0b603c..88c4b2b25d 100644 --- a/libs/deck-layers/src/layers/rulers/RulersLayer.ts +++ b/libs/deck-layers/src/layers/rulers/RulersLayer.ts @@ -1,19 +1,21 @@ import { GeoJsonLayerProps, GeoJsonLayer } from '@deck.gl/layers' -import { CompositeLayer } from '@deck.gl/core' +import { Color, CompositeLayer, DefaultProps } from '@deck.gl/core' import { PathStyleExtension } from '@deck.gl/extensions' import { Feature, Point } from '@turf/turf' import { LayerGroup, getLayerGroupOffset } from '../../utils' -import { RulerData, RulerPointProperties } from '../../types' +import { RulersLayerProps, RulerData, RulerPointProperties } from './rulers.types' import { getGreatCircleMultiLine, getRulerCenterPointWithLabel, getRulerStartAndEndPoints, hasRulerStartAndEnd, } from './rulers.utils' -import { COLOR } from './rulers.config' -type RulersLayerProps = GeoJsonLayerProps & { - rulers: RulerData[] +const RULERS_COLOR = [255, 170, 0, 255] as Color + +const defaultProps: DefaultProps = { + rulers: [], + color: RULERS_COLOR, } const getFeaturesFromRulers = (rulers: RulerData[]) => { @@ -25,8 +27,9 @@ const getFeaturesFromRulers = (rulers: RulerData[]) => { export class RulersLayer extends CompositeLayer { static layerName = 'RulersLayer' + static defaultProps = defaultProps renderLayers() { - const { rulers, visible } = this.props + const { rulers, color, visible } = this.props if (!hasRulerStartAndEnd(rulers)) return null @@ -44,8 +47,8 @@ export class RulersLayer extends CompositeLayer { stroked: true, filled: true, visible, - getFillColor: COLOR, - getLineColor: COLOR, + getFillColor: color, + getLineColor: color, pointType: 'circle+text', pointRadiusUnits: 'pixels', getPointRadius: (d: Feature) => @@ -53,7 +56,7 @@ export class RulersLayer extends CompositeLayer { getText: (d: Feature) => d.properties?.text, getTextAngle: (d: Feature) => d.properties?.bearing, getTextSize: 12, - getTextColor: COLOR, + getTextColor: color, getTextAlignmentBaseline: 'bottom', lineWidthMinPixels: 2, getDashArray: [4, 2], diff --git a/libs/deck-layers/src/layers/rulers/index.ts b/libs/deck-layers/src/layers/rulers/index.ts new file mode 100644 index 0000000000..30ff6f21ea --- /dev/null +++ b/libs/deck-layers/src/layers/rulers/index.ts @@ -0,0 +1 @@ +export * from './RulersLayer' diff --git a/libs/deck-layers/src/layers/rulers/rulers.config.ts b/libs/deck-layers/src/layers/rulers/rulers.config.ts deleted file mode 100644 index deff107851..0000000000 --- a/libs/deck-layers/src/layers/rulers/rulers.config.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { Color } from '@deck.gl/core' - -export const COLOR = [255, 170, 0, 255] as Color diff --git a/libs/deck-layers/src/layers/rulers/rulers.types.ts b/libs/deck-layers/src/layers/rulers/rulers.types.ts new file mode 100644 index 0000000000..e2cbdf3626 --- /dev/null +++ b/libs/deck-layers/src/layers/rulers/rulers.types.ts @@ -0,0 +1,27 @@ +import { Color } from '@deck.gl/core' +import { GeoJsonLayerProps } from '@deck.gl/layers' +import { BaseLayerProps } from '../../types' + +export type RulerPointProperties = { + id?: number + order: 'start' | 'center' | 'end' + bearing?: number + text?: string +} + +export type RulerData = { + id: number + start: { + latitude: number + longitude: number + } + end: { + latitude: number + longitude: number + } +} + +export type RulersLayerProps = GeoJsonLayerProps & { + rulers: RulerData[] + color?: Color +} diff --git a/libs/deck-layers/src/layers/rulers/rulers.utils.ts b/libs/deck-layers/src/layers/rulers/rulers.utils.ts index 1b58829d3a..5986923eb1 100644 --- a/libs/deck-layers/src/layers/rulers/rulers.utils.ts +++ b/libs/deck-layers/src/layers/rulers/rulers.utils.ts @@ -3,7 +3,7 @@ import length from '@turf/length' import { Coord, Feature, LineString, Point, Position, point } from '@turf/helpers' import { rhumbBearing } from '@turf/turf' import { MultiLineString } from 'geojson' -import { RulerData, RulerPointProperties } from '../../types' +import { RulerData, RulerPointProperties } from './rulers.types' export const getRulerCoordsPairs = ( ruler: RulerData diff --git a/libs/deck-layers/src/types.ts b/libs/deck-layers/src/types.ts index 6d2ae4e81e..3eb645902e 100644 --- a/libs/deck-layers/src/types.ts +++ b/libs/deck-layers/src/types.ts @@ -1,14 +1,17 @@ import type { Layer } from '@deck.gl/core' +import { DataviewCategory } from '@globalfishingwatch/api-types' import type { BaseMapLayer } from './layers/basemap/BasemapLayer' import type { ContextLayer } from './layers/context/ContextLayer' import type { FourwingsLayer } from './layers/fourwings/FourwingsLayer' import type { VesselLayer } from './layers/vessel/VesselLayer' import type { RulersLayer } from './layers/rulers/RulersLayer' -export enum BasemapType { - Satellite = 'satellite', - Default = 'basemap_default', - Labels = 'basemap_labels', +export type BaseLayerProps = { + category: DataviewCategory +} + +export type BasePickingInfo = { + category: DataviewCategory } export type AnyDeckLayer = @@ -20,21 +23,3 @@ export type AnyDeckLayer = | RulersLayer export type LayerWithIndependentSublayersLoadState = VesselLayer - -export type RulerPointProperties = { - id?: number - order: 'start' | 'center' | 'end' - bearing?: number - text?: string -} -export type RulerData = { - id: number - start: { - latitude: number - longitude: number - } - end: { - latitude: number - longitude: number - } -} From 9e8e8cf8565f9f79a01c4021a358b5c94bdb00b5 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 9 Apr 2024 09:39:43 +0200 Subject: [PATCH 16/43] fix encounter events tooltip --- .../features/map/map-interactions.hooks.ts | 7 ++++--- .../features/map/popups/PopupWrapper.tsx | 20 +++++++++---------- libs/deck-layer-composer/src/types.ts | 5 ++--- .../src/layers/cluster/cluster.types.ts | 6 ++---- 4 files changed, 18 insertions(+), 20 deletions(-) diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index cc3a58c0be..88d56c8aed 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -10,7 +10,7 @@ import { useMapClick, InteractionEvent, } from '@globalfishingwatch/deck-layer-composer' -import { ClusterLayer, ClusterPickingObject } from '@globalfishingwatch/deck-layers' +import { ClusterPickingObject } from '@globalfishingwatch/deck-layers' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' import { @@ -117,8 +117,9 @@ export const useClickedEventConnect = () => { } const clusterFeature = event?.features?.find( - (f) => f.layer instanceof ClusterLayer + (f) => f.category === DataviewCategory.Events ) as ClusterPickingObject + if (clusterFeature?.properties?.expansionZoom) { const { count, expansionZoom, lat, lon } = clusterFeature.properties if (count > 1) { @@ -165,7 +166,7 @@ export const useClickedEventConnect = () => { // get temporal grid clicked features and order them by sublayerindex const fishingActivityFeatures = event.features.filter((feature) => { - if (feature?.sublayers.every((sublayer) => !sublayer.visible)) { + if (feature?.sublayers?.every((sublayer) => !sublayer.visible)) { return false } return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes(feature.category) diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index be383d7102..9679ce0067 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -68,7 +68,8 @@ function PopupWrapper({ const featureByCategory = groupBy( interaction.features - // .map((feature) => feature.object) + // Needed to create a new array and not muting with sort + .map((feature) => feature) .sort( (a, b) => POPUP_CATEGORY_ORDER.indexOf(a?.category as DataviewCategory) - @@ -125,15 +126,14 @@ function PopupWrapper({ showFeaturesDetails={type === 'click'} /> )) - // TODO: deck restore this popup - // case DataviewCategory.Events: - // return ( - // - // ) + case DataviewCategory.Events: + return ( + + ) // TODO: deck restore this popup // case DataviewCategory.Environment: { // const contextEnvironmentalFeatures = features.filter( diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 6ed29233b0..9e1bd732db 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -2,11 +2,9 @@ import { ClusterLayer, ClusterPickingObject, ContextLayer, - ContextPickingInfo, ContextPickingObject, FourwingsDeckSublayer, FourwingsLayer, - FourwingsPickingInfo, FourwingsPickingObject, FourwingsTileLayerColorDomain, FourwingsTileLayerColorRange, @@ -55,5 +53,6 @@ export interface DeckLegendBivariate extends DeckLegend { export type DeckLayerInteractionFeature = | (FourwingsPickingObject & { layer: FourwingsLayer }) - | ((ContextPickingObject | UserContextPickingObject) & { layer: ContextLayer }) + | (ContextPickingObject & { layer: ContextLayer }) + | (UserContextPickingObject & { layer: ContextLayer }) | (ClusterPickingObject & { layer: ClusterLayer }) diff --git a/libs/deck-layers/src/layers/cluster/cluster.types.ts b/libs/deck-layers/src/layers/cluster/cluster.types.ts index cfc8ed1e49..95475a3641 100644 --- a/libs/deck-layers/src/layers/cluster/cluster.types.ts +++ b/libs/deck-layers/src/layers/cluster/cluster.types.ts @@ -30,8 +30,6 @@ type ClusterFeatureProps = { } export type ClusterFeature = Feature +export type ClusterPickingObject = ClusterFeature & BasePickingInfo -export type ClusterPickingInfo = PickingInfo< - ClusterFeature & BasePickingInfo, - { tile?: Tile2DHeader } -> +export type ClusterPickingInfo = PickingInfo From 31a252089857f12fef78177f5f0eb1ba3ffb4e76 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 9 Apr 2024 12:07:36 +0200 Subject: [PATCH 17/43] remove intermediate temporalgrid parse --- .../features/map/map-interactions.hooks.ts | 27 +++++---- apps/fishing-map/features/map/map.hooks.ts | 7 ++- apps/fishing-map/features/map/map.slice.ts | 58 +++++++++++------- .../features/map/popups/ActivityLayers.tsx | 4 +- .../features/map/popups/PopupWrapper.tsx | 10 +++- .../features/map/popups/VesselsTable.tsx | 38 +++++++----- .../deck-layers-interaction.hooks.ts | 20 +++---- .../src/interactions/index.ts | 2 +- .../interaction-features.utils.ts | 60 +++++++------------ .../src/interactions/types.ts | 25 +------- libs/deck-layer-composer/src/types.ts | 16 +++-- .../layers/fourwings/FourwingsHeatmapLayer.ts | 15 +++-- .../src/layers/fourwings/fourwings.types.ts | 3 + 13 files changed, 145 insertions(+), 140 deletions(-) diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 88d56c8aed..5d13f50d77 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -9,8 +9,10 @@ import { useSetMapHoverInteraction, useMapClick, InteractionEvent, + DeckLayerInteractionPickingInfo, + parseDeckPickingInfoToFeatures, } from '@globalfishingwatch/deck-layer-composer' -import { ClusterPickingObject } from '@globalfishingwatch/deck-layers' +import { ClusterPickingObject, FourwingsPickingObject } from '@globalfishingwatch/deck-layers' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' import { @@ -165,17 +167,19 @@ export const useClickedEventConnect = () => { dispatch(setClickedEvent(event as SliceInteractionEvent)) // get temporal grid clicked features and order them by sublayerindex - const fishingActivityFeatures = event.features.filter((feature) => { - if (feature?.sublayers?.every((sublayer) => !sublayer.visible)) { - return false + const fishingActivityFeatures = (event.features as FourwingsPickingObject[]).filter( + (feature) => { + if (feature?.sublayers?.every((sublayer) => !sublayer.visible)) { + return false + } + return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes(feature.category) } - return SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION.includes(feature.category) - }) + ) if (fishingActivityFeatures?.length) { dispatch(setHintDismissed('clickingOnAGridCellToShowVessels')) const activityProperties = fishingActivityFeatures.map((feature) => - feature.temporalgrid?.sublayerInteractionType === 'detections' ? 'detections' : 'hours' + feature.category === 'detections' ? 'detections' : 'hours' ) fishingPromiseRef.current = dispatch( fetchFishingActivityInteractionThunk({ fishingActivityFeatures, activityProperties }) @@ -201,7 +205,7 @@ export const useClickedEventConnect = () => { } } -const defaultEmptyFeatures = [] as PickingInfo[] +const defaultEmptyFeatures = [] as DeckLayerInteractionPickingInfo[] export const useMapMouseHover = (style?: ExtendedStyle) => { const map = useDeckMap() const setMapHoverFeatures = useSetMapHoverInteraction() @@ -234,7 +238,7 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { x: info.x, y: info.y, radius: 0, - }) + }) as DeckLayerInteractionPickingInfo[] } catch (e) { console.warn(e) } @@ -358,16 +362,17 @@ export const useMapMouseClick = () => { x: info.x, y: info.y, radius: 0, - }) + }) as DeckLayerInteractionPickingInfo[] } catch (e) { console.warn(e) } const mapClickInteraction: InteractionEvent = { + type: 'click', longitude: info.coordinate[0], latitude: info.coordinate[1], point: { x: info.x, y: info.y }, - features, + features: parseDeckPickingInfoToFeatures(features), } const toolsClickedHandled = handleMapToolsClick(mapClickInteraction) !== undefined diff --git a/apps/fishing-map/features/map/map.hooks.ts b/apps/fishing-map/features/map/map.hooks.ts index 0d2c2cbe00..e6e6c47b57 100644 --- a/apps/fishing-map/features/map/map.hooks.ts +++ b/apps/fishing-map/features/map/map.hooks.ts @@ -48,12 +48,11 @@ export const SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION = ['activity', ' export const getVesselsInfoConfig = (vessels: ExtendedFeatureVessel[]) => { return { - vessels, numVessels: vessels.length, overflow: vessels.length > MAX_TOOLTIP_LIST, - overflowNumber: vessels.length - MAX_TOOLTIP_LIST, + overflowNumber: Math.max(vessels.length - MAX_TOOLTIP_LIST, 0), overflowLoad: vessels.length > MAX_TOOLTIP_LIST, - overflowLoadNumber: vessels.length - MAX_TOOLTIP_LIST, + overflowLoadNumber: Math.max(vessels.length - MAX_TOOLTIP_LIST, 0), } } @@ -206,6 +205,7 @@ export const useMapHighlightedEvent = (features?: TooltipEventFeature[]) => { }, [features]) } +// TODO:deck ideally remove this intermediate step export const parseMapTooltipFeatures = ( features: SliceExtendedFeature[], dataviews: UrlDataviewInstance[], @@ -215,6 +215,7 @@ export const parseMapTooltipFeatures = ( const { temporalgrid, generatorId, generatorType } = feature const baseFeature = { source: feature.source, + category: feature.category, sourceLayer: feature.sourceLayer, layerId: feature.layerId as string, type: generatorType as DataviewType, diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index d721fd955b..1d1b4c7795 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -1,7 +1,6 @@ import { createSlice, PayloadAction, createAsyncThunk } from '@reduxjs/toolkit' import { current } from 'immer' import { uniqBy } from 'lodash' -import { InteractionEvent, ExtendedFeature } from '@globalfishingwatch/react-hooks' import { GFWAPI } from '@globalfishingwatch/api-client' import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { resolveEndpoint } from '@globalfishingwatch/datasets-client' @@ -17,8 +16,15 @@ import { APIPagination, } from '@globalfishingwatch/api-types' import { VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' -import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layer-composer' -import { FourwingsPickingInfo, FourwingsPickingObject } from '@globalfishingwatch/deck-layers' +import { InteractionEvent } from '@globalfishingwatch/deck-layer-composer' +import { + ClusterPickingObject, + ContextPickingObject, + FourwingsDeckSublayer, + FourwingsPickingObject, + UserContextPickingObject, +} from '@globalfishingwatch/deck-layers' +import { getUTCDate } from '@globalfishingwatch/data-transforms' import { AsyncReducerStatus } from 'utils/async-slice' import { AppDispatch } from 'store' import { @@ -49,10 +55,17 @@ export type ExtendedEventVessel = EventVessel & { dataset?: string } export type ExtendedFeatureEvent = ApiEvent & { dataset: Dataset } -export type SliceExtendedFeature = DeckLayerInteractionFeature & { - event?: ExtendedFeatureEvent - vessels?: ExtendedFeatureVessel[] +export type SliceExtendedFourwingsDeckSublayer = FourwingsDeckSublayer & { + vessels: ExtendedFeatureVessel[] +} +export type SliceExtendedFourwingsFeature = Omit & { + sublayers: SliceExtendedFourwingsDeckSublayer[] } +export type SliceExtendedFeature = + | SliceExtendedFourwingsFeature + | ContextPickingObject + | UserContextPickingObject + | ClusterPickingObject // Extends the default extendedEvent including event and vessels information from API export type SliceInteractionEvent = Omit & { @@ -81,23 +94,22 @@ type SublayerVessels = { } const getInteractionEndpointDatasetConfig = ( - features: ExtendedFeature[], + features: FourwingsPickingObject[], temporalgridDataviews: UrlDataviewInstance[] = [] ) => { // use the first feature/dv for common parameters const mainFeature = features[0] // Currently only one timerange is supported, which is OK since we only need interaction on the activity heatmaps and all // activity heatmaps use the same time intervals, This will need to be revised in case we support interactivity on environment layers - const start = mainFeature.temporalgrid?.visibleStartDate - const end = mainFeature.temporalgrid?.visibleEndDate + const start = getUTCDate(mainFeature?.startTime).toISOString() + const end = getUTCDate(mainFeature?.endTime).toISOString() // get corresponding dataviews const featuresDataviews = features.flatMap((feature) => { - return feature.temporalgrid - ? temporalgridDataviews.find( - (dataview) => dataview.id === feature?.temporalgrid?.sublayerId - ) || [] - : [] + if (!feature.sublayers?.length) return [] + return feature.sublayers.flatMap((sublayer) => { + return temporalgridDataviews.find((dataview) => dataview.id === sublayer.id) || [] + }) }) const fourWingsDataset = featuresDataviews[0]?.datasets?.find( (d) => d.type === DatasetTypes.Fourwings @@ -109,14 +121,14 @@ const getInteractionEndpointDatasetConfig = ( }) const datasetConfig: DataviewDatasetConfig = { - datasetId: fourWingsDataset.id, + datasetId: fourWingsDataset?.id, endpoint: EndpointId.FourwingsInteraction, params: [ { id: 'z', value: mainFeature.tile?.z }, { id: 'x', value: mainFeature.tile?.x }, { id: 'y', value: mainFeature.tile?.y }, - { id: 'rows', value: mainFeature.temporalgrid?.row as number }, - { id: 'cols', value: mainFeature.temporalgrid?.col as number }, + { id: 'rows', value: mainFeature.properties?.row as number }, + { id: 'cols', value: mainFeature.properties?.col as number }, ], query: [ { id: 'date-range', value: [start, end].join(',') }, @@ -196,7 +208,7 @@ export const fetchVesselInfo = async ( export type ActivityProperty = 'hours' | 'detections' export const fetchFishingActivityInteractionThunk = createAsyncThunk< { vessels: SublayerVessels[] } | undefined, - { fishingActivityFeatures: ExtendedFeature[]; activityProperties?: ActivityProperty[] }, + { fishingActivityFeatures: FourwingsPickingObject[]; activityProperties?: ActivityProperty[] }, { dispatch: AppDispatch } @@ -210,6 +222,7 @@ export const fetchFishingActivityInteractionThunk = createAsyncThunk< console.warn('fetchInteraction not possible, 0 features') return } + const { featuresDataviews, fourWingsDataset, datasetConfig } = getInteractionEndpointDatasetConfig(fishingActivityFeatures, temporalgridDataviews) @@ -286,8 +299,8 @@ export const fetchFishingActivityInteractionThunk = createAsyncThunk< const vesselsInfo = await fetchVesselInfo(infoDatasets, topActivityVesselIds, signal) - const sublayersIds = fishingActivityFeatures.map( - (feature) => feature.temporalgrid?.sublayerId || '' + const sublayersIds = fishingActivityFeatures.flatMap( + (feature) => feature.sublayers?.map((sublayer) => sublayer.id) || '' ) const sublayersVessels: SublayerVessels[] = vesselsBySource.map((sublayerVessels, i) => { @@ -332,7 +345,7 @@ export const fetchFishingActivityInteractionThunk = createAsyncThunk< export const fetchEncounterEventThunk = createAsyncThunk< ExtendedFeatureEvent | undefined, - ExtendedFeature, + FourwingsPickingObject, { dispatch: AppDispatch } @@ -430,7 +443,7 @@ export const fetchEncounterEventThunk = createAsyncThunk< type BQClusterEvent = Record export const fetchBQEventThunk = createAsyncThunk< BQClusterEvent | undefined, - ExtendedFeature, + any, // TODO: deck fix this type once the layer is implemented in deck { dispatch: AppDispatch } @@ -478,7 +491,6 @@ const slice = createSlice({ builder.addCase(fetchFishingActivityInteractionThunk.fulfilled, (state, action) => { state.fishingStatus = AsyncReducerStatus.Finished state.currentFishingRequestId = '' - if (!state.clicked || !state.clicked.features || !action.payload) return if (state?.clicked?.features?.length && action.payload?.vessels?.length) { state.clicked.features = state.clicked.features.map((feature: any) => { const sublayers = (feature as FourwingsPickingObject).sublayers.map((sublayer) => { diff --git a/apps/fishing-map/features/map/popups/ActivityLayers.tsx b/apps/fishing-map/features/map/popups/ActivityLayers.tsx index 845bb772f5..2099086b78 100644 --- a/apps/fishing-map/features/map/popups/ActivityLayers.tsx +++ b/apps/fishing-map/features/map/popups/ActivityLayers.tsx @@ -2,13 +2,15 @@ import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { Icon } from '@globalfishingwatch/ui-components' import { FourwingsDeckSublayer } from '@globalfishingwatch/deck-layers' +import { DataviewCategory } from '@globalfishingwatch/api-types' import I18nNumber from 'features/i18n/i18nNumber' import { TooltipEventFeature } from 'features/map/map.hooks' +import { SliceExtendedFourwingsDeckSublayer } from '../map.slice' import popupStyles from './Popup.module.css' import VesselsTable, { getVesselTableTitle } from './VesselsTable' type ActivityTooltipRowProps = { - feature: FourwingsDeckSublayer + feature: SliceExtendedFourwingsDeckSublayer & { category: DataviewCategory } showFeaturesDetails: boolean } diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index 9679ce0067..9da08d37d2 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -20,7 +20,11 @@ import AnnotationTooltip from 'features/map/popups/AnnotationTooltip' import RulerTooltip from 'features/map/popups/RulerTooltip' import { useDeckMap } from 'features/map/map-context.hooks' import { useMapViewport } from 'features/map/map-viewport.hooks' -import { selectApiEventStatus, selectFishingInteractionStatus } from '../map.slice' +import { + SliceExtendedFourwingsFeature, + selectApiEventStatus, + selectFishingInteractionStatus, +} from '../map.slice' import styles from './Popup.module.css' import ActivityTooltipRow from './ActivityLayers' import EncounterTooltipRow from './EncounterTooltipRow' @@ -109,11 +113,11 @@ function PopupWrapper({ // /> // ) case DataviewCategory.Activity: - return (features as FourwingsPickingObject[])?.map((feature) => { + return (features as SliceExtendedFourwingsFeature[])?.map((feature) => { return feature.sublayers.map((sublayer, i) => ( )) diff --git a/apps/fishing-map/features/map/popups/VesselsTable.tsx b/apps/fishing-map/features/map/popups/VesselsTable.tsx index adc7f8ef59..e2a0d203f6 100644 --- a/apps/fishing-map/features/map/popups/VesselsTable.tsx +++ b/apps/fishing-map/features/map/popups/VesselsTable.tsx @@ -3,7 +3,12 @@ import cx from 'classnames' import { useTranslation } from 'react-i18next' import { DateTime } from 'luxon' import { Tooltip } from '@globalfishingwatch/ui-components' -import { DatasetSubCategory, VesselIdentitySourceEnum } from '@globalfishingwatch/api-types' +import { + DatasetSubCategory, + DataviewCategory, + VesselIdentitySourceEnum, +} from '@globalfishingwatch/api-types' +import { FourwingsDeckSublayer } from '@globalfishingwatch/deck-layers' import { EMPTY_FIELD_PLACEHOLDER, formatInfoField, @@ -14,7 +19,12 @@ import { } from 'utils/info' import { getDatasetLabel } from 'features/datasets/datasets.utils' import I18nNumber from 'features/i18n/i18nNumber' -import { ActivityProperty, ExtendedFeatureVessel, MAX_TOOLTIP_LIST } from 'features/map/map.slice' +import { + ActivityProperty, + ExtendedFeatureVessel, + MAX_TOOLTIP_LIST, + SliceExtendedFourwingsDeckSublayer, +} from 'features/map/map.slice' import { t } from 'features/i18n/i18n' import I18nDate from 'features/i18n/i18nDate' import { useTimerangeConnect } from 'features/timebar/timebar.hooks' @@ -29,6 +39,7 @@ import { getVesselIdentityTooltipSummary } from 'features/workspace/vessels/Vess import { SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION, TooltipEventFeature, + getVesselsInfoConfig, } from '../map.hooks' import styles from './VesselsTable.module.css' @@ -92,19 +103,19 @@ function VesselsTable({ activityType = DatasetSubCategory.Fishing, testId = 'vessels-table', }: { - feature: TooltipEventFeature + feature: SliceExtendedFourwingsDeckSublayer & { category: DataviewCategory } vesselProperty?: ActivityProperty activityType?: DatasetSubCategory testId?: string }) { const { t } = useTranslation() - // const interactionAllowed = [...SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION].includes( - // feature?.sublayerInteractionType || '' - // ) - // TODO:deck fix this - const interactionAllowed = true + const interactionAllowed = [...SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION].includes( + feature?.category || '' + ) + const vessels = feature?.vessels?.slice(0, MAX_TOOLTIP_LIST) + const vesselsInfo = getVesselsInfoConfig(feature.vessels) const hasPinColumn = interactionAllowed && @@ -129,10 +140,9 @@ function VesselsTable({ {/* Disabled for detections to allocate some space for timestamps interaction */} {vesselProperty !== 'detections' && {t('vessel.source_short', 'source')}} - {feature.temporalgrid?.unit === 'hours' && t('common.hour_other', 'hours')} - {feature.temporalgrid?.unit === 'days' && t('common.days_other', 'days')} - {feature.temporalgrid?.unit === 'detections' && - t('common.detection_other', 'detections')} + {feature?.unit === 'hours' && t('common.hour_other', 'hours')} + {feature?.unit === 'days' && t('common.days_other', 'days')} + {feature?.unit === 'detections' && t('common.detection_other', 'detections')} @@ -230,9 +240,9 @@ function VesselsTable({ )} - {feature.vesselsInfo && feature.vesselsInfo.overflow && ( + {vesselsInfo && vesselsInfo.overflow && (

      - + {feature.vesselsInfo.overflowNumber} {t('common.more', 'more')} + + {vesselsInfo.overflowNumber} {t('common.more', 'more')}

      )}
      diff --git a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts index f6c8e49c6d..85b035238a 100644 --- a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts +++ b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts @@ -1,8 +1,7 @@ import { atom, useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' -import { DeckLayerInteractionFeature } from '../types' -import { ExtendedFeature, InteractionEvent, InteractionEventCallback } from './types' -import { filterUniqueFeatureInteraction, getExtendedFeatures } from './interaction-features.utils' +import { InteractionEvent, InteractionEventCallback } from './types' +import { filterUniqueFeatureInteraction } from './interaction-features.utils' export const deckHoverInteractionAtom = atom({} as InteractionEvent) @@ -14,21 +13,20 @@ export const useSetMapHoverInteraction = () => { return useCallback(setDeckInteraction, [setDeckInteraction]) } +// TODO:deck move the stopPropagation and the filterUniqueFeatureInteraction to utils and consume in the app to remove this hook export const useMapClick = (clickCallback: InteractionEventCallback) => { // const { updateFeatureState, cleanFeatureState } = useFeatureState(map) const onMapClick = useCallback( (event: InteractionEvent) => { if (!clickCallback) return - const interactionEvent: InteractionEvent = { - type: 'click', - longitude: event.longitude, - latitude: event.latitude, - point: event.point, - } + const interactionEvent: InteractionEvent = { ...event } if (event.features?.length) { - const extendedFeatures: ExtendedFeature[] = getExtendedFeatures(event.features) - const extendedFeaturesLimit = filterUniqueFeatureInteraction(extendedFeatures) + // const stopPropagationFeature = event.features.find((f) => f.layer.metadata?.stopPropagation) + // if (stopPropagationFeature) { + // return getExtendedFeature(stopPropagationFeature, metadata, debug) + // } + const extendedFeaturesLimit = filterUniqueFeatureInteraction(event.features) if (extendedFeaturesLimit.length) { interactionEvent.features = extendedFeaturesLimit diff --git a/libs/deck-layer-composer/src/interactions/index.ts b/libs/deck-layer-composer/src/interactions/index.ts index b62f43c929..c87c334f5a 100644 --- a/libs/deck-layer-composer/src/interactions/index.ts +++ b/libs/deck-layer-composer/src/interactions/index.ts @@ -1,3 +1,3 @@ export * from './deck-layers-interaction.hooks' -// export * from './interaction-features.utils' +export * from './interaction-features.utils' export * from './types' diff --git a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts index 55694fa728..4be36f88b2 100644 --- a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts +++ b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts @@ -4,11 +4,10 @@ import { FourwingsLayer, FourwingsPickingObject, } from '@globalfishingwatch/deck-layers' -import { getUTCDate } from '@globalfishingwatch/data-transforms' import { VALUE_MULTIPLIER } from '@globalfishingwatch/fourwings-aggregate' -import { ExtendedFeature } from './types' +import { DeckLayerInteractionFeature, DeckLayerInteractionPickingInfo } from '../types' -export const filterUniqueFeatureInteraction = (features: ExtendedFeature[]) => { +export const filterUniqueFeatureInteraction = (features: DeckLayerInteractionFeature[]) => { const uniqueLayerIdFeatures: Record = {} const filtered = features?.filter(({ layerId, id, uniqueFeatureInteraction }) => { if (!uniqueFeatureInteraction) { @@ -23,7 +22,9 @@ export const filterUniqueFeatureInteraction = (features: ExtendedFeature[]) => { return filtered } -const getExtendedFeature = (feature: ExtendedFeature): ExtendedFeature[] => { +const parseDeckPickingInfoToFeature = ( + pickingInfo: DeckLayerInteractionPickingInfo +): DeckLayerInteractionFeature[] => { // const generatorType = feature.layer.metadata?.generatorType ?? null // const generatorId = feature.layer.metadata?.generatorId ?? null @@ -40,16 +41,16 @@ const getExtendedFeature = (feature: ExtendedFeature): ExtendedFeature[] => { // TODO:deck implement the stopPropagation feature // const stopPropagation = feature.layer?.metadata?.stopPropagation ?? false - const extendedFeature: ExtendedFeature = { - ...feature.object, - layerId: feature.layer.id, + const extendedFeature: DeckLayerInteractionFeature = { + ...pickingInfo.object, + layerId: pickingInfo.layer.id, // uniqueFeatureInteraction, // stopPropagation, } - if (feature.layer instanceof FourwingsLayer) { - const object = feature.object as FourwingsPickingObject - if (feature.layer?.props.static) { + if (pickingInfo.layer instanceof FourwingsLayer) { + const object = pickingInfo.object as FourwingsPickingObject + if (pickingInfo.layer?.props.static) { return [ { ...extendedFeature, @@ -58,42 +59,21 @@ const getExtendedFeature = (feature: ExtendedFeature): ExtendedFeature[] => { }, ] } else { - // const values = object.sublayers.map((sublayer) => sublayer.value!) - - // This is used when querying the interaction endpoint, so that start begins at the start of the frame (ie start of a 10days interval) - // This avoids querying a cell visible on the map, when its actual timerange is not included in the app-overall time range - // const getDate = CONFIG_BY_INTERVAL[timeChunks.interval as Interval].getDate - const layer = feature.layer as FourwingsLayer - const visibleStartDate = getUTCDate(layer?.props?.startTime).toISOString() - const visibleEndDate = getUTCDate(layer?.props?.endTime).toISOString() - return object.sublayers.flatMap((sublayer, i) => { + return object?.sublayers?.flatMap((sublayer, i) => { if (sublayer.value === 0) return [] - const temporalGridExtendedFeature: ExtendedFeature = { + const temporalGridExtendedFeature: DeckLayerInteractionFeature = { ...extendedFeature, - temporalgrid: { - sublayerIndex: i, - sublayerId: sublayer.id, - sublayerInteractionType: object.category, - sublayerCombinationMode: layer.props.comparisonMode, - visible: true, - col: object.properties.col as number, - row: object.properties.row as number, - interval: layer.getInterval(), - visibleStartDate, - visibleEndDate, - unit: sublayer.unit, - }, - value: sublayer.value, + value: sublayer.value!, } return [temporalGridExtendedFeature] }) } - } else if (feature.layer instanceof ContextLayer) { + } else if (pickingInfo.layer instanceof ContextLayer) { // TODO: deck add support for these layers // case DataviewType.Context: // case DataviewType.UserPoints: // case DataviewType.UserContext: - const object = feature.object as ContextPickingObject + const object = pickingInfo.object as ContextPickingObject return [ { ...extendedFeature, @@ -106,14 +86,16 @@ const getExtendedFeature = (feature: ExtendedFeature): ExtendedFeature[] => { return [extendedFeature] } -export const getExtendedFeatures = (features: ExtendedFeature[]): ExtendedFeature[] => { +export const parseDeckPickingInfoToFeatures = ( + features: DeckLayerInteractionPickingInfo[] +): DeckLayerInteractionFeature[] => { // TODO: deck implement the stopPropagation feature // const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) // if (stopPropagationFeature) { // return getExtendedFeature(stopPropagationFeature, metadata, debug) // } - const extendedFeatures: ExtendedFeature[] = features.flatMap((feature) => { - return getExtendedFeature(feature) || [] + const extendedFeatures: DeckLayerInteractionFeature[] = features.flatMap((feature) => { + return parseDeckPickingInfoToFeature(feature) || [] }) return extendedFeatures } diff --git a/libs/deck-layer-composer/src/interactions/types.ts b/libs/deck-layer-composer/src/interactions/types.ts index 6f9133a81d..6386676a03 100644 --- a/libs/deck-layer-composer/src/interactions/types.ts +++ b/libs/deck-layer-composer/src/interactions/types.ts @@ -1,33 +1,10 @@ -import { - ContextLayerId, - ContextPickingObject, - FourwingsPickingObject, -} from '@globalfishingwatch/deck-layers' import { DeckLayerInteractionFeature } from '../types' export type InteractionEventCallback = (event: InteractionEvent | null) => void -export type ExtendedFeature = DeckLayerInteractionFeature & { - layerId: string - datasetId?: string - promoteId?: string - id: string - value: any - geometry?: any - stopPropagation?: boolean - uniqueFeatureInteraction?: boolean - unit?: string - // TODO:deck review if this is needed anywhere else - // tile: { - // x: number - // y: number - // z: number - // } -} - export type InteractionEvent = { type: 'click' | 'hover' - features?: ExtendedFeature[] + features?: DeckLayerInteractionFeature[] latitude: number longitude: number point: { x: number; y: number } diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 9e1bd732db..7783944684 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -1,10 +1,13 @@ import { ClusterLayer, + ClusterPickingInfo, ClusterPickingObject, ContextLayer, + ContextPickingInfo, ContextPickingObject, FourwingsDeckSublayer, FourwingsLayer, + FourwingsPickingInfo, FourwingsPickingObject, FourwingsTileLayerColorDomain, FourwingsTileLayerColorRange, @@ -52,7 +55,12 @@ export interface DeckLegendBivariate extends DeckLegend { } export type DeckLayerInteractionFeature = - | (FourwingsPickingObject & { layer: FourwingsLayer }) - | (ContextPickingObject & { layer: ContextLayer }) - | (UserContextPickingObject & { layer: ContextLayer }) - | (ClusterPickingObject & { layer: ClusterLayer }) + | FourwingsPickingObject + | ContextPickingObject + | UserContextPickingObject + | ClusterPickingObject + +export type DeckLayerInteractionPickingInfo = + | (FourwingsPickingInfo & { layer: FourwingsLayer }) + | (ContextPickingInfo & { layer: ContextLayer }) + | (ClusterPickingInfo & { layer: ClusterLayer }) diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts index 013a5f50f6..5d45dc205a 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts @@ -28,20 +28,23 @@ export class FourwingsHeatmapLayer extends CompositeLayer & { title: string tile: { x: number; y: number; z: number } + startTime: number + endTime: number + interval: FourwingsInterval category: string sublayers: FourwingsDeckSublayer[] } From 55cca2ef51cd0c9d08a388644c2415e9233e45dd Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 9 Apr 2024 12:24:39 +0200 Subject: [PATCH 18/43] remove intermediate useMapClick --- .../features/map/map-interactions.hooks.ts | 32 +++----- apps/fishing-map/features/map/map.hooks.ts | 1 + .../deck-layers-interaction.hooks.ts | 32 +------- .../interaction-features.utils.ts | 78 ------------------- .../src/interactions/types.ts | 2 - 5 files changed, 13 insertions(+), 132 deletions(-) diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 5d13f50d77..3ac326f3de 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -7,10 +7,8 @@ import { DataviewCategory, DataviewType } from '@globalfishingwatch/api-types' import { useMapHoverInteraction, useSetMapHoverInteraction, - useMapClick, InteractionEvent, DeckLayerInteractionPickingInfo, - parseDeckPickingInfoToFeatures, } from '@globalfishingwatch/deck-layer-composer' import { ClusterPickingObject, FourwingsPickingObject } from '@globalfishingwatch/deck-layers' import { useMapDrawConnect } from 'features/map/map-draw.hooks' @@ -54,11 +52,10 @@ import { import { useSetViewState } from './map-viewport.hooks' function cleanFeatureState(state: any) { - console.warn('TODO: handle this in deck') + console.warn('TODO:deck handle this in deck') } export const useClickedEventConnect = () => { - const map = useMapInstance() const dispatch = useAppDispatch() const clickedEvent = useSelector(selectClickedEvent) const locationType = useSelector(selectLocationType) @@ -67,7 +64,6 @@ export const useClickedEventConnect = () => { const { dispatchLocation } = useLocationConnect() // const { cleanFeatureState } = useFeatureState(map) const setViewState = useSetViewState() - const { setMapAnnotation } = useMapAnnotation() const tilesClusterLoaded = useMapClusterTilesLoaded() const fishingPromiseRef = useRef() const presencePromiseRef = useRef() @@ -137,14 +133,6 @@ export const useClickedEventConnect = () => { } } - const annotatedFeature = event?.features?.find( - (f) => f.generatorType === DataviewType.Annotation - ) - if (annotatedFeature?.properties?.id) { - setMapAnnotation(annotatedFeature.properties) - return - } - // Cancel all pending promises cancelPendingInteractionRequests() @@ -244,9 +232,11 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { } setMapHoverFeatures({ + type: 'hover', longitude: info.coordinate[0], latitude: info.coordinate[1], - features, + point: { x: info.x, y: info.y }, + features: features.flatMap((f) => f.object || []), }) // onRulerDrag(features) @@ -319,7 +309,6 @@ export const useMapMouseClick = () => { const temporalgridDataviews = useSelector(selectActiveTemporalgridDataviews) const { clickedEvent, dispatchClickedEvent } = useClickedEventConnect() - const onClick = useMapClick(dispatchClickedEvent) const clickedTooltipEvent = parseMapTooltipEvent(clickedEvent, dataviews, temporalgridDataviews) const clickedCellLayers = useMemo(() => { @@ -366,21 +355,20 @@ export const useMapMouseClick = () => { } catch (e) { console.warn(e) } - const mapClickInteraction: InteractionEvent = { type: 'click', longitude: info.coordinate[0], latitude: info.coordinate[1], point: { x: info.x, y: info.y }, - features: parseDeckPickingInfoToFeatures(features), + features: features.flatMap((f) => f.object || []), } - const toolsClickedHandled = handleMapToolsClick(mapClickInteraction) !== undefined - if (!toolsClickedHandled) { - onClick(mapClickInteraction) + const clickStopPropagation = handleMapToolsClick(mapClickInteraction) !== undefined + if (!clickStopPropagation) { + dispatchClickedEvent(mapClickInteraction) } }, - [map, clickedCellLayers, handleMapToolsClick, onClick] + [map, clickedCellLayers, handleMapToolsClick, dispatchClickedEvent] ) return { onMapClick, clickedTooltipEvent } @@ -478,7 +466,7 @@ export const useMapCursor = () => { const getCursor = useCallback( ({ isDragging }: { isDragging: boolean }) => { if (isMapAnnotating || isErrorNotificationEditing || rulersEditing) { - if (rulersEditing && hoverFeatures.some(isRulerLayerPoint)) { + if (rulersEditing && hoverFeatures?.some(isRulerLayerPoint)) { return 'move' } return 'crosshair' diff --git a/apps/fishing-map/features/map/map.hooks.ts b/apps/fishing-map/features/map/map.hooks.ts index e6e6c47b57..ca8e1180b3 100644 --- a/apps/fishing-map/features/map/map.hooks.ts +++ b/apps/fishing-map/features/map/map.hooks.ts @@ -47,6 +47,7 @@ import { useViewStateAtom } from './map-viewport.hooks' export const SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION = ['activity', 'detections'] export const getVesselsInfoConfig = (vessels: ExtendedFeatureVessel[]) => { + if (!vessels?.length) return {} return { numVessels: vessels.length, overflow: vessels.length > MAX_TOOLTIP_LIST, diff --git a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts index 85b035238a..41247c23ff 100644 --- a/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts +++ b/libs/deck-layer-composer/src/interactions/deck-layers-interaction.hooks.ts @@ -1,42 +1,14 @@ import { atom, useAtomValue, useSetAtom } from 'jotai' import { useCallback } from 'react' -import { InteractionEvent, InteractionEventCallback } from './types' -import { filterUniqueFeatureInteraction } from './interaction-features.utils' +import { InteractionEvent } from './types' export const deckHoverInteractionAtom = atom({} as InteractionEvent) export const useMapHoverInteraction = () => { return useAtomValue(deckHoverInteractionAtom) } + export const useSetMapHoverInteraction = () => { const setDeckInteraction = useSetAtom(deckHoverInteractionAtom) return useCallback(setDeckInteraction, [setDeckInteraction]) } - -// TODO:deck move the stopPropagation and the filterUniqueFeatureInteraction to utils and consume in the app to remove this hook -export const useMapClick = (clickCallback: InteractionEventCallback) => { - // const { updateFeatureState, cleanFeatureState } = useFeatureState(map) - const onMapClick = useCallback( - (event: InteractionEvent) => { - if (!clickCallback) return - - const interactionEvent: InteractionEvent = { ...event } - if (event.features?.length) { - // const stopPropagationFeature = event.features.find((f) => f.layer.metadata?.stopPropagation) - // if (stopPropagationFeature) { - // return getExtendedFeature(stopPropagationFeature, metadata, debug) - // } - const extendedFeaturesLimit = filterUniqueFeatureInteraction(event.features) - - if (extendedFeaturesLimit.length) { - interactionEvent.features = extendedFeaturesLimit - // updateFeatureState(extendedFeaturesLimit, 'click') - } - } - clickCallback(interactionEvent) - }, - [clickCallback] - ) - - return onMapClick -} diff --git a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts index 4be36f88b2..3380a1978e 100644 --- a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts +++ b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts @@ -21,81 +21,3 @@ export const filterUniqueFeatureInteraction = (features: DeckLayerInteractionFea }) return filtered } - -const parseDeckPickingInfoToFeature = ( - pickingInfo: DeckLayerInteractionPickingInfo -): DeckLayerInteractionFeature[] => { - // const generatorType = feature.layer.metadata?.generatorType ?? null - // const generatorId = feature.layer.metadata?.generatorId ?? null - - // TODO: if no generatorMetadata is found, fallback to feature.layer.metadata, but the former should be prefered - // let generatorMetadata: any - // if (generatorId && metadata?.generatorsMetadata && metadata?.generatorsMetadata[generatorId]) { - // generatorMetadata = metadata?.generatorsMetadata[generatorId] - // } else { - // generatorMetadata = feature.layer.metadata - // } - - // TODO:deck implement the uniqueFeatureInteraction feature inside the getPickingInfo - // const uniqueFeatureInteraction = feature?.metadata?.uniqueFeatureInteraction ?? false - // TODO:deck implement the stopPropagation feature - // const stopPropagation = feature.layer?.metadata?.stopPropagation ?? false - - const extendedFeature: DeckLayerInteractionFeature = { - ...pickingInfo.object, - layerId: pickingInfo.layer.id, - // uniqueFeatureInteraction, - // stopPropagation, - } - - if (pickingInfo.layer instanceof FourwingsLayer) { - const object = pickingInfo.object as FourwingsPickingObject - if (pickingInfo.layer?.props.static) { - return [ - { - ...extendedFeature, - value: extendedFeature.value / VALUE_MULTIPLIER, - unit: object.sublayers[0].unit, - }, - ] - } else { - return object?.sublayers?.flatMap((sublayer, i) => { - if (sublayer.value === 0) return [] - const temporalGridExtendedFeature: DeckLayerInteractionFeature = { - ...extendedFeature, - value: sublayer.value!, - } - return [temporalGridExtendedFeature] - }) - } - } else if (pickingInfo.layer instanceof ContextLayer) { - // TODO: deck add support for these layers - // case DataviewType.Context: - // case DataviewType.UserPoints: - // case DataviewType.UserContext: - const object = pickingInfo.object as ContextPickingObject - return [ - { - ...extendedFeature, - datasetId: object.datasetId, - geometry: object.geometry, - }, - ] - } - - return [extendedFeature] -} - -export const parseDeckPickingInfoToFeatures = ( - features: DeckLayerInteractionPickingInfo[] -): DeckLayerInteractionFeature[] => { - // TODO: deck implement the stopPropagation feature - // const stopPropagationFeature = features.find((f) => f.layer.metadata?.stopPropagation) - // if (stopPropagationFeature) { - // return getExtendedFeature(stopPropagationFeature, metadata, debug) - // } - const extendedFeatures: DeckLayerInteractionFeature[] = features.flatMap((feature) => { - return parseDeckPickingInfoToFeature(feature) || [] - }) - return extendedFeatures -} diff --git a/libs/deck-layer-composer/src/interactions/types.ts b/libs/deck-layer-composer/src/interactions/types.ts index 6386676a03..9d2e364ae3 100644 --- a/libs/deck-layer-composer/src/interactions/types.ts +++ b/libs/deck-layer-composer/src/interactions/types.ts @@ -1,7 +1,5 @@ import { DeckLayerInteractionFeature } from '../types' -export type InteractionEventCallback = (event: InteractionEvent | null) => void - export type InteractionEvent = { type: 'click' | 'hover' features?: DeckLayerInteractionFeature[] From 52ff91956c9f246483ccd2354540d166abc0451c Mon Sep 17 00:00:00 2001 From: j8seangel Date: Tue, 9 Apr 2024 12:27:20 +0200 Subject: [PATCH 19/43] fix detections tooltip --- .../features/map/popups/PopupWrapper.tsx | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index 9da08d37d2..5c74ce091c 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -113,23 +113,25 @@ function PopupWrapper({ // /> // ) case DataviewCategory.Activity: - return (features as SliceExtendedFourwingsFeature[])?.map((feature) => { - return feature.sublayers.map((sublayer, i) => ( + return (features as SliceExtendedFourwingsFeature[])?.map((feature, i) => { + return feature.sublayers.map((sublayer, j) => ( )) }) case DataviewCategory.Detections: - return (features as FourwingsPickingObject[])?.map((feature, i) => ( - - )) + return (features as FourwingsPickingObject[])?.map((feature, i) => { + return feature.sublayers.map((sublayer, j) => ( + + )) + }) case DataviewCategory.Events: return ( Date: Tue, 9 Apr 2024 12:59:48 +0200 Subject: [PATCH 20/43] show spinner only for each popup category --- apps/fishing-map/features/map/map.slice.ts | 2 +- .../features/map/popups/ActivityLayers.tsx | 14 +- .../features/map/popups/DetectionsLayers.tsx | 16 +- .../features/map/popups/PopupWrapper.tsx | 286 +++++++++--------- 4 files changed, 169 insertions(+), 149 deletions(-) diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index 1d1b4c7795..1879d301ff 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -493,7 +493,7 @@ const slice = createSlice({ state.currentFishingRequestId = '' if (state?.clicked?.features?.length && action.payload?.vessels?.length) { state.clicked.features = state.clicked.features.map((feature: any) => { - const sublayers = (feature as FourwingsPickingObject).sublayers.map((sublayer) => { + const sublayers = (feature as FourwingsPickingObject).sublayers?.map((sublayer) => { const vessels = action.payload?.vessels.find((v) => v.sublayerId === sublayer.id)?.vessels || [] return { ...sublayer, vessels } diff --git a/apps/fishing-map/features/map/popups/ActivityLayers.tsx b/apps/fishing-map/features/map/popups/ActivityLayers.tsx index 2099086b78..1d0f619c49 100644 --- a/apps/fishing-map/features/map/popups/ActivityLayers.tsx +++ b/apps/fishing-map/features/map/popups/ActivityLayers.tsx @@ -1,20 +1,19 @@ import { Fragment } from 'react' import { useTranslation } from 'react-i18next' -import { Icon } from '@globalfishingwatch/ui-components' -import { FourwingsDeckSublayer } from '@globalfishingwatch/deck-layers' +import { Icon, Spinner } from '@globalfishingwatch/ui-components' import { DataviewCategory } from '@globalfishingwatch/api-types' import I18nNumber from 'features/i18n/i18nNumber' -import { TooltipEventFeature } from 'features/map/map.hooks' import { SliceExtendedFourwingsDeckSublayer } from '../map.slice' import popupStyles from './Popup.module.css' import VesselsTable, { getVesselTableTitle } from './VesselsTable' type ActivityTooltipRowProps = { feature: SliceExtendedFourwingsDeckSublayer & { category: DataviewCategory } + loading?: boolean showFeaturesDetails: boolean } -function ActivityTooltipRow({ feature, showFeaturesDetails }: ActivityTooltipRowProps) { +function ActivityTooltipRow({ feature, showFeaturesDetails, loading }: ActivityTooltipRowProps) { const { t } = useTranslation() const title = getVesselTableTitle(feature) // TODO get the value based on the sublayer @@ -36,8 +35,13 @@ function ActivityTooltipRow({ feature, showFeaturesDetails }: ActivityTooltipRow })} + {loading && ( +
      + +
      + )} {/* // TODO:deck add subcategory info */} - {showFeaturesDetails && ( + {!loading && showFeaturesDetails && ( )} diff --git a/apps/fishing-map/features/map/popups/DetectionsLayers.tsx b/apps/fishing-map/features/map/popups/DetectionsLayers.tsx index 30a7b337b1..8132dcb6cc 100644 --- a/apps/fishing-map/features/map/popups/DetectionsLayers.tsx +++ b/apps/fishing-map/features/map/popups/DetectionsLayers.tsx @@ -1,6 +1,6 @@ import { Fragment } from 'react' import { useTranslation } from 'react-i18next' -import { Icon } from '@globalfishingwatch/ui-components' +import { Icon, Spinner } from '@globalfishingwatch/ui-components' import I18nNumber from 'features/i18n/i18nNumber' import { TooltipEventFeature, TooltipEventFeatureVesselsInfo } from 'features/map/map.hooks' import VesselsTable, { VesselDetectionTimestamps } from 'features/map/popups/VesselsTable' @@ -8,9 +8,14 @@ import styles from './Popup.module.css' type ViirsMatchTooltipRowProps = { feature: TooltipEventFeature + loading?: boolean showFeaturesDetails: boolean } -function ViirsMatchTooltipRow({ feature, showFeaturesDetails }: ViirsMatchTooltipRowProps) { +function ViirsMatchTooltipRow({ + feature, + showFeaturesDetails, + loading, +}: ViirsMatchTooltipRowProps) { const { t } = useTranslation() // Avoid showing not matched detections const matchedVessels: TooltipEventFeatureVesselsInfo['vessels'] = ( @@ -50,7 +55,12 @@ function ViirsMatchTooltipRow({ feature, showFeaturesDetails }: ViirsMatchToolti )} - {showFeaturesDetails && ( + {loading && ( +
      + +
      + )} + {!loading && showFeaturesDetails && ( )} diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index 5c74ce091c..b59f5902e4 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -57,12 +57,12 @@ function PopupWrapper({ const timeCompareTimeDescription = useTimeCompareTimeDescription() const mapViewport = useMapViewport() - const fishingInteractionStatus = useSelector(selectFishingInteractionStatus) + const activityInteractionStatus = useSelector(selectFishingInteractionStatus) const apiEventStatus = useSelector(selectApiEventStatus) - const popupNeedsLoading = [fishingInteractionStatus, apiEventStatus].some( - (s) => s === AsyncReducerStatus.Loading - ) + // const popupNeedsLoading = [fishingInteractionStatus, apiEventStatus].some( + // (s) => s === AsyncReducerStatus.Loading + // ) if (!mapViewport || !interaction || !interaction.features) return null @@ -88,105 +88,112 @@ function PopupWrapper({ style={{ position: 'absolute', top, left, maxWidth: '600px' }} className={cx(styles.popup, styles[type], className)} > - {popupNeedsLoading ? ( -
      - -
      - ) : ( -
      - {/*
      +
      + {/*
      */} -
      - {timeCompareTimeDescription && ( -
      {timeCompareTimeDescription}
      - )} - {Object.entries(featureByCategory)?.map(([featureCategory, features]) => { - switch (featureCategory) { - // TODO: deck restore this popup - // case DataviewCategory.Comparison: - // return ( - // - // ) - case DataviewCategory.Activity: - return (features as SliceExtendedFourwingsFeature[])?.map((feature, i) => { - return feature.sublayers.map((sublayer, j) => ( - - )) - }) - case DataviewCategory.Detections: - return (features as FourwingsPickingObject[])?.map((feature, i) => { - return feature.sublayers.map((sublayer, j) => ( - - )) - }) - case DataviewCategory.Events: - return ( - + {timeCompareTimeDescription && ( +
      {timeCompareTimeDescription}
      + )} + {Object.entries(featureByCategory)?.map(([featureCategory, features]) => { + switch (featureCategory) { + // TODO: deck restore this popup + // case DataviewCategory.Comparison: + // return ( + // + // ) + case DataviewCategory.Activity: { + return (features as SliceExtendedFourwingsFeature[])?.map((feature, i) => { + return feature.sublayers.map((sublayer, j) => ( + - ) - // TODO: deck restore this popup - // case DataviewCategory.Environment: { - // const contextEnvironmentalFeatures = features.filter( - // (feature) => - // feature.type === DataviewType.Context || - // feature.type === DataviewType.UserContext - // ) - // const environmentalFeatures = features.filter( - // (feature) => - // feature.type !== DataviewType.Context && - // feature.type !== DataviewType.UserContext - // ) - // return ( - // - // - // - // - // ) - // } - case DataviewCategory.Context: { - // const defaultContextFeatures = features.filter( - // (feature) => feature.type === DataviewType.Context - // ) - // const workspacePointsFeatures = features.filter( - // (feature) => feature.source === WORKSPACE_GENERATOR_ID - // ) - // const areaBufferFeatures = features.filter( - // (feature) => feature.source === REPORT_BUFFER_GENERATOR_ID - // ) - // const annotationFeatures = features.filter( - // (feature) => feature.type === DataviewType.Annotation - // ) - // const rulersFeatures = features.filter( - // (feature) => feature.type === DataviewType.Rulers - // ) - // const userPointFeatures = features.filter( - // (feature) => feature.type === DataviewType.UserPoints - // ) + )) + }) + } + case DataviewCategory.Detections: { + return (features as FourwingsPickingObject[])?.map((feature, i) => { + return feature.sublayers.map((sublayer, j) => ( + + )) + }) + } + case DataviewCategory.Events: { + if (apiEventStatus === AsyncReducerStatus.Loading) { return ( - - {/* +
      + +
      + ) + } + return ( + + ) + } + // TODO: deck restore this popup + // case DataviewCategory.Environment: { + // const contextEnvironmentalFeatures = features.filter( + // (feature) => + // feature.type === DataviewType.Context || + // feature.type === DataviewType.UserContext + // ) + // const environmentalFeatures = features.filter( + // (feature) => + // feature.type !== DataviewType.Context && + // feature.type !== DataviewType.UserContext + // ) + // return ( + // + // + // + // + // ) + // } + case DataviewCategory.Context: { + // const defaultContextFeatures = features.filter( + // (feature) => feature.type === DataviewType.Context + // ) + // const workspacePointsFeatures = features.filter( + // (feature) => feature.source === WORKSPACE_GENERATOR_ID + // ) + // const areaBufferFeatures = features.filter( + // (feature) => feature.source === REPORT_BUFFER_GENERATOR_ID + // ) + // const annotationFeatures = features.filter( + // (feature) => feature.type === DataviewType.Annotation + // ) + // const rulersFeatures = features.filter( + // (feature) => feature.type === DataviewType.Rulers + // ) + // const userPointFeatures = features.filter( + // (feature) => feature.type === DataviewType.UserPoints + // ) + return ( + + {/* // TODO: deck restore this popup */} - - - ) - } - // TODO: deck restore this popup - // case DataviewCategory.User: { - // const userPointFeatures = features.filter( - // (feature) => feature.type === DataviewType.UserPoints - // ) - // const userContextFeatures = features.filter( - // (feature) => feature.type === DataviewType.UserContext - // ) - // return ( - // - // - // - // - // ) - // } + +
      + ) + } + // TODO: deck restore this popup + // case DataviewCategory.User: { + // const userPointFeatures = features.filter( + // (feature) => feature.type === DataviewType.UserPoints + // ) + // const userContextFeatures = features.filter( + // (feature) => feature.type === DataviewType.UserContext + // ) + // return ( + // + // + // + // + // ) + // } - // case DataviewCategory.Vessels: - // return ( - // - // ) + // case DataviewCategory.Vessels: + // return ( + // + // ) - default: - return null - } - })} -
      + default: + return null + } + })}
      - )} +
      ) } From 3da19b4c706d17751f486ec17e46d70bf8bab470 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 15:37:52 +0200 Subject: [PATCH 21/43] add interaction to vessel events layer --- .../features/map/map-interaction.utils.ts | 8 ++- .../features/map/map-interactions.hooks.ts | 9 ++- .../features/map/popups/PopupWrapper.tsx | 16 +++--- .../map/popups/VesselEventsLayers.tsx | 13 +++-- .../src/resolvers/vessels.ts | 1 + libs/deck-layer-composer/src/types.ts | 5 ++ libs/deck-layers/src/index.ts | 2 +- libs/deck-layers/src/layers/rulers/index.ts | 1 + .../src/layers/rulers/rulers.types.ts | 10 +++- .../src/layers/vessel/VesselLayer.ts | 55 ++++++++++++------- libs/deck-layers/src/layers/vessel/index.ts | 2 + .../src/layers/vessel/vessel.config.ts | 2 + .../src/layers/vessel/vessel.types.ts | 38 +++++++++++++ libs/deck-layers/src/types.ts | 6 +- libs/deck-loaders/project.json | 20 +++++++ .../deck-loaders/src/vessels/events-loader.ts | 9 ++- 16 files changed, 148 insertions(+), 49 deletions(-) create mode 100644 libs/deck-layers/src/layers/vessel/index.ts create mode 100644 libs/deck-layers/src/layers/vessel/vessel.types.ts diff --git a/apps/fishing-map/features/map/map-interaction.utils.ts b/apps/fishing-map/features/map/map-interaction.utils.ts index 3b8b74c1dd..bf18e76719 100644 --- a/apps/fishing-map/features/map/map-interaction.utils.ts +++ b/apps/fishing-map/features/map/map-interaction.utils.ts @@ -1,4 +1,6 @@ -import { PickingInfo } from '@deck.gl/core' +import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layer-composer' +import { RulerPickingObject } from '@globalfishingwatch/deck-layers' -export const isRulerLayerPoint = (info: PickingInfo) => - info.sourceLayer?.id === 'RulersLayer-ruler-layer' && info.object.geometry.type === 'Point' +export const isRulerLayerPoint = (feature: DeckLayerInteractionFeature) => + feature.category === 'rulers' && + (feature as unknown as RulerPickingObject).geometry?.type === 'Point' diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 3ac326f3de..b4177da549 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -389,9 +389,7 @@ const hasToolFeature = (hoveredTooltipEvent?: ReturnType -) => { +export const _deprecatedUseMapCursor = (hoveredTooltipEvent?: any) => { const map = useMapInstance() const { isMapAnnotating } = useMapAnnotation() const { isErrorNotificationEditing } = useMapErrorNotification() @@ -420,7 +418,8 @@ export const _deprecatedUseMapCursor = ( const eventsCount = clusterConfig?.config?.duplicatedEventsWorkaround ? 2 : 1 const clusterFeature = hoveredTooltipEvent.features.find( - (f) => f.type === DataviewType.TileCluster && parseInt(f.properties.count) > eventsCount + (f: any) => + f.type === DataviewType.TileCluster && parseInt(f.properties.count) > eventsCount ) if (clusterFeature) { @@ -432,7 +431,7 @@ export const _deprecatedUseMapCursor = ( return expansionZoom && lat && longitude ? 'zoom-in' : 'grab' } const vesselFeatureEvents = hoveredTooltipEvent.features.filter( - (f) => f.category === DataviewCategory.Vessels + (f: any) => f.category === DataviewCategory.Vessels ) if (vesselFeatureEvents.length > 1) { return 'grab' diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index b59f5902e4..3a50e3641a 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -235,14 +235,14 @@ function PopupWrapper({ // ) // } - // case DataviewCategory.Vessels: - // return ( - // - // ) + case DataviewCategory.Vessels: + return ( + + ) default: return null diff --git a/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx b/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx index fb25ef000a..3860184884 100644 --- a/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx +++ b/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx @@ -4,7 +4,7 @@ import { groupBy } from 'lodash' import { useSelector } from 'react-redux' import { Icon } from '@globalfishingwatch/ui-components' import { EventTypes, IdentityVessel, SelfReportedInfo } from '@globalfishingwatch/api-types' -import { TooltipEventFeature } from 'features/map/map.hooks' +import { VesselEventPickingObject } from '@globalfishingwatch/deck-layers' import { getEventDescriptionComponent } from 'utils/events' import { selectVisibleResources } from 'features/resources/resources.selectors' import { getVesselProperty } from 'features/vessel/vessel.utils' @@ -12,7 +12,7 @@ import { MAX_TOOLTIP_LIST } from '../map.slice' import styles from './Popup.module.css' type VesselEventsTooltipRowProps = { - features: TooltipEventFeature[] + features: VesselEventPickingObject[] showFeaturesDetails?: boolean } @@ -26,14 +26,15 @@ function VesselEventsTooltipSection({ const maxFeatures = overflows ? features.slice(0, MAX_TOOLTIP_LIST) : features return groupBy(maxFeatures, 'properties.vesselId') }, [overflows, features]) + console.log('🚀 ~ featuresByType ~ featuresByType:', featuresByType) const resources = useSelector(selectVisibleResources) const vesselNamesByType = useMemo(() => { return Object.values(featuresByType).map((features) => { - const vesselId = features[0].properties.vesselId + const vesselId = features[0].properties?.vesselId const vesselResource = Object.values(resources).find((resource) => { return (resource.data as IdentityVessel)?.selfReportedInfo?.some( - (identity: SelfReportedInfo) => vesselId.includes(identity.id) + (identity: SelfReportedInfo) => vesselId?.includes(identity.id) ) }) @@ -56,7 +57,7 @@ function VesselEventsTooltipSection({ {vesselNamesByType[index] && showFeaturesDetails && (

      {vesselNamesByType[index]}

      )} - {featureByType.map((feature, index) => { + {/* {featureByType.map((feature, index) => { const { start, end, @@ -82,7 +83,7 @@ function VesselEventsTooltipSection({ {showFeaturesDetails ? DescriptionComponent : description} ) - })} + })} */} {overflows && (
      + {features.length - MAX_TOOLTIP_LIST} {t('common.more', 'more')} diff --git a/libs/deck-layer-composer/src/resolvers/vessels.ts b/libs/deck-layer-composer/src/resolvers/vessels.ts index b95949906f..a71a6737c5 100644 --- a/libs/deck-layer-composer/src/resolvers/vessels.ts +++ b/libs/deck-layer-composer/src/resolvers/vessels.ts @@ -18,6 +18,7 @@ export function resolveDeckVesselLayerProps( return { id: dataview.id, visible: dataview.config?.visible ?? true, + category: dataview.category!, name: dataview.config?.name!, endTime: getUTCDateTime(globalConfig.end!).toMillis(), startTime: getUTCDateTime(globalConfig.start!).toMillis(), diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 7783944684..142a82b5a0 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -11,6 +11,9 @@ import { FourwingsPickingObject, FourwingsTileLayerColorDomain, FourwingsTileLayerColorRange, + RulerPickingInfo, + RulerPickingObject, + RulersLayer, UserContextPickingObject, } from '@globalfishingwatch/deck-layers' @@ -59,8 +62,10 @@ export type DeckLayerInteractionFeature = | ContextPickingObject | UserContextPickingObject | ClusterPickingObject + | RulerPickingObject export type DeckLayerInteractionPickingInfo = | (FourwingsPickingInfo & { layer: FourwingsLayer }) | (ContextPickingInfo & { layer: ContextLayer }) | (ClusterPickingInfo & { layer: ClusterLayer }) + | (RulerPickingInfo & { layer: RulersLayer }) diff --git a/libs/deck-layers/src/index.ts b/libs/deck-layers/src/index.ts index acddfb812d..dcb8eb22c7 100644 --- a/libs/deck-layers/src/index.ts +++ b/libs/deck-layers/src/index.ts @@ -3,6 +3,6 @@ export * from './layers/cluster' export * from './layers/context' export * from './layers/fourwings' export * from './layers/rulers' -export * from './layers/vessel/VesselLayer' +export * from './layers/vessel' export * from './types' export * from './utils' diff --git a/libs/deck-layers/src/layers/rulers/index.ts b/libs/deck-layers/src/layers/rulers/index.ts index 30ff6f21ea..5e739fe40b 100644 --- a/libs/deck-layers/src/layers/rulers/index.ts +++ b/libs/deck-layers/src/layers/rulers/index.ts @@ -1 +1,2 @@ export * from './RulersLayer' +export * from './rulers.types' diff --git a/libs/deck-layers/src/layers/rulers/rulers.types.ts b/libs/deck-layers/src/layers/rulers/rulers.types.ts index e2cbdf3626..db17c30595 100644 --- a/libs/deck-layers/src/layers/rulers/rulers.types.ts +++ b/libs/deck-layers/src/layers/rulers/rulers.types.ts @@ -1,6 +1,8 @@ -import { Color } from '@deck.gl/core' +import { Color, PickingInfo } from '@deck.gl/core' import { GeoJsonLayerProps } from '@deck.gl/layers' -import { BaseLayerProps } from '../../types' +import { Feature, Point } from 'geojson' +import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' +import { BasePickingInfo } from '../../types' export type RulerPointProperties = { id?: number @@ -25,3 +27,7 @@ export type RulersLayerProps = GeoJsonLayerProps & { rulers: RulerData[] color?: Color } + +export type RulerFeature = Feature +export type RulerPickingObject = RulerFeature & BasePickingInfo +export type RulerPickingInfo = PickingInfo diff --git a/libs/deck-layers/src/layers/vessel/VesselLayer.ts b/libs/deck-layers/src/layers/vessel/VesselLayer.ts index 4d76417bd6..1671b9b420 100644 --- a/libs/deck-layers/src/layers/vessel/VesselLayer.ts +++ b/libs/deck-layers/src/layers/vessel/VesselLayer.ts @@ -1,11 +1,10 @@ import { DataFilterExtension } from '@deck.gl/extensions' import { CompositeLayer, Layer, LayersList, LayerProps, Color } from '@deck.gl/core' -// Layers import { ApiEvent, + DataviewCategory, EventTypes, EventVessel, - ResourceStatus, TrackSegment, } from '@globalfishingwatch/api-types' import { @@ -13,37 +12,52 @@ import { VesselEventsLoader, VesselTrackLoader, } from '@globalfishingwatch/deck-loaders' +import { GFWAPI } from '@globalfishingwatch/api-client' import { deckToHexColor } from '../../utils/colors' import { getLayerGroupOffset, LayerGroup } from '../../utils' +import { BaseLayerProps } from '../../types' import { VesselEventsLayer, _VesselEventsLayerProps } from './VesselEventsLayer' import { VesselTrackLayer, _VesselTrackLayerProps } from './VesselTrackLayer' import { getVesselTrackThunks } from './vessel.utils' -import { EVENTS_COLORS } from './vessel.config' +import { EVENTS_COLORS, TRACK_LAYER_TYPE } from './vessel.config' +import { + VesselDataStatus, + VesselDataType, + VesselDeckLayersEvent, + VesselEventPickingInfo, + VesselEventPickingObject, + VesselEventProperties, + _VesselLayerProps, +} from './vessel.types' -export const TRACK_LAYER_TYPE = 'track' -export interface VesselDeckLayersEvent { - type: EventTypes - url: string -} -export type VesselDataType = typeof TRACK_LAYER_TYPE | EventTypes -export type VesselDataStatus = { - type: VesselDataType - status: ResourceStatus -} -export type _VesselLayerProps = { - name: string - color: Color - visible: boolean - onVesselDataLoad?: (layers: VesselDataStatus[]) => void -} export type VesselEventsLayerProps = Omit<_VesselEventsLayerProps, 'type'> & { events: VesselDeckLayersEvent[] } -export type VesselLayerProps = _VesselTrackLayerProps & VesselEventsLayerProps & _VesselLayerProps + +export type VesselLayerProps = BaseLayerProps & + _VesselTrackLayerProps & + VesselEventsLayerProps & + _VesselLayerProps export class VesselLayer extends CompositeLayer { dataStatus: VesselDataStatus[] = [] + getPickingInfo = ({ info }: { info: VesselEventPickingInfo }): VesselEventPickingInfo => { + if (!info.object) { + info.object = {} as VesselEventPickingObject + info.object.properties = {} as VesselEventProperties + } + info.object.category = DataviewCategory.Vessels + info.object.properties = { + ...info.object.properties, + vesselId: this.props.id, + } + // info.object.getDetail = async () => { + // return GFWAPI.fetch(`/events/${info.object?.properties.id}`) + // } + return info + } + onSublayerError = (error: any) => { console.warn(error) this.setState({ error }) @@ -105,6 +119,7 @@ export class VesselLayer extends CompositeLayer { type, onError: this.onSublayerError, loaders: [VesselEventsLoader], + // loaderOptions: { worker: false }, pickable: true, getPolygonOffset: (params: any) => getLayerGroupOffset(LayerGroup.Point, params), getFillColor: (d: any): Color => { diff --git a/libs/deck-layers/src/layers/vessel/index.ts b/libs/deck-layers/src/layers/vessel/index.ts new file mode 100644 index 0000000000..2ebdd38632 --- /dev/null +++ b/libs/deck-layers/src/layers/vessel/index.ts @@ -0,0 +1,2 @@ +export * from './VesselLayer' +export * from './vessel.types' diff --git a/libs/deck-layers/src/layers/vessel/vessel.config.ts b/libs/deck-layers/src/layers/vessel/vessel.config.ts index 9598b4a221..e844dffc73 100644 --- a/libs/deck-layers/src/layers/vessel/vessel.config.ts +++ b/libs/deck-layers/src/layers/vessel/vessel.config.ts @@ -2,6 +2,8 @@ import { Color } from '@deck.gl/core' import { EventTypes } from '@globalfishingwatch/api-types' import { hexToDeckColor } from '../../utils' +export const TRACK_LAYER_TYPE = 'track' + type EventShape = 'circle' | 'square' | 'diamond' | 'diamondStroke' export const SHAPES_ORDINALS: Record = { circle: 0, diff --git a/libs/deck-layers/src/layers/vessel/vessel.types.ts b/libs/deck-layers/src/layers/vessel/vessel.types.ts new file mode 100644 index 0000000000..d814bbf598 --- /dev/null +++ b/libs/deck-layers/src/layers/vessel/vessel.types.ts @@ -0,0 +1,38 @@ +import { Color, PickingInfo } from '@deck.gl/core' +import { Feature, LineString, MultiLineString, Point } from 'geojson' +import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' +import { ApiEvent, EventTypes, ResourceStatus } from '@globalfishingwatch/api-types' +import { BasePickingInfo } from '../../types' +import { TRACK_LAYER_TYPE } from './vessel.config' + +export interface VesselDeckLayersEvent { + type: EventTypes + url: string +} + +export type VesselDataType = typeof TRACK_LAYER_TYPE | EventTypes + +export type VesselDataStatus = { + type: VesselDataType + status: ResourceStatus +} +export type _VesselLayerProps = { + name: string + color: Color + visible: boolean + onVesselDataLoad?: (layers: VesselDataStatus[]) => void +} + +// type VesselTrackProperties = { +// timestamp: number +// } +// export type VesselTrackFeature = Feature +// export type VesselTrackPickingObject = VesselTrackFeature & BasePickingInfo +// export type VesselTrackPickingInfo = PickingInfo + +export type VesselEventProperties = ApiEvent & { + vesselId: string +} +export type VesselEventFeature = Feature +export type VesselEventPickingObject = VesselEventFeature & BasePickingInfo +export type VesselEventPickingInfo = PickingInfo diff --git a/libs/deck-layers/src/types.ts b/libs/deck-layers/src/types.ts index 3eb645902e..6ea585d7e2 100644 --- a/libs/deck-layers/src/types.ts +++ b/libs/deck-layers/src/types.ts @@ -6,12 +6,14 @@ import type { FourwingsLayer } from './layers/fourwings/FourwingsLayer' import type { VesselLayer } from './layers/vessel/VesselLayer' import type { RulersLayer } from './layers/rulers/RulersLayer' +export type DeckLayerCategory = `${DataviewCategory}` | 'rulers' + export type BaseLayerProps = { - category: DataviewCategory + category: DeckLayerCategory } export type BasePickingInfo = { - category: DataviewCategory + category: DeckLayerCategory } export type AnyDeckLayer = diff --git a/libs/deck-loaders/project.json b/libs/deck-loaders/project.json index 68d5cdea4d..5d9e96c46f 100644 --- a/libs/deck-loaders/project.json +++ b/libs/deck-loaders/project.json @@ -23,6 +23,7 @@ "commands": [ "nx fourwings:worker deck-loaders", "nx tracks:worker deck-loaders", + "nx events:worker deck-loaders", "rm -rf dist/libs/deck-loaders/workers/package.json" ], "parallel": false @@ -66,6 +67,25 @@ "minify": false } }, + "events:worker": { + "executor": "@nx/esbuild:esbuild", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/libs/deck-loaders/workers", + "outputFileName": "vessel-events-worker.js", + "main": "libs/deck-loaders/src/vessels/workers/events-worker.ts", + "generateExportsField": true, + "tsConfig": "libs/deck-loaders/tsconfig.lib.json", + "watch": false, + "target": "esnext", + "platform": "browser", + "deleteOutputPath": false, + "bundle": true, + "thirdParty": true, + "generatePackageJson": false, + "minify": false + } + }, "publish": { "executor": "@nx/js:release-publish", "dependsOn": ["dist"], diff --git a/libs/deck-loaders/src/vessels/events-loader.ts b/libs/deck-loaders/src/vessels/events-loader.ts index 8a7fb1e865..2f5399fea6 100644 --- a/libs/deck-loaders/src/vessels/events-loader.ts +++ b/libs/deck-loaders/src/vessels/events-loader.ts @@ -1,5 +1,6 @@ import type { Loader, LoaderWithParser } from '@loaders.gl/loader-utils' import packageJson from '../../package.json' +import { PATH_BASENAME } from '../loaders.config' import { parseEvents } from './lib/parse-events' /** @@ -12,8 +13,12 @@ export const VesselEventsWorkerLoader: Loader = { version: packageJson.version, extensions: ['json'], mimeTypes: ['application/json'], - worker: false, - options: {}, + worker: true, + options: { + 'vessel-events': { + workerUrl: `${PATH_BASENAME}/workers/vessel-events-worker.js`, + }, + }, } /** From c186d27f8ead8e7ed3c240ad6d9394c1676f31fb Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 19:13:57 +0200 Subject: [PATCH 22/43] fix vessel event interaction tooltip --- .../features/map/popups/PopupWrapper.tsx | 8 ++- .../map/popups/VesselEventsLayers.tsx | 31 ++-------- apps/fishing-map/utils/events.tsx | 61 ++++++------------- libs/deck-layer-composer/src/types.ts | 5 ++ .../src/layers/vessel/VesselLayer.ts | 8 +-- .../src/layers/vessel/vessel.types.ts | 6 +- 6 files changed, 43 insertions(+), 76 deletions(-) diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index 3a50e3641a..cb1dda9f02 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -7,7 +7,11 @@ import { useSelector } from 'react-redux' import { DataviewCategory, DataviewType } from '@globalfishingwatch/api-types' import { IconButton, Spinner } from '@globalfishingwatch/ui-components' import { InteractionEvent } from '@globalfishingwatch/deck-layer-composer' -import { ContextFeature, FourwingsPickingObject } from '@globalfishingwatch/deck-layers' +import { + ContextFeature, + FourwingsPickingObject, + VesselEventPickingObject, +} from '@globalfishingwatch/deck-layers' import { TooltipEvent } from 'features/map/map.hooks' import { POPUP_CATEGORY_ORDER } from 'data/config' import { useTimeCompareTimeDescription } from 'features/reports/reports-timecomparison.hooks' @@ -239,7 +243,7 @@ function PopupWrapper({ return ( ) diff --git a/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx b/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx index 3860184884..d2638d7fc4 100644 --- a/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx +++ b/apps/fishing-map/features/map/popups/VesselEventsLayers.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { groupBy } from 'lodash' import { useSelector } from 'react-redux' import { Icon } from '@globalfishingwatch/ui-components' -import { EventTypes, IdentityVessel, SelfReportedInfo } from '@globalfishingwatch/api-types' +import { IdentityVessel, SelfReportedInfo } from '@globalfishingwatch/api-types' import { VesselEventPickingObject } from '@globalfishingwatch/deck-layers' import { getEventDescriptionComponent } from 'utils/events' import { selectVisibleResources } from 'features/resources/resources.selectors' @@ -26,12 +26,12 @@ function VesselEventsTooltipSection({ const maxFeatures = overflows ? features.slice(0, MAX_TOOLTIP_LIST) : features return groupBy(maxFeatures, 'properties.vesselId') }, [overflows, features]) - console.log('🚀 ~ featuresByType ~ featuresByType:', featuresByType) + const resources = useSelector(selectVisibleResources) const vesselNamesByType = useMemo(() => { return Object.values(featuresByType).map((features) => { - const vesselId = features[0].properties?.vesselId + const vesselId = features[0]?.vesselId const vesselResource = Object.values(resources).find((resource) => { return (resource.data as IdentityVessel)?.selfReportedInfo?.some( (identity: SelfReportedInfo) => vesselId?.includes(identity.id) @@ -57,33 +57,14 @@ function VesselEventsTooltipSection({ {vesselNamesByType[index] && showFeaturesDetails && (

      {vesselNamesByType[index]}

      )} - {/* {featureByType.map((feature, index) => { - const { - start, - end, - type, - vesselName, - encounterVesselName, - encounterVesselId, - portName, - portFlag, - } = feature.properties - const { description, DescriptionComponent } = getEventDescriptionComponent({ - start: start as any, - end: end as any, - type: type as EventTypes, - mainVesselName: vesselName, - encounterVesselName, - encounterVesselId, - portName, - portFlag, - }) + {featureByType.map((feature, index) => { + const { description, DescriptionComponent } = getEventDescriptionComponent(feature) return (
      {showFeaturesDetails ? DescriptionComponent : description}
      ) - })} */} + })} {overflows && (
      + {features.length - MAX_TOOLTIP_LIST} {t('common.more', 'more')} diff --git a/apps/fishing-map/utils/events.tsx b/apps/fishing-map/utils/events.tsx index 9e3908a8eb..14e7a6e946 100644 --- a/apps/fishing-map/utils/events.tsx +++ b/apps/fishing-map/utils/events.tsx @@ -1,7 +1,7 @@ import { Fragment } from 'react' import { DateTime, Duration } from 'luxon' import { Trans } from 'react-i18next' -import { EventTypes } from '@globalfishingwatch/api-types' +import { ApiEvent, EventTypes } from '@globalfishingwatch/api-types' import { t } from 'features/i18n/i18n' import { formatI18nDate } from 'features/i18n/i18nDate' import { EVENTS_COLORS } from 'data/config' @@ -9,19 +9,7 @@ import { formatInfoField } from 'utils/info' import VesselPin from 'features/vessel/VesselPin' import { SupportedDateType, getUTCDateTime } from './dates' -type EventProps = { - start: number - end: number - type: EventTypes - mainVesselName?: string - encounterVesselName?: string - encounterVesselId?: string - className?: string - portName?: string - portFlag?: string -} - -export const getEventColors = ({ type }: { type: EventProps['type'] }) => { +export const getEventColors = ({ type }: { type: ApiEvent['type'] }) => { const colorKey = type // TODO not supporting authorization status yet // if (event.type === 'encounter' && showAuthorizationStatus) { @@ -80,16 +68,17 @@ export const getEventDescription = ({ start, end, type, - mainVesselName, - encounterVesselName, - portName, - portFlag, -}: EventProps) => { + vessel, + encounter, + port_visit, +}: ApiEvent) => { const time = getTimeLabels({ start, end }) let description: string - let descriptionGeneric + let descriptionGeneric: string switch (type) { case EventTypes.Encounter: { + const mainVesselName = vessel?.name + const encounterVesselName = encounter?.vessel?.name if (mainVesselName && encounterVesselName) { description = t( 'event.encounterActionWithVessels', @@ -116,7 +105,9 @@ export const getEventDescription = ({ descriptionGeneric = t('event.encounter') break } - case EventTypes.Port: + case EventTypes.Port: { + const portName = port_visit?.intermediateAnchorage.name + const portFlag = port_visit?.intermediateAnchorage.flag if (portName && portFlag) { const portLabel = [ formatInfoField(portName, 'port'), @@ -132,6 +123,7 @@ export const getEventDescription = ({ } descriptionGeneric = t('event.port') break + } case EventTypes.Loitering: description = t( 'event.loiteringAction', @@ -155,28 +147,13 @@ export const getEventDescription = ({ } } -export const getEventDescriptionComponent = ({ - start, - end, - type, - mainVesselName, - encounterVesselName, - encounterVesselId, - className, - portName, - portFlag, -}: EventProps) => { - let DescriptionComponent +export const getEventDescriptionComponent = (event: ApiEvent, className = '') => { + const { start, end, type, encounter } = event const { color, colorLabels } = getEventColors({ type }) - const { descriptionGeneric, description } = getEventDescription({ - start, - end, - type, - mainVesselName, - encounterVesselName, - portName, - portFlag, - }) + let DescriptionComponent + const encounterVesselName = encounter?.vessel?.name + const encounterVesselId = encounter?.vessel?.id + const { descriptionGeneric, description } = getEventDescription(event) if (type === EventTypes.Encounter && encounterVesselName && encounterVesselId) { const time = getTimeLabels({ start, end }) DescriptionComponent = ( diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 142a82b5a0..41469d73ec 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -15,6 +15,9 @@ import { RulerPickingObject, RulersLayer, UserContextPickingObject, + VesselEventPickingInfo, + VesselEventPickingObject, + VesselLayer, } from '@globalfishingwatch/deck-layers' export const DECK_LAYER_LIFECYCLE = { @@ -63,9 +66,11 @@ export type DeckLayerInteractionFeature = | UserContextPickingObject | ClusterPickingObject | RulerPickingObject + | VesselEventPickingObject export type DeckLayerInteractionPickingInfo = | (FourwingsPickingInfo & { layer: FourwingsLayer }) | (ContextPickingInfo & { layer: ContextLayer }) | (ClusterPickingInfo & { layer: ClusterLayer }) | (RulerPickingInfo & { layer: RulersLayer }) + | (VesselEventPickingInfo & { layer: VesselLayer }) diff --git a/libs/deck-layers/src/layers/vessel/VesselLayer.ts b/libs/deck-layers/src/layers/vessel/VesselLayer.ts index 1671b9b420..f14df527a3 100644 --- a/libs/deck-layers/src/layers/vessel/VesselLayer.ts +++ b/libs/deck-layers/src/layers/vessel/VesselLayer.ts @@ -45,13 +45,11 @@ export class VesselLayer extends CompositeLayer { getPickingInfo = ({ info }: { info: VesselEventPickingInfo }): VesselEventPickingInfo => { if (!info.object) { info.object = {} as VesselEventPickingObject - info.object.properties = {} as VesselEventProperties } + info.object.title = this.props.name + info.object.vesselId = this.props.id info.object.category = DataviewCategory.Vessels - info.object.properties = { - ...info.object.properties, - vesselId: this.props.id, - } + info.object.color = deckToHexColor(this.props.color) // info.object.getDetail = async () => { // return GFWAPI.fetch(`/events/${info.object?.properties.id}`) // } diff --git a/libs/deck-layers/src/layers/vessel/vessel.types.ts b/libs/deck-layers/src/layers/vessel/vessel.types.ts index d814bbf598..891e2b77d4 100644 --- a/libs/deck-layers/src/layers/vessel/vessel.types.ts +++ b/libs/deck-layers/src/layers/vessel/vessel.types.ts @@ -31,8 +31,10 @@ export type _VesselLayerProps = { // export type VesselTrackPickingInfo = PickingInfo export type VesselEventProperties = ApiEvent & { + color: string + title: string vesselId: string } -export type VesselEventFeature = Feature -export type VesselEventPickingObject = VesselEventFeature & BasePickingInfo + +export type VesselEventPickingObject = VesselEventProperties & BasePickingInfo export type VesselEventPickingInfo = PickingInfo From 9c4809b3cdd85534864b7e74e435a80a6392e498 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 20:09:21 +0200 Subject: [PATCH 23/43] move types to deck-layers --- libs/deck-layer-composer/src/types.ts | 15 ----------- .../src/layers/cluster/ClusterLayer.ts | 1 + .../src/layers/context/context.types.ts | 4 +-- .../layers/fourwings/FourwingsHeatmapLayer.ts | 19 ++++++++++---- .../fourwings/FourwingsHeatmapStaticLayer.ts | 5 ++-- .../fourwings/FourwingsPositionsTileLayer.ts | 6 +++-- .../src/layers/fourwings/fourwings.types.ts | 24 ++++++++++-------- .../src/layers/vessel/VesselLayer.ts | 1 + libs/deck-layers/src/types.ts | 25 +++++++++++++++++++ libs/deck-layers/src/utils/layers.ts | 11 ++++---- 10 files changed, 68 insertions(+), 43 deletions(-) diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 41469d73ec..7e07be9cd9 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -59,18 +59,3 @@ export interface DeckLegendBivariate extends DeckLegend { sublayersBreaks: [number[], number[]] bivariateRamp: string[] } - -export type DeckLayerInteractionFeature = - | FourwingsPickingObject - | ContextPickingObject - | UserContextPickingObject - | ClusterPickingObject - | RulerPickingObject - | VesselEventPickingObject - -export type DeckLayerInteractionPickingInfo = - | (FourwingsPickingInfo & { layer: FourwingsLayer }) - | (ContextPickingInfo & { layer: ContextLayer }) - | (ClusterPickingInfo & { layer: ClusterLayer }) - | (RulerPickingInfo & { layer: RulersLayer }) - | (VesselEventPickingInfo & { layer: VesselLayer }) diff --git a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts index 1135f5ae9f..e0117661e9 100644 --- a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts +++ b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts @@ -28,6 +28,7 @@ export class ClusterLayer extends CompositeLayer { let { object } = info if (object) { + object.layerId = this.props.id object.category = this.props.category } return info diff --git a/libs/deck-layers/src/layers/context/context.types.ts b/libs/deck-layers/src/layers/context/context.types.ts index 44ebb04a30..e7f863c437 100644 --- a/libs/deck-layers/src/layers/context/context.types.ts +++ b/libs/deck-layers/src/layers/context/context.types.ts @@ -30,8 +30,8 @@ export type ContextLayerProps = { color: string idProperty?: string valueProperties?: string[] - hoveredFeatures?: PickingInfo[] - clickedFeatures?: PickingInfo[] + hoveredFeatures?: ContextPickingObject[] + clickedFeatures?: ContextPickingObject[] } export type ContextFeatureProperties = { diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts index 5d45dc205a..fc513fed75 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts @@ -25,8 +25,17 @@ export class FourwingsHeatmapLayer extends CompositeLayer }): FourwingsPickingInfo => { - const { id, tile, startTime, endTime, availableIntervals, category, sublayers, tilesCache } = - this.props + const { + id, + tile, + startTime, + endTime, + availableIntervals, + category, + sublayers, + tilesCache, + comparisonMode, + } = this.props const { startFrame, endFrame, interval } = getIntervalFrames({ startTime, @@ -36,6 +45,7 @@ export class FourwingsHeatmapLayer extends CompositeLayer f.layer?.id === this.root.id - )?.object + const layerHoveredFeature = hoveredFeatures?.find((f) => f.layerId === this.root.id) if (layerHoveredFeature) { this.layers.push( new PathLayer( diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts index 6879d38c30..fab43dd262 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts @@ -146,6 +146,7 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< if (info.object?.properties?.count) { info.object.properties.values = [[info.object.properties.count]] } + info.object.layerId = this.props.id info.object.category = this.props.category return info } @@ -208,9 +209,7 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< }), ] - const layerHoveredFeature: FourwingsStaticFeature = this.props.hoveredFeatures?.find( - (f) => f.layer?.id === this.root.id - )?.object + const layerHoveredFeature = this.props.hoveredFeatures?.find((f) => f.layerId === this.root.id) if (layerHoveredFeature) { layers.push( diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts index 90ac0d174e..eec4a73180 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts @@ -165,10 +165,12 @@ export class FourwingsPositionsTileLayer extends CompositeLayer< updateState() { const clickedVesselId = this.props?.clickedFeatures?.flatMap( - (f) => f.sourceLayer?.id === this.id && f.object?.properties?.id + // TODO:deck review if this still works after the clickedFeatures refactor + (f: any) => f.sourceLayer?.id === this.id && f.object?.properties?.id ) const highlightedVesselId = this.props?.hoveredFeatures?.flatMap( - (f) => f.sourceLayer?.id === this.id && f.object?.properties?.id + // TODO:deck review if this still works after the clickedFeatures refactor + (f: any) => f.sourceLayer?.id === this.id && f.object?.properties?.id ) if (highlightedVesselId && highlightedVesselId[0]) { this.setState({ diff --git a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts index 37df7d7e62..ed37355379 100644 --- a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts +++ b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts @@ -11,6 +11,7 @@ import { FourwingsInterval, Cell, } from '@globalfishingwatch/deck-loaders' +import { BasePickingInfo } from '../../types' import { HEATMAP_ID, POSITIONS_ID } from './fourwings.config' export type FourwingsSublayerId = string @@ -59,15 +60,16 @@ export type SublayerColorRanges = ColorRange[] export type FourwingsPickingObject = FourwingsFeature< FourwingsFeatureProperties & Partial -> & { - title: string - tile: { x: number; y: number; z: number } - startTime: number - endTime: number - interval: FourwingsInterval - category: string - sublayers: FourwingsDeckSublayer[] -} +> & + BasePickingInfo & { + title: string + tile: { x: number; y: number; z: number } + startTime: number + endTime: number + interval: FourwingsInterval + sublayers: FourwingsDeckSublayer[] + comparisonMode?: FourwingsComparisonMode + } export type FourwingsPickingInfo = PickingInfo export type FourwingsHeatmapLayerProps = FourwingsHeatmapTileLayerProps & { @@ -108,8 +110,8 @@ type BaseFourwingsLayerProps = { category: DataviewCategory sublayers: FourwingsDeckSublayer[] tilesUrl?: string - clickedFeatures?: PickingInfo[] - hoveredFeatures?: PickingInfo[] + clickedFeatures?: FourwingsPickingObject[] + hoveredFeatures?: FourwingsPickingObject[] } export type FourwingsResolution = 'default' | 'high' diff --git a/libs/deck-layers/src/layers/vessel/VesselLayer.ts b/libs/deck-layers/src/layers/vessel/VesselLayer.ts index f14df527a3..44b31556e2 100644 --- a/libs/deck-layers/src/layers/vessel/VesselLayer.ts +++ b/libs/deck-layers/src/layers/vessel/VesselLayer.ts @@ -46,6 +46,7 @@ export class VesselLayer extends CompositeLayer { if (!info.object) { info.object = {} as VesselEventPickingObject } + info.object.layerId = this.props.id info.object.title = this.props.name info.object.vesselId = this.props.id info.object.category = DataviewCategory.Vessels diff --git a/libs/deck-layers/src/types.ts b/libs/deck-layers/src/types.ts index 6ea585d7e2..125ab4aa39 100644 --- a/libs/deck-layers/src/types.ts +++ b/libs/deck-layers/src/types.ts @@ -5,6 +5,15 @@ import type { ContextLayer } from './layers/context/ContextLayer' import type { FourwingsLayer } from './layers/fourwings/FourwingsLayer' import type { VesselLayer } from './layers/vessel/VesselLayer' import type { RulersLayer } from './layers/rulers/RulersLayer' +import { ClusterPickingObject, ClusterPickingInfo, ClusterLayer } from './layers/cluster' +import { + ContextPickingObject, + UserContextPickingObject, + ContextPickingInfo, +} from './layers/context' +import { FourwingsPickingObject, FourwingsPickingInfo } from './layers/fourwings' +import { RulerPickingObject, RulerPickingInfo } from './layers/rulers' +import { VesselEventPickingObject, VesselEventPickingInfo } from './layers/vessel' export type DeckLayerCategory = `${DataviewCategory}` | 'rulers' @@ -13,6 +22,7 @@ export type BaseLayerProps = { } export type BasePickingInfo = { + layerId: string category: DeckLayerCategory } @@ -25,3 +35,18 @@ export type AnyDeckLayer = | RulersLayer export type LayerWithIndependentSublayersLoadState = VesselLayer + +export type DeckLayerInteractionFeature = + | FourwingsPickingObject + | ContextPickingObject + | UserContextPickingObject + | ClusterPickingObject + | RulerPickingObject + | VesselEventPickingObject + +export type DeckLayerInteractionPickingInfo = + | (FourwingsPickingInfo & { layer: FourwingsLayer }) + | (ContextPickingInfo & { layer: ContextLayer }) + | (ClusterPickingInfo & { layer: ClusterLayer }) + | (RulerPickingInfo & { layer: RulersLayer }) + | (VesselEventPickingInfo & { layer: VesselLayer }) diff --git a/libs/deck-layers/src/utils/layers.ts b/libs/deck-layers/src/utils/layers.ts index 3dcd9359c3..d97c1a3ef9 100644 --- a/libs/deck-layers/src/utils/layers.ts +++ b/libs/deck-layers/src/utils/layers.ts @@ -1,22 +1,23 @@ -import { COORDINATE_SYSTEM, PickingInfo } from '@deck.gl/core' +import { COORDINATE_SYSTEM } from '@deck.gl/core' import { ClipExtension } from '@deck.gl/extensions' import { TileLayerProps } from '@deck.gl/geo-layers' import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' import { Matrix4 } from '@math.gl/core' +import { DeckLayerInteractionFeature } from '../types' const WORLD_SIZE = 512 export function getPickedFeatureToHighlight( data: any, - pickedFeatures: PickingInfo[], + pickedFeatures: DeckLayerInteractionFeature[], idProperty: string ) { return ( pickedFeatures && pickedFeatures.some( - (f: PickingInfo) => - f.object.type === 'Feature' && - f.object.properties[idProperty] === data.properties[idProperty] + (f) => + // TODO:deck remove this any and fix typings + f.type === 'Feature' && (f.properties as any)?.[idProperty] === data.properties[idProperty] ) ) } From 66938b3f8f11574766b5f9fb3c155924e23afda5 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 20:09:35 +0200 Subject: [PATCH 24/43] improve resolvers typing --- .../src/interactions/types.ts | 2 +- .../src/resolvers/basemap.ts | 4 ++-- .../src/resolvers/clusters.ts | 4 ++-- .../src/resolvers/context.ts | 12 +++++------ .../src/resolvers/fourwings.ts | 14 ++++++------- .../src/resolvers/index.ts | 20 ++++++++++++++----- .../src/resolvers/tile-cluster.ts | 7 ++++--- .../src/resolvers/types.ts | 17 +++++++++++++++- .../src/resolvers/vessels.ts | 15 +++++++------- 9 files changed, 59 insertions(+), 36 deletions(-) diff --git a/libs/deck-layer-composer/src/interactions/types.ts b/libs/deck-layer-composer/src/interactions/types.ts index 9d2e364ae3..84648e27f6 100644 --- a/libs/deck-layer-composer/src/interactions/types.ts +++ b/libs/deck-layer-composer/src/interactions/types.ts @@ -1,4 +1,4 @@ -import { DeckLayerInteractionFeature } from '../types' +import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layers' export type InteractionEvent = { type: 'click' | 'hover' diff --git a/libs/deck-layer-composer/src/resolvers/basemap.ts b/libs/deck-layer-composer/src/resolvers/basemap.ts index 161cc6e01c..0d6bc42920 100644 --- a/libs/deck-layer-composer/src/resolvers/basemap.ts +++ b/libs/deck-layer-composer/src/resolvers/basemap.ts @@ -1,7 +1,7 @@ -import { DataviewInstance } from '@globalfishingwatch/api-types' import { BaseMapLayerProps, BasemapType } from '@globalfishingwatch/deck-layers' +import { DeckResolverFunction } from './types' -export function resolveDeckBasemapLayerProps(dataview: DataviewInstance): BaseMapLayerProps { +export const resolveDeckBasemapLayerProps: DeckResolverFunction = (dataview) => { return { id: dataview.id, category: dataview.category!, diff --git a/libs/deck-layer-composer/src/resolvers/clusters.ts b/libs/deck-layer-composer/src/resolvers/clusters.ts index 594cfdf7ef..72137320d7 100644 --- a/libs/deck-layer-composer/src/resolvers/clusters.ts +++ b/libs/deck-layer-composer/src/resolvers/clusters.ts @@ -1,10 +1,10 @@ import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { ClusterLayerProps } from '@globalfishingwatch/deck-layers' import { resolveEndpoint } from '@globalfishingwatch/datasets-client' -import { ResolverGlobalConfig } from './types' +import { DeckResolverFunction, ResolverGlobalConfig } from './types' // TODO: decide if include static here or create a new one -export const resolveDeckClusterLayerProps = ( +export const resolveDeckClusterLayerProps: DeckResolverFunction = ( dataview: UrlDataviewInstance, { start, end }: ResolverGlobalConfig ): ClusterLayerProps => { diff --git a/libs/deck-layer-composer/src/resolvers/context.ts b/libs/deck-layer-composer/src/resolvers/context.ts index b68bcb45f7..4b16a7cca3 100644 --- a/libs/deck-layer-composer/src/resolvers/context.ts +++ b/libs/deck-layer-composer/src/resolvers/context.ts @@ -1,6 +1,7 @@ import { PickingInfo } from '@deck.gl/core' import { Dataset, DatasetTypes, DataviewInstance } from '@globalfishingwatch/api-types' import { + ContextFeature, ContextLayerConfig, ContextLayerId, ContextLayerProps, @@ -11,13 +12,12 @@ import { getDatasetConfiguration, resolveEndpoint, } from '@globalfishingwatch/datasets-client' -import { ResolverGlobalConfig } from '../resolvers/types' +import { DeckResolverFunction, ResolverGlobalConfig } from './types' -export function resolveDeckContextLayerProps( - dataview: DataviewInstance, - globalConfig: ResolverGlobalConfig, - interactions: PickingInfo[] -): ContextLayerProps { +export const resolveDeckContextLayerProps: DeckResolverFunction< + ContextLayerProps, + ContextFeature +> = (dataview, globalConfig, interactions) => { // TODO make this work for auxiliar layers // https://github.com/GlobalFishingWatch/frontend/blob/master/libs/dataviews-client/src/resolve-dataviews-generators.ts#L606 const { url } = resolveDataviewDatasetResource(dataview, DatasetTypes.Context) diff --git a/libs/deck-layer-composer/src/resolvers/fourwings.ts b/libs/deck-layer-composer/src/resolvers/fourwings.ts index 47862f9080..cd0f720412 100644 --- a/libs/deck-layer-composer/src/resolvers/fourwings.ts +++ b/libs/deck-layer-composer/src/resolvers/fourwings.ts @@ -1,11 +1,10 @@ -import { PickingInfo } from '@deck.gl/core' import { uniq } from 'lodash' -import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { FourwingsAggregationOperation, FourwingsComparisonMode, FourwingsDeckSublayer, FourwingsLayerProps, + FourwingsPickingObject, FourwingsVisualizationMode, TIME_COMPARISON_NOT_SUPPORTED_INTERVALS, getUTCDateTime, @@ -19,14 +18,13 @@ import { import { ColorRampId } from '@globalfishingwatch/deck-layers' import { resolveEndpoint } from '@globalfishingwatch/datasets-client' import { getDataviewAvailableIntervals } from './dataviews' -import { ResolverGlobalConfig } from './types' +import { DeckResolverFunction } from './types' // TODO: decide if include static here or create a new one -export const resolveDeckFourwingsLayerProps = ( - dataview: UrlDataviewInstance, - { start, end, resolution, debug }: ResolverGlobalConfig, - interactions: PickingInfo[] -): FourwingsLayerProps => { +export const resolveDeckFourwingsLayerProps: DeckResolverFunction< + FourwingsLayerProps, + FourwingsPickingObject +> = (dataview, { start, end, resolution, debug }, interactions): FourwingsLayerProps => { const startTime = start ? getUTCDateTime(start).toMillis() : 0 const endTime = end ? getUTCDateTime(end).toMillis() : Infinity diff --git a/libs/deck-layer-composer/src/resolvers/index.ts b/libs/deck-layer-composer/src/resolvers/index.ts index 76518e6366..dd26455582 100644 --- a/libs/deck-layer-composer/src/resolvers/index.ts +++ b/libs/deck-layer-composer/src/resolvers/index.ts @@ -1,11 +1,13 @@ -import { PickingInfo } from '@deck.gl/core' import { DataviewType, DataviewInstance } from '@globalfishingwatch/api-types' import { AnyDeckLayer, BaseMapLayer, ClusterLayer, + ContextFeature, ContextLayer, + DeckLayerInteractionFeature, FourwingsLayer, + FourwingsPickingObject, VesselLayer, } from '@globalfishingwatch/deck-layers' import { ResolverGlobalConfig } from './types' @@ -27,22 +29,30 @@ export * from './vessels' export const dataviewToDeckLayer = ( dataview: DataviewInstance, globalConfig: ResolverGlobalConfig, - interactions = [] as PickingInfo[] + interactions = [] as DeckLayerInteractionFeature[] ): AnyDeckLayer => { if (dataview.config?.type === DataviewType.Basemap) { - const deckLayerProps = resolveDeckBasemapLayerProps(dataview) + const deckLayerProps = resolveDeckBasemapLayerProps(dataview, globalConfig) return new BaseMapLayer(deckLayerProps) } if ( dataview.config?.type === DataviewType.HeatmapAnimated || dataview.config?.type === DataviewType.HeatmapStatic ) { - const deckLayerProps = resolveDeckFourwingsLayerProps(dataview, globalConfig, interactions) + const deckLayerProps = resolveDeckFourwingsLayerProps( + dataview, + globalConfig, + interactions as FourwingsPickingObject[] + ) const layer = new FourwingsLayer(deckLayerProps) return layer } if (dataview.config?.type === DataviewType.Context) { - const deckLayerProps = resolveDeckContextLayerProps(dataview, globalConfig, interactions) + const deckLayerProps = resolveDeckContextLayerProps( + dataview, + globalConfig, + interactions as ContextFeature[] + ) const layer = new ContextLayer(deckLayerProps) return layer } diff --git a/libs/deck-layer-composer/src/resolvers/tile-cluster.ts b/libs/deck-layer-composer/src/resolvers/tile-cluster.ts index 65f9a455a4..46e4e94082 100644 --- a/libs/deck-layer-composer/src/resolvers/tile-cluster.ts +++ b/libs/deck-layer-composer/src/resolvers/tile-cluster.ts @@ -1,11 +1,12 @@ import { DatasetTypes, DataviewInstance } from '@globalfishingwatch/api-types' import { resolveDataviewDatasetResource } from '@globalfishingwatch/dataviews-client' +import { DeckResolverFunction } from './types' type TileClusterDeckLayerProps = any -export function resolveDeckTileClusterLayerProps( - dataview: DataviewInstance -): TileClusterDeckLayerProps { +export const resolveDeckTileClusterLayerProps: DeckResolverFunction = ( + dataview +) => { const { dataset: tileClusterDataset, url: tileClusterUrl } = resolveDataviewDatasetResource( dataview, DatasetTypes.Events diff --git a/libs/deck-layer-composer/src/resolvers/types.ts b/libs/deck-layer-composer/src/resolvers/types.ts index 7cf720f159..d229affa78 100644 --- a/libs/deck-layer-composer/src/resolvers/types.ts +++ b/libs/deck-layer-composer/src/resolvers/types.ts @@ -1,5 +1,11 @@ import { EventTypes } from '@globalfishingwatch/api-types' -import { FourwingsResolution, FourwingsVisualizationMode } from '@globalfishingwatch/deck-layers' +import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' +import { + AnyDeckLayer, + DeckLayerInteractionFeature, + FourwingsResolution, + FourwingsVisualizationMode, +} from '@globalfishingwatch/deck-layers' export type ResolverGlobalConfig = { start: string @@ -18,3 +24,12 @@ export type ResolverGlobalConfig = { locale?: string visibleEvents: EventTypes[] } + +export type DeckResolverFunction< + LayerProps = AnyDeckLayer['props'], + InteractionFeature = DeckLayerInteractionFeature +> = ( + dataview: UrlDataviewInstance, + { start, end, resolution, debug }: ResolverGlobalConfig, + interactions?: InteractionFeature[] +) => LayerProps diff --git a/libs/deck-layer-composer/src/resolvers/vessels.ts b/libs/deck-layer-composer/src/resolvers/vessels.ts index a71a6737c5..29b85194ae 100644 --- a/libs/deck-layer-composer/src/resolvers/vessels.ts +++ b/libs/deck-layer-composer/src/resolvers/vessels.ts @@ -1,18 +1,17 @@ -import { PickingInfo } from '@deck.gl/core' -import { DatasetTypes, DataviewInstance, EventTypes } from '@globalfishingwatch/api-types' +import { DatasetTypes, EventTypes } from '@globalfishingwatch/api-types' import { VesselLayerProps, getUTCDateTime, hexToDeckColor } from '@globalfishingwatch/deck-layers' import { API_GATEWAY, GFWAPI } from '@globalfishingwatch/api-client' import { resolveDataviewDatasetResource, resolveDataviewDatasetResources, } from '@globalfishingwatch/dataviews-client' -import { ResolverGlobalConfig } from './types' +import { DeckResolverFunction } from './types' -export function resolveDeckVesselLayerProps( - dataview: DataviewInstance, - globalConfig: ResolverGlobalConfig, - interactions: PickingInfo[] -): VesselLayerProps { +export const resolveDeckVesselLayerProps: DeckResolverFunction = ( + dataview, + globalConfig, + interactions +) => { const trackUrl = resolveDataviewDatasetResource(dataview, DatasetTypes.Tracks)?.url return { From c5e0b8bddccefd3713b19db35d2a20f46e66c06d Mon Sep 17 00:00:00 2001 From: j8seangel Date: Wed, 10 Apr 2024 20:09:57 +0200 Subject: [PATCH 25/43] cleanup interactions leftlovers and fix build --- apps/fishing-map/data/config.ts | 10 +- .../features/map/map-interaction.utils.ts | 2 +- .../features/map/map-interactions.hooks.ts | 204 +++++++------- apps/fishing-map/features/map/map.hooks.ts | 257 +++++++----------- apps/fishing-map/features/map/map.slice.ts | 14 +- .../features/map/popups/ActivityLayers.tsx | 7 +- .../features/map/popups/Popup.module.css | 2 +- .../features/map/popups/VesselsTable.tsx | 2 +- .../features/timebar/timebar.utils.ts | 9 +- apps/fishing-map/project.json | 2 +- .../src/hooks/deck-layers-legends.hooks.ts | 6 +- .../interaction-features.utils.ts | 29 +- 12 files changed, 240 insertions(+), 304 deletions(-) diff --git a/apps/fishing-map/data/config.ts b/apps/fishing-map/data/config.ts index b55eaa101f..8c9d2ff79e 100644 --- a/apps/fishing-map/data/config.ts +++ b/apps/fishing-map/data/config.ts @@ -131,11 +131,11 @@ export const THINNING_CONFIG: { user: ThinningConfig; guest: ThinningConfig } = export const REPLACE_URL_PARAMS = ['latitude', 'longitude', 'zoom'] export const POPUP_CATEGORY_ORDER = [ - DataviewCategory.Activity, - DataviewCategory.Detections, - DataviewCategory.Events, - DataviewCategory.Environment, - DataviewCategory.Context, + `${DataviewCategory.Activity}`, + `${DataviewCategory.Detections}`, + `${DataviewCategory.Events}`, + `${DataviewCategory.Environment}`, + `${DataviewCategory.Context}`, ] export const FIT_BOUNDS_REPORT_PADDING = 30 diff --git a/apps/fishing-map/features/map/map-interaction.utils.ts b/apps/fishing-map/features/map/map-interaction.utils.ts index bf18e76719..bb7118335e 100644 --- a/apps/fishing-map/features/map/map-interaction.utils.ts +++ b/apps/fishing-map/features/map/map-interaction.utils.ts @@ -1,4 +1,4 @@ -import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layer-composer' +import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layers' import { RulerPickingObject } from '@globalfishingwatch/deck-layers' export const isRulerLayerPoint = (feature: DeckLayerInteractionFeature) => diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index b4177da549..394b107505 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -8,16 +8,17 @@ import { useMapHoverInteraction, useSetMapHoverInteraction, InteractionEvent, - DeckLayerInteractionPickingInfo, } from '@globalfishingwatch/deck-layer-composer' -import { ClusterPickingObject, FourwingsPickingObject } from '@globalfishingwatch/deck-layers' +import { + ClusterPickingObject, + DeckLayerInteractionPickingInfo, + FourwingsPickingObject, +} from '@globalfishingwatch/deck-layers' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' import { SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION, TooltipEventFeature, - parseMapTooltipEvent, - useMapHighlightedEvent, } from 'features/map/map.hooks' import useRulers from 'features/map/overlays/rulers/rulers.hooks' import useMapInstance, { useDeckMap } from 'features/map/map-context.hooks' @@ -27,22 +28,16 @@ import { getEventLabel } from 'utils/analytics' import { POPUP_CATEGORY_ORDER } from 'data/config' import { selectIsMarineManagerLocation, selectLocationType } from 'routes/routes.selectors' import { useMapClusterTilesLoaded } from 'features/map/map-sources.hooks' -import { WORKSPACES_POINTS_TYPE } from 'features/map/map.config' import { useMapErrorNotification } from 'features/map/overlays/error-notification/error-notification.hooks' import { selectIsGFWUser } from 'features/user/selectors/user.selectors' import { selectCurrentDataviewInstancesResolved } from 'features/dataviews/selectors/dataviews.instances.selectors' -import { DEFAULT_WORKSPACE_ID, DEFAULT_WORKSPACE_CATEGORY } from 'data/workspaces' import { useAppDispatch } from 'features/app/app.hooks' -import { ENCOUNTER_EVENTS_SOURCE_ID } from 'features/dataviews/dataviews.utils' import { setHintDismissed } from 'features/help/hints.slice' -import { USER, WORKSPACES_LIST, HOME, WORKSPACE } from 'routes/routes' import { useLocationConnect } from 'routes/routes.hook' import { useMapRulersDrag } from './overlays/rulers/rulers-drag.hooks' import { isRulerLayerPoint } from './map-interaction.utils' import { SliceInteractionEvent, - fetchBQEventThunk, - fetchEncounterEventThunk, fetchFishingActivityInteractionThunk, selectApiEventStatus, selectClickedEvent, @@ -85,34 +80,34 @@ export const useClickedEventConnect = () => { } // Used on workspaces-list or user panel to go to the workspace detail page - if (locationType === USER || locationType === WORKSPACES_LIST) { - const workspace = event?.features?.find( - (feature: any) => feature.properties.type === WORKSPACES_POINTS_TYPE - ) - if (workspace) { - const isDefaultWorkspace = workspace.properties.id === DEFAULT_WORKSPACE_ID - dispatchLocation( - isDefaultWorkspace ? HOME : WORKSPACE, - isDefaultWorkspace - ? {} - : { - payload: { - category: - workspace.properties?.category && workspace.properties.category !== 'null' - ? workspace.properties.category - : DEFAULT_WORKSPACE_CATEGORY, - workspaceId: workspace.properties.id, - }, - }, - { replaceQuery: true } - ) - const { latitude, longitude, zoom } = workspace.properties - if (latitude && longitude && zoom) { - setViewState({ latitude, longitude, zoom }) - } - return - } - } + // if (locationType === USER || locationType === WORKSPACES_LIST) { + // const workspace = event?.features?.find( + // (feature: any) => feature.properties.type === WORKSPACES_POINTS_TYPE + // ) + // if (workspace) { + // const isDefaultWorkspace = workspace.properties.id === DEFAULT_WORKSPACE_ID + // dispatchLocation( + // isDefaultWorkspace ? HOME : WORKSPACE, + // isDefaultWorkspace + // ? {} + // : { + // payload: { + // category: + // workspace.properties?.category && workspace.properties.category !== 'null' + // ? workspace.properties.category + // : DEFAULT_WORKSPACE_CATEGORY, + // workspaceId: workspace.properties.id, + // }, + // }, + // { replaceQuery: true } + // ) + // const { latitude, longitude, zoom } = workspace.properties + // if (latitude && longitude && zoom) { + // setViewState({ latitude, longitude, zoom }) + // } + // return + // } + // } const clusterFeature = event?.features?.find( (f) => f.category === DataviewCategory.Events @@ -144,13 +139,13 @@ export const useClickedEventConnect = () => { } // When hovering in a vessel event we don't want to have clicked events - const areAllFeaturesVesselEvents = event.features.every( - (f) => f.generatorType === DataviewType.VesselEvents - ) + // const areAllFeaturesVesselEvents = event.features.every( + // (f) => f.generatorType === DataviewType.VesselEvents + // ) - if (areAllFeaturesVesselEvents) { - return - } + // if (areAllFeaturesVesselEvents) { + // return + // } dispatch(setClickedEvent(event as SliceInteractionEvent)) @@ -174,14 +169,14 @@ export const useClickedEventConnect = () => { ) } - const tileClusterFeature = event.features.find( - (f) => f.generatorType === DataviewType.TileCluster - ) - if (tileClusterFeature) { - const bqPocQuery = tileClusterFeature.source !== ENCOUNTER_EVENTS_SOURCE_ID - const fetchFn = bqPocQuery ? fetchBQEventThunk : fetchEncounterEventThunk - eventsPromiseRef.current = dispatch(fetchFn(tileClusterFeature)) - } + // const tileClusterFeature = event.features.find( + // (f) => f.generatorType === DataviewType.TileCluster + // ) + // if (tileClusterFeature) { + // const bqPocQuery = tileClusterFeature.source !== ENCOUNTER_EVENTS_SOURCE_ID + // const fetchFn = bqPocQuery ? fetchBQEventThunk : fetchEncounterEventThunk + // eventsPromiseRef.current = dispatch(fetchFn(tileClusterFeature)) + // } } return { @@ -221,6 +216,10 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { if (!map || !info.coordinate) return let features = defaultEmptyFeatures + // TODO:deck handle when hovering a cluster point as we don't want to show anything else + // const clusterFeature = event.features.find( + // (f) => f.generatorType === DataviewType.TileCluster && parseInt(f.properties.count) > 1 + // ) try { features = map?.pickMultipleObjects({ x: info.x, @@ -247,8 +246,8 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { [map, onRulerMapHover, rulersEditing, setMapHoverFeatures] ) - const hoveredTooltipEvent = parseMapTooltipEvent(hoveredEvent, dataviews, temporalgridDataviews) - useMapHighlightedEvent(hoveredTooltipEvent?.features) + // const hoveredTooltipEvent = parseMapTooltipEvent(hoveredEvent, dataviews, temporalgridDataviews) + // useMapHighlightedEvent(hoveredTooltipEvent?.features) // const resetHoverState = useCallback(() => { // setHoveredEvent(null) @@ -261,7 +260,7 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { // resetHoverState, hoveredEvent, hoveredDebouncedEvent, - hoveredTooltipEvent, + // hoveredTooltipEvent, } } @@ -309,7 +308,7 @@ export const useMapMouseClick = () => { const temporalgridDataviews = useSelector(selectActiveTemporalgridDataviews) const { clickedEvent, dispatchClickedEvent } = useClickedEventConnect() - const clickedTooltipEvent = parseMapTooltipEvent(clickedEvent, dataviews, temporalgridDataviews) + const clickedTooltipEvent = { ...clickedEvent } const clickedCellLayers = useMemo(() => { if (!clickedEvent || !clickedTooltipEvent) return @@ -374,21 +373,6 @@ export const useMapMouseClick = () => { return { onMapClick, clickedTooltipEvent } } -const hasTooltipEventFeature = ( - hoveredTooltipEvent: ReturnType, - type: DataviewType -) => { - return hoveredTooltipEvent?.features.find((f) => f.type === type) !== undefined -} - -const hasToolFeature = (hoveredTooltipEvent?: ReturnType) => { - if (!hoveredTooltipEvent) return false - return ( - hasTooltipEventFeature(hoveredTooltipEvent, DataviewType.Annotation) || - hasTooltipEventFeature(hoveredTooltipEvent, DataviewType.Rulers) - ) -} - export const _deprecatedUseMapCursor = (hoveredTooltipEvent?: any) => { const map = useMapInstance() const { isMapAnnotating } = useMapAnnotation() @@ -401,45 +385,45 @@ export const _deprecatedUseMapCursor = (hoveredTooltipEvent?: any) => { const tilesClusterLoaded = useMapClusterTilesLoaded() const getCursor = useCallback(() => { - if (hoveredTooltipEvent && hasToolFeature(hoveredTooltipEvent)) { - if (hasTooltipEventFeature(hoveredTooltipEvent, DataviewType.Annotation) && !gfwUser) { - return 'grab' - } - return 'all-scroll' - } else if (isMapAnnotating || rulersEditing || isErrorNotificationEditing) { - return 'crosshair' - } else if (isMapDrawing || isMarineManagerLocation) { - // updating cursor using css at style.css as the library sets classes depending on the state - return undefined - } else if (hoveredTooltipEvent) { - // Workaround to fix cluster events duplicated, only working for encounters and needs - // TODO if wanted to scale it to other layers - const clusterConfig = dataviews.find((d) => d.config?.type === DataviewType.TileCluster) - const eventsCount = clusterConfig?.config?.duplicatedEventsWorkaround ? 2 : 1 - - const clusterFeature = hoveredTooltipEvent.features.find( - (f: any) => - f.type === DataviewType.TileCluster && parseInt(f.properties.count) > eventsCount - ) - - if (clusterFeature) { - if (!tilesClusterLoaded) { - return 'progress' - } - const { expansionZoom, lat, lng, lon } = clusterFeature.properties - const longitude = lng || lon - return expansionZoom && lat && longitude ? 'zoom-in' : 'grab' - } - const vesselFeatureEvents = hoveredTooltipEvent.features.filter( - (f: any) => f.category === DataviewCategory.Vessels - ) - if (vesselFeatureEvents.length > 1) { - return 'grab' - } - return 'pointer' - } else if (map?.isMoving()) { - return 'grabbing' - } + // if (hoveredTooltipEvent && hasToolFeature(hoveredTooltipEvent)) { + // if (hasTooltipEventFeature(hoveredTooltipEvent, DataviewType.Annotation) && !gfwUser) { + // return 'grab' + // } + // return 'all-scroll' + // } else if (isMapAnnotating || rulersEditing || isErrorNotificationEditing) { + // return 'crosshair' + // } else if (isMapDrawing || isMarineManagerLocation) { + // // updating cursor using css at style.css as the library sets classes depending on the state + // return undefined + // } else if (hoveredTooltipEvent) { + // // Workaround to fix cluster events duplicated, only working for encounters and needs + // // TODO if wanted to scale it to other layers + // const clusterConfig = dataviews.find((d) => d.config?.type === DataviewType.TileCluster) + // const eventsCount = clusterConfig?.config?.duplicatedEventsWorkaround ? 2 : 1 + + // const clusterFeature = hoveredTooltipEvent.features.find( + // (f: any) => + // f.type === DataviewType.TileCluster && parseInt(f.properties.count) > eventsCount + // ) + + // if (clusterFeature) { + // if (!tilesClusterLoaded) { + // return 'progress' + // } + // const { expansionZoom, lat, lng, lon } = clusterFeature.properties + // const longitude = lng || lon + // return expansionZoom && lat && longitude ? 'zoom-in' : 'grab' + // } + // const vesselFeatureEvents = hoveredTooltipEvent.features.filter( + // (f: any) => f.category === DataviewCategory.Vessels + // ) + // if (vesselFeatureEvents.length > 1) { + // return 'grab' + // } + // return 'pointer' + // } else if (map?.isMoving()) { + // return 'grabbing' + // } return 'grab' }, [ hoveredTooltipEvent, diff --git a/apps/fishing-map/features/map/map.hooks.ts b/apps/fishing-map/features/map/map.hooks.ts index ca8e1180b3..af62e0ca6a 100644 --- a/apps/fishing-map/features/map/map.hooks.ts +++ b/apps/fishing-map/features/map/map.hooks.ts @@ -2,24 +2,11 @@ import { useSelector } from 'react-redux' import { useCallback, useEffect, useMemo } from 'react' import { debounce } from 'lodash' import { useTranslation } from 'react-i18next' -import { - UrlDataviewInstance, - MULTILAYER_SEPARATOR, - isMergedAnimatedGenerator, -} from '@globalfishingwatch/dataviews-client' -import { - DatasetSubCategory, - DataviewCategory, - DataviewType, - Locale, -} from '@globalfishingwatch/api-types' +import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' +import { DataviewCategory, DataviewType, Locale } from '@globalfishingwatch/api-types' import { GFWAPI } from '@globalfishingwatch/api-client' -import { SublayerCombinationMode } from '@globalfishingwatch/fourwings-aggregate' import { ResolverGlobalConfig } from '@globalfishingwatch/deck-layer-composer' -import { - getActiveDatasetsInActivityDataviews, - getDatasetTitleByDataview, -} from 'features/datasets/datasets.utils' +import { FourwingsComparisonMode } from '@globalfishingwatch/deck-layers' import { useTimerangeConnect } from 'features/timebar/timebar.hooks' import { selectHighlightedEvents, setHighlightedEvents } from 'features/timebar/timebar.slice' import { useAppDispatch } from 'features/app/app.hooks' @@ -35,12 +22,12 @@ import { } from 'features/app/selectors/app.selectors' import { selectWorkspaceVisibleEventsArray } from 'features/workspace/workspace.selectors' import { selectDebugOptions } from 'features/debug/debug.slice' -import { WORKSPACE_GENERATOR_ID, REPORT_BUFFER_GENERATOR_ID } from './map.config' import { MAX_TOOLTIP_LIST, SliceInteractionEvent, ExtendedFeatureVessel, SliceExtendedFeature, + SliceExtendedFourwingsFeature, } from './map.slice' import { useViewStateAtom } from './map-viewport.hooks' @@ -208,166 +195,124 @@ export const useMapHighlightedEvent = (features?: TooltipEventFeature[]) => { // TODO:deck ideally remove this intermediate step export const parseMapTooltipFeatures = ( - features: SliceExtendedFeature[], - dataviews: UrlDataviewInstance[], - temporalgridDataviews?: UrlDataviewInstance[] + features: SliceExtendedFeature[] + // dataviews: UrlDataviewInstance[], + // temporalgridDataviews?: UrlDataviewInstance[] ): TooltipEventFeature[] => { const tooltipEventFeatures: TooltipEventFeature[] = features.flatMap((feature) => { - const { temporalgrid, generatorId, generatorType } = feature + const { category, id, comparisonMode, sublayers } = feature as SliceExtendedFourwingsFeature const baseFeature = { - source: feature.source, category: feature.category, - sourceLayer: feature.sourceLayer, - layerId: feature.layerId as string, - type: generatorType as DataviewType, + layerId: id as string, + type: category, } - if (temporalgrid?.sublayerCombinationMode === SublayerCombinationMode.TimeCompare) { + if (comparisonMode === FourwingsComparisonMode.TimeCompare) { return { ...baseFeature, category: DataviewCategory.Comparison, - value: features[0]?.value, + value: sublayers[0]?.value, visible: true, - unit: features[0]?.temporalgrid?.unit, + unit: sublayers[0]?.unit, } as TooltipEventFeature } let dataview - if (isMergedAnimatedGenerator(generatorId as string)) { - if (!temporalgrid || temporalgrid.sublayerId === undefined || !temporalgrid.visible) { - return [] - } + // if (isMergedAnimatedGenerator(generatorId as string)) { + // if (!temporalgrid || temporalgrid.sublayerId === undefined || !temporalgrid.visible) { + // return [] + // } - dataview = temporalgridDataviews?.find((dataview) => dataview.id === temporalgrid.sublayerId) - } else { - dataview = dataviews?.find((dataview) => { - // Needed to get only the initial part to support multiple generator - // from the same dataview, see map.selectors L137 - const cleanGeneratorId = (generatorId as string)?.split(MULTILAYER_SEPARATOR)[0] - return dataview.id === cleanGeneratorId - }) - } + // dataview = temporalgridDataviews?.find((dataview) => dataview.id === temporalgrid.sublayerId) + // } else { + // dataview = dataviews?.find((dataview) => { + // // Needed to get only the initial part to support multiple generator + // // from the same dataview, see map.selectors L137 + // const cleanGeneratorId = (generatorId as string)?.split(MULTILAYER_SEPARATOR)[0] + // return dataview.id === cleanGeneratorId + // }) + // } - if (!dataview) { - // There are three use cases when there is no dataview and we want interaction - // 1. Wworkspaces list - if (generatorId && (generatorId as string).includes(WORKSPACE_GENERATOR_ID)) { - const tooltipWorkspaceFeature: TooltipEventFeature = { - ...baseFeature, - type: DataviewType.GL, - value: feature.properties.label, - properties: {}, - category: DataviewCategory.Context, - } - return tooltipWorkspaceFeature - } - // 2. Report buffer - else if (generatorId === REPORT_BUFFER_GENERATOR_ID) { - const tooltipWorkspaceFeature: TooltipEventFeature = { - ...baseFeature, - category: DataviewCategory.Context, - properties: {}, - value: feature.properties.label, - visible: true, - } - return tooltipWorkspaceFeature - } - // 3. Tools (Annotations and Rulers) - else if (generatorType === DataviewType.Annotation || generatorType === DataviewType.Rulers) { - const tooltipToolFeature: TooltipEventFeature = { - ...baseFeature, - category: DataviewCategory.Context, - properties: feature.properties, - value: feature.properties.label, - visible: true, - } - return tooltipToolFeature - } - return [] - } + // TODO: deck check if this is still neded + // if (!dataview) { + // // There are three use cases when there is no dataview and we want interaction + // // 1. Wworkspaces list + // if (generatorId && (generatorId as string).includes(WORKSPACE_GENERATOR_ID)) { + // const tooltipWorkspaceFeature: TooltipEventFeature = { + // ...baseFeature, + // type: DataviewType.GL, + // value: feature.properties.label, + // properties: {}, + // category: DataviewCategory.Context, + // } + // return tooltipWorkspaceFeature + // } + // // 2. Report buffer + // else if (generatorId === REPORT_BUFFER_GENERATOR_ID) { + // const tooltipWorkspaceFeature: TooltipEventFeature = { + // ...baseFeature, + // category: DataviewCategory.Context, + // properties: {}, + // value: feature.properties.label, + // visible: true, + // } + // return tooltipWorkspaceFeature + // } + // // 3. Tools (Annotations and Rulers) + // else if (generatorType === DataviewType.Annotation || generatorType === DataviewType.Rulers) { + // const tooltipToolFeature: TooltipEventFeature = { + // ...baseFeature, + // category: DataviewCategory.Context, + // properties: feature.properties, + // value: feature.properties.label, + // visible: true, + // } + // return tooltipToolFeature + // } + // return [] + // } - const title = getDatasetTitleByDataview(dataview) + // const title = getDatasetTitleByDataview(dataview) - const datasets = - dataview.category === DataviewCategory.Activity || - dataview.category === DataviewCategory.Detections - ? getActiveDatasetsInActivityDataviews([dataview]) - : (dataview.datasets || [])?.map((d) => d.id) + // const datasets = + // dataview.category === DataviewCategory.Activity || + // dataview.category === DataviewCategory.Detections + // ? getActiveDatasetsInActivityDataviews([dataview]) + // : (dataview.datasets || [])?.map((d) => d.id) - const dataset = dataview?.datasets?.find(({ id }) => datasets.includes(id)) - const subcategory = dataset?.subcategory as DatasetSubCategory - const tooltipEventFeature: TooltipEventFeature = { - title, - type: dataview.config?.type, - color: dataview.config?.color, - visible: dataview.config?.visible, - category: dataview.category || DataviewCategory.Context, - subcategory, - datasetSource: dataset?.source, - ...feature, - properties: { ...feature.properties }, - } - // Insert custom properties by each dataview configuration - const properties = dataview.datasetsConfig - ? dataview.datasetsConfig.flatMap((datasetConfig) => { - if (!datasetConfig.query?.length) return [] - return datasetConfig.query.flatMap((query) => - query.id === 'properties' ? (query.value as string) : [] - ) - }) - : [] - properties.forEach((property) => { - if (feature.properties[property]) { - tooltipEventFeature.properties[property] = feature.properties[property] - } - }) + // const dataset = dataview?.datasets?.find(({ id }) => datasets.includes(id)) + // const subcategory = dataset?.subcategory as DatasetSubCategory + // const tooltipEventFeature: TooltipEventFeature = { + // title, + // type: dataview.config?.type, + // color: dataview.config?.color, + // visible: dataview.config?.visible, + // category: dataview.category || DataviewCategory.Context, + // subcategory, + // datasetSource: dataset?.source, + // ...feature, + // properties: { ...feature.properties }, + // } + // // Insert custom properties by each dataview configuration + // const properties = dataview.datasetsConfig + // ? dataview.datasetsConfig.flatMap((datasetConfig) => { + // if (!datasetConfig.query?.length) return [] + // return datasetConfig.query.flatMap((query) => + // query.id === 'properties' ? (query.value as string) : [] + // ) + // }) + // : [] + // properties.forEach((property) => { + // if (feature.properties[property]) { + // tooltipEventFeature.properties[property] = feature.properties[property] + // } + // }) - if (feature.vessels) { - tooltipEventFeature.vesselsInfo = getVesselsInfoConfig(feature.vessels) - } - return tooltipEventFeature + // if (feature.vessels) { + // tooltipEventFeature.vesselsInfo = getVesselsInfoConfig(feature.vessels) + // } + // return tooltipEventFeature }) return tooltipEventFeatures } - -export const parseMapTooltipEvent = ( - event: SliceInteractionEvent | null, - dataviews: UrlDataviewInstance[], - temporalgridDataviews: UrlDataviewInstance[] -) => { - if (!event || !event.features) return null - - const baseEvent = { - point: event.point, - latitude: event.latitude, - longitude: event.longitude, - } - - const clusterFeature = event.features.find( - (f) => f.generatorType === DataviewType.TileCluster && parseInt(f.properties.count) > 1 - ) - - // We don't want to show anything else when hovering a cluster point - if (clusterFeature) { - return { - ...baseEvent, - features: [ - { - type: clusterFeature.generatorType, - properties: clusterFeature.properties, - } as TooltipEventFeature, - ], - } - } - const tooltipEventFeatures = parseMapTooltipFeatures( - event.features, - dataviews, - temporalgridDataviews - ) - if (!tooltipEventFeatures.length) return null - return { - ...baseEvent, - features: tooltipEventFeatures, - } -} diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index 1879d301ff..b97bc35dc7 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -22,7 +22,9 @@ import { ContextPickingObject, FourwingsDeckSublayer, FourwingsPickingObject, + RulerPickingObject, UserContextPickingObject, + VesselEventPickingObject, } from '@globalfishingwatch/deck-layers' import { getUTCDate } from '@globalfishingwatch/data-transforms' import { AsyncReducerStatus } from 'utils/async-slice' @@ -66,6 +68,8 @@ export type SliceExtendedFeature = | ContextPickingObject | UserContextPickingObject | ClusterPickingObject + | RulerPickingObject + | VesselEventPickingObject // Extends the default extendedEvent including event and vessels information from API export type SliceInteractionEvent = Omit & { @@ -349,7 +353,7 @@ export const fetchEncounterEventThunk = createAsyncThunk< { dispatch: AppDispatch } ->('map/fetchEncounterEvent', async (eventFeature, { signal, getState }) => { +>('map/fetchEncounterEvent', async (eventFeature: any, { signal, getState }) => { const state = getState() as any const eventDataviews = selectEventsDataviews(state) || [] const dataview = eventDataviews.find((d) => d.id === eventFeature.generatorId) @@ -518,7 +522,9 @@ const slice = createSlice({ builder.addCase(fetchEncounterEventThunk.fulfilled, (state, action) => { state.apiEventStatus = AsyncReducerStatus.Finished if (!state.clicked || !state.clicked.features || !action.payload) return - const feature = state.clicked?.features?.find((feature) => feature.id && action.meta.arg.id) + const feature = state.clicked?.features?.find( + (feature) => feature.id && action.meta.arg.id + ) as any if (feature) { feature.event = action.payload } @@ -536,7 +542,9 @@ const slice = createSlice({ builder.addCase(fetchBQEventThunk.fulfilled, (state, action) => { state.apiEventStatus = AsyncReducerStatus.Finished if (!state.clicked || !state.clicked.features || !action.payload) return - const feature = state.clicked?.features?.find((feature) => feature.id && action.meta.arg.id) + const feature = state.clicked?.features?.find( + (feature) => feature.id && action.meta.arg.id + ) as any if (feature && action.payload) { feature.properties = { ...feature.properties, ...action.payload } } diff --git a/apps/fishing-map/features/map/popups/ActivityLayers.tsx b/apps/fishing-map/features/map/popups/ActivityLayers.tsx index 1d0f619c49..392b3dd83b 100644 --- a/apps/fishing-map/features/map/popups/ActivityLayers.tsx +++ b/apps/fishing-map/features/map/popups/ActivityLayers.tsx @@ -42,7 +42,12 @@ function ActivityTooltipRow({ feature, showFeaturesDetails, loading }: ActivityT )} {/* // TODO:deck add subcategory info */} {!loading && showFeaturesDetails && ( - + )}
      diff --git a/apps/fishing-map/features/map/popups/Popup.module.css b/apps/fishing-map/features/map/popups/Popup.module.css index 10bcfc3f76..da3d46367d 100644 --- a/apps/fishing-map/features/map/popups/Popup.module.css +++ b/apps/fishing-map/features/map/popups/Popup.module.css @@ -19,11 +19,11 @@ .click .loading { background: var(--color-white); color: var(--color-primary-blue); - box-shadow: var(--box-shadow); } .click .content { min-width: var(--sidebar-width); + box-shadow: var(--box-shadow); } .click .loading { diff --git a/apps/fishing-map/features/map/popups/VesselsTable.tsx b/apps/fishing-map/features/map/popups/VesselsTable.tsx index e2a0d203f6..e5b5a6d04f 100644 --- a/apps/fishing-map/features/map/popups/VesselsTable.tsx +++ b/apps/fishing-map/features/map/popups/VesselsTable.tsx @@ -105,7 +105,7 @@ function VesselsTable({ }: { feature: SliceExtendedFourwingsDeckSublayer & { category: DataviewCategory } vesselProperty?: ActivityProperty - activityType?: DatasetSubCategory + activityType?: `${DatasetSubCategory}` testId?: string }) { const { t } = useTranslation() diff --git a/apps/fishing-map/features/timebar/timebar.utils.ts b/apps/fishing-map/features/timebar/timebar.utils.ts index 47b3871a0b..dd2a44d0de 100644 --- a/apps/fishing-map/features/timebar/timebar.utils.ts +++ b/apps/fishing-map/features/timebar/timebar.utils.ts @@ -17,14 +17,7 @@ export const parseTrackEventChunkProps = ( event: ApiEvent, eventKey?: string ): ApiEvent & { props: TrackEventChunkProps } => { - const { description, descriptionGeneric } = getEventDescription({ - start: event.start as number, - end: event.end as number, - type: event.type as EventTypes, - encounterVesselName: event.encounter?.vessel?.name, - portName: event.port_visit?.intermediateAnchorage?.name, - portFlag: event.port_visit?.intermediateAnchorage?.flag, - }) + const { description, descriptionGeneric } = getEventDescription(event) const { color, colorLabels } = getEventColors({ type: event.type as EventTypes }) return { diff --git a/apps/fishing-map/project.json b/apps/fishing-map/project.json index 3285d19c9f..85d9a35320 100644 --- a/apps/fishing-map/project.json +++ b/apps/fishing-map/project.json @@ -25,7 +25,7 @@ "build": { "executor": "nx:run-commands", "options": { - "commands": ["nx prepare-loaders fishing-map", "nx build:app fishing-map"], + "commands": ["nx build:app fishing-map", "nx prepare-loaders fishing-map"], "parallel": false } }, diff --git a/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts b/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts index 60ccfe9250..44c029224d 100644 --- a/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts +++ b/libs/deck-layer-composer/src/hooks/deck-layers-legends.hooks.ts @@ -18,7 +18,7 @@ export const deckLayersLegendsAtom = atom((get) => { if (!layer.instance || !(layer.instance instanceof FourwingsLayer)) { return [] } - const interaction = deckLayerHoverFeatures?.features?.find((i) => i.layer?.id === layer.id) + const interaction = deckLayerHoverFeatures?.features?.find((i: any) => i.layer?.id === layer.id) const { domain, range } = layer.instance.getColorScale() || {} let label = layer.instance.props.sublayers?.[0]?.unit || '' @@ -46,9 +46,7 @@ export const deckLayersLegendsAtom = atom((get) => { layer.instance.props.sublayers.map((sublayer) => sublayer.colorRamp as ColorRampId) ) : range, - currentValues: (interaction?.object as FourwingsPickingObject)?.sublayers?.map( - (s: any) => s.value - )!, + currentValues: (interaction as FourwingsPickingObject)?.sublayers?.map((s: any) => s.value)!, label, } }) diff --git a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts index 3380a1978e..6f2acfc9ca 100644 --- a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts +++ b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts @@ -1,23 +1,26 @@ import { ContextLayer, ContextPickingObject, + DeckLayerInteractionFeature, FourwingsLayer, FourwingsPickingObject, } from '@globalfishingwatch/deck-layers' import { VALUE_MULTIPLIER } from '@globalfishingwatch/fourwings-aggregate' -import { DeckLayerInteractionFeature, DeckLayerInteractionPickingInfo } from '../types' +// import { DeckLayerInteractionPickingInfo } from '../types' +// TODO: deck check if this is still needed export const filterUniqueFeatureInteraction = (features: DeckLayerInteractionFeature[]) => { - const uniqueLayerIdFeatures: Record = {} - const filtered = features?.filter(({ layerId, id, uniqueFeatureInteraction }) => { - if (!uniqueFeatureInteraction) { - return true - } - if (uniqueLayerIdFeatures[layerId] === undefined) { - uniqueLayerIdFeatures[layerId] = id - return true - } - return uniqueLayerIdFeatures[layerId] === id - }) - return filtered + return features + // const uniqueLayerIdFeatures: Record = {} + // const filtered = features?.filter(({ layerId, id, uniqueFeatureInteraction }) => { + // if (!uniqueFeatureInteraction) { + // return true + // } + // if (uniqueLayerIdFeatures[layerId] === undefined) { + // uniqueLayerIdFeatures[layerId] = id + // return true + // } + // return uniqueLayerIdFeatures[layerId] === id + // }) + // return filtered } From 6db88cb7e3e0ffa198cb8adb049fdf3b38a76c0f Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Thu, 11 Apr 2024 12:46:45 +0200 Subject: [PATCH 26/43] fix insight without permissions message --- .../features/vessel/insights/InsightErrorMessage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/features/vessel/insights/InsightErrorMessage.tsx b/apps/fishing-map/features/vessel/insights/InsightErrorMessage.tsx index e5a1c86189..02c83d7cd7 100644 --- a/apps/fishing-map/features/vessel/insights/InsightErrorMessage.tsx +++ b/apps/fishing-map/features/vessel/insights/InsightErrorMessage.tsx @@ -5,7 +5,7 @@ import { EMPTY_FIELD_PLACEHOLDER } from 'utils/info' const InsightError = ({ error }: { error: ParsedAPIError }) => { const { t } = useTranslation() - if (error.status === 401) { + if (error.status === 403) { return ( Date: Thu, 11 Apr 2024 12:48:23 +0200 Subject: [PATCH 27/43] New translations translations.json (English) --- apps/fishing-map/public/locales/en/translations.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/public/locales/en/translations.json b/apps/fishing-map/public/locales/en/translations.json index c9f96464fd..7625549cbf 100644 --- a/apps/fishing-map/public/locales/en/translations.json +++ b/apps/fishing-map/public/locales/en/translations.json @@ -797,7 +797,7 @@ "insights": { "coverage": "AIS Coverage", "disclaimerTimeRangeBeforeMinYear": "Insights available from 1 January {{year}} onwards. Adjust your time range to view insights.", - "errorPermisions": "You don't have permissions to see this", + "errorPermisions": "This insight is restricted to limited access, contact us for further details", "gaps": "AIS Off Events", "gapsEvents": "{{count}} AIS Off events detected", "gapsEventsEmpty": "No AIS Off events detected", From 4bdbb4e8f23f7b9d1685511959d1d6c90f7b9899 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 11 Apr 2024 17:39:54 +0200 Subject: [PATCH 28/43] improve typings --- .../features/map/map-interaction.utils.ts | 7 +++---- .../features/map/map-interactions.hooks.ts | 2 +- .../features/map/popups/ContextLayers.tsx | 4 ++-- .../interaction-features.utils.ts | 10 ++-------- .../src/interactions/types.ts | 4 ++-- .../src/resolvers/context.ts | 9 ++++----- .../src/resolvers/fourwings.ts | 20 +++++++++++++++++++ .../src/resolvers/index.ts | 8 +++++--- .../src/resolvers/types.ts | 6 +++--- .../src/layers/cluster/ClusterLayer.ts | 14 ++++++------- .../src/layers/context/context.types.ts | 6 +++--- .../src/layers/vessel/VesselLayer.ts | 1 - libs/deck-layers/src/types.ts | 4 +++- libs/deck-layers/src/utils/layers.ts | 4 ++-- 14 files changed, 57 insertions(+), 42 deletions(-) diff --git a/apps/fishing-map/features/map/map-interaction.utils.ts b/apps/fishing-map/features/map/map-interaction.utils.ts index bb7118335e..7270e47f15 100644 --- a/apps/fishing-map/features/map/map-interaction.utils.ts +++ b/apps/fishing-map/features/map/map-interaction.utils.ts @@ -1,6 +1,5 @@ -import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layers' +import { DeckLayerPickingObject } from '@globalfishingwatch/deck-layers' import { RulerPickingObject } from '@globalfishingwatch/deck-layers' -export const isRulerLayerPoint = (feature: DeckLayerInteractionFeature) => - feature.category === 'rulers' && - (feature as unknown as RulerPickingObject).geometry?.type === 'Point' +export const isRulerLayerPoint = (feature: DeckLayerPickingObject) => + feature.category === 'rulers' && (feature as RulerPickingObject).geometry?.type === 'Point' diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 394b107505..ed56d81e5f 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -314,7 +314,7 @@ export const useMapMouseClick = () => { if (!clickedEvent || !clickedTooltipEvent) return const layersByCategory = (clickedTooltipEvent?.features ?? []) - .sort( + .toSorted( (a, b) => POPUP_CATEGORY_ORDER.indexOf(a.category) - POPUP_CATEGORY_ORDER.indexOf(b.category) ) diff --git a/apps/fishing-map/features/map/popups/ContextLayers.tsx b/apps/fishing-map/features/map/popups/ContextLayers.tsx index e173428e43..b37fe117a1 100644 --- a/apps/fishing-map/features/map/popups/ContextLayers.tsx +++ b/apps/fishing-map/features/map/popups/ContextLayers.tsx @@ -1,7 +1,7 @@ import { Fragment, useCallback } from 'react' import { groupBy } from 'lodash' import { Icon } from '@globalfishingwatch/ui-components' -import { ContextFeature } from '@globalfishingwatch/deck-layers' +import { ContextPickingObject } from '@globalfishingwatch/deck-layers' import { TooltipEventFeature } from 'features/map/map.hooks' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import styles from './Popup.module.css' @@ -9,7 +9,7 @@ import ContextLayersRow from './ContextLayersRow' import { useContextInteractions } from './ContextLayers.hooks' type ContextTooltipRowProps = { - features: ContextFeature[] + features: ContextPickingObject[] showFeaturesDetails: boolean } diff --git a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts index 6f2acfc9ca..3a23e3b249 100644 --- a/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts +++ b/libs/deck-layer-composer/src/interactions/interaction-features.utils.ts @@ -1,15 +1,9 @@ -import { - ContextLayer, - ContextPickingObject, - DeckLayerInteractionFeature, - FourwingsLayer, - FourwingsPickingObject, -} from '@globalfishingwatch/deck-layers' +import { DeckLayerPickingObject } from '@globalfishingwatch/deck-layers' import { VALUE_MULTIPLIER } from '@globalfishingwatch/fourwings-aggregate' // import { DeckLayerInteractionPickingInfo } from '../types' // TODO: deck check if this is still needed -export const filterUniqueFeatureInteraction = (features: DeckLayerInteractionFeature[]) => { +export const filterUniqueFeatureInteraction = (features: DeckLayerPickingObject[]) => { return features // const uniqueLayerIdFeatures: Record = {} // const filtered = features?.filter(({ layerId, id, uniqueFeatureInteraction }) => { diff --git a/libs/deck-layer-composer/src/interactions/types.ts b/libs/deck-layer-composer/src/interactions/types.ts index 84648e27f6..7bd5bb1c36 100644 --- a/libs/deck-layer-composer/src/interactions/types.ts +++ b/libs/deck-layer-composer/src/interactions/types.ts @@ -1,8 +1,8 @@ -import { DeckLayerInteractionFeature } from '@globalfishingwatch/deck-layers' +import { DeckLayerPickingObject } from '@globalfishingwatch/deck-layers' export type InteractionEvent = { type: 'click' | 'hover' - features?: DeckLayerInteractionFeature[] + features?: DeckLayerPickingObject[] latitude: number longitude: number point: { x: number; y: number } diff --git a/libs/deck-layer-composer/src/resolvers/context.ts b/libs/deck-layer-composer/src/resolvers/context.ts index 4b16a7cca3..c5f568a45d 100644 --- a/libs/deck-layer-composer/src/resolvers/context.ts +++ b/libs/deck-layer-composer/src/resolvers/context.ts @@ -1,10 +1,9 @@ -import { PickingInfo } from '@deck.gl/core' -import { Dataset, DatasetTypes, DataviewInstance } from '@globalfishingwatch/api-types' +import { Dataset, DatasetTypes } from '@globalfishingwatch/api-types' import { - ContextFeature, ContextLayerConfig, ContextLayerId, ContextLayerProps, + ContextPickingObject, } from '@globalfishingwatch/deck-layers' import { resolveDataviewDatasetResource } from '@globalfishingwatch/dataviews-client' import { @@ -12,11 +11,11 @@ import { getDatasetConfiguration, resolveEndpoint, } from '@globalfishingwatch/datasets-client' -import { DeckResolverFunction, ResolverGlobalConfig } from './types' +import { DeckResolverFunction } from './types' export const resolveDeckContextLayerProps: DeckResolverFunction< ContextLayerProps, - ContextFeature + ContextPickingObject > = (dataview, globalConfig, interactions) => { // TODO make this work for auxiliar layers // https://github.com/GlobalFishingWatch/frontend/blob/master/libs/dataviews-client/src/resolve-dataviews-generators.ts#L606 diff --git a/libs/deck-layer-composer/src/resolvers/fourwings.ts b/libs/deck-layer-composer/src/resolvers/fourwings.ts index cd0f720412..8740650553 100644 --- a/libs/deck-layer-composer/src/resolvers/fourwings.ts +++ b/libs/deck-layer-composer/src/resolvers/fourwings.ts @@ -74,6 +74,7 @@ export const resolveDeckFourwingsLayerProps: DeckResolverFunction< const dataset = dataview.config?.sublayers ?.flatMap((sublayer) => sublayer.datasets) ?.find((dataset) => dataset.type === DatasetTypes.Fourwings) + const tilesUrl = dataset ? resolveEndpoint( dataset, @@ -93,6 +94,25 @@ export const resolveDeckFourwingsLayerProps: DeckResolverFunction< ) : undefined + const interactionUrl = dataset + ? resolveEndpoint( + dataset, + { + datasetId: dataset.id, + endpoint: EndpointId.FourwingsTiles, + params: [ + { + id: 'type', + // api enpdoint needs 'position' instead of 'positions' + // TODO: discuss this with Raul before the release + value: visualizationMode === 'positions' ? 'position' : 'heatmap', + }, + ], + }, + { absolute: true } + ) + : undefined + return { id: dataview.id, startTime, diff --git a/libs/deck-layer-composer/src/resolvers/index.ts b/libs/deck-layer-composer/src/resolvers/index.ts index dd26455582..7ba82a21fa 100644 --- a/libs/deck-layer-composer/src/resolvers/index.ts +++ b/libs/deck-layer-composer/src/resolvers/index.ts @@ -5,7 +5,9 @@ import { ClusterLayer, ContextFeature, ContextLayer, - DeckLayerInteractionFeature, + ContextPickingInfo, + ContextPickingObject, + DeckLayerPickingObject, FourwingsLayer, FourwingsPickingObject, VesselLayer, @@ -29,7 +31,7 @@ export * from './vessels' export const dataviewToDeckLayer = ( dataview: DataviewInstance, globalConfig: ResolverGlobalConfig, - interactions = [] as DeckLayerInteractionFeature[] + interactions = [] as DeckLayerPickingObject[] ): AnyDeckLayer => { if (dataview.config?.type === DataviewType.Basemap) { const deckLayerProps = resolveDeckBasemapLayerProps(dataview, globalConfig) @@ -51,7 +53,7 @@ export const dataviewToDeckLayer = ( const deckLayerProps = resolveDeckContextLayerProps( dataview, globalConfig, - interactions as ContextFeature[] + interactions as ContextPickingObject[] ) const layer = new ContextLayer(deckLayerProps) return layer diff --git a/libs/deck-layer-composer/src/resolvers/types.ts b/libs/deck-layer-composer/src/resolvers/types.ts index d229affa78..45a2f8ed34 100644 --- a/libs/deck-layer-composer/src/resolvers/types.ts +++ b/libs/deck-layer-composer/src/resolvers/types.ts @@ -2,7 +2,7 @@ import { EventTypes } from '@globalfishingwatch/api-types' import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { AnyDeckLayer, - DeckLayerInteractionFeature, + DeckLayerPickingObject, FourwingsResolution, FourwingsVisualizationMode, } from '@globalfishingwatch/deck-layers' @@ -27,9 +27,9 @@ export type ResolverGlobalConfig = { export type DeckResolverFunction< LayerProps = AnyDeckLayer['props'], - InteractionFeature = DeckLayerInteractionFeature + InteractionFeature = DeckLayerPickingObject > = ( dataview: UrlDataviewInstance, - { start, end, resolution, debug }: ResolverGlobalConfig, + globalConfig: ResolverGlobalConfig, interactions?: InteractionFeature[] ) => LayerProps diff --git a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts index e0117661e9..541fa4919c 100644 --- a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts +++ b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts @@ -1,4 +1,4 @@ -import { CompositeLayer, DefaultProps, LayerProps } from '@deck.gl/core' +import { CompositeLayer, DefaultProps, LayerProps, PickingInfo } from '@deck.gl/core' import { MVTLayer, TileLayerProps } from '@deck.gl/geo-layers' import { stringify } from 'qs' import { GFWAPI } from '@globalfishingwatch/api-client' @@ -25,13 +25,13 @@ export class ClusterLayer extends CompositeLayer { - let { object } = info - if (object) { - object.layerId = this.props.id - object.category = this.props.category + getPickingInfo = ({ info }: { info: PickingInfo }): ClusterPickingInfo => { + const object = { + ...(info.object || ({} as ClusterFeature)), + layerId: this.props.id, + category: this.props.category, } - return info + return { ...info, object } } renderLayers() { diff --git a/libs/deck-layers/src/layers/context/context.types.ts b/libs/deck-layers/src/layers/context/context.types.ts index e7f863c437..a1c875079b 100644 --- a/libs/deck-layers/src/layers/context/context.types.ts +++ b/libs/deck-layers/src/layers/context/context.types.ts @@ -44,13 +44,13 @@ export type ContextFeatureProperties = { category: DataviewCategory link?: string } -export type ContextFeature = Feature> & - ContextFeatureProperties + +export type ContextFeature = Feature> // TODO:deck create this type in the proper deck class layer export type UserContextFeature = Feature> & ContextFeatureProperties -export type ContextPickingObject = ContextFeature +export type ContextPickingObject = ContextFeature & ContextFeatureProperties export type UserContextPickingObject = UserContextFeature export type ContextPickingInfo = PickingInfo< diff --git a/libs/deck-layers/src/layers/vessel/VesselLayer.ts b/libs/deck-layers/src/layers/vessel/VesselLayer.ts index 44b31556e2..7747727745 100644 --- a/libs/deck-layers/src/layers/vessel/VesselLayer.ts +++ b/libs/deck-layers/src/layers/vessel/VesselLayer.ts @@ -118,7 +118,6 @@ export class VesselLayer extends CompositeLayer { type, onError: this.onSublayerError, loaders: [VesselEventsLoader], - // loaderOptions: { worker: false }, pickable: true, getPolygonOffset: (params: any) => getLayerGroupOffset(LayerGroup.Point, params), getFillColor: (d: any): Color => { diff --git a/libs/deck-layers/src/types.ts b/libs/deck-layers/src/types.ts index 125ab4aa39..b3750d44e3 100644 --- a/libs/deck-layers/src/types.ts +++ b/libs/deck-layers/src/types.ts @@ -17,10 +17,12 @@ import { VesselEventPickingObject, VesselEventPickingInfo } from './layers/vesse export type DeckLayerCategory = `${DataviewCategory}` | 'rulers' +// TODO:deck move this type to a generic like DeckLayerProps export type BaseLayerProps = { category: DeckLayerCategory } +// TODO:deck move this type to a generic like DeckPickingInfo export type BasePickingInfo = { layerId: string category: DeckLayerCategory @@ -36,7 +38,7 @@ export type AnyDeckLayer = export type LayerWithIndependentSublayersLoadState = VesselLayer -export type DeckLayerInteractionFeature = +export type DeckLayerPickingObject = | FourwingsPickingObject | ContextPickingObject | UserContextPickingObject diff --git a/libs/deck-layers/src/utils/layers.ts b/libs/deck-layers/src/utils/layers.ts index d97c1a3ef9..a0e6f59fd8 100644 --- a/libs/deck-layers/src/utils/layers.ts +++ b/libs/deck-layers/src/utils/layers.ts @@ -3,13 +3,13 @@ import { ClipExtension } from '@deck.gl/extensions' import { TileLayerProps } from '@deck.gl/geo-layers' import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' import { Matrix4 } from '@math.gl/core' -import { DeckLayerInteractionFeature } from '../types' +import { DeckLayerPickingObject } from '../types' const WORLD_SIZE = 512 export function getPickedFeatureToHighlight( data: any, - pickedFeatures: DeckLayerInteractionFeature[], + pickedFeatures: DeckLayerPickingObject[], idProperty: string ) { return ( From fd2b79de678533e953c1facfb894d890384a90ed Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 11 Apr 2024 17:49:53 +0200 Subject: [PATCH 29/43] fix workers build order --- apps/fishing-map/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/project.json b/apps/fishing-map/project.json index 85d9a35320..3285d19c9f 100644 --- a/apps/fishing-map/project.json +++ b/apps/fishing-map/project.json @@ -25,7 +25,7 @@ "build": { "executor": "nx:run-commands", "options": { - "commands": ["nx build:app fishing-map", "nx prepare-loaders fishing-map"], + "commands": ["nx prepare-loaders fishing-map", "nx build:app fishing-map"], "parallel": false } }, From 0bf708aac51fe7a13030100f2b0bee6b4f7e38d8 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 11 Apr 2024 18:04:54 +0200 Subject: [PATCH 30/43] fix build --- .../features/map/popups/ContextLayersRow.tsx | 3 +- .../features/map/popups/PopupWrapper.tsx | 3 +- .../features/map/popups/UserContextLayers.tsx | 4 +-- .../src/layers/context/ContextLayer.ts | 29 ++++++++++++------- .../src/layers/context/context.utils.ts | 9 ++++-- 5 files changed, 31 insertions(+), 17 deletions(-) diff --git a/apps/fishing-map/features/map/popups/ContextLayersRow.tsx b/apps/fishing-map/features/map/popups/ContextLayersRow.tsx index 296390d147..d6ff483b5c 100644 --- a/apps/fishing-map/features/map/popups/ContextLayersRow.tsx +++ b/apps/fishing-map/features/map/popups/ContextLayersRow.tsx @@ -129,7 +129,8 @@ export const ReportPopupLink = ({ feature, onClick }: ReportPopupButtonProps) => payload: { category: workspace?.category || DEFAULT_WORKSPACE_CATEGORY, workspaceId: workspace?.id || DEFAULT_WORKSPACE_ID, - datasetId: feature.datasetId, + // TODO: deck fix this typing + datasetId: (feature as any).datasetId, areaId, }, query: { diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index cb1dda9f02..791dd5e5da 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -9,6 +9,7 @@ import { IconButton, Spinner } from '@globalfishingwatch/ui-components' import { InteractionEvent } from '@globalfishingwatch/deck-layer-composer' import { ContextFeature, + ContextPickingObject, FourwingsPickingObject, VesselEventPickingObject, } from '@globalfishingwatch/deck-layers' @@ -211,7 +212,7 @@ function PopupWrapper({ showFeaturesDetails={type === 'click'} /> */} diff --git a/apps/fishing-map/features/map/popups/UserContextLayers.tsx b/apps/fishing-map/features/map/popups/UserContextLayers.tsx index 60991df130..d0a3d0d711 100644 --- a/apps/fishing-map/features/map/popups/UserContextLayers.tsx +++ b/apps/fishing-map/features/map/popups/UserContextLayers.tsx @@ -3,14 +3,14 @@ import { groupBy } from 'lodash' import { useTranslation } from 'react-i18next' import { Icon } from '@globalfishingwatch/ui-components' import { DRAW_DATASET_SOURCE } from '@globalfishingwatch/api-types' -import { ContextFeature } from '@globalfishingwatch/deck-layers' +import { ContextFeature, ContextPickingObject } from '@globalfishingwatch/deck-layers' import { TooltipEventFeature } from 'features/map/map.hooks' import styles from './Popup.module.css' import ContextLayersRow from './ContextLayersRow' import { useContextInteractions } from './ContextLayers.hooks' type UserContextLayersProps = { - features: ContextFeature[] + features: ContextPickingObject[] showFeaturesDetails: boolean } diff --git a/libs/deck-layers/src/layers/context/ContextLayer.ts b/libs/deck-layers/src/layers/context/ContextLayer.ts index 394ec00655..b9a6fee7d4 100644 --- a/libs/deck-layers/src/layers/context/ContextLayer.ts +++ b/libs/deck-layers/src/layers/context/ContextLayer.ts @@ -61,23 +61,30 @@ export class ContextLayer extends CompositeLayer<_ContextLayerProps getPickingInfo = ({ info, }: { - info: PickingInfo + info: PickingInfo }): ContextPickingInfo => { const { idProperty, valueProperties } = this.props + const object = { + ...transformTileCoordsToWGS84( + info.object as ContextFeature, + info.tile!.bbox as GeoBoundingBox, + this.context.viewport + ), + title: this.props.id, + color: this.props.color, + layerId: this.props.layers[0].id, + datasetId: this.props.layers[0].datasetId, + category: this.props.category, + id: getContextId(info.object as ContextFeature, idProperty), + value: getContextValue(info.object as ContextFeature, valueProperties), + link: getContextLink(info.object as ContextPickingObject), + } as ContextPickingObject info.object = transformTileCoordsToWGS84( info.object as ContextFeature, info.tile!.bbox as GeoBoundingBox, this.context.viewport - ) as ContextFeature - info.object.title = this.props.id - info.object.color = this.props.color - info.object.layerId = this.props.layers[0].id - info.object.datasetId = this.props.layers[0].datasetId - info.object.category = this.props.category - info.object.id = getContextId(info.object, idProperty) - info.object.value = getContextValue(info.object, valueProperties) - info.object.link = getContextLink(info.object) - return info + ) as ContextPickingObject + return { ...info, object } } renderLayers() { diff --git a/libs/deck-layers/src/layers/context/context.utils.ts b/libs/deck-layers/src/layers/context/context.utils.ts index 8bbab61d05..75384cf483 100644 --- a/libs/deck-layers/src/layers/context/context.utils.ts +++ b/libs/deck-layers/src/layers/context/context.utils.ts @@ -1,4 +1,9 @@ -import { ContextFeature, ContextLayerId } from './context.types' +import { + ContextFeature, + ContextLayerId, + ContextPickingInfo, + ContextPickingObject, +} from './context.types' export const getContextId = (feature: ContextFeature, idProperty = 'gfw_id'): string => { return feature.properties?.[idProperty] || feature.properties?.gfw_id || feature.properties.id @@ -48,7 +53,7 @@ const RFMO_LINKS: Record = { WCPFC: 'https://www.wcpfc.int/', } -export const getContextLink = (feature: ContextFeature) => { +export const getContextLink = (feature: ContextPickingObject) => { if (!feature?.layerId) { return '' } From b1d163b3d5f7c2f8dd59ddfa89224e349e2540ee Mon Sep 17 00:00:00 2001 From: j8seangel Date: Thu, 11 Apr 2024 18:30:27 +0200 Subject: [PATCH 31/43] fix positions vessel sprite path --- .../layers/fourwings/FourwingsPositionsTileLayer.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts index eec4a73180..bc4447ab93 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsPositionsTileLayer.ts @@ -12,7 +12,11 @@ import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' import { GFWAPI } from '@globalfishingwatch/api-client' import { COLOR_RAMP_DEFAULT_NUM_STEPS } from '@globalfishingwatch/layer-composer' import { getLayerGroupOffset, LayerGroup } from '../../utils' -import { POSITIONS_API_TILES_URL, POSITIONS_VISUALIZATION_MIN_ZOOM } from './fourwings.config' +import { + PATH_BASENAME, + POSITIONS_API_TILES_URL, + POSITIONS_VISUALIZATION_MIN_ZOOM, +} from './fourwings.config' import { getRoundedDateFromTS } from './fourwings.utils' import { FourwingsTileLayerColorDomain, @@ -231,7 +235,7 @@ export class FourwingsPositionsTileLayer extends CompositeLayer< new IconLayerClass(this.props, { id: 'allPositions', data: allPositions, - iconAtlas: '/vessel-sprite.png', + iconAtlas: `${PATH_BASENAME}/vessel-sprite.png`, iconMapping: ICON_MAPPING, getIcon: () => 'rect', getPosition: (d: any) => d.geometry.coordinates, @@ -248,7 +252,7 @@ export class FourwingsPositionsTileLayer extends CompositeLayer< new IconLayer(this.props as any, { id: 'lastPositions', data: lastPositions, - iconAtlas: '/vessel-sprite.png', + iconAtlas: `${PATH_BASENAME}/vessel-sprite.png`, iconMapping: ICON_MAPPING, getIcon: () => 'vesselHighlight', getPosition: (d) => d.geometry.coordinates, @@ -262,7 +266,7 @@ export class FourwingsPositionsTileLayer extends CompositeLayer< new IconLayerClass(this.props, { id: 'lastPositionsOver', data: lastPositions, - iconAtlas: '/vessel-sprite.png', + iconAtlas: `${PATH_BASENAME}/vessel-sprite.png`, iconMapping: ICON_MAPPING, getIcon: () => 'vessel', getSize: 19, From bb453d91c8d94da38e4aadf1108bd9034be4e1d8 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 12 Apr 2024 10:25:14 +0200 Subject: [PATCH 32/43] New Crowdin updates (#2608) New translations --- apps/fishing-map/public/locales/en/translations.json | 2 +- apps/fishing-map/public/locales/es/translations.json | 6 +++--- apps/fishing-map/public/locales/fr/translations.json | 6 +++--- apps/fishing-map/public/locales/pt/translations.json | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/apps/fishing-map/public/locales/en/translations.json b/apps/fishing-map/public/locales/en/translations.json index 7625549cbf..5df8e40737 100644 --- a/apps/fishing-map/public/locales/en/translations.json +++ b/apps/fishing-map/public/locales/en/translations.json @@ -797,7 +797,7 @@ "insights": { "coverage": "AIS Coverage", "disclaimerTimeRangeBeforeMinYear": "Insights available from 1 January {{year}} onwards. Adjust your time range to view insights.", - "errorPermisions": "This insight is restricted to limited access, contact us for further details", + "errorPermisions": "This insight is restricted to limited account users, contact us at support@globalfishingwatch.org for more details", "gaps": "AIS Off Events", "gapsEvents": "{{count}} AIS Off events detected", "gapsEventsEmpty": "No AIS Off events detected", diff --git a/apps/fishing-map/public/locales/es/translations.json b/apps/fishing-map/public/locales/es/translations.json index bfe0d27cdf..678151a95f 100644 --- a/apps/fishing-map/public/locales/es/translations.json +++ b/apps/fishing-map/public/locales/es/translations.json @@ -797,7 +797,7 @@ "insights": { "coverage": "Cobertura de AIS", "disclaimerTimeRangeBeforeMinYear": "Los indicadores están disponibles a partir del 1º de enero de {{year}}. Ajuste el rango del tiempo para ver los indicadores.", - "errorPermisions": "No tiene permiso para ver este indicador", + "errorPermisions": "Esta información está restringida a usuarios concretos; contáctenos en support@globalfishingwatch.org para obtener más detalles", "gaps": "Eventos de desactivación de AIS", "gapsEvents": "{{count}} eventos de desactivación de AIS", "gapsEventsEmpty": "No se detectaron eventos de desactivación de AIS", @@ -863,9 +863,9 @@ "registrySources": "Fuentes", "sectionActivity": "Actividad", "sectionAreas": "Áreas", - "sectionInsights": "Insights", + "sectionInsights": "Indicadores", "sectionRelatedVessel": "Related vessels", - "sectionRelatedVessels": "Related vessels", + "sectionRelatedVessels": "Embarcaciones relacionadas", "sectionSummary": "Resumen", "share": "Compartir embarcación", "shipname": "Nombre", diff --git a/apps/fishing-map/public/locales/fr/translations.json b/apps/fishing-map/public/locales/fr/translations.json index 7291faf0c6..814a4c5c71 100644 --- a/apps/fishing-map/public/locales/fr/translations.json +++ b/apps/fishing-map/public/locales/fr/translations.json @@ -797,7 +797,7 @@ "insights": { "coverage": "Couverture AIS", "disclaimerTimeRangeBeforeMinYear": "Indicateurs disponibles à partir du 1er janvier {{year}}. Ajustez votre plage de temps pour visualiser les indicateurs.", - "errorPermisions": "Vous ne disposez pas des permissions requises pour visualiser cet indicateur", + "errorPermisions": "Cet aperçu est réservé aux utilisateurs de comptes limités, contactez-nous à support@globalfishingwatch.org pour plus de détails", "gaps": "Événements de désactivation de l'AIS", "gapsEvents": "{{count}} événements de désactivation de l'AIS détectés", "gapsEventsEmpty": "Aucun événement de désactivation de l'AIS détecté", @@ -863,9 +863,9 @@ "registrySources": "sources de registres", "sectionActivity": "Activité", "sectionAreas": "Zones", - "sectionInsights": "Insights", + "sectionInsights": "Indicateurs", "sectionRelatedVessel": "Related vessels", - "sectionRelatedVessels": "Related vessels", + "sectionRelatedVessels": "Navires associés", "sectionSummary": "Synthèse", "share": "Partager le navire", "shipname": "Nom", diff --git a/apps/fishing-map/public/locales/pt/translations.json b/apps/fishing-map/public/locales/pt/translations.json index 5dd422528c..dbe90dbf20 100644 --- a/apps/fishing-map/public/locales/pt/translations.json +++ b/apps/fishing-map/public/locales/pt/translations.json @@ -797,7 +797,7 @@ "insights": { "coverage": "Cobertura AIS", "disclaimerTimeRangeBeforeMinYear": "Indicadores disponíveis a partir de 1º de janeiro de {{year}}. Ajuste seu intervalo de tempo para visualizar os indicadores.", - "errorPermisions": "Você não tem permissão visualizar esses indicadores", + "errorPermisions": "Este insight é restrito a usuários de contas limitadas. Entre em contato conosco pelo e-mail support@globalfishingwatch.org para obter mais detalhes", "gaps": "Eventos de desativação do AIS", "gapsEvents": "{{count}} eventos de desativação do AIS detectados", "gapsEventsEmpty": "Nenhum evento de desativação do AIS detectado", @@ -863,9 +863,9 @@ "registrySources": "fontes de registro", "sectionActivity": "Atividade", "sectionAreas": "Áreas", - "sectionInsights": "Insights", + "sectionInsights": "Indicadores", "sectionRelatedVessel": "Related vessels", - "sectionRelatedVessels": "Related vessels", + "sectionRelatedVessels": "Embarcações relacionadas", "sectionSummary": "Resumo", "share": "Compartilhar embarcação", "shipname": "Nome", From a8d5bfea38b9c0c83b80f52431553f9a01cafb81 Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 12 Apr 2024 11:45:13 +0200 Subject: [PATCH 33/43] filter visible values in environment layers --- .../layers/fourwings/FourwingsHeatmapLayer.ts | 18 +++++- .../fourwings/FourwingsHeatmapStaticLayer.ts | 22 +++---- .../fourwings/FourwingsHeatmapTileLayer.ts | 60 ++++++++++--------- .../src/layers/fourwings/fourwings.utils.ts | 6 +- libs/deck-layers/src/utils/colorRamps.ts | 35 ++++------- 5 files changed, 72 insertions(+), 69 deletions(-) diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts index fc513fed75..20b10d1025 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts @@ -89,6 +89,8 @@ export class FourwingsHeatmapLayer extends CompositeLayer maxVisibleValue) + ) { target = EMPTY_CELL_COLOR return target } @@ -187,7 +193,15 @@ export class FourwingsHeatmapLayer extends CompositeLayer getLayerGroupOffset(LayerGroup.Heatmap, params), updateTriggers: { // This tells deck.gl to recalculate fillColor on changes - getFillColor: [startTime, endTime, colorDomain, colorRanges, comparisonMode], + getFillColor: [ + startTime, + endTime, + colorDomain, + colorRanges, + comparisonMode, + minVisibleValue, + maxVisibleValue, + ], }, }) ), diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts index fab43dd262..19fd41f4b4 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts @@ -36,7 +36,7 @@ import { deckToRgbaColor, getLayerGroupOffset, } from '../../utils' -import { EMPTY_CELL_COLOR, filterElementByPercentOfIndex } from './fourwings.utils' +import { EMPTY_CELL_COLOR, filterCells } from './fourwings.utils' import { FOURWINGS_MAX_ZOOM, HEATMAP_API_TILES_URL, @@ -92,24 +92,16 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< // NO_DATA_VALUE = 0 // SCALE_VALUE = 0.01 // OFFSET_VALUE = 0 + const { minVisibleValue, maxVisibleValue } = this.props const currentZoomData = this.getData() if (!currentZoomData.length) { return this.getColorDomain() } - const dataSample = - currentZoomData.length > MAX_RAMP_VALUES_PER_TILE - ? currentZoomData.filter(filterElementByPercentOfIndex) - : currentZoomData - - const allValues = dataSample.flatMap((feature) => { - if ( - (this.props.minVisibleValue && feature.properties.count < this.props.minVisibleValue) || - (this.props.maxVisibleValue && feature.properties.count > this.props.maxVisibleValue) - ) { - return [] - } - return feature.properties?.count - }) + const values = currentZoomData.flatMap((d) => d.properties?.count || []) + const allValues = + values.length > MAX_RAMP_VALUES_PER_TILE + ? values.filter((d, i) => filterCells(d, i, minVisibleValue, maxVisibleValue)) + : values const steps = ckmeans(allValues, Math.min(allValues.length, COLOR_RAMP_DEFAULT_NUM_STEPS)).map( (step) => step[0] diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapTileLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapTileLayer.ts index e1577bbb5f..a59679e936 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapTileLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapTileLayer.ts @@ -18,7 +18,6 @@ import { FourwingsLoader, ParseFourwingsOptions, } from '@globalfishingwatch/deck-loaders' -import { HEATMAP_COLOR_RAMPS, ColorRampsIds } from '@globalfishingwatch/layer-composer' import { GFWAPI } from '@globalfishingwatch/api-client' import { filterFeaturesByBounds } from '@globalfishingwatch/data-transforms' import { @@ -26,12 +25,13 @@ import { COLOR_RAMP_DEFAULT_NUM_STEPS, ColorRampId, getBivariateRamp, + getColorRamp, } from '../../utils/colorRamps' import { aggregateCellTimeseries, - filterElementByPercentOfIndex, getFourwingsChunk, getDataUrlBySublayer, + filterCells, } from './fourwings.utils' import { FourwingsHeatmapLayer } from './FourwingsHeatmapLayer' import { @@ -88,11 +88,8 @@ export class FourwingsHeatmapTileLayer extends CompositeLayer< if (this.props.comparisonMode === FourwingsComparisonMode.Bivariate) { return getBivariateRamp(this.props.sublayers.map((s) => s?.colorRamp) as ColorRampId[]) } - return this.props.sublayers.map( - ({ colorRamp }) => - HEATMAP_COLOR_RAMPS[ - (this.props.colorRampWhiteEnd ? `${colorRamp}_toWhite` : colorRamp) as ColorRampsIds - ] + return this.props.sublayers.map(({ colorRamp }) => + getColorRamp({ rampId: colorRamp as ColorRampId, whiteEnd: this.props.colorRampWhiteEnd }) ) } @@ -101,14 +98,14 @@ export class FourwingsHeatmapTileLayer extends CompositeLayer< // NO_DATA_VALUE = 0 // SCALE_VALUE = 0.01 // OFFSET_VALUE = 0 - const { comparisonMode, aggregationOperation } = this.props + const { comparisonMode, aggregationOperation, minVisibleValue, maxVisibleValue } = this.props const currentZoomData = this.getData() if (!currentZoomData.length) { return this.getColorDomain() } const dataSample = currentZoomData.length > MAX_RAMP_VALUES_PER_TILE - ? currentZoomData.filter(filterElementByPercentOfIndex) + ? currentZoomData.filter((d, i) => filterCells(d, i)) : currentZoomData // TODO:deck remove this and calculate values equal to Compare @@ -116,19 +113,21 @@ export class FourwingsHeatmapTileLayer extends CompositeLayer< let allValues: [number[], number[]] = [[], []] dataSample.forEach((feature) => { feature.properties?.values.forEach((sublayerValues, sublayerIndex) => { - allValues[sublayerIndex].push(...sublayerValues.filter(filterElementByPercentOfIndex)) + allValues[sublayerIndex].push(...sublayerValues.filter((d, i) => filterCells(d, i))) }) }) if (!allValues.length) { return this.getColorDomain() } - const steps = allValues.filter((sublayer) => sublayer.length).map((sublayerValues) => - ckmeans( - sublayerValues, - Math.min(sublayerValues.length, COLOR_RAMP_BIVARIATE_NUM_STEPS) - ).map((step) => step[0]) - ) + const steps = allValues + .filter((sublayer) => sublayer.length) + .map((sublayerValues) => + ckmeans( + sublayerValues, + Math.min(sublayerValues.length, COLOR_RAMP_BIVARIATE_NUM_STEPS) + ).map((step) => step[0]) + ) return steps } @@ -137,7 +136,7 @@ export class FourwingsHeatmapTileLayer extends CompositeLayer< if (!values || !values.length || !Array.isArray(values)) { return [] } - return values.filter(filterElementByPercentOfIndex) + return values.filter((d, i) => filterCells(d, i, minVisibleValue, maxVisibleValue)) }) ) @@ -173,12 +172,6 @@ export class FourwingsHeatmapTileLayer extends CompositeLayer< colorRanges: FourwingsTileLayerColorRange ): FourwinsTileLayerScale[] => { if (this.props.comparisonMode === FourwingsComparisonMode.Bivariate) { - console.log('colorRanges:', colorRanges) - console.log('colorDomain:', colorDomain) - // return colorRanges.map((cr) => - // scaleLinear(colorDomain as number[], cr as string[]).clamp(true) - // ) - return (colorDomain as number[][]).map((cd, i) => { return scaleLinear(cd, colorRanges[i] as string[]).clamp(true) }) @@ -306,14 +299,25 @@ export class FourwingsHeatmapTileLayer extends CompositeLayer< } updateState({ props, oldProps }: UpdateParameters) { - const { startTime, endTime, availableIntervals } = props + const { + startTime, + endTime, + availableIntervals, + comparisonMode, + minVisibleValue, + maxVisibleValue, + } = props const { tilesCache, colorRanges, colorDomain } = this.state as FourwingsTileLayerState const newSublayerColorRanges = this._getColorRanges() const sublayersHaveNewColors = colorRanges.join() !== newSublayerColorRanges.join() - const newMode = oldProps.comparisonMode && props.comparisonMode !== oldProps.comparisonMode - - if (sublayersHaveNewColors || newMode) { - const newColorDomain = newMode ? this._calculateColorDomain() : colorDomain + const newMode = oldProps.comparisonMode && comparisonMode !== oldProps.comparisonMode + const newVisibleValueLimits = + (oldProps.minVisibleValue && minVisibleValue !== oldProps.minVisibleValue) || + (oldProps.maxVisibleValue && maxVisibleValue !== oldProps.maxVisibleValue) + + if (sublayersHaveNewColors || newMode || newVisibleValueLimits) { + const newColorDomain = + newMode || newVisibleValueLimits ? this._calculateColorDomain() : colorDomain const scales = this._getColorScales(newColorDomain, newSublayerColorRanges) this.setState({ colorRanges: newSublayerColorRanges, colorDomain: newColorDomain, scales }) } diff --git a/libs/deck-layers/src/layers/fourwings/fourwings.utils.ts b/libs/deck-layers/src/layers/fourwings/fourwings.utils.ts index 4e9ec47201..8ab8cd65b0 100644 --- a/libs/deck-layers/src/layers/fourwings/fourwings.utils.ts +++ b/libs/deck-layers/src/layers/fourwings/fourwings.utils.ts @@ -335,7 +335,9 @@ export function getIntervalFrames({ return { interval, tileStartFrame, startFrame, endFrame } } -export function filterElementByPercentOfIndex(value: any, index: number) { +export function filterCells(value: any, index: number, minValue?: number, maxValue?: number) { // Select only 5% of elements - return value && index % 20 === 1 + return ( + value && index % 20 === 1 && (!minValue || value > minValue) && (!maxValue || value < maxValue) + ) } diff --git a/libs/deck-layers/src/utils/colorRamps.ts b/libs/deck-layers/src/utils/colorRamps.ts index 7c95e5212b..a1895a0737 100644 --- a/libs/deck-layers/src/utils/colorRamps.ts +++ b/libs/deck-layers/src/utils/colorRamps.ts @@ -117,7 +117,7 @@ export const getBlend = (color1: RGBA, color2: RGBA) => { return normal({ ...hexToRgb(BLEND_BACKGROUND), a: 1 }, screen(color1 as RGBA, color2 as RGBA)) } -export const HEATMAP_COLORS_BY_ID = { +export const HEATMAP_COLORS_BY_ID: Record = { teal: '#00FFBC', magenta: '#FF64CE', lilac: '#9CA4FF', @@ -141,25 +141,16 @@ export const TIME_COMPARE_COLOR_RAMP = [ '#FF677D', ] -export const HEATMAP_COLOR_RAMPS: Record = { - teal: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.teal), - teal_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.teal), - magenta: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.magenta), - magenta_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.magenta), - lilac: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.lilac), - lilac_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.lilac), - salmon: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.salmon), - salmon_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.salmon), - sky: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.sky), - sky_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.sky), - red: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.red), - red_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.red), - yellow: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.yellow), - yellow_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.yellow), - green: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.green), - green_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.green), - orange: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.orange), - orange_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.orange), - bathymetry: getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID.bathymetry).reverse(), - bathymetry_toWhite: getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID.bathymetry).reverse(), +export const getColorRamp = ({ + rampId, + whiteEnd = false, +}: { + rampId: ColorRampId + whiteEnd?: boolean +}) => { + const ramp = whiteEnd + ? getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID[rampId]) + : getColorRampByOpacitySteps(rampId, COLOR_RAMP_BIVARIATE_NUM_STEPS) + if (rampId === 'bathymetry') ramp.reverse() + return ramp } From 7220ed1dd034fb890cc29b846e86b410ff9254af Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 12 Apr 2024 12:29:00 +0200 Subject: [PATCH 34/43] fix static heatmap color ramp --- .../fourwings/FourwingsHeatmapStaticLayer.ts | 25 +++++++------------ .../src/layers/fourwings/fourwings.types.ts | 1 + libs/deck-layers/src/utils/colorRamps.ts | 2 +- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts index 19fd41f4b4..f3c794cac5 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts @@ -22,19 +22,14 @@ import { FourwingsStaticFeature, FourwingsStaticFeatureProperties, } from '@globalfishingwatch/deck-loaders' -import { - HEATMAP_COLOR_RAMPS, - rgbaStringToComponents, - ColorRampsIds, -} from '@globalfishingwatch/layer-composer' import { filterFeaturesByBounds } from '@globalfishingwatch/data-transforms' -import { COLOR_RAMP_DEFAULT_NUM_STEPS } from '../../utils/colorRamps' +import { COLOR_RAMP_DEFAULT_NUM_STEPS, ColorRampId, getColorRamp } from '../../utils/colorRamps' import { COLOR_HIGHLIGHT_LINE, GFWMVTLoader, LayerGroup, - deckToRgbaColor, getLayerGroupOffset, + rgbaStringToComponents, } from '../../utils' import { EMPTY_CELL_COLOR, filterCells } from './fourwings.utils' import { @@ -46,7 +41,6 @@ import { FourwingsHeatmapTileLayerProps, FourwingsTileLayerState, FourwingsAggregationOperation, - FourwinsTileLayerScale, FourwingsHeatmapStaticLayerProps, FourwingsPickingInfo, FourwingsPickingObject, @@ -66,14 +60,13 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< > { static layerName = 'FourwingsHeatmapStaticLayer' static defaultProps = defaultProps - scale: typeof scaleLinear | undefined = undefined initializeState(context: LayerContext) { super.initializeState(context) this.state = { colorDomain: [], colorRanges: this._getColorRanges(), - scale: scaleLinear([], []), + scale: undefined, } } @@ -83,7 +76,7 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< _getColorRanges = () => { return this.props.sublayers.map(({ colorRamp }) => - HEATMAP_COLOR_RAMPS[colorRamp as ColorRampsIds].map((c) => rgbaStringToComponents(c)) + getColorRamp({ rampId: colorRamp as ColorRampId }) ) } @@ -106,13 +99,12 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< const steps = ckmeans(allValues, Math.min(allValues.length, COLOR_RAMP_DEFAULT_NUM_STEPS)).map( (step) => step[0] ) - return steps } _updateColorDomain = () => { const colorDomain = this._calculateColorDomain() as number[] - const colorRanges = this._getColorRanges()?.[0]?.map((c) => deckToRgbaColor(c)) + const colorRanges = this._getColorRanges()[0] this.setState({ colorDomain, scale: scaleLinear(colorDomain, colorRanges) }) } @@ -157,7 +149,8 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< ) { return EMPTY_CELL_COLOR } - const value = (this.state.scale as FourwinsTileLayerScale)(feature.properties.count) + + const value = (this.state as FourwingsTileLayerState).scale?.(feature.properties.count) if (!value) { return EMPTY_CELL_COLOR } @@ -176,7 +169,7 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< renderLayers(): Layer<{}> | LayersList { const { tilesUrl, sublayers, resolution, minVisibleValue, maxVisibleValue, maxZoom } = this.props - const { colorDomain, colorRanges, scales } = this.state as FourwingsTileLayerState + const { colorDomain, colorRanges, scale } = this.state as FourwingsTileLayerState const params = { datasets: sublayers.flatMap((sublayer) => sublayer.datasets), format: 'MVT', @@ -196,7 +189,7 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< getPolygonOffset: (params) => getLayerGroupOffset(LayerGroup.HeatmapStatic, params), getFillColor: this.getFillColor, updateTriggers: { - getFillColor: [colorDomain, colorRanges, scales, minVisibleValue, maxVisibleValue], + getFillColor: [colorDomain, colorRanges, scale, minVisibleValue, maxVisibleValue], }, }), ] diff --git a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts index ed37355379..e437cf492e 100644 --- a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts +++ b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts @@ -176,5 +176,6 @@ export type FourwingsTileLayerState = { colorDomain: FourwingsTileLayerColorDomain colorRanges: FourwingsTileLayerColorRange comparisonMode?: FourwingsComparisonMode + scale?: FourwinsTileLayerScale scales?: FourwinsTileLayerScale[] } diff --git a/libs/deck-layers/src/utils/colorRamps.ts b/libs/deck-layers/src/utils/colorRamps.ts index a1895a0737..7ff7ad49ab 100644 --- a/libs/deck-layers/src/utils/colorRamps.ts +++ b/libs/deck-layers/src/utils/colorRamps.ts @@ -150,7 +150,7 @@ export const getColorRamp = ({ }) => { const ramp = whiteEnd ? getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID[rampId]) - : getColorRampByOpacitySteps(rampId, COLOR_RAMP_BIVARIATE_NUM_STEPS) + : getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID[rampId], COLOR_RAMP_DEFAULT_NUM_STEPS) if (rampId === 'bathymetry') ramp.reverse() return ramp } From ae5ec79f9230c33b8b6f364da283b614eab5d72a Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 12 Apr 2024 13:12:06 +0200 Subject: [PATCH 35/43] code review --- .../fourwings/FourwingsHeatmapStaticLayer.ts | 21 +++++++++---------- .../src/layers/fourwings/fourwings.types.ts | 1 - libs/deck-layers/src/utils/colorRamps.ts | 2 +- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts index f3c794cac5..e40d186017 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapStaticLayer.ts @@ -66,7 +66,7 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< this.state = { colorDomain: [], colorRanges: this._getColorRanges(), - scale: undefined, + scales: [], } } @@ -105,7 +105,7 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< _updateColorDomain = () => { const colorDomain = this._calculateColorDomain() as number[] const colorRanges = this._getColorRanges()[0] - this.setState({ colorDomain, scale: scaleLinear(colorDomain, colorRanges) }) + this.setState({ colorDomain, scales: [scaleLinear(colorDomain, colorRanges)] }) } debouncedUpdateColorDomain = debounce(() => { @@ -136,24 +136,22 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< } getFillColor = (feature: Feature) => { - if (!this.state.scale) { - return EMPTY_CELL_COLOR - } - if (!feature.properties.count) { - // TODO make the filter for the visible data here - return EMPTY_CELL_COLOR - } + const { scales } = this.state as FourwingsTileLayerState + const scale = scales?.[0] if ( + !scale || + !feature.properties.count || (this.props.minVisibleValue && feature.properties.count < this.props.minVisibleValue) || (this.props.maxVisibleValue && feature.properties.count > this.props.maxVisibleValue) ) { return EMPTY_CELL_COLOR } - const value = (this.state as FourwingsTileLayerState).scale?.(feature.properties.count) + const value = scale(feature.properties.count) if (!value) { return EMPTY_CELL_COLOR } + return rgbaStringToComponents(value) as Color } @@ -169,7 +167,8 @@ export class FourwingsHeatmapStaticLayer extends CompositeLayer< renderLayers(): Layer<{}> | LayersList { const { tilesUrl, sublayers, resolution, minVisibleValue, maxVisibleValue, maxZoom } = this.props - const { colorDomain, colorRanges, scale } = this.state as FourwingsTileLayerState + const { colorDomain, colorRanges, scales } = this.state as FourwingsTileLayerState + const scale = scales?.[0] const params = { datasets: sublayers.flatMap((sublayer) => sublayer.datasets), format: 'MVT', diff --git a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts index e437cf492e..ed37355379 100644 --- a/libs/deck-layers/src/layers/fourwings/fourwings.types.ts +++ b/libs/deck-layers/src/layers/fourwings/fourwings.types.ts @@ -176,6 +176,5 @@ export type FourwingsTileLayerState = { colorDomain: FourwingsTileLayerColorDomain colorRanges: FourwingsTileLayerColorRange comparisonMode?: FourwingsComparisonMode - scale?: FourwinsTileLayerScale scales?: FourwinsTileLayerScale[] } diff --git a/libs/deck-layers/src/utils/colorRamps.ts b/libs/deck-layers/src/utils/colorRamps.ts index 7ff7ad49ab..f4d16a5d45 100644 --- a/libs/deck-layers/src/utils/colorRamps.ts +++ b/libs/deck-layers/src/utils/colorRamps.ts @@ -150,7 +150,7 @@ export const getColorRamp = ({ }) => { const ramp = whiteEnd ? getMixedOpacityToWhiteColorRamp(HEATMAP_COLORS_BY_ID[rampId]) - : getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID[rampId], COLOR_RAMP_DEFAULT_NUM_STEPS) + : getColorRampByOpacitySteps(HEATMAP_COLORS_BY_ID[rampId]) if (rampId === 'bathymetry') ramp.reverse() return ramp } From 6f87dafcc3e3cd5071be86bc73cae03618784fee Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 12 Apr 2024 09:12:05 +0200 Subject: [PATCH 36/43] remove another TooltipEventFeature middle step --- apps/fishing-map/features/map/Map.tsx | 48 +-- .../features/map/map-interactions.hooks.ts | 55 ++- apps/fishing-map/features/map/map.hooks.ts | 331 ++++++++---------- apps/fishing-map/features/map/map.slice.ts | 4 +- .../features/map/popups/ActivityLayers.tsx | 10 +- .../features/map/popups/AnnotationTooltip.tsx | 4 +- .../features/map/popups/ComparisonRow.tsx | 4 +- .../map/popups/ContextLayers.hooks.ts | 30 +- .../features/map/popups/ContextLayers.tsx | 10 +- .../features/map/popups/ContextLayersRow.tsx | 19 +- .../features/map/popups/DetectionsLayers.tsx | 45 ++- .../map/popups/EncounterTooltipRow.tsx | 10 +- .../features/map/popups/EnvironmentLayers.tsx | 10 +- .../features/map/popups/PopupWrapper.tsx | 44 +-- .../map/popups/ReportBufferLayers.tsx | 4 +- .../features/map/popups/RulerTooltip.tsx | 9 +- .../features/map/popups/UserContextLayers.tsx | 4 +- .../features/map/popups/UserPointsLayers.tsx | 4 +- .../features/map/popups/VesselsTable.tsx | 7 - .../map/popups/WorkspacePointsLayers.tsx | 4 +- libs/deck-layer-composer/src/types.ts | 16 - .../src/layers/cluster/cluster.types.ts | 2 +- .../src/layers/rulers/rulers.types.ts | 2 + libs/deck-layers/src/types.ts | 1 + 24 files changed, 285 insertions(+), 392 deletions(-) diff --git a/apps/fishing-map/features/map/Map.tsx b/apps/fishing-map/features/map/Map.tsx index 34fc3eb99b..dad424eab1 100644 --- a/apps/fishing-map/features/map/Map.tsx +++ b/apps/fishing-map/features/map/Map.tsx @@ -1,29 +1,11 @@ -import { - Fragment, - ReactEventHandler, - ReactNode, - SyntheticEvent, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from 'react' +import { useCallback, useEffect, useRef } from 'react' import { useSelector } from 'react-redux' import { DeckGL, DeckGLRef } from '@deck.gl/react' -import { DeckProps, LayersList, PickingInfo, Position } from '@deck.gl/core' +import { LayersList } from '@deck.gl/core' import dynamic from 'next/dynamic' // import { atom, useAtom } from 'jotai' import { ViewState } from 'react-map-gl' import { GFWAPI } from '@globalfishingwatch/api-client' -import { - useMapLegend, - useFeatureState, - useLayerComposer, - defaultStyleTransformations, - useDebounce, - useMemoCompare, -} from '@globalfishingwatch/react-hooks' import { LayerComposer } from '@globalfishingwatch/layer-composer' import type { RequestParameters } from '@globalfishingwatch/maplibre-gl' import { RulersLayer } from '@globalfishingwatch/deck-layers' @@ -32,22 +14,11 @@ import { useSetDeckLayerComposer, useSetDeckLayerLoadedState, } from '@globalfishingwatch/deck-layer-composer' -import useMapInstance, { useSetMapInstance } from 'features/map/map-context.hooks' +import { useSetMapInstance } from 'features/map/map-context.hooks' // import { useClickedEventConnect, useGeneratorsConnect } from 'features/map/map.hooks' -import MapInfo from 'features/map/controls/MapInfo' import MapControls from 'features/map/controls/MapControls' -import { selectDebugOptions } from 'features/debug/debug.slice' -import { selectShowTimeComparison } from 'features/reports/reports.selectors' -import { - selectIsAnyReportLocation, - selectIsMapDrawing, - selectIsWorkspaceLocation, -} from 'routes/routes.selectors' -import { useMapLoaded, useSetMapIdleAtom } from 'features/map/map-state.hooks' -import { mapReadyAtom } from 'features/map/map-state.atom' -import { useMapDrawConnect } from 'features/map/map-draw.hooks' -import { selectHighlightedTime } from 'features/timebar/timebar.slice' -import { hasMapTimeseriesAtom } from 'features/reports/reports-timeseries.hooks' +import { selectIsAnyReportLocation, selectIsWorkspaceLocation } from 'routes/routes.selectors' +import { useSetMapIdleAtom } from 'features/map/map-state.hooks' import { useMapCursor, useMapDrag, @@ -56,10 +27,7 @@ import { } from 'features/map/map-interactions.hooks' import { useMapRulersDrag } from 'features/map/overlays/rulers/rulers-drag.hooks' import ErrorNotification from 'features/map/overlays/error-notification/ErrorNotification' -import { selectCurrentDataviewInstancesResolved } from 'features/dataviews/selectors/dataviews.instances.selectors' -import { useMapDeckLayers, useMapLayersLoaded } from 'features/map/map-layers.hooks' -import { MapCoordinates } from 'types' -import { DEFAULT_VIEWPORT } from 'data/config' +import { useMapDeckLayers } from 'features/map/map-layers.hooks' import MapPopups from 'features/map/popups/MapPopups' import { MAP_VIEW, @@ -68,8 +36,6 @@ import { useDisablePositionsOnZoomChanges, } from './map-viewport.hooks' import styles from './Map.module.css' -import { useAllMapSourceTilesLoaded, useMapSourceTilesLoadedAtom } from './map-sources.hooks' -import MapLegends from './MapLegends' import MapAnnotations from './overlays/annotations/Annotations' import MapAnnotationsDialog from './overlays/annotations/AnnotationsDialog' import useRulers from './overlays/rulers/rulers.hooks' @@ -133,7 +99,7 @@ const MapWrapper = () => { ) useUpdateViewStateUrlParams() useDisablePositionsOnZoomChanges() - const { onMapClick } = useMapMouseClick() + const onMapClick = useMapMouseClick() const { onMouseMove } = useMapMouseHover() const { getCursor } = useMapCursor() const { onMapDrag, onMapDragStart, onMapDragEnd } = useMapDrag() diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index ed56d81e5f..3cbd1afca2 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -1,9 +1,9 @@ -import { useCallback, useMemo, useRef, useState } from 'react' +import { useCallback, useRef, useState } from 'react' import { useSelector } from 'react-redux' import { DeckProps, PickingInfo, Position } from '@deck.gl/core' import { InteractionEventCallback, useSimpleMapHover } from '@globalfishingwatch/react-hooks' import { ExtendedStyle } from '@globalfishingwatch/layer-composer' -import { DataviewCategory, DataviewType } from '@globalfishingwatch/api-types' +import { DataviewCategory } from '@globalfishingwatch/api-types' import { useMapHoverInteraction, useSetMapHoverInteraction, @@ -12,14 +12,12 @@ import { import { ClusterPickingObject, DeckLayerInteractionPickingInfo, + DeckLayerPickingObject, FourwingsPickingObject, } from '@globalfishingwatch/deck-layers' import { useMapDrawConnect } from 'features/map/map-draw.hooks' import { useMapAnnotation } from 'features/map/overlays/annotations/annotations.hooks' -import { - SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION, - TooltipEventFeature, -} from 'features/map/map.hooks' +import { SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION } from 'features/map/map.hooks' import useRulers from 'features/map/overlays/rulers/rulers.hooks' import useMapInstance, { useDeckMap } from 'features/map/map-context.hooks' import { selectActiveTemporalgridDataviews } from 'features/dataviews/selectors/dataviews.selectors' @@ -308,29 +306,33 @@ export const useMapMouseClick = () => { const temporalgridDataviews = useSelector(selectActiveTemporalgridDataviews) const { clickedEvent, dispatchClickedEvent } = useClickedEventConnect() - const clickedTooltipEvent = { ...clickedEvent } - - const clickedCellLayers = useMemo(() => { - if (!clickedEvent || !clickedTooltipEvent) return + const trackMapClickEvent = useCallback((interactionEvent: InteractionEvent) => { + if (!interactionEvent || !interactionEvent?.features) return - const layersByCategory = (clickedTooltipEvent?.features ?? []) + const layersByCategory = (interactionEvent?.features ?? []) .toSorted( (a, b) => POPUP_CATEGORY_ORDER.indexOf(a.category) - POPUP_CATEGORY_ORDER.indexOf(b.category) ) .reduce( - (prev: Record, current) => ({ + (prev: Record, current) => ({ ...prev, [current.category]: [...(prev[current.category] ?? []), current], }), - {} as Record + {} as Record ) - return Object.entries(layersByCategory).map( + const clickedCellLayers = Object.entries(layersByCategory).map( ([featureCategory, features]) => `${featureCategory}: ${(features as any[]).map((f) => f.layerId).join(',')}` ) - }, [clickedEvent, clickedTooltipEvent]) + + trackEvent({ + category: TrackCategory.EnvironmentalData, + action: `Click in grid cell`, + label: getEventLabel(clickedCellLayers ?? []), + }) + }, []) const onMapClick: DeckProps['onClick'] = useCallback( (info: PickingInfo, event: any) => { @@ -339,11 +341,6 @@ export const useMapMouseClick = () => { // this is needed to allow interacting with overlay elements content return true } - trackEvent({ - category: TrackCategory.EnvironmentalData, - action: `Click in grid cell`, - label: getEventLabel(clickedCellLayers ?? []), - }) let features = defaultEmptyFeatures try { features = map?.pickMultipleObjects({ @@ -366,11 +363,12 @@ export const useMapMouseClick = () => { if (!clickStopPropagation) { dispatchClickedEvent(mapClickInteraction) } + trackMapClickEvent(mapClickInteraction) }, - [map, clickedCellLayers, handleMapToolsClick, dispatchClickedEvent] + [map, handleMapToolsClick, trackMapClickEvent, dispatchClickedEvent] ) - return { onMapClick, clickedTooltipEvent } + return onMapClick } export const _deprecatedUseMapCursor = (hoveredTooltipEvent?: any) => { @@ -425,18 +423,7 @@ export const _deprecatedUseMapCursor = (hoveredTooltipEvent?: any) => { // return 'grabbing' // } return 'grab' - }, [ - hoveredTooltipEvent, - isMapAnnotating, - rulersEditing, - isErrorNotificationEditing, - isMapDrawing, - isMarineManagerLocation, - map, - gfwUser, - dataviews, - tilesClusterLoaded, - ]) + }, []) return getCursor() } diff --git a/apps/fishing-map/features/map/map.hooks.ts b/apps/fishing-map/features/map/map.hooks.ts index af62e0ca6a..8fa7e0532b 100644 --- a/apps/fishing-map/features/map/map.hooks.ts +++ b/apps/fishing-map/features/map/map.hooks.ts @@ -2,11 +2,10 @@ import { useSelector } from 'react-redux' import { useCallback, useEffect, useMemo } from 'react' import { debounce } from 'lodash' import { useTranslation } from 'react-i18next' -import { UrlDataviewInstance } from '@globalfishingwatch/dataviews-client' import { DataviewCategory, DataviewType, Locale } from '@globalfishingwatch/api-types' import { GFWAPI } from '@globalfishingwatch/api-client' import { ResolverGlobalConfig } from '@globalfishingwatch/deck-layer-composer' -import { FourwingsComparisonMode } from '@globalfishingwatch/deck-layers' +import { DeckLayerPickingObject } from '@globalfishingwatch/deck-layers' import { useTimerangeConnect } from 'features/timebar/timebar.hooks' import { selectHighlightedEvents, setHighlightedEvents } from 'features/timebar/timebar.slice' import { useAppDispatch } from 'features/app/app.hooks' @@ -22,13 +21,7 @@ import { } from 'features/app/selectors/app.selectors' import { selectWorkspaceVisibleEventsArray } from 'features/workspace/workspace.selectors' import { selectDebugOptions } from 'features/debug/debug.slice' -import { - MAX_TOOLTIP_LIST, - SliceInteractionEvent, - ExtendedFeatureVessel, - SliceExtendedFeature, - SliceExtendedFourwingsFeature, -} from './map.slice' +import { MAX_TOOLTIP_LIST, ExtendedFeatureVessel } from './map.slice' import { useViewStateAtom } from './map-viewport.hooks' export const SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION = ['activity', 'detections'] @@ -94,58 +87,6 @@ export const useGlobalConfigConnect = () => { ]) } -// TODO:deck fuerte -// hack to allow building the app wihtout migrating the rest of the interactions -// needs to be updated with the new deck-layers -export type TooltipEventFeatureVesselsInfo = { - overflow: boolean - overflowNumber: number - overflowLoad: boolean - overflowLoadNumber: number - numVessels: number - vessels: ExtendedFeatureVessel[] -} -export type TooltipEventFeature = { - vesselsInfo?: TooltipEventFeatureVesselsInfo - [key: string]: any -} -// export type TooltipEventFeature = { -// category: DataviewCategory -// color?: string -// datasetId?: string -// datasetSource?: string -// event?: ExtendedFeatureEvent -// generatorContextLayer?: ContextLayerType | null -// geometry?: Point | Polygon | MultiPolygon -// id?: string -// layerId: string -// promoteId?: string -// properties: Record -// source: string -// sourceLayer: string -// subcategory?: DatasetSubCategory -// temporalgrid?: TemporalGridFeature -// title?: string -// type?: DataviewType -// unit?: string -// value: string // TODO Why not a number? -// visible?: boolean -// vesselsInfo?: { -// overflow: boolean -// overflowNumber: number -// overflowLoad: boolean -// overflowLoadNumber: number -// numVessels: number -// vessels: ExtendedFeatureVessel[] -// } -// } - -export type TooltipEvent = { - latitude: number - longitude: number - features: TooltipEventFeature[] -} - export const useDebouncedDispatchHighlightedEvent = () => { const dispatch = useAppDispatch() // eslint-disable-next-line react-hooks/exhaustive-deps @@ -161,30 +102,31 @@ export const useDebouncedDispatchHighlightedEvent = () => { ) } -export const useMapHighlightedEvent = (features?: TooltipEventFeature[]) => { +// TODO:deck do this within the deck layer +export const useMapHighlightedEvent = (features?: DeckLayerPickingObject[]) => { const highlightedEvents = useSelector(selectHighlightedEvents) const debounceDispatch = useDebouncedDispatchHighlightedEvent() const setHighlightedEventDebounced = useCallback(() => { - let highlightEvent: string | undefined - const vesselFeature = features?.find((f) => f.category === DataviewCategory.Vessels) - const clusterFeature = features?.find((f) => f.type === DataviewType.TileCluster) - if (!clusterFeature && vesselFeature) { - highlightEvent = vesselFeature.properties?.id - } else if (clusterFeature) { - highlightEvent = clusterFeature.properties?.event_id - } - if (highlightEvent) { - if ( - !highlightedEvents || - highlightedEvents.length !== 1 || - highlightedEvents[0] !== highlightEvent - ) { - debounceDispatch(highlightEvent) - } - } else if (highlightedEvents && highlightedEvents.length) { - debounceDispatch(undefined) - } + // let highlightEvent: string | undefined + // const vesselFeature = features?.find((f) => f.category === DataviewCategory.Vessels) + // const clusterFeature = features?.find((f) => f.type === DataviewType.TileCluster) + // if (!clusterFeature && vesselFeature) { + // highlightEvent = vesselFeature.properties?.id + // } else if (clusterFeature) { + // highlightEvent = clusterFeature.properties?.event_id + // } + // if (highlightEvent) { + // if ( + // !highlightedEvents || + // highlightedEvents.length !== 1 || + // highlightedEvents[0] !== highlightEvent + // ) { + // debounceDispatch(highlightEvent) + // } + // } else if (highlightedEvents && highlightedEvents.length) { + // debounceDispatch(undefined) + // } }, [features, highlightedEvents, debounceDispatch]) useEffect(() => { @@ -194,125 +136,126 @@ export const useMapHighlightedEvent = (features?: TooltipEventFeature[]) => { } // TODO:deck ideally remove this intermediate step -export const parseMapTooltipFeatures = ( - features: SliceExtendedFeature[] - // dataviews: UrlDataviewInstance[], - // temporalgridDataviews?: UrlDataviewInstance[] -): TooltipEventFeature[] => { - const tooltipEventFeatures: TooltipEventFeature[] = features.flatMap((feature) => { - const { category, id, comparisonMode, sublayers } = feature as SliceExtendedFourwingsFeature - const baseFeature = { - category: feature.category, - layerId: id as string, - type: category, - } +// export const parseMapTooltipFeatures = ( +// features: SliceExtendedFeature[] +// // dataviews: UrlDataviewInstance[], +// // temporalgridDataviews?: UrlDataviewInstance[] +// ) => { +// const tooltipEventFeatures: TooltipEventFeature[] = features.flatMap((feature) => { +// const { category, id, comparisonMode, sublayers } = +// feature as SliceExtendedFourwingsPickingObject +// const baseFeature = { +// category: feature.category, +// layerId: id as string, +// type: category, +// } - if (comparisonMode === FourwingsComparisonMode.TimeCompare) { - return { - ...baseFeature, - category: DataviewCategory.Comparison, - value: sublayers[0]?.value, - visible: true, - unit: sublayers[0]?.unit, - } as TooltipEventFeature - } +// if (comparisonMode === FourwingsComparisonMode.TimeCompare) { +// return { +// ...baseFeature, +// category: DataviewCategory.Comparison, +// value: sublayers[0]?.value, +// visible: true, +// unit: sublayers[0]?.unit, +// } as TooltipEventFeature +// } - let dataview +// let dataview - // if (isMergedAnimatedGenerator(generatorId as string)) { - // if (!temporalgrid || temporalgrid.sublayerId === undefined || !temporalgrid.visible) { - // return [] - // } +// if (isMergedAnimatedGenerator(generatorId as string)) { +// if (!temporalgrid || temporalgrid.sublayerId === undefined || !temporalgrid.visible) { +// return [] +// } - // dataview = temporalgridDataviews?.find((dataview) => dataview.id === temporalgrid.sublayerId) - // } else { - // dataview = dataviews?.find((dataview) => { - // // Needed to get only the initial part to support multiple generator - // // from the same dataview, see map.selectors L137 - // const cleanGeneratorId = (generatorId as string)?.split(MULTILAYER_SEPARATOR)[0] - // return dataview.id === cleanGeneratorId - // }) - // } +// dataview = temporalgridDataviews?.find((dataview) => dataview.id === temporalgrid.sublayerId) +// } else { +// dataview = dataviews?.find((dataview) => { +// // Needed to get only the initial part to support multiple generator +// // from the same dataview, see map.selectors L137 +// const cleanGeneratorId = (generatorId as string)?.split(MULTILAYER_SEPARATOR)[0] +// return dataview.id === cleanGeneratorId +// }) +// } - // TODO: deck check if this is still neded - // if (!dataview) { - // // There are three use cases when there is no dataview and we want interaction - // // 1. Wworkspaces list - // if (generatorId && (generatorId as string).includes(WORKSPACE_GENERATOR_ID)) { - // const tooltipWorkspaceFeature: TooltipEventFeature = { - // ...baseFeature, - // type: DataviewType.GL, - // value: feature.properties.label, - // properties: {}, - // category: DataviewCategory.Context, - // } - // return tooltipWorkspaceFeature - // } - // // 2. Report buffer - // else if (generatorId === REPORT_BUFFER_GENERATOR_ID) { - // const tooltipWorkspaceFeature: TooltipEventFeature = { - // ...baseFeature, - // category: DataviewCategory.Context, - // properties: {}, - // value: feature.properties.label, - // visible: true, - // } - // return tooltipWorkspaceFeature - // } - // // 3. Tools (Annotations and Rulers) - // else if (generatorType === DataviewType.Annotation || generatorType === DataviewType.Rulers) { - // const tooltipToolFeature: TooltipEventFeature = { - // ...baseFeature, - // category: DataviewCategory.Context, - // properties: feature.properties, - // value: feature.properties.label, - // visible: true, - // } - // return tooltipToolFeature - // } - // return [] - // } +// TODO:deck check if this is still neded +// if (!dataview) { +// // There are three use cases when there is no dataview and we want interaction +// // 1. Wworkspaces list +// if (generatorId && (generatorId as string).includes(WORKSPACE_GENERATOR_ID)) { +// const tooltipWorkspaceFeature: TooltipEventFeature = { +// ...baseFeature, +// type: DataviewType.GL, +// value: feature.properties.label, +// properties: {}, +// category: DataviewCategory.Context, +// } +// return tooltipWorkspaceFeature +// } +// // 2. Report buffer +// else if (generatorId === REPORT_BUFFER_GENERATOR_ID) { +// const tooltipWorkspaceFeature: TooltipEventFeature = { +// ...baseFeature, +// category: DataviewCategory.Context, +// properties: {}, +// value: feature.properties.label, +// visible: true, +// } +// return tooltipWorkspaceFeature +// } +// // 3. Tools (Annotations and Rulers) +// else if (generatorType === DataviewType.Annotation || generatorType === DataviewType.Rulers) { +// const tooltipToolFeature: TooltipEventFeature = { +// ...baseFeature, +// category: DataviewCategory.Context, +// properties: feature.properties, +// value: feature.properties.label, +// visible: true, +// } +// return tooltipToolFeature +// } +// return [] +// } - // const title = getDatasetTitleByDataview(dataview) +// const title = getDatasetTitleByDataview(dataview) - // const datasets = - // dataview.category === DataviewCategory.Activity || - // dataview.category === DataviewCategory.Detections - // ? getActiveDatasetsInActivityDataviews([dataview]) - // : (dataview.datasets || [])?.map((d) => d.id) +// const datasets = +// dataview.category === DataviewCategory.Activity || +// dataview.category === DataviewCategory.Detections +// ? getActiveDatasetsInActivityDataviews([dataview]) +// : (dataview.datasets || [])?.map((d) => d.id) - // const dataset = dataview?.datasets?.find(({ id }) => datasets.includes(id)) - // const subcategory = dataset?.subcategory as DatasetSubCategory - // const tooltipEventFeature: TooltipEventFeature = { - // title, - // type: dataview.config?.type, - // color: dataview.config?.color, - // visible: dataview.config?.visible, - // category: dataview.category || DataviewCategory.Context, - // subcategory, - // datasetSource: dataset?.source, - // ...feature, - // properties: { ...feature.properties }, - // } - // // Insert custom properties by each dataview configuration - // const properties = dataview.datasetsConfig - // ? dataview.datasetsConfig.flatMap((datasetConfig) => { - // if (!datasetConfig.query?.length) return [] - // return datasetConfig.query.flatMap((query) => - // query.id === 'properties' ? (query.value as string) : [] - // ) - // }) - // : [] - // properties.forEach((property) => { - // if (feature.properties[property]) { - // tooltipEventFeature.properties[property] = feature.properties[property] - // } - // }) +// const dataset = dataview?.datasets?.find(({ id }) => datasets.includes(id)) +// const subcategory = dataset?.subcategory as DatasetSubCategory +// const tooltipEventFeature: TooltipEventFeature = { +// title, +// type: dataview.config?.type, +// color: dataview.config?.color, +// visible: dataview.config?.visible, +// category: dataview.category || DataviewCategory.Context, +// subcategory, +// datasetSource: dataset?.source, +// ...feature, +// properties: { ...feature.properties }, +// } +// // Insert custom properties by each dataview configuration +// const properties = dataview.datasetsConfig +// ? dataview.datasetsConfig.flatMap((datasetConfig) => { +// if (!datasetConfig.query?.length) return [] +// return datasetConfig.query.flatMap((query) => +// query.id === 'properties' ? (query.value as string) : [] +// ) +// }) +// : [] +// properties.forEach((property) => { +// if (feature.properties[property]) { +// tooltipEventFeature.properties[property] = feature.properties[property] +// } +// }) - // if (feature.vessels) { - // tooltipEventFeature.vesselsInfo = getVesselsInfoConfig(feature.vessels) - // } - // return tooltipEventFeature - }) - return tooltipEventFeatures -} +// if (feature.vessels) { +// tooltipEventFeature.vesselsInfo = getVesselsInfoConfig(feature.vessels) +// } +// return tooltipEventFeature +// }) +// return tooltipEventFeatures +// } diff --git a/apps/fishing-map/features/map/map.slice.ts b/apps/fishing-map/features/map/map.slice.ts index b97bc35dc7..d89607a9eb 100644 --- a/apps/fishing-map/features/map/map.slice.ts +++ b/apps/fishing-map/features/map/map.slice.ts @@ -60,11 +60,11 @@ export type ExtendedFeatureEvent = ApiEvent & { dataset: Dataset } export type SliceExtendedFourwingsDeckSublayer = FourwingsDeckSublayer & { vessels: ExtendedFeatureVessel[] } -export type SliceExtendedFourwingsFeature = Omit & { +export type SliceExtendedFourwingsPickingObject = Omit & { sublayers: SliceExtendedFourwingsDeckSublayer[] } export type SliceExtendedFeature = - | SliceExtendedFourwingsFeature + | SliceExtendedFourwingsPickingObject | ContextPickingObject | UserContextPickingObject | ClusterPickingObject diff --git a/apps/fishing-map/features/map/popups/ActivityLayers.tsx b/apps/fishing-map/features/map/popups/ActivityLayers.tsx index 392b3dd83b..cb4d536766 100644 --- a/apps/fishing-map/features/map/popups/ActivityLayers.tsx +++ b/apps/fishing-map/features/map/popups/ActivityLayers.tsx @@ -5,17 +5,16 @@ import { DataviewCategory } from '@globalfishingwatch/api-types' import I18nNumber from 'features/i18n/i18nNumber' import { SliceExtendedFourwingsDeckSublayer } from '../map.slice' import popupStyles from './Popup.module.css' -import VesselsTable, { getVesselTableTitle } from './VesselsTable' +import VesselsTable from './VesselsTable' type ActivityTooltipRowProps = { - feature: SliceExtendedFourwingsDeckSublayer & { category: DataviewCategory } + feature: SliceExtendedFourwingsDeckSublayer & { category: DataviewCategory; title?: string } loading?: boolean showFeaturesDetails: boolean } function ActivityTooltipRow({ feature, showFeaturesDetails, loading }: ActivityTooltipRowProps) { const { t } = useTranslation() - const title = getVesselTableTitle(feature) // TODO get the value based on the sublayer const value = feature?.value as number if (!value) { @@ -26,7 +25,9 @@ function ActivityTooltipRow({ feature, showFeaturesDetails, loading }: ActivityT
      - {showFeaturesDetails &&

      {title}

      } + {showFeaturesDetails && feature.title && ( +

      {feature.title}

      + )}
      {' '} @@ -40,7 +41,6 @@ function ActivityTooltipRow({ feature, showFeaturesDetails, loading }: ActivityT
      )} - {/* // TODO:deck add subcategory info */} {!loading && showFeaturesDetails && ( { +export const getFeatureBounds = (feature: ContextPickingObject) => { if (feature.geometry) { const geometry = getGeometryDissolved(feature.geometry) const bounds = getBufferedAreaBbox({ area: { geometry } } as any) @@ -51,10 +51,13 @@ export const useHighlightArea = () => { ) } -export const getAreaIdFromFeature = (feature: TooltipEventFeature): AreaKeyId => { +export const getAreaIdFromFeature = ( + feature: ContextPickingObject | UserContextPickingObject +): AreaKeyId => { return ( feature.properties?.gfw_id || - feature.properties?.[feature.promoteId as string] || + // TODO:deck check if promoteId is covered for every case in the getPickingInfo function + feature.properties?.[(feature as any).promoteId as string] || (feature.id as string) ) } @@ -70,7 +73,7 @@ export const useContextInteractions = () => { const fitMapBounds = useMapFitBounds() const onDownloadClick = useCallback( - (ev: React.MouseEvent, feature: TooltipEventFeature) => { + (ev: React.MouseEvent, feature: ContextPickingObject) => { const areaId = getAreaIdFromFeature(feature) if (!areaId) { console.warn('No gfw_id available in the feature to analyze', feature) @@ -86,7 +89,7 @@ export const useContextInteractions = () => { const areaName = dataview?.config?.type === DataviewType.UserContext ? dataview?.datasets?.[0]?.name - : feature.value || feature.title + : feature.value.toString() || feature.title dispatch(setDownloadActivityAreaKey({ datasetId, areaId, areaName })) dispatch(setClickedEvent(null)) dispatch(fetchAreaDetailThunk({ dataset, areaId, areaName })) @@ -98,8 +101,8 @@ export const useContextInteractions = () => { ) const setReportArea = useCallback( - (feature: TooltipEventFeature) => { - const { source: sourceId, title, value } = feature + (feature: ContextPickingObject | UserContextPickingObject) => { + const { title, value } = feature const areaId = getAreaIdFromFeature(feature) as string // Report already does it on page reload but to avoid waiting // this moves the map to the same position @@ -119,21 +122,24 @@ export const useContextInteractions = () => { trackEvent({ category: TrackCategory.Analysis, action: `Open report`, - label: getEventLabel([title ?? '', value ?? '']), + label: getEventLabel([title ?? '', value.toString()]), }) }, [highlightArea, dispatch] ) const onReportClick = useCallback( - (ev: React.MouseEvent, feature: TooltipEventFeature) => { + ( + ev: React.MouseEvent, + feature: ContextPickingObject | UserContextPickingObject + ) => { const featureAreaId = getAreaIdFromFeature(feature) if (!featureAreaId) { console.warn('No areaId available in the feature to report', feature) return } - - if (areaId?.toString() !== featureAreaId || sourceId !== feature.source) { + // TODO:deck check if we can remove source from the url + if (areaId?.toString() !== featureAreaId /*|| sourceId !== feature.source */) { setReportArea(feature) } }, diff --git a/apps/fishing-map/features/map/popups/ContextLayers.tsx b/apps/fishing-map/features/map/popups/ContextLayers.tsx index b37fe117a1..33bb631c12 100644 --- a/apps/fishing-map/features/map/popups/ContextLayers.tsx +++ b/apps/fishing-map/features/map/popups/ContextLayers.tsx @@ -1,15 +1,14 @@ import { Fragment, useCallback } from 'react' import { groupBy } from 'lodash' import { Icon } from '@globalfishingwatch/ui-components' -import { ContextPickingObject } from '@globalfishingwatch/deck-layers' -import { TooltipEventFeature } from 'features/map/map.hooks' +import { ContextPickingObject, UserContextPickingObject } from '@globalfishingwatch/deck-layers' import { TrackCategory, trackEvent } from 'features/app/analytics.hooks' import styles from './Popup.module.css' import ContextLayersRow from './ContextLayersRow' import { useContextInteractions } from './ContextLayers.hooks' type ContextTooltipRowProps = { - features: ContextPickingObject[] + features: (ContextPickingObject | UserContextPickingObject)[] showFeaturesDetails: boolean } @@ -18,12 +17,13 @@ function ContextTooltipSection({ features, showFeaturesDetails = false }: Contex const featuresByType = groupBy(features, 'layerId') const trackOnDownloadClick = useCallback( - (event: any, feature: TooltipEventFeature) => { + (event: any, feature: ContextPickingObject | UserContextPickingObject) => { trackEvent({ category: TrackCategory.DataDownloads, action: `Click on polygon, click on download icon`, }) - onDownloadClick(event, feature) + // TODO:deck review typings here + onDownloadClick(event, feature as any) }, [onDownloadClick] ) diff --git a/apps/fishing-map/features/map/popups/ContextLayersRow.tsx b/apps/fishing-map/features/map/popups/ContextLayersRow.tsx index d6ff483b5c..64a1c8c6a4 100644 --- a/apps/fishing-map/features/map/popups/ContextLayersRow.tsx +++ b/apps/fishing-map/features/map/popups/ContextLayersRow.tsx @@ -3,7 +3,7 @@ import parse from 'html-react-parser' import { useTranslation } from 'react-i18next' import Link from 'redux-first-router-link' import { IconButton } from '@globalfishingwatch/ui-components' -import { ContextFeature, UserContextFeature } from '@globalfishingwatch/deck-layers' +import { ContextPickingObject, UserContextPickingObject } from '@globalfishingwatch/deck-layers' import { selectActiveHeatmapDowloadDataviews, selectHasReportLayersVisible, @@ -16,8 +16,7 @@ import { DEFAULT_WORKSPACE_CATEGORY, DEFAULT_WORKSPACE_ID } from 'data/workspace import { selectWorkspace } from 'features/workspace/workspace.selectors' import { selectLocationAreaId, selectLocationQuery } from 'routes/routes.selectors' import { selectSidebarOpen } from 'features/app/selectors/app.selectors' -import { TooltipEventFeature } from 'features/map/map.hooks' -import { getAreaIdFromFeature, getFeatureBounds } from 'features/map/popups/ContextLayers.hooks' +import { getAreaIdFromFeature } from 'features/map/popups/ContextLayers.hooks' import { resetSidebarScroll } from 'features/sidebar/sidebar.utils' import { resetReportData } from 'features/reports/report.slice' import { useAppDispatch } from 'features/app/app.hooks' @@ -74,8 +73,11 @@ const DownloadPopupButton: React.FC = ({ } interface ReportPopupButtonProps { - feature: ContextFeature | UserContextFeature - onClick?: (e: React.MouseEvent, feature: TooltipEventFeature) => void + feature: ContextPickingObject | UserContextPickingObject + onClick?: ( + e: React.MouseEvent, + feature: ContextPickingObject | UserContextPickingObject + ) => void } export const ReportPopupLink = ({ feature, onClick }: ReportPopupButtonProps) => { @@ -117,7 +119,8 @@ export const ReportPopupLink = ({ feature, onClick }: ReportPopupButtonProps) => dispatch(resetReportData()) dispatch(cleanCurrentWorkspaceStateBufferParams()) if (onClick) { - onClick(e, feature) + // TODO:deck review typing here + onClick(e, feature as any) } } @@ -159,14 +162,14 @@ export const ReportPopupLink = ({ feature, onClick }: ReportPopupButtonProps) => interface ContextLayersRowProps { id: string label: string - feature: ContextFeature | UserContextFeature + feature: ContextPickingObject | UserContextPickingObject showFeaturesDetails: boolean showActions?: boolean linkHref?: string handleDownloadClick?: (e: React.MouseEvent) => void handleReportClick?: ( e: React.MouseEvent, - feature: TooltipEventFeature + feature: ContextPickingObject | UserContextPickingObject ) => void } const ContextLayersRow = ({ diff --git a/apps/fishing-map/features/map/popups/DetectionsLayers.tsx b/apps/fishing-map/features/map/popups/DetectionsLayers.tsx index 8132dcb6cc..0e4099fb27 100644 --- a/apps/fishing-map/features/map/popups/DetectionsLayers.tsx +++ b/apps/fishing-map/features/map/popups/DetectionsLayers.tsx @@ -1,13 +1,15 @@ import { Fragment } from 'react' import { useTranslation } from 'react-i18next' import { Icon, Spinner } from '@globalfishingwatch/ui-components' +import { DataviewCategory } from '@globalfishingwatch/api-types' import I18nNumber from 'features/i18n/i18nNumber' -import { TooltipEventFeature, TooltipEventFeatureVesselsInfo } from 'features/map/map.hooks' import VesselsTable, { VesselDetectionTimestamps } from 'features/map/popups/VesselsTable' +import { SliceExtendedFourwingsDeckSublayer } from '../map.slice' +import { getVesselsInfoConfig } from '../map.hooks' import styles from './Popup.module.css' type ViirsMatchTooltipRowProps = { - feature: TooltipEventFeature + feature: SliceExtendedFourwingsDeckSublayer & { category: DataviewCategory; title?: string } loading?: boolean showFeaturesDetails: boolean } @@ -18,34 +20,42 @@ function ViirsMatchTooltipRow({ }: ViirsMatchTooltipRowProps) { const { t } = useTranslation() // Avoid showing not matched detections - const matchedVessels: TooltipEventFeatureVesselsInfo['vessels'] = ( - feature.vesselsInfo?.vessels || [] + const vesselsInfo = getVesselsInfoConfig(feature.vessels || []) + const hasVesselsResolved = feature?.vessels?.length > 0 + const matchedVessels: SliceExtendedFourwingsDeckSublayer['vessels'] = ( + feature?.vessels || [] ).filter((v: any) => v.id !== null) - const matchedDetections = matchedVessels + const matchedDetections = hasVesselsResolved ? matchedVessels.reduce((acc, vessel: any) => acc + vessel.detections, 0) : 0 - const featureVesselsFilter: any = { + const featureVesselsFilter = { ...feature, vesselsInfo: { - ...feature.vesselsInfo, + vesselsInfo, vessels: matchedVessels, - } as TooltipEventFeatureVesselsInfo, + }, } - const notMatchedDetectionsCount = parseInt(feature.value) - matchedDetections - const notMatchedDetection = feature.vesselsInfo?.vessels?.find((v: any) => v.id === null) + const notMatchedDetectionsCount = feature.value! - matchedDetections + const notMatchedDetection = feature?.vessels?.find((v: any) => v.id === null) return (
      - {showFeaturesDetails &&

      {feature.title}

      } + {showFeaturesDetails && feature.title && ( +

      {feature.title}

      + )}
      - {' '} - {t([`common.${feature.temporalgrid?.unit}` as any, 'common.detection'], 'detections', { - count: parseInt(feature.value), // neded to select the plural automatically + {feature.value && ( + + {' '} + + )} + {t([`common.${feature?.unit}` as any, 'common.detection'], 'detections', { + count: feature.value, // neded to select the plural automatically })}{' '} - {showFeaturesDetails && notMatchedDetectionsCount >= 0 && ( + {hasVesselsResolved && showFeaturesDetails && notMatchedDetectionsCount >= 0 && ( {' - '} {' '} @@ -61,7 +71,10 @@ function ViirsMatchTooltipRow({
      )} {!loading && showFeaturesDetails && ( - + )}
      diff --git a/apps/fishing-map/features/map/popups/EncounterTooltipRow.tsx b/apps/fishing-map/features/map/popups/EncounterTooltipRow.tsx index 9b2cd29e8d..04e47b996f 100644 --- a/apps/fishing-map/features/map/popups/EncounterTooltipRow.tsx +++ b/apps/fishing-map/features/map/popups/EncounterTooltipRow.tsx @@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next' import { stringify } from 'qs' import { Button, Icon } from '@globalfishingwatch/ui-components' import { EventVessel } from '@globalfishingwatch/api-types' -import { TooltipEventFeature } from 'features/map/map.hooks' +import { ClusterPickingObject, UserContextPickingObject } from '@globalfishingwatch/deck-layers' import { AsyncReducerStatus } from 'utils/async-slice' import I18nDate from 'features/i18n/i18nDate' import { ENCOUNTER_EVENTS_SOURCE_ID } from 'features/dataviews/dataviews.utils' @@ -36,7 +36,8 @@ const parseEvent = (event: ExtendedFeatureEvent | undefined): ExtendedFeatureEve } type EncountersLayerProps = { - feature: TooltipEventFeature + // TODO:deck type this with its own type + feature: any showFeaturesDetails: boolean } @@ -186,7 +187,7 @@ function GenericClusterTooltipRow({ feature, showFeaturesDetails }: EncountersLa } type UserContextLayersProps = { - features: TooltipEventFeature[] + features: UserContextPickingObject[] showFeaturesDetails: boolean } @@ -195,7 +196,8 @@ function TileClusterTooltipRow({ features, showFeaturesDetails }: UserContextLay {features.map((feature, index) => { const key = `${feature.title}-${index}` - if (feature.source === ENCOUNTER_EVENTS_SOURCE_ID) { + // TODO:deck decide how to manage this + if ((feature as any).source === ENCOUNTER_EVENTS_SOURCE_ID) { return ( - {features.map((pickedFeature, index) => { - const feature = pickedFeature.object + {features.map((feature, index) => { const isHeatmapFeature = feature.type === DataviewType.HeatmapAnimated || feature.type === DataviewType.HeatmapStatic @@ -49,7 +49,7 @@ function EnvironmentTooltipSection({ {parseEnvironmentalValue(value)}{' '} {/* TODO will need to not pick from temporalgrid once user polygons support units */} - {feature.temporalgrid?.unit && {feature.temporalgrid?.unit}} + {feature?.unit && {feature.temporalgrid?.unit}} {feature.unit && {feature.unit}}
      diff --git a/apps/fishing-map/features/map/popups/PopupWrapper.tsx b/apps/fishing-map/features/map/popups/PopupWrapper.tsx index 791dd5e5da..05cc29e3a1 100644 --- a/apps/fishing-map/features/map/popups/PopupWrapper.tsx +++ b/apps/fishing-map/features/map/popups/PopupWrapper.tsx @@ -1,44 +1,27 @@ import { Fragment } from 'react' import cx from 'classnames' import { groupBy } from 'lodash' -import { Popup } from 'react-map-gl' import type { Anchor } from 'react-map-gl' import { useSelector } from 'react-redux' -import { DataviewCategory, DataviewType } from '@globalfishingwatch/api-types' -import { IconButton, Spinner } from '@globalfishingwatch/ui-components' +import { DataviewCategory } from '@globalfishingwatch/api-types' +import { Spinner } from '@globalfishingwatch/ui-components' import { InteractionEvent } from '@globalfishingwatch/deck-layer-composer' -import { - ContextFeature, - ContextPickingObject, - FourwingsPickingObject, - VesselEventPickingObject, -} from '@globalfishingwatch/deck-layers' -import { TooltipEvent } from 'features/map/map.hooks' +import { ContextPickingObject, VesselEventPickingObject } from '@globalfishingwatch/deck-layers' import { POPUP_CATEGORY_ORDER } from 'data/config' import { useTimeCompareTimeDescription } from 'features/reports/reports-timecomparison.hooks' import DetectionsTooltipRow from 'features/map/popups/DetectionsLayers' -import UserPointsTooltipSection from 'features/map/popups/UserPointsLayers' import { AsyncReducerStatus } from 'utils/async-slice' -import { WORKSPACE_GENERATOR_ID, REPORT_BUFFER_GENERATOR_ID } from 'features/map/map.config' -import WorkspacePointsTooltipSection from 'features/map/popups/WorkspacePointsLayers' -import AnnotationTooltip from 'features/map/popups/AnnotationTooltip' -import RulerTooltip from 'features/map/popups/RulerTooltip' -import { useDeckMap } from 'features/map/map-context.hooks' import { useMapViewport } from 'features/map/map-viewport.hooks' import { - SliceExtendedFourwingsFeature, + SliceExtendedFourwingsPickingObject, selectApiEventStatus, selectFishingInteractionStatus, } from '../map.slice' import styles from './Popup.module.css' import ActivityTooltipRow from './ActivityLayers' import EncounterTooltipRow from './EncounterTooltipRow' -import EnvironmentTooltipSection from './EnvironmentLayers' import ContextTooltipSection from './ContextLayers' -import UserContextTooltipSection from './UserContextLayers' import VesselEventsLayers from './VesselEventsLayers' -import ComparisonRow from './ComparisonRow' -import ReportBufferTooltip from './ReportBufferLayers' type PopupWrapperProps = { interaction: InteractionEvent | null @@ -113,24 +96,32 @@ function PopupWrapper({ // /> // ) case DataviewCategory.Activity: { - return (features as SliceExtendedFourwingsFeature[])?.map((feature, i) => { + return (features as SliceExtendedFourwingsPickingObject[])?.map((feature, i) => { return feature.sublayers.map((sublayer, j) => ( )) }) } case DataviewCategory.Detections: { - return (features as FourwingsPickingObject[])?.map((feature, i) => { + return (features as SliceExtendedFourwingsPickingObject[])?.map((feature, i) => { return feature.sublayers.map((sublayer, j) => ( )) @@ -147,7 +138,8 @@ function PopupWrapper({ return ( ) diff --git a/apps/fishing-map/features/map/popups/ReportBufferLayers.tsx b/apps/fishing-map/features/map/popups/ReportBufferLayers.tsx index 1836ec42d9..07e3832f23 100644 --- a/apps/fishing-map/features/map/popups/ReportBufferLayers.tsx +++ b/apps/fishing-map/features/map/popups/ReportBufferLayers.tsx @@ -1,10 +1,10 @@ import { Icon } from '@globalfishingwatch/ui-components' -import { TooltipEventFeature } from 'features/map/map.hooks' +import { UserContextPickingObject } from '@globalfishingwatch/deck-layers' import { BUFFER_PREVIEW_COLOR } from 'data/config' import styles from './Popup.module.css' type ReportBufferLayersProps = { - features: TooltipEventFeature[] + features: UserContextPickingObject[] } function ReportBufferTooltip({ features }: ReportBufferLayersProps) { diff --git a/apps/fishing-map/features/map/popups/RulerTooltip.tsx b/apps/fishing-map/features/map/popups/RulerTooltip.tsx index 9735634a09..599a7a87c5 100644 --- a/apps/fishing-map/features/map/popups/RulerTooltip.tsx +++ b/apps/fishing-map/features/map/popups/RulerTooltip.tsx @@ -1,14 +1,14 @@ import cx from 'classnames' import { useTranslation } from 'react-i18next' import { IconButton } from '@globalfishingwatch/ui-components' -import { TooltipEventFeature } from 'features/map/map.hooks' +import { RulerPickingObject } from '@globalfishingwatch/deck-layers' import useRulers from 'features/map/overlays/rulers/rulers.hooks' import { useAppDispatch } from 'features/app/app.hooks' import { setClickedEvent } from 'features/map/map.slice' import styles from './Popup.module.css' type RulerTooltipProps = { - features: TooltipEventFeature[] + features: RulerPickingObject[] showFeaturesDetails: boolean } @@ -21,10 +21,13 @@ function RulerTooltip({ features, showFeaturesDetails }: RulerTooltipProps) { if (!feature) { return null } + // TODO:deck review if getRulerLength is needed const { id, lengthLabel } = feature.properties const onDeleteClick = () => { - deleteMapRuler(id) + if (id) { + deleteMapRuler(id) + } dispatch(setClickedEvent(null)) } diff --git a/apps/fishing-map/features/map/popups/UserContextLayers.tsx b/apps/fishing-map/features/map/popups/UserContextLayers.tsx index d0a3d0d711..089078d993 100644 --- a/apps/fishing-map/features/map/popups/UserContextLayers.tsx +++ b/apps/fishing-map/features/map/popups/UserContextLayers.tsx @@ -2,9 +2,7 @@ import { Fragment } from 'react' import { groupBy } from 'lodash' import { useTranslation } from 'react-i18next' import { Icon } from '@globalfishingwatch/ui-components' -import { DRAW_DATASET_SOURCE } from '@globalfishingwatch/api-types' -import { ContextFeature, ContextPickingObject } from '@globalfishingwatch/deck-layers' -import { TooltipEventFeature } from 'features/map/map.hooks' +import { ContextPickingObject } from '@globalfishingwatch/deck-layers' import styles from './Popup.module.css' import ContextLayersRow from './ContextLayersRow' import { useContextInteractions } from './ContextLayers.hooks' diff --git a/apps/fishing-map/features/map/popups/UserPointsLayers.tsx b/apps/fishing-map/features/map/popups/UserPointsLayers.tsx index 4a20bd8c40..d40a613198 100644 --- a/apps/fishing-map/features/map/popups/UserPointsLayers.tsx +++ b/apps/fishing-map/features/map/popups/UserPointsLayers.tsx @@ -1,13 +1,13 @@ import { Fragment } from 'react' import { groupBy } from 'lodash' import { Icon } from '@globalfishingwatch/ui-components' -import { UserContextFeature } from '@globalfishingwatch/deck-layers' +import { UserContextPickingObject } from '@globalfishingwatch/deck-layers' import styles from './Popup.module.css' import ContextLayersRow from './ContextLayersRow' import { useContextInteractions } from './ContextLayers.hooks' type UserPointsLayersProps = { - features: UserContextFeature[] + features: UserContextPickingObject[] showFeaturesDetails: boolean } diff --git a/apps/fishing-map/features/map/popups/VesselsTable.tsx b/apps/fishing-map/features/map/popups/VesselsTable.tsx index e5b5a6d04f..1904482a73 100644 --- a/apps/fishing-map/features/map/popups/VesselsTable.tsx +++ b/apps/fishing-map/features/map/popups/VesselsTable.tsx @@ -8,7 +8,6 @@ import { DataviewCategory, VesselIdentitySourceEnum, } from '@globalfishingwatch/api-types' -import { FourwingsDeckSublayer } from '@globalfishingwatch/deck-layers' import { EMPTY_FIELD_PLACEHOLDER, formatInfoField, @@ -38,16 +37,10 @@ import VesselPin from 'features/vessel/VesselPin' import { getVesselIdentityTooltipSummary } from 'features/workspace/vessels/VesselLayerPanel' import { SUBLAYER_INTERACTION_TYPES_WITH_VESSEL_INTERACTION, - TooltipEventFeature, getVesselsInfoConfig, } from '../map.hooks' import styles from './VesselsTable.module.css' -export const getVesselTableTitle = (feature: TooltipEventFeature) => { - let title = feature.title - return title -} - export const VesselDetectionTimestamps = ({ vessel }: { vessel: ExtendedFeatureVessel }) => { const { setTimerange } = useTimerangeConnect() const detectionsTimestamps = getDetectionsTimestamps(vessel) diff --git a/apps/fishing-map/features/map/popups/WorkspacePointsLayers.tsx b/apps/fishing-map/features/map/popups/WorkspacePointsLayers.tsx index 67ba63a7d7..ed6dbf4954 100644 --- a/apps/fishing-map/features/map/popups/WorkspacePointsLayers.tsx +++ b/apps/fishing-map/features/map/popups/WorkspacePointsLayers.tsx @@ -1,11 +1,11 @@ import { Fragment } from 'react' import { Icon } from '@globalfishingwatch/ui-components' -import { TooltipEventFeature } from 'features/map/map.hooks' import styles from './Popup.module.css' import ContextLayersRow from './ContextLayersRow' type WorkspacePointsLayersProps = { - features: TooltipEventFeature[] + // TODO:deck type this with its own type + features: any[] } function WorkspacePointsTooltipSection({ features }: WorkspacePointsLayersProps) { diff --git a/libs/deck-layer-composer/src/types.ts b/libs/deck-layer-composer/src/types.ts index 7e07be9cd9..3bf6a631c5 100644 --- a/libs/deck-layer-composer/src/types.ts +++ b/libs/deck-layer-composer/src/types.ts @@ -1,23 +1,7 @@ import { - ClusterLayer, - ClusterPickingInfo, - ClusterPickingObject, - ContextLayer, - ContextPickingInfo, - ContextPickingObject, FourwingsDeckSublayer, - FourwingsLayer, - FourwingsPickingInfo, - FourwingsPickingObject, FourwingsTileLayerColorDomain, FourwingsTileLayerColorRange, - RulerPickingInfo, - RulerPickingObject, - RulersLayer, - UserContextPickingObject, - VesselEventPickingInfo, - VesselEventPickingObject, - VesselLayer, } from '@globalfishingwatch/deck-layers' export const DECK_LAYER_LIFECYCLE = { diff --git a/libs/deck-layers/src/layers/cluster/cluster.types.ts b/libs/deck-layers/src/layers/cluster/cluster.types.ts index 95475a3641..5844734547 100644 --- a/libs/deck-layers/src/layers/cluster/cluster.types.ts +++ b/libs/deck-layers/src/layers/cluster/cluster.types.ts @@ -1,5 +1,5 @@ import { PickingInfo } from '@deck.gl/core' -import { Feature, Polygon, MultiPolygon, Geometry, Point } from 'geojson' +import { Feature, Point } from 'geojson' import { Tile2DHeader } from '@deck.gl/geo-layers/dist/tileset-2d' import { EventTypes } from '@globalfishingwatch/api-types' import { BaseLayerProps, BasePickingInfo } from '../../types' diff --git a/libs/deck-layers/src/layers/rulers/rulers.types.ts b/libs/deck-layers/src/layers/rulers/rulers.types.ts index db17c30595..14085d1f45 100644 --- a/libs/deck-layers/src/layers/rulers/rulers.types.ts +++ b/libs/deck-layers/src/layers/rulers/rulers.types.ts @@ -9,6 +9,8 @@ export type RulerPointProperties = { order: 'start' | 'center' | 'end' bearing?: number text?: string + // TODO:deck review if we still need this legacy from layer-composer + lengthLabel?: string } export type RulerData = { diff --git a/libs/deck-layers/src/types.ts b/libs/deck-layers/src/types.ts index b3750d44e3..77d948ec87 100644 --- a/libs/deck-layers/src/types.ts +++ b/libs/deck-layers/src/types.ts @@ -24,6 +24,7 @@ export type BaseLayerProps = { // TODO:deck move this type to a generic like DeckPickingInfo export type BasePickingInfo = { + title?: string layerId: string category: DeckLayerCategory } From 7b9b4a87f06e17c1d06838dbf52f61f1b6673fbe Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 12 Apr 2024 15:26:32 +0200 Subject: [PATCH 37/43] fix translation key --- apps/fishing-map/features/vessel/insights/InsightFishing.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/features/vessel/insights/InsightFishing.tsx b/apps/fishing-map/features/vessel/insights/InsightFishing.tsx index af66d15f8f..e5970191b7 100644 --- a/apps/fishing-map/features/vessel/insights/InsightFishing.tsx +++ b/apps/fishing-map/features/vessel/insights/InsightFishing.tsx @@ -83,7 +83,7 @@ const InsightFishing = ({ ) : (

      {t( - 'vessel.insights.noFishingEventsInNoTakeMpasEmpty', + 'vessel.insights.fishingEventsInNoTakeMpasEmpty', 'No fishing events detected in no-take MPAs' )}

      From 74da920c141fe51c4e198bb7120b33892a69e409 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 12 Apr 2024 15:32:47 +0200 Subject: [PATCH 38/43] increase package version --- apps/fishing-map/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/fishing-map/package.json b/apps/fishing-map/package.json index 66a7f48af7..fa3b0d18a2 100644 --- a/apps/fishing-map/package.json +++ b/apps/fishing-map/package.json @@ -1,6 +1,6 @@ { "name": "@globalfishingwatchapp/fishing-map", - "version": "2.2.0", + "version": "2.2.1", "private": true, "scripts": { "dev": "next", From 4bf0a19cde8f33d54cac2830c87638d6d1d04116 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 12 Apr 2024 16:17:39 +0200 Subject: [PATCH 39/43] remove duplicated features by id --- .../features/map/map-interactions.hooks.ts | 112 ++++++++++-------- .../src/interactions/types.ts | 3 +- .../src/layers/cluster/ClusterLayer.ts | 3 + .../layers/fourwings/FourwingsHeatmapLayer.ts | 3 +- .../src/layers/vessel/VesselLayer.ts | 24 ++-- libs/deck-layers/src/types.ts | 1 + 6 files changed, 87 insertions(+), 59 deletions(-) diff --git a/apps/fishing-map/features/map/map-interactions.hooks.ts b/apps/fishing-map/features/map/map-interactions.hooks.ts index 3cbd1afca2..f7ef27996b 100644 --- a/apps/fishing-map/features/map/map-interactions.hooks.ts +++ b/apps/fishing-map/features/map/map-interactions.hooks.ts @@ -2,12 +2,12 @@ import { useCallback, useRef, useState } from 'react' import { useSelector } from 'react-redux' import { DeckProps, PickingInfo, Position } from '@deck.gl/core' import { InteractionEventCallback, useSimpleMapHover } from '@globalfishingwatch/react-hooks' -import { ExtendedStyle } from '@globalfishingwatch/layer-composer' import { DataviewCategory } from '@globalfishingwatch/api-types' import { useMapHoverInteraction, useSetMapHoverInteraction, InteractionEvent, + InteractionEventType, } from '@globalfishingwatch/deck-layer-composer' import { ClusterPickingObject, @@ -186,9 +186,55 @@ export const useClickedEventConnect = () => { } } -const defaultEmptyFeatures = [] as DeckLayerInteractionPickingInfo[] -export const useMapMouseHover = (style?: ExtendedStyle) => { +export const useGetPickingInteraction = () => { const map = useDeckMap() + + const getPickingInteraction = useCallback( + (info: PickingInfo, type: InteractionEventType): InteractionEvent | undefined => { + if (!map || !info?.coordinate) { + return + } + + let pickingInfo = [] as DeckLayerInteractionPickingInfo[] + // TODO:deck handle when hovering a cluster point as we don't want to show anything else + // const clusterFeature = event.features.find( + // (f) => f.generatorType === DataviewType.TileCluster && parseInt(f.properties.count) > 1 + // ) + try { + pickingInfo = map?.pickMultipleObjects({ + x: info.x, + y: info.y, + radius: 0, + }) as DeckLayerInteractionPickingInfo[] + } catch (e) { + console.warn(e) + } + const uniqFeatureIds = [] as string[] + const features = pickingInfo.flatMap((f) => { + if (f.object?.id) { + if (!uniqFeatureIds.includes(f.object.id as string)) { + uniqFeatureIds.push(f.object.id as string) + return f.object + } + return [] + } + return f.object || [] + }) + return { + type, + longitude: info.coordinate[0], + latitude: info.coordinate[1], + point: { x: info.x, y: info.y }, + features, + } + }, + [map] + ) + return getPickingInteraction +} + +export const useMapMouseHover = () => { + const getPickingInteraction = useGetPickingInteraction() const setMapHoverFeatures = useSetMapHoverInteraction() const { isMapDrawing } = useMapDrawConnect() const { isMapAnnotating } = useMapAnnotation() @@ -211,37 +257,23 @@ export const useMapMouseHover = (style?: ExtendedStyle) => { const onMouseMove: DeckProps['onHover'] = useCallback( (info: PickingInfo, event: any) => { - if (!map || !info.coordinate) return + if (!info.coordinate) return - let features = defaultEmptyFeatures // TODO:deck handle when hovering a cluster point as we don't want to show anything else // const clusterFeature = event.features.find( // (f) => f.generatorType === DataviewType.TileCluster && parseInt(f.properties.count) > 1 // ) - try { - features = map?.pickMultipleObjects({ - x: info.x, - y: info.y, - radius: 0, - }) as DeckLayerInteractionPickingInfo[] - } catch (e) { - console.warn(e) + const hoverInteraction = getPickingInteraction(info, 'hover') + if (hoverInteraction) { + setMapHoverFeatures(hoverInteraction) } - - setMapHoverFeatures({ - type: 'hover', - longitude: info.coordinate[0], - latitude: info.coordinate[1], - point: { x: info.x, y: info.y }, - features: features.flatMap((f) => f.object || []), - }) // onRulerDrag(features) if (rulersEditing) { onRulerMapHover(info) } }, - [map, onRulerMapHover, rulersEditing, setMapHoverFeatures] + [getPickingInteraction, onRulerMapHover, rulersEditing, setMapHoverFeatures] ) // const hoveredTooltipEvent = parseMapTooltipEvent(hoveredEvent, dataviews, temporalgridDataviews) @@ -298,8 +330,7 @@ export const useHandleMapToolsClick = () => { } export const useMapMouseClick = () => { - // const map = useMapInstance() - const map = useDeckMap() + const getPickingInteraction = useGetPickingInteraction() const handleMapToolsClick = useHandleMapToolsClick() // const setMapClickFeatures = useSetMapClickInteraction() const dataviews = useSelector(selectCurrentDataviewInstancesResolved) @@ -336,36 +367,21 @@ export const useMapMouseClick = () => { const onMapClick: DeckProps['onClick'] = useCallback( (info: PickingInfo, event: any) => { - if (!map || !info.coordinate) return + if (!info.coordinate) return if (event.srcEvent.defaultPrevented) { // this is needed to allow interacting with overlay elements content return true } - let features = defaultEmptyFeatures - try { - features = map?.pickMultipleObjects({ - x: info.x, - y: info.y, - radius: 0, - }) as DeckLayerInteractionPickingInfo[] - } catch (e) { - console.warn(e) - } - const mapClickInteraction: InteractionEvent = { - type: 'click', - longitude: info.coordinate[0], - latitude: info.coordinate[1], - point: { x: info.x, y: info.y }, - features: features.flatMap((f) => f.object || []), - } - - const clickStopPropagation = handleMapToolsClick(mapClickInteraction) !== undefined - if (!clickStopPropagation) { - dispatchClickedEvent(mapClickInteraction) + const clickInteraction = getPickingInteraction(info, 'click') + if (clickInteraction) { + const clickStopPropagation = handleMapToolsClick(clickInteraction) !== undefined + if (!clickStopPropagation) { + dispatchClickedEvent(clickInteraction) + } + trackMapClickEvent(clickInteraction) } - trackMapClickEvent(mapClickInteraction) }, - [map, handleMapToolsClick, trackMapClickEvent, dispatchClickedEvent] + [getPickingInteraction, handleMapToolsClick, trackMapClickEvent, dispatchClickedEvent] ) return onMapClick diff --git a/libs/deck-layer-composer/src/interactions/types.ts b/libs/deck-layer-composer/src/interactions/types.ts index 7bd5bb1c36..bfc711d1b6 100644 --- a/libs/deck-layer-composer/src/interactions/types.ts +++ b/libs/deck-layer-composer/src/interactions/types.ts @@ -1,7 +1,8 @@ import { DeckLayerPickingObject } from '@globalfishingwatch/deck-layers' +export type InteractionEventType = 'click' | 'hover' export type InteractionEvent = { - type: 'click' | 'hover' + type: InteractionEventType features?: DeckLayerPickingObject[] latitude: number longitude: number diff --git a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts index 541fa4919c..9ac36dbe17 100644 --- a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts +++ b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts @@ -28,6 +28,9 @@ export class ClusterLayer extends CompositeLayer }): ClusterPickingInfo => { const object = { ...(info.object || ({} as ClusterFeature)), + id: + info.object?.properties.event_id || + `${(info.object?.geometry?.coordinates || []).join('-')}`, layerId: this.props.id, category: this.props.category, } diff --git a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts index 20b10d1025..55a5b216b8 100644 --- a/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts +++ b/libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts @@ -46,7 +46,8 @@ export class FourwingsHeatmapLayer extends CompositeLayer { dataStatus: VesselDataStatus[] = [] - getPickingInfo = ({ info }: { info: VesselEventPickingInfo }): VesselEventPickingInfo => { + getPickingInfo = ({ + info, + }: { + info: PickingInfo + }): VesselEventPickingInfo => { + const object = { + ...(info.object || ({} as VesselEventProperties)), + layerId: this.props.id, + title: this.props.name, + vesselId: this.props.id, + category: DataviewCategory.Vessels, + color: deckToHexColor(this.props.color), + } if (!info.object) { info.object = {} as VesselEventPickingObject } - info.object.layerId = this.props.id - info.object.title = this.props.name - info.object.vesselId = this.props.id - info.object.category = DataviewCategory.Vessels - info.object.color = deckToHexColor(this.props.color) // info.object.getDetail = async () => { // return GFWAPI.fetch(`/events/${info.object?.properties.id}`) // } - return info + return { ...info, object } } onSublayerError = (error: any) => { diff --git a/libs/deck-layers/src/types.ts b/libs/deck-layers/src/types.ts index 77d948ec87..ae9684a4b5 100644 --- a/libs/deck-layers/src/types.ts +++ b/libs/deck-layers/src/types.ts @@ -24,6 +24,7 @@ export type BaseLayerProps = { // TODO:deck move this type to a generic like DeckPickingInfo export type BasePickingInfo = { + id: string title?: string layerId: string category: DeckLayerCategory From b499a7212464874cd7dffad304e2a498cc0c788e Mon Sep 17 00:00:00 2001 From: satellitestudiodesign Date: Fri, 12 Apr 2024 14:41:42 +0200 Subject: [PATCH 40/43] recover layerHoveredFeatures --- libs/deck-layers/src/layers/cluster/ClusterLayer.ts | 2 +- libs/deck-layers/src/layers/fourwings/FourwingsHeatmapLayer.ts | 2 +- .../src/layers/fourwings/FourwingsHeatmapStaticLayer.ts | 2 +- libs/deck-layers/src/layers/vessel/VesselLayer.ts | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts index 9ac36dbe17..48b6039bb2 100644 --- a/libs/deck-layers/src/layers/cluster/ClusterLayer.ts +++ b/libs/deck-layers/src/layers/cluster/ClusterLayer.ts @@ -31,7 +31,7 @@ export class ClusterLayer extends CompositeLayer { }): VesselEventPickingInfo => { const object = { ...(info.object || ({} as VesselEventProperties)), - layerId: this.props.id, + layerId: this.root.id, title: this.props.name, vesselId: this.props.id, category: DataviewCategory.Vessels, From c5ff6d2c9c0c7e50f7fb7dfde96e0b8c7b760cbb Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 12 Apr 2024 17:32:48 +0200 Subject: [PATCH 41/43] render track segments in timebar --- apps/fishing-map/features/timebar/Timebar.tsx | 4 +++- .../features/timebar/timebar-vessel.hooks.ts | 7 ++++--- .../src/layers/vessel/VesselTrackLayer.ts | 16 ++++++++-------- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/apps/fishing-map/features/timebar/Timebar.tsx b/apps/fishing-map/features/timebar/Timebar.tsx index c5c1ef5e5e..279714579a 100644 --- a/apps/fishing-map/features/timebar/Timebar.tsx +++ b/apps/fishing-map/features/timebar/Timebar.tsx @@ -326,7 +326,9 @@ const TimebarWrapper = () => { )}
      ) - } else if (!tracks || tracks?.length > MAX_TIMEBAR_VESSELS) { + } else if (!tracks) { + return null + } else if (tracks?.length > MAX_TIMEBAR_VESSELS) { return (