Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix multiple tooltips bugs after dragging map #221

Merged
merged 5 commits into from
Jan 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 1 addition & 9 deletions src/components/Map/FcsMap/FcsChoropleth.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import L from 'leaflet';
import { useTheme } from 'next-themes';
import React, { useEffect, useRef } from 'react';
import { GeoJSON, useMap } from 'react-leaflet';
import { GeoJSON } from 'react-leaflet';

import { useSelectedCountryId } from '@/domain/contexts/SelectedCountryIdContext';
import FcsChoroplethProps from '@/domain/props/FcsChoroplethProps';
import { AccessibilityOperations } from '@/operations/map/AccessibilityOperations';
import FcsChoroplethOperations from '@/operations/map/FcsChoroplethOperations';
import { MapOperations } from '@/operations/map/MapOperations';

import FscCountryChoropleth from './FcsCountryChoropleth';

Expand All @@ -29,13 +28,6 @@ export default function FcsChoropleth({
const geoJsonRef = useRef<L.GeoJSON | null>(null);
const { selectedCountryId, setSelectedCountryId } = useSelectedCountryId();
const { theme } = useTheme();
const map = useMap();

// adding the country name as a tooltip to each layer (on hover); the tooltip is not shown if the country is selected
useEffect(() => {
if (!geoJsonRef.current || !map) return () => {};
return MapOperations.handleCountryTooltip(geoJsonRef, map, fcsData);
}, [selectedCountryId]);

useEffect(() => {
const geoJsonLayer = geoJsonRef.current;
Expand Down
1 change: 1 addition & 0 deletions src/components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ export default function Map({ countries, disputedAreas, fcsData, alertData }: Ma
{selectedMapType === GlobalInsight.NUTRITION &&
countries.features.map((country) => (
<NutritionChoropleth
key={country.properties.adm0_id}
countryId={country.properties.adm0_id}
data={{ type: 'FeatureCollection', features: [country as Feature<Geometry, CountryProps>] }}
onDataUnavailable={onDataUnavailable}
Expand Down
13 changes: 2 additions & 11 deletions src/components/Map/NutritionMap/NutritionChoropleth.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import L from 'leaflet';
import { useTheme } from 'next-themes';
import React, { useEffect, useRef, useState } from 'react';
import { GeoJSON, useMap } from 'react-leaflet';
import React, { useRef, useState } from 'react';
import { GeoJSON } from 'react-leaflet';

import { useSelectedCountryId } from '@/domain/contexts/SelectedCountryIdContext';
import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts';
import { NutrientType } from '@/domain/enums/NutrientType.ts';
import { useNutritionQuery } from '@/domain/hooks/globalHooks';
import NutritionChoroplethProps from '@/domain/props/NutritionChoroplethProps';
import { MapOperations } from '@/operations/map/MapOperations';
import NutritionChoroplethOperations from '@/operations/map/NutritionChoroplethOperations';

import NutritionStateChoropleth from './NutritionStateChoropleth';
Expand All @@ -28,14 +27,6 @@ export default function NutritionChoropleth({ data, countryId, onDataUnavailable
const { theme } = useTheme();
const { data: nutritionData } = useNutritionQuery(true);
const [selectedNutrient, setSelectedNutrient] = useState<NutrientType>(NutrientType.MINI_SIMPLE);
const map = useMap();

// adding the country name as a tooltip to each layer (on hover)
// the tooltip is not shown if the country is selected or there is no data available for the country
useEffect(() => {
if (!geoJsonRef.current || !nutritionData || !map) return () => {};
return MapOperations.handleCountryTooltip(geoJsonRef, map, undefined, nutritionData, data);
}, [selectedCountryId, nutritionData]);

return (
<div>
Expand Down
3 changes: 1 addition & 2 deletions src/components/Map/NutritionMap/NutritionStateChoropleth.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,7 @@ export default function NutritionStateChoropleth({
style={(feature) => NutritionStateChoroplethOperations.dynamicStyle(feature, selectedNutrient)}
onEachFeature={(feature, layer) => {
layersRef.current.push(layer);
NutritionStateChoroplethOperations.addNutritionTooltip(layer, feature, selectedNutrient);
NutritionStateChoroplethOperations.addHoverEffect(layer);
NutritionStateChoroplethOperations.addEvents(layer, feature, selectedNutrient);
}}
/>
</>
Expand Down
3 changes: 3 additions & 0 deletions src/operations/map/FcsChoroplethOperations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import L from 'leaflet';

import { CountryFcsData } from '@/domain/entities/country/CountryFcsData.ts';
import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts';
import { MapOperations } from '@/operations/map/MapOperations';
import { inactiveCountryOverlayStyling } from '@/styles/MapColors';

class FcsChoroplethOperations {
Expand Down Expand Up @@ -40,12 +41,14 @@ class FcsChoroplethOperations {
if (this.checkIfActive(feature as CountryMapData, fcsData)) {
pathLayer.setStyle({ fillOpacity: 0.3, fillColor: 'hsl(var(--nextui-countryHover))' });
document.getElementsByClassName('leaflet-container').item(0)?.classList.add('interactive');
MapOperations.attachCountryNameTooltip(feature, layer);
}
},
mouseout: () => {
if (this.checkIfActive(feature as CountryMapData, fcsData)) {
pathLayer.setStyle({ fillOpacity: 0 });
document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive');
layer.unbindTooltip();
}
},
});
Expand Down
35 changes: 18 additions & 17 deletions src/operations/map/IpcChoroplethOperations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,16 +130,13 @@ export class IpcChoroplethOperations {
) {
// Check if the country is active
if (this.checkIfActive(feature as CountryMapData, ipcData)) {
// If the country is not the currently selected country, create a tooltip
if (feature.properties?.adm0_id !== selectedCountryId) {
this.createTooltip(feature, layer, ipcData);
}
// to handle tooltip for regions with Not Analyzed Data in country view
else {
if (feature.properties?.adm0_id === selectedCountryId) {
IpcChoroplethOperations.initializeRegionLayer(feature, layer);
}

// Attach events to the layer
this.attachEvents(feature, layer, setSelectedCountryId, selectedCountryId);
this.attachEvents(feature, layer, setSelectedCountryId, selectedCountryId, ipcData);
}
}

Expand All @@ -149,12 +146,14 @@ export class IpcChoroplethOperations {
* @param layer - Leaflet layer object
* @param setSelectedCountryId - Function to set the selected country id
* @param selectedCountryId - The currently selected country id
* @param ipcData - Array of country IPC data objects
*/
static attachEvents(
feature: Feature<Geometry, GeoJsonProperties>,
layer: L.Layer,
setSelectedCountryId: (id: number | null) => void,
selectedCountryId: number
selectedCountryId: number,
ipcData: CountryIpcData[]
) {
const pathLayer = layer as L.Path;
const originalStyle = { ...pathLayer.options };
Expand All @@ -175,14 +174,15 @@ export class IpcChoroplethOperations {
}

document.getElementsByClassName('leaflet-container').item(0)?.classList.add('interactive');
pathLayer.openTooltip();

this.createTooltip(feature, layer, ipcData);
},

mouseout: () => {
// Restore the original layer style
pathLayer.setStyle(originalStyle);
document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive');
pathLayer.closeTooltip();
pathLayer.unbindTooltip();
},
});
}
Expand Down Expand Up @@ -243,7 +243,7 @@ export class IpcChoroplethOperations {
);

// Bind the tooltip to the layer
layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true, direction: 'top' });
layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true, direction: 'top' }).openTooltip();
}

/**
Expand All @@ -252,7 +252,6 @@ export class IpcChoroplethOperations {
* @param layer - Leaflet layer object to which the tooltip and events are bound
*/
static initializeRegionLayer(feature: Feature<Geometry, GeoJsonProperties>, layer: L.Layer) {
IpcChoroplethOperations.createPhaseTooltip(feature, layer);
IpcChoroplethOperations.attachEventsRegion(feature, layer);
}

Expand All @@ -275,12 +274,12 @@ export class IpcChoroplethOperations {
fillColor: 'hsl(var(--nextui-ipcHoverRegion))',
});
}
pathLayer.openTooltip();
IpcChoroplethOperations.createPhaseTooltip(feature, layer);
},
// Restore the original layer style
mouseout: () => {
pathLayer.setStyle(originalStyle);
pathLayer.closeTooltip();
pathLayer.unbindTooltip();
},
});
}
Expand All @@ -302,10 +301,12 @@ export class IpcChoroplethOperations {
);

// Bind the tooltip to the layer
layer.bindTooltip(tooltipContainer, {
className: 'leaflet-tooltip',
sticky: true,
});
layer
.bindTooltip(tooltipContainer, {
className: 'leaflet-tooltip',
sticky: true,
})
.openTooltip();
}

/**
Expand Down
76 changes: 8 additions & 68 deletions src/operations/map/MapOperations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import CountryHoverPopover from '@/components/CountryHoverPopover/CountryHoverPo
import { MAP_MAX_ZOOM, REGION_LABEL_SENSITIVITY, SELECTED_COUNTRY_ZOOM_THRESHOLD } from '@/domain/constant/map/Map.ts';
import { CommonRegionProperties } from '@/domain/entities/common/CommonRegionProperties';
import { Feature } from '@/domain/entities/common/Feature';
import { CountryFcsData } from '@/domain/entities/country/CountryFcsData.ts';
import { CountryMapData, CountryProps } from '@/domain/entities/country/CountryMapData.ts';
import { CountryNutrition } from '@/domain/entities/country/CountryNutrition.ts';
import { LayerWithFeature } from '@/domain/entities/map/LayerWithFeature.ts';
import FcsChoroplethOperations from '@/operations/map/FcsChoroplethOperations.ts';
import NutritionChoroplethOperations from '@/operations/map/NutritionChoroplethOperations.ts';

export class MapOperations {
/**
Expand All @@ -23,18 +17,6 @@ export class MapOperations {
features: countryFeatures as GeoJsonFeature<Geometry, GeoJsonProperties>[],
});

/**
* Creates a 'HTMLDivElement' rendering the given 'countryName' within a 'CountryHoverPopover'.
* Needed because leaflet tooltips do not accept React components.
* @param countryName The name to be displayed in the popover
*/
static createCountryNameTooltipElement(countryName: string): HTMLDivElement {
const tooltipContainer = document.createElement('div');
const root = createRoot(tooltipContainer);
root.render(<CountryHoverPopover header={countryName} />);
return tooltipContainer;
}

/**
* Updates the region labels for example when the user is zooming. Recalculates if the full label or "..." should be displayed.
* @param feature region data in GeoJSON format
Expand Down Expand Up @@ -97,56 +79,14 @@ export class MapOperations {
}

/**
* Handle the tooltip functionality for the country names in the world view
* @param geoJsonRef reference to the cloropleth element i.e. the country the tooltip is being attached to
* @param map the leaflet map
* @param fcsData (Optional) food consumption data - needs to be set when calling from FCS cloropleth
* @param nutritionData (Optional) nutrition data - needs to be set when calling from nutrition cloropleth
* @param countryMapData (Optional) general data about the country the tooltip is being attached to - needs to be set when calling from nutrition cloropleth
* @returns A method destructing the map listeners
* Attach a very basic tooltip showing the name of the passed country to the passed layer
* @param feature - country feature with the name that has to be shown in the tooltip
* @param layer - layer the tooltip is attached to
*/
static handleCountryTooltip(
geoJsonRef: React.MutableRefObject<L.GeoJSON | null>,
map: L.Map,
fcsData?: Record<string, CountryFcsData>,
nutritionData?: CountryNutrition,
countryMapData?: FeatureCollection<Geometry, CountryProps>
) {
const disableTooltips = () => {
geoJsonRef.current?.eachLayer((layer) => {
if (layer instanceof L.Path) {
const layerElement = layer.getElement();
if (layerElement && !layerElement.matches(':hover')) {
layer.unbindTooltip();
}
}
});
};

const enableTooltips = () => {
geoJsonRef.current?.eachLayer((layer: LayerWithFeature) => {
layer.unbindTooltip();
const feature = layer.feature as CountryMapData;
if (
(fcsData && FcsChoroplethOperations.checkIfActive(feature as CountryMapData, fcsData)) ||
(nutritionData &&
countryMapData &&
NutritionChoroplethOperations.checkIfActive(countryMapData.features[0] as CountryMapData, nutritionData))
) {
const tooltipContainer = MapOperations.createCountryNameTooltipElement(feature?.properties?.adm0_name);
layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true });
}
});
};

enableTooltips();

map.on('dragstart', disableTooltips);
map.on('dragend', enableTooltips);

return () => {
map.off('dragstart', disableTooltips);
map.off('dragend', enableTooltips);
};
static attachCountryNameTooltip(feature: GeoJsonFeature<Geometry, GeoJsonProperties>, layer: L.Layer) {
const tooltipContainer = document.createElement('div');
const root = createRoot(tooltipContainer);
root.render(<CountryHoverPopover header={feature?.properties?.adm0_name} />);
layer.bindTooltip(tooltipContainer, { className: 'leaflet-tooltip', sticky: true }).openTooltip();
}
}
4 changes: 4 additions & 0 deletions src/operations/map/NutritionChoroplethOperations.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Feature } from 'geojson';
import L from 'leaflet';

import { CountryMapData } from '@/domain/entities/country/CountryMapData.ts';
import { CountryNutrition } from '@/domain/entities/country/CountryNutrition.ts';
import { MapOperations } from '@/operations/map/MapOperations';
import { inactiveCountryOverlayStyling } from '@/styles/MapColors.ts';

export default class NutritionChoroplethOperations {
Expand Down Expand Up @@ -49,6 +51,7 @@ export default class NutritionChoroplethOperations {
fillOpacity: 0.8,
});
document.getElementsByClassName('leaflet-container').item(0)?.classList.add('interactive');
MapOperations.attachCountryNameTooltip(feature as Feature, layer);
}
});
pathLayer.on('mouseout', () => {
Expand All @@ -57,6 +60,7 @@ export default class NutritionChoroplethOperations {
fillOpacity: 0.5,
});
document.getElementsByClassName('leaflet-container').item(0)?.classList.remove('interactive');
layer.unbindTooltip();
}
});
}
Expand Down
26 changes: 17 additions & 9 deletions src/operations/map/NutritionStateChoroplethOperations.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Feature } from 'geojson';
import { Feature, GeoJsonProperties, Geometry } from 'geojson';
import { PathOptions } from 'leaflet';
import { createRoot } from 'react-dom/client';

Expand Down Expand Up @@ -40,14 +40,20 @@ export default class NutritionStateChoroplethOperations {
};
}

public static addHoverEffect(layer: LayerWithFeature): void {
public static addEvents(
layer: LayerWithFeature,
feature: Feature<Geometry, GeoJsonProperties> | undefined,
selectedNutrient: NutrientType
): void {
const pathLayer = layer as L.Path;
pathLayer.on({
mouseover: () => {
pathLayer.setStyle({ fillOpacity: 0.6 });
this.addNutritionTooltip(layer, feature, selectedNutrient);
},
mouseout: () => {
pathLayer.setStyle({ fillOpacity: 1 });
pathLayer.unbindTooltip();
},
});
}
Expand All @@ -61,12 +67,14 @@ export default class NutritionStateChoroplethOperations {
root.render(<NutritionRegionTooltip feature={feature} selectedNutrient={selectedNutrient} />);

layer.unbindTooltip();
layer.bindTooltip(tooltipContainer, {
className: 'state-tooltip',
direction: 'top',
offset: [0, -10],
permanent: false,
sticky: true,
});
layer
.bindTooltip(tooltipContainer, {
className: 'state-tooltip',
direction: 'top',
offset: [0, -10],
permanent: false,
sticky: true,
})
.openTooltip();
}
}