Skip to content

Commit

Permalink
Merge pull request #1463 from jolevesq/1461-store-ref
Browse files Browse the repository at this point in the history
{REFACTOR] Refactor store to clean components (#1463)

Co-Authored-By: Johann Levesque <[email protected]>
  • Loading branch information
jolevesq and Johann Levesque authored Nov 6, 2023
2 parents bd9a278 + 79ec7bb commit 39f9e7b
Show file tree
Hide file tree
Showing 23 changed files with 308 additions and 364 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fromLonLat } from 'ol/proj';
import { GeoViewStoreType } from '@/core/stores/geoview-store';
import { AbstractEventProcessor } from './abstract-event-processor';
import { api, NORTH_POLE_POSITION } from '@/app';
import { CustomAttribution } from '@/geo/utils/custom-attribution';
import {
mapPayload,
lngLatPayload,
Expand Down Expand Up @@ -120,7 +121,7 @@ export class MapEventProcessor extends AbstractEventProcessor {
map.getView().on('change:resolution', store.getState().mapState.onMapZoomEnd);
map.getView().on('change:rotation', store.getState().mapState.onMapRotation);

// add map controls
// add map controls (scale)
const scaleBar = new ScaleLine({
units: 'metric',
target: document.getElementById(`${mapId}-scaleControlBar`) as HTMLElement,
Expand All @@ -135,6 +136,22 @@ export class MapEventProcessor extends AbstractEventProcessor {
map.addControl(scaleLine);
map.addControl(scaleBar);

// add map controls (attribution)
const attributionTextElement = document.getElementById(`${mapId}-attribution-text`) as HTMLElement;
const attributionControl = new CustomAttribution(
{
target: attributionTextElement,
collapsible: false,
collapsed: false,
label: document.createElement('div'),
collapseLabel: document.createElement('div'),
},
mapId
);

attributionControl.formatAttribution();
map.addControl(attributionControl);

// add map overlays
// create overlay for north pole icon
const northPoleId = `${mapId}-northpole`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,68 +3,10 @@ import { useContext, useEffect, useState } from 'react';

import { useTheme } from '@mui/material/styles';

import OLAttribution, { Options } from 'ol/control/Attribution';

import { MapContext } from '@/core/app-start';
import { Tooltip, Box } from '@/ui';
import { getSxClasses } from './attribution-style';
import { useUIFooterBarExpanded } from '@/core/stores/store-interface-and-intial-values/ui-state';
import { useMapElement } from '@/core/stores/store-interface-and-intial-values/map-state';

/**
* Custom Attribution control that extends Openlayers Attribution control.
* Class adds title to attribution text to show a tooltip when mouse is over it.
*
* @class CustomAttribution
*/
class CustomAttribution extends OLAttribution {
attributions: string[] = [];

mapId: string;

/**
* Constructor that enables attribution text tooltip.
*
* @param {Options} optOptions control options
*/
constructor(optOptions: Options, mapId: string) {
const options = optOptions || {};

super(options);

this.mapId = mapId;
}

/**
* Format the attribution element by removing duplicate
*/
formatAttribution() {
// find ul element in attribution control
const ulElement = this.element.getElementsByTagName('UL')[0];
const compAttribution: string[] = [];

if (ulElement) {
// find li elements in ul element
const liElements = ulElement.getElementsByTagName('LI');

if (liElements && liElements.length > 0) {
// add title attribute to li elements
for (let liElementIndex = 0; liElementIndex < liElements.length; liElementIndex++) {
const liElement = liElements[liElementIndex] as HTMLElement;
const attributionText = liElement.innerText;

// if elemetn doat not exist, add. Otherwise remove
if (!compAttribution.includes(attributionText.toLowerCase().replaceAll(' ', ''))) {
this.attributions.push(attributionText);
compAttribution.push(attributionText.toLowerCase().replaceAll(' ', ''));
} else {
liElement.remove();
}
}
}
}
}
}

/**
* Create an Attribution component that will display an attribution box
Expand All @@ -82,37 +24,9 @@ export function Attribution(): JSX.Element {
// internal component state
const [attribution, setAttribution] = useState('');

// get store values
const mapElement = useMapElement();
// get store value
const expanded = useUIFooterBarExpanded();

useEffect(() => {
let attributionControl: CustomAttribution;

if (mapElement !== undefined) {
const attributionTextElement = document.getElementById(`${mapId}-attribution-text`) as HTMLElement;

attributionControl = new CustomAttribution(
{
target: attributionTextElement,
collapsible: false,
collapsed: false,
label: document.createElement('div'),
collapseLabel: document.createElement('div'),
},
mapId
);

attributionControl.formatAttribution();
mapElement.addControl(attributionControl);
}
return () => {
if (mapElement !== undefined) {
mapElement.removeControl(attributionControl);
}
};
}, [mapId, mapElement]);

useEffect(() => {
const attributionTextElement = document.getElementById(`${mapId}-attribution-text`) as HTMLElement;

Expand Down
108 changes: 28 additions & 80 deletions packages/geoview-core/src/core/components/crosshair/crosshair.tsx
Original file line number Diff line number Diff line change
@@ -1,129 +1,77 @@
import { useEffect, useRef, useContext } from 'react';
import { useCallback, useEffect, useRef } from 'react';

import { useTheme } from '@mui/material/styles';

import { useTranslation } from 'react-i18next';

import { toLonLat } from 'ol/proj';
import { KeyboardPan } from 'ol/interaction';

import { getGeoViewStore } from '@/core/stores/stores-managers';

import { MapContext } from '@/core/app-start';

import { Box, Fade, Typography } from '@/ui';
import { TypeMapMouseInfo } from '@/api/events/payloads';

import { getSxClasses } from './crosshair-style';
import { CrosshairIcon } from './crosshair-icon';
import { useAppCrosshairsActive } from '@/core/stores/store-interface-and-intial-values/app-state';
import { useMapCenterCoordinates, useMapElement, useMapProjection } from '@/core/stores/store-interface-and-intial-values/map-state';
import { useMapElement, useMapStoreActions } from '@/core/stores/store-interface-and-intial-values/map-state';

/**
* Create a Crosshair when map is focus with the keyboard so user can click on the map
* @returns {JSX.Element} the crosshair component
*/
export function Crosshair(): JSX.Element {
const mapConfig = useContext(MapContext);
const { mapId } = mapConfig;

const { t } = useTranslation<string>();

const theme = useTheme();
const sxClasses = getSxClasses(theme);

// get store values
// tracks if the last action was done through a keyboard (map navigation) or mouse (mouse movement)
const store = getGeoViewStore(mapId);
const isCrosshairsActive = useAppCrosshairsActive();
const projection = useMapProjection();
const mapCoord = useMapCenterCoordinates();
const mapElement = useMapElement();

// use reference as the mapElement from the store is undefined
// TODO: Find what is going on with mapElement for focus-trap and crosshair and crosshair + map coord for this component
// ? maybe because simulate click is in an event listener, it is best to use useRef
const isCrosshairsActiveRef = useRef(isCrosshairsActive);
isCrosshairsActiveRef.current = isCrosshairsActive;
const mapCoordRef = useRef(mapCoord);
mapCoordRef.current = mapCoord;
const { setClickCoordinates, setMapKeyboardPanInteractions } = useMapStoreActions();

// do not use useState for item used inside function only without rendering... use useRef
const panelButtonId = useRef('');

let panDelta = 128;
const panDelta = useRef(128);

/**
* Simulate map mouse click to trigger details panel
* @function simulateClick
* @param {KeyboardEvent} evt the keyboard event
* @param {KeyboardEvent} event the keyboard event
*/
function simulateClick(evt: KeyboardEvent): void {
if (evt.key === 'Enter') {
if (isCrosshairsActiveRef.current) {
// updater store with the lnglat point
const mapClickCoordinatesFetch: TypeMapMouseInfo = {
projected: [0, 0],
pixel: [0, 0],
lnglat: toLonLat(mapCoordRef.current, `EPSG:${projection}`),
dragging: false,
};
store.setState({
mapState: { ...store.getState().mapState, clickCoordinates: mapClickCoordinatesFetch },
});
}
const simulateClick = useCallback((event: KeyboardEvent) => {
if (event.key === 'Enter') {
setClickCoordinates();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

/**
* Modify the pixelDelta value for the keyboard pan on Shift arrow up or down
*
* @param {KeyboardEvent} evt the keyboard event to trap
* @param {KeyboardEvent} event the keyboard event to trap
*/
function managePanDelta(evt: KeyboardEvent): void {
if ((evt.key === 'ArrowDown' && evt.shiftKey) || (evt.key === 'ArrowUp' && evt.shiftKey)) {
panDelta = evt.key === 'ArrowDown' ? (panDelta -= 10) : (panDelta += 10);
panDelta = panDelta < 10 ? 10 : panDelta; // minus panDelta reset the value so we need to trap
const managePanDelta = useCallback((event: KeyboardEvent) => {
if ((event.key === 'ArrowDown' && event.shiftKey) || (event.key === 'ArrowUp' && event.shiftKey)) {
panDelta.current = event.key === 'ArrowDown' ? (panDelta.current -= 10) : (panDelta.current += 10);
panDelta.current = panDelta.current < 10 ? 10 : panDelta.current; // minus panDelta reset the value so we need to trap

// replace the KeyboardPan interraction by a new one
// const mapElement = mapElementRef.current;
mapElement.getInteractions().forEach((interactionItem) => {
if (interactionItem instanceof KeyboardPan) {
mapElement.removeInteraction(interactionItem);
}
});
mapElement.addInteraction(new KeyboardPan({ pixelDelta: panDelta }));
setMapKeyboardPanInteractions(panDelta.current);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

useEffect(() => {
const unsubIsCrosshair = getGeoViewStore(mapId).subscribe(
(state) => state.appState.isCrosshairsActive,
(curCrosshair, prevCrosshair) => {
if (curCrosshair !== prevCrosshair) {
const mapHTMLElement = mapElement.getTargetElement();

if (curCrosshair) {
panelButtonId.current = 'detailsPanel';

mapHTMLElement.addEventListener('keydown', simulateClick);
mapHTMLElement.addEventListener('keydown', managePanDelta);
} else {
mapHTMLElement.removeEventListener('keydown', simulateClick);
mapHTMLElement.removeEventListener('keydown', managePanDelta);
}
}
}
);
const mapHTMLElement = mapElement.getTargetElement();
if (isCrosshairsActive) {
panelButtonId.current = 'detailsPanel';
mapHTMLElement.addEventListener('keydown', simulateClick);
mapHTMLElement.addEventListener('keydown', managePanDelta);
} else {
mapHTMLElement.removeEventListener('keydown', simulateClick);
mapHTMLElement.removeEventListener('keydown', managePanDelta);
}

return () => {
const mapHTMLElement = mapElement.getTargetElement();
unsubIsCrosshair();
mapHTMLElement.removeEventListener('keydown', simulateClick);
mapHTMLElement.removeEventListener('keydown', managePanDelta);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [isCrosshairsActive, mapElement, simulateClick, managePanDelta]);

return (
<Box
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { MouseEventHandler, useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { Button, Dialog, DialogActions, DialogTitle } from '@mui/material';
import { Button, Dialog, DialogActions, DialogTitle } from '@mui/material'; // TODO: refactor to use UI

import { MapContext } from '@/core/app-start';
import { exportPNG } from '../../utils/utilities';
import { exportPNG } from '@/core/utils/utilities';

/**
* Interface used for home button properties
Expand All @@ -28,10 +28,12 @@ const defaultProps = {
*/
export default function ExportModal(props: ExportModalProps): JSX.Element {
const { className, isShown, closeModal } = props;
const { t } = useTranslation();

const mapConfig = useContext(MapContext);
const { mapId } = mapConfig;

const { t } = useTranslation();

return (
<Dialog open={isShown} onClose={closeModal} className={className}>
<form
Expand Down
Loading

0 comments on commit 39f9e7b

Please sign in to comment.