Skip to content

Commit

Permalink
Use icons for point markers
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfranklin committed Dec 14, 2024
1 parent e0b9e7a commit ddf4e07
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 88 deletions.
22 changes: 12 additions & 10 deletions react/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"eslint-plugin-react-hooks": "^4.6.0",
"formik": "^2.4.5",
"jwt-decode": "^4.0.0",
"leaflet": "^1.9.3",
"leaflet": "^1.9.4",
"leaflet.markercluster": "^1.5.3",
"postcss-nesting": "^12.0.3",
"prettier": "^2.7.1",
Expand All @@ -62,8 +62,8 @@
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-esri-leaflet": "^2.0.1",
"react-leaflet": "^4.2.0",
"react-leaflet-cluster": "^2.1.0",
"react-leaflet": "^4.2.1",
"react-leaflet-markercluster": "^4.2.1",
"react-query": "^3.39.3",
"react-redux": "^8.0.2",
"react-resize-detector": "^7.1.2",
Expand Down
39 changes: 3 additions & 36 deletions react/src/components/FeatureIcon/FeatureIcon.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,16 @@
import React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import {
faCameraRetro,
faVideo,
faClipboardList,
faMapMarkerAlt,
faDrawPolygon,
faCloud,
faBezierCurve,
faRoad,
faLayerGroup,
faQuestionCircle,
} from '@fortawesome/free-solid-svg-icons';

import { FeatureType, FeatureTypeNullable } from '@hazmapper/types';
import { FeatureTypeNullable } from '@hazmapper/types';
import { featureTypeToIcon } from '@hazmapper/utils/featureIconUtil';
import styles from './FeatureIcon.module.css';

const featureTypeToIcon: Record<FeatureType, IconDefinition> = {
// Asset types
image: faCameraRetro,
video: faVideo,
questionnaire: faClipboardList,
point_cloud: faCloud /* https://tacc-main.atlassian.net/browse/WG-391 */,
streetview: faRoad,

// Geometry types
Point: faMapMarkerAlt,
LineString: faBezierCurve,
Polygon: faDrawPolygon,
MultiPoint: faMapMarkerAlt,
MultiLineString: faBezierCurve,
MultiPolygon: faDrawPolygon,
GeometryCollection: faLayerGroup,

// Collection type
collection: faLayerGroup,
};

interface Props {
featureType: FeatureTypeNullable;
}

export const FeatureIcon: React.FC<Props> = ({ featureType }) => {
const icon = featureType ? featureTypeToIcon[featureType] : faQuestionCircle;
const icon = featureTypeToIcon(featureType);

return <FontAwesomeIcon className={styles.icon} icon={icon} size="sm" />;
};
49 changes: 28 additions & 21 deletions react/src/components/Map/FitBoundsHandler.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import React, { useEffect, useMemo, useCallback, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import React, { useEffect, useCallback, useRef } from 'react';
import * as turf from '@turf/turf';
import { useMap } from 'react-leaflet';
import { FeatureCollection, Feature } from '@hazmapper/types';
import { useFeatureSelection } from '@hazmapper/hooks';
import { MAP_CONFIG } from './config';
import L from 'leaflet';

/**
* Handles map bounds adjustments based on features.
* When features are first loaded: Fits bounds to show all features in collection
* When selectedFeature changes: Zooms to that feature
*/
const FitBoundsHandler: React.FC<{
featureCollection: FeatureCollection;
}> = ({ featureCollection }) => {
const location = useLocation();
const map = useMap();

// Track if we've seen features
const hasFeatures = useRef(false);

const selectedFeatureId = useMemo(() => {
const searchParams = new URLSearchParams(location.search);
const rawId = searchParams.get('selectedFeature');
return rawId ? Number(rawId) : undefined;
}, [location.search]);
const { selectedFeatureId } = useFeatureSelection();

const getBoundsFromFeature = useCallback(
(feature: FeatureCollection | Feature) => {
Expand All @@ -36,6 +24,27 @@ const FitBoundsHandler: React.FC<{
[]
);

const zoomToFeature = useCallback(
(feature: Feature) => {
if (feature.geometry.type === 'Point') {
const coordinates = feature.geometry.coordinates;
const point = L.latLng(coordinates[1], coordinates[0]);

map.setView(point, MAP_CONFIG.maxPointSelectedFeatureZoom, {
animate: false,
});
} else {
const bounds = getBoundsFromFeature(feature);
map.fitBounds(bounds, {
maxZoom: MAP_CONFIG.maxFitBoundsSelectedFeatureZoom,
padding: [50, 50],
animate: false,
});
}
},
[map, getBoundsFromFeature]
);

// Handle initial bounds when features are loaded
useEffect(() => {
if (
Expand All @@ -46,6 +55,7 @@ const FitBoundsHandler: React.FC<{
const bounds = getBoundsFromFeature(featureCollection);
map.fitBounds(bounds, {
maxZoom: MAP_CONFIG.maxFitBoundsInitialZoom,
padding: [50, 50],
});
hasFeatures.current = true;
}
Expand All @@ -59,13 +69,10 @@ const FitBoundsHandler: React.FC<{
);

if (activeFeature) {
const bounds = getBoundsFromFeature(activeFeature);
map.fitBounds(bounds, {
maxZoom: MAP_CONFIG.maxFitBoundsSelectedFeatureZoom,
});
zoomToFeature(activeFeature);
}
}
}, [map, selectedFeatureId, featureCollection, getBoundsFromFeature]);
}, [map, selectedFeatureId, featureCollection, zoomToFeature]);

return null;
};
Expand Down
76 changes: 61 additions & 15 deletions react/src/components/Map/Map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@ import {
WMSTileLayer,
GeoJSON,
} from 'react-leaflet';
import MarkerClusterGroup from 'react-leaflet-markercluster';
import { TiledMapLayer } from 'react-esri-leaflet';
import MarkerClusterGroup from '@changey/react-leaflet-markercluster';
import * as L from 'leaflet';
import { MarkerCluster } from 'leaflet';
import 'leaflet.markercluster';

import 'leaflet/dist/leaflet.css';
import L, { MarkerCluster } from 'leaflet';

import {
TileServerLayer,
FeatureCollection,
Feature,
getFeatureType,
FeatureType,

Check failure on line 19 in react/src/components/Map/Map.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'FeatureType' is defined but never used
} from '@hazmapper/types';
import { useFeatureSelection } from '@hazmapper/hooks';
import { MAP_CONFIG } from './config';
import FitBoundsHandler from './FitBoundsHandler';
import { calculatePointCloudMarkerPosition } from './utils';
import styles from './Map.module.css';

Check failure on line 25 in react/src/components/Map/Map.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

'styles' is defined but never used
import './Map.css';

import 'leaflet/dist/leaflet.css';
import 'react-leaflet-markercluster/styles';

import { featureTypeToIcon } from '@hazmapper/utils/featureIconUtil';

interface LeafletMapProps {
/**
Expand All @@ -51,10 +55,13 @@ const getFeatureStyle = (feature: any) => {
return feature.properties?.style || defaultGeoJsonOptions.style;
};

const ClusterMarkerIcon = (childCount: number) => {
// NOTE: iconCreateFunction being run by leaflet, which is not support ES6 arrow func syntax
// eslint-disable-next-line
const createClusterCustomIcon = function (cluster: MarkerCluster) {
return L.divIcon({
html: `<div><b>${childCount}</b></div>`,
className: 'marker-cluster',
html: `<span>${cluster.getChildCount()}</span>`,
className: 'custom-marker-cluster',
iconSize: L.point(25, 25, true),
});
};

Expand All @@ -67,17 +74,50 @@ const LeafletMap: React.FC<LeafletMapProps> = ({
baseLayers = [],
featureCollection,
}) => {
const { selectedFeatureId, setSelectedFeature } = useFeatureSelection();
const { selectedFeatureId, setSelectedFeatureId } = useFeatureSelection();

const handleFeatureClick = useCallback(
(feature: any) => {
setSelectedFeature(feature.id);
debugger;

Check failure on line 81 in react/src/components/Map/Map.tsx

View workflow job for this annotation

GitHub Actions / React-Linting

Unexpected 'debugger' statement
setSelectedFeatureId(feature.id);

//TODO handle clicking on streetview https://tacc-main.atlassian.net/browse/WG-392
},
[selectedFeatureId]
);

const createCustomIcon = useCallback((feature: Feature) => {
const featureFAIcon = featureTypeToIcon(getFeatureType(feature));
// Get SVG path directly from the icon object
const iconPath = featureFAIcon.icon[4];

return L.divIcon({
html: `
<div style="
background-color: #3498db;
width: 40px;
height: 40px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
">
<svg
viewBox="0 0 ${featureFAIcon.icon[0]} ${featureFAIcon.icon[1]}"
style="width: 20px; height: 20px; fill: white;"
>
<path d="${iconPath}"></path>
</svg>
</div>
`,
className: 'custom-marker',
iconSize: L.point(40, 40),
iconAnchor: L.point(20, 20),
});
}, []);

const activeBaseLayers = useMemo(
() => baseLayers.filter((layer) => layer.uiOptions.isActive),
[baseLayers]
Expand Down Expand Up @@ -179,16 +219,21 @@ const LeafletMap: React.FC<LeafletMapProps> = ({
))}
{/* Marker Features with Clustering (also includes point cloud markers) */}
<MarkerClusterGroup
iconCreateFunction={(cluster: MarkerCluster) =>
ClusterMarkerIcon(cluster.getChildCount())
iconCreateFunction={createClusterCustomIcon}
maxFitBoundsSelectedFeatureZoom={
MAP_CONFIG.maxFitBoundsSelectedFeatureZoom
}
zIndexOffset={1}
spiderfyOnMaxZoom={true}
spiderfyOnZoom={MAP_CONFIG.maxPointSelectedFeatureZoom}
showCoverageOnHover={true}
zoomToBoundsOnClick={false}
>
{markerFeatures.map((feature) => {
const geometry = feature.geometry as GeoJSON.Point;
return (
<Marker
key={feature.id}
icon={createCustomIcon(feature)}
position={[geometry.coordinates[1], geometry.coordinates[0]]}
eventHandlers={{
click: () => handleFeatureClick(feature),
Expand All @@ -198,7 +243,8 @@ const LeafletMap: React.FC<LeafletMapProps> = ({
);
})}
</MarkerClusterGroup>
<FitBoundsHandler featureCollection={featureCollection} />{' '}
{/* Handles zooming to a specific feature or to all features */}
<FitBoundsHandler featureCollection={featureCollection} />
<ZoomControl position="bottomright" />
</MapContainer>
);
Expand Down
5 changes: 3 additions & 2 deletions react/src/components/Map/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { LatLngTuple } from 'leaflet';

export const MAP_CONFIG = {
startingCenter: [40, -80] as LatLngTuple,
minZoom: 2, // 2 typically prevents zooming out too far to see multiple earths
maxZoom: 24, // Maximum possible detail
maxFitBoundsInitialZoom: 18,
maxFitBoundsSelectedFeatureZoom: 18,
maxPointSelectedFeatureZoom: 15, // Single agreed-upon zoom level for points/cluster
maxBounds: [
[-90, -180], // Southwest coordinates
[90, 180], // Northeast coordinates
] as L.LatLngBoundsExpression,
minZoom: 2, // 2 typically prevents zooming out too far to see multiple earths
maxZoom: 24,
} as const;
1 change: 0 additions & 1 deletion react/src/types/react-leaflet-markercluster.d.ts

This file was deleted.

Loading

0 comments on commit ddf4e07

Please sign in to comment.