Skip to content

Commit

Permalink
PIMS-2220 Project Properties Map (#2863)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbarkowsky authored Nov 14, 2024
1 parent 700a86a commit 3ecdadc
Show file tree
Hide file tree
Showing 11 changed files with 193 additions and 47 deletions.
11 changes: 10 additions & 1 deletion react-app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,16 @@ const Router = () => {
const showMap = () => (
<BaseLayout>
<AuthRouteGuard permittedRoles={[Roles.ADMIN, Roles.AUDITOR, Roles.GENERAL_USER]}>
<ParcelMap height="100%" loadProperties={true} popupSize="large" scrollOnClick />
<ParcelMap
height="100%"
loadProperties={true}
popupSize="large"
scrollOnClick
hideControls={false}
showClusterPopup
showSideBar
zoomOnScroll={true}
/>
</AuthRouteGuard>
</BaseLayout>
);
Expand Down
12 changes: 10 additions & 2 deletions react-app/src/components/map/InventoryLayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,14 @@ export const InventoryLayer = (props: InventoryLayerProps) => {
}),
});

// Converts mouse event on clusters to a usable Point
const convertEventToPoint = (e: MouseEvent): Point => {
return {
x: e.clientX,
y: e.clientY,
} as Point;
};

return (
<>
{/* For all cluster objects */}
Expand All @@ -208,7 +216,7 @@ export const InventoryLayer = (props: InventoryLayerProps) => {
eventHandlers={{
click: () => zoomOnCluster(property),
mouseover: (e) => {
openClusterPopup(property, e.containerPoint);
openClusterPopup(property, convertEventToPoint(e.originalEvent));
},
mouseout: cancelOpenPopup,
}}
Expand All @@ -232,7 +240,7 @@ export const InventoryLayer = (props: InventoryLayerProps) => {
},
);
},
mouseover: (e) => openClusterPopup(property, e.containerPoint),
mouseover: (e) => openClusterPopup(property, convertEventToPoint(e.originalEvent)),
mouseout: cancelOpenPopup,
}}
/>
Expand Down
21 changes: 17 additions & 4 deletions react-app/src/components/map/MapLayers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,12 +97,25 @@ interface MapLayersProps {
const MapLayers = (props: MapLayersProps) => {
const { hideControls } = props;
// If layer control is hidden, must still return a default tileset to use
// Also showing parcel boundaries
if (hideControls) {
const parcelBoundaryLayer = LAYER_CONFIGS.landOwnership.find(
(layer) => layer.name === 'Parcel Boundaries',
);
return (
<TileLayer
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors'
/>
<>
<TileLayer
url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href=\"http://osm.org/copyright\">OpenStreetMap</a> contributors'
/>
<WMSTileLayer
url={parcelBoundaryLayer.url}
format="image/png"
transparent={true}
layers={parcelBoundaryLayer.layers}
opacity={0.5}
/>
</>
);
}
return (
Expand Down
53 changes: 37 additions & 16 deletions react-app/src/components/map/ParcelMap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ type ParcelMapProps = {
hideControls?: boolean;
defaultZoom?: number;
defaultLocation?: LatLngExpression;
overrideProperties?: PropertyGeo[];
showClusterPopup?: boolean;
showSideBar?: boolean;
} & PropsWithChildren;

export const SelectedMarkerContext = createContext(null);
Expand Down Expand Up @@ -130,10 +133,13 @@ const ParcelMap = (props: ParcelMapProps) => {
loadProperties = false,
popupSize,
scrollOnClick,
zoomOnScroll = true,
zoomOnScroll = false,
hideControls = false,
defaultLocation,
defaultZoom,
overrideProperties,
showClusterPopup = false,
showSideBar = false,
} = props;

// To access map outside of MapContainer
Expand Down Expand Up @@ -167,6 +173,13 @@ const ParcelMap = (props: ParcelMapProps) => {
}
}, [data, isLoading]);

// If override properties were supplied, set them.
useEffect(() => {
if (overrideProperties) {
setProperties(overrideProperties);
}
}, [overrideProperties]);

// Loops through any array and pairs it down to a flat list of its base elements
// Used here for breaking shape geography down to bounds coordinates
const extractLowestElements: (arr: any[]) => [number, number][] = (arr) => {
Expand Down Expand Up @@ -292,14 +305,16 @@ const ParcelMap = (props: ParcelMapProps) => {
],
),
{
paddingBottomRight: [500, 0], // Padding for map sidebar
paddingBottomRight: showSideBar ? [500, 0] : [0, 0], // Padding for map sidebar
},
);
}
}, [properties]);
// Reference to containing div to help centre cluster popups
const mapBoxRef = useRef<HTMLDivElement>();

return (
<Box height={height} display={'flex'}>
<Box height={height} display={'flex'} ref={mapBoxRef} position={'relative'}>
{loadProperties ? <LoadingCover show={isLoading} /> : <></>}
<MapContainer
id="parcel-map"
Expand Down Expand Up @@ -331,7 +346,7 @@ const ParcelMap = (props: ParcelMapProps) => {
mapEventsDisabled={mapEventsDisabled}
/>
<MapEvents />
{loadProperties ? (
{loadProperties || overrideProperties ? (
<InventoryLayer
isLoading={isLoading}
properties={properties}
Expand All @@ -347,18 +362,24 @@ const ParcelMap = (props: ParcelMapProps) => {
))}
{props.children}
</MapContainer>
{loadProperties ? (
<>
<MapSidebar
properties={properties}
map={localMapRef}
setFilter={setFilter}
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
filter={filter}
/>
<ClusterPopup popupState={popupState} setPopupState={controlledSetPopupState} />
</>
{loadProperties && showSideBar ? (
<MapSidebar
properties={properties}
map={localMapRef}
setFilter={setFilter}
sidebarOpen={sidebarOpen}
setSidebarOpen={setSidebarOpen}
filter={filter}
/>
) : (
<></>
)}
{(loadProperties || overrideProperties) && showClusterPopup ? (
<ClusterPopup
popupState={popupState}
setPopupState={controlledSetPopupState}
boundingBox={mapBoxRef.current?.getBoundingClientRect()}
/>
) : (
<></>
)}
Expand Down
55 changes: 37 additions & 18 deletions react-app/src/components/map/clusterPopup/ClusterPopup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { formatNumber, pidFormatter } from '@/utilities/formatters';
import { ArrowCircleLeft, ArrowCircleRight } from '@mui/icons-material';
import { Box, Grid, IconButton, Typography } from '@mui/material';
import { Point } from 'leaflet';
import React, { useContext, useEffect } from 'react';
import React, { useContext, useEffect, useRef } from 'react';

export interface PopupState {
open: boolean;
Expand All @@ -23,6 +23,7 @@ export interface PopupState {
interface ClusterPopupProps {
popupState: PopupState;
setPopupState: (stateUpdates: Partial<PopupState>) => void;
boundingBox?: DOMRect;
}

/**
Expand All @@ -34,46 +35,60 @@ interface ClusterPopupProps {
* @returns {JSX.Element} A React component representing the ClusterPopup.
*/
const ClusterPopup = (props: ClusterPopupProps) => {
const { popupState, setPopupState } = props;
const { popupState, setPopupState, boundingBox } = props;
const { getLookupValueById } = useContext(LookupContext);

// Backups for when surrounding box isn't initialized
const within = boundingBox ?? {
x: 0,
y: 0,
height: 0,
width: 0,
right: 0,
left: 0,
top: 0,
bottom: 0,
};

/**
* The following block of code determines which direction and position the popup should open with.
* Depending on the screen size, it determines the quadrant of the mouse event and choses a position and offset.
* Depending on the map size, it determines the quadrant of the mouse event and choses a position and offset.
*/
const screenCentre = { x: window.innerWidth / 2 - 100, y: window.innerHeight / 2 }; // -100 to account for the side menu being open
const mapCentre = { x: within.width / 2 - 100, y: within.height / 2 }; // -100 to account for the side menu being open
const mousePositionOnMap = popupState.position;

let offset: { x: number; y: number } = { x: 0, y: 0 };
// Depending on how many properties are available, y displacement changes. 1 = -60, 2 = -180, else -220
// Depending on how many properties are available, y displacement changes. 1 = -170, 2 = -260, else -300
const bottomYOffset =
popupState.properties.length < 3 ? (popupState.properties.length === 2 ? -180 : -60) : -220;
popupState.properties.length < 3 ? (popupState.properties.length === 2 ? -260 : -170) : -300;
// Determine quadrant and set offset
const leftXOffset = 5;
const rightXOffset = -415;
const topYOffset = 80;
const leftXOffset = 0;
const rightXOffset = -400;
const topYOffset = 0;
switch (true) {
// Top-left quadant
case popupState.position.x <= screenCentre.x && popupState.position.y <= screenCentre.y:
case mousePositionOnMap.x <= mapCentre.x && mousePositionOnMap.y <= mapCentre.y:
offset = {
x: leftXOffset,
y: topYOffset,
};
break;
// Top-right quadrant
case popupState.position.x > screenCentre.x && popupState.position.y <= screenCentre.y:
case mousePositionOnMap.x > mapCentre.x && mousePositionOnMap.y <= mapCentre.y:
offset = {
x: rightXOffset,
y: topYOffset,
};
break;
// Bottom-left quadrant
case popupState.position.x <= screenCentre.x && popupState.position.y > screenCentre.y:
case mousePositionOnMap.x <= mapCentre.x && mousePositionOnMap.y > mapCentre.y:
offset = {
x: leftXOffset,
y: bottomYOffset,
};
break;
// Bottom-right quadrant
case popupState.position.x > screenCentre.x && popupState.position.y > screenCentre.y:
case mousePositionOnMap.x > mapCentre.x && mousePositionOnMap.y > mapCentre.y:
offset = {
x: rightXOffset,
y: bottomYOffset,
Expand All @@ -95,16 +110,18 @@ const ClusterPopup = (props: ClusterPopupProps) => {
}
}, [popupState.pageIndex]);

const popupRef = useRef<HTMLDivElement>();
return (
<Box
id={'clusterPopup'}
position={'fixed'}
ref={popupRef}
position={'absolute'}
width={'400px'}
height={'fit-content'}
maxHeight={'300px'}
left={popupState.position.x + offset.x}
top={popupState.position.y + offset.y}
zIndex={900}
left={mousePositionOnMap.x - within.left + offset.x}
top={mousePositionOnMap.y - within.top + offset.y}
zIndex={10000}
display={popupState.open ? 'flex' : 'none'}
flexDirection={'column'}
overflow={'clip'}
Expand Down Expand Up @@ -162,7 +179,9 @@ const ClusterPopup = (props: ClusterPopupProps) => {
? property.properties.Name.match(/^\d*$/) || property.properties.Name == ''
? property.properties.Address1
: property.properties.Name
: pidFormatter(property.properties.PID) ?? String(property.properties.PIN)
: property.properties.PID != null && property.properties.PID != 0
? pidFormatter(property.properties.PID)
: String(property.properties.PIN)
}
content={[
property.properties.Address1,
Expand Down
4 changes: 3 additions & 1 deletion react-app/src/components/map/sidebar/MapSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,9 @@ const MapSidebar = (props: MapSidebarProps) => {
? property.properties.Name.match(/^\d*$/) || property.properties.Name == ''
? property.properties.Address1
: property.properties.Name
: pidFormatter(property.properties.PID) ?? String(property.properties.PIN)
: property.properties.PID != null && property.properties.PID != 0
? pidFormatter(property.properties.PID)
: String(property.properties.PIN)
}
content={[
property.properties.Address1,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { PropertyTypes } from '@/constants/propertyTypes';
import { Agency } from '@/hooks/api/useAgencyApi';
import { formatMoney, pidFormatter } from '@/utilities/formatters';
import { Box, Typography } from '@mui/material';
import { DataGrid, GridColDef } from '@mui/x-data-grid';
import { Box, Typography, useTheme } from '@mui/material';
import { DataGrid, GridCellParams, GridColDef } from '@mui/x-data-grid';
import React from 'react';
import { Link } from 'react-router-dom';

interface IDisposalPropertiesTable {
rows: Record<string, any>[];
}

const DisposalPropertiesTable = (props: IDisposalPropertiesTable) => {
const theme = useTheme();
const columns: GridColDef[] = [
{
field: 'PropertyType',
Expand All @@ -23,6 +25,19 @@ const DisposalPropertiesTable = (props: IDisposalPropertiesTable) => {
row.PropertyTypeId === PropertyTypes.BUILDING && row.Address1
? row.Address1
: pidFormatter(row.PID) ?? row.PIN,
renderCell: (params: GridCellParams) => {
const urlType = params.row.PropertyTypeId === 0 ? 'parcel' : 'building';
return (
<Link
to={`/properties/${urlType}/${params.row.Id}`}
target="_blank"
rel="noopener noreferrer"
style={{ color: theme.palette.primary.main, textDecoration: 'none' }}
>
{String(params.value)}
</Link>
);
},
},
{
field: 'Agency',
Expand Down
Loading

0 comments on commit 3ecdadc

Please sign in to comment.