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

PIMS-2220 Project Properties Map #2863

Merged
merged 6 commits into from
Nov 14, 2024
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
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