Skip to content

Commit

Permalink
INSPIRE pipeline: Allow super users to view pending polygons (#324)
Browse files Browse the repository at this point in the history
  • Loading branch information
rogup authored Jul 4, 2024
1 parent 71e1ba5 commit 074cfd7
Show file tree
Hide file tree
Showing 9 changed files with 241 additions and 32 deletions.
6 changes: 3 additions & 3 deletions docs/intro.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ On this project, we like our data in GeoJSON form. However, sometimes it isn't p
Currently we use ogr2ogr, which comes as a package of tools designed to manipulate geographical data. Installing these tools can be annoying, so I use QGIS to install it for me. This is a graphical GIS manipulation that installs ogr2ogr along with a lot of other stuff.

This is an example of the command you're working towards getting to run. We're running it in the directory with the files in that we want to transform.
`ogr2ogr -f GeoJSON -skipfailures scotland_and_wales.geojson scotland_and_wales_region.shp -t_srs "EPSG:4269"`
`ogr2ogr -f GeoJSON -skipfailures scotland_and_wales.geojson scotland_and_wales_region.shp -t_srs "EPSG:4326"`

`scotland_and_wales.geojson` is the output file. `scotland_and_wales_region.sh`p is the input file and `"EPSG:4269"` is the projection that mapbox uses, also known as web-mercator.
`scotland_and_wales.geojson` is the output file. `scotland_and_wales_region.sh`p is the input file and `EPSG:4326` is one of the most common projections in latitude-longitude format, also used for GPS, based on the WGS84 standard. Note that Mapbox uses a different projection, "Web Mercator" or `EPSG:3857`, so this may lead to slight innaccuracies in the shapes of lines between coordinates when visualised on the map. There isn't really an easy way to solve this, since we are trying to visualise the surface of a 3D ellipsoid on a 2D surface.

## Land Explorer Database

Expand All @@ -78,4 +78,4 @@ Mapbox Studio contains custom tilesets that we use to host most of the common La

## Property Boundaries Database

The property boundaries database is a separate MySQL instance, used to store property boundary information originally from the Land Registry. The database is separate from the main LX database because of its size.
The property boundaries database is a separate MySQL instance, used to store property boundary information originally from the Land Registry. The database is separate from the main LX database because of its size.
2 changes: 1 addition & 1 deletion scripts/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ git pull

# Install dependencies
# TODO: move to yarn v2 and zero-installs https://yarnpkg.com/features/zero-installs
yarn install
yarn install --frozen-lockfile

# Bundle js, this takes 1-2 minutes
yarn build
Expand Down
6 changes: 6 additions & 0 deletions src/actions/LandOwnershipActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ export const togglePropertyDisplay = () => {
};
}

export const togglePendingPropertyDisplay = () => {
return (dispatch) => {
dispatch({ type: "TOGGLE_PENDING_PROPERTY_DISPLAY" });
};
};

export const highlightProperties = (properties) => {
return dispatch => {
dispatch({
Expand Down
33 changes: 25 additions & 8 deletions src/components/left-pane/LeftPaneLandData.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import LeftPaneToggle from "./LeftPaneToggle";
import Draggable from "./Draggable";
import LandDataLayerToggle from "./LandDataLayerToggle";
import { toggleDataGroup } from "../../actions/DataGroupActions";
import { togglePropertyDisplay } from "../../actions/LandOwnershipActions";
import {
togglePropertyDisplay,
togglePendingPropertyDisplay,
} from "../../actions/LandOwnershipActions";
import constants from "../../constants";

const DataLayersContainer = ({ children, title }) => {
const [expanded, setExpanded] = useState(true);
Expand Down Expand Up @@ -45,6 +49,7 @@ const DataLayersContainer = ({ children, title }) => {

const LeftPaneLandData = ({ open, active, onClose }) => {
const dispatch = useDispatch();
const user = useSelector((state) => state.user);

const userGroupTitlesAndIDs = useSelector(
(state) => state.dataGroups.userGroupTitlesAndIDs
Expand All @@ -56,6 +61,9 @@ const LeftPaneLandData = ({ open, active, onClose }) => {
const landOwnershipActive = useSelector(
(state) => state.landOwnership.displayActive
);
const pendingLandOwnershipActive = useSelector(
(state) => state.landOwnership.pendingDisplayActive
);

const description = (
<p className="land-data-description">
Expand Down Expand Up @@ -117,13 +125,22 @@ const LeftPaneLandData = ({ open, active, onClose }) => {
/>
</Draggable>
</DataLayersContainer>
<DataLayersContainer title={"Land Ownership"}>
<LeftPaneToggle
title={"Property Boundaries"}
on={landOwnershipActive}
onToggle={() => dispatch(togglePropertyDisplay())}
/>
</DataLayersContainer>
{constants.LR_POLYGONS_ENABLED && (
<DataLayersContainer title={"Land Ownership"}>
<LeftPaneToggle
title={"Property Boundaries"}
on={landOwnershipActive}
onToggle={() => dispatch(togglePropertyDisplay())}
/>
{user.privileged && (
<LeftPaneToggle
title={"Pending Property Boundaries"}
on={pendingLandOwnershipActive}
onToggle={() => dispatch(togglePendingPropertyDisplay())}
/>
)}
</DataLayersContainer>
)}
<DataLayersContainer title={"Administrative Boundaries"}>
<LandDataLayerToggle title="Wards" layerId="wards-cu4dni" />
<LandDataLayerToggle title="Parishes" layerId="parish" />
Expand Down
7 changes: 5 additions & 2 deletions src/components/map-controls/ControlButtons.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ const ControlButtons = () => {
const [menuKeyOpen, setMenuKeyOpen] = useState(false);
const [zooming, setZooming] = useState(false);
const landDataLayers = useSelector((state) => state.mapLayers.landDataLayers);
const propertiesDisplayed = useSelector((state) => state.landOwnership.displayActive);
const zoom = useSelector((state) => state.map.zoom);
const propertiesDisplayed = useSelector(
(state) =>
state.landOwnership.displayActive ||
state.landOwnership.pendingDisplayActive
);
const dispatch = useDispatch();

const getLocation = () => {
Expand Down
171 changes: 171 additions & 0 deletions src/components/map/MapPendingProperties.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import React, { useState, useEffect } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Layer, Feature } from "react-mapbox-gl";
import axios from "axios";
import constants from "../../constants";
import { getAuthHeader } from "../../utils/Auth";
import LoadingData from "./LoadingData";
import {
highlightProperties,
setActiveProperty,
} from "../../actions/LandOwnershipActions";

/** Polygons that are pending from an INSPIRE pipeline run. This is just visible to super users */
const MapPendingProperties = ({ center, map }) => {
const [properties, setProperties] = useState([]);
const [loadingProperties, setLoadingProperties] = useState(false);

const displayActive = useSelector(
(state) => state.landOwnership.pendingDisplayActive
);
const zoom = useSelector((state) => state.map.zoom);
const highlightedProperties = useSelector(
(state) => state.landOwnership.highlightedProperties
);
const activePropertyId = useSelector(
(state) => state.landOwnership.activePropertyId
);
const activeProperty = highlightedProperties[activePropertyId] || null;

const activePanel = useSelector((state) => state.leftPane.active);

const dispatch = useDispatch();

const getProperties = async () => {
setLoadingProperties(true);

try {
const mapBoundaries = map.getBounds();

const response = await axios.get(
`${constants.ROOT_URL}/api/ownership?sw_lng=` +
mapBoundaries._sw.lng +
"&sw_lat=" +
mapBoundaries._sw.lat +
"&ne_lng=" +
mapBoundaries._ne.lng +
"&ne_lat=" +
mapBoundaries._ne.lat +
"&pending=true",
getAuthHeader()
);

const newProperties = response.data.map((property) => ({
...property,
poly_id: `${property.poly_id}-pending`,
coordinates: property.geom.coordinates[0].map((coordinate) =>
coordinate.reverse()
), //mapbox wants [lng,lat] but db gives [lat,lng]
}));

if (newProperties.length > 0) {
setProperties(newProperties);
}
setLoadingProperties(false);
} catch (error) {
console.error("failed to retrieve property boundaries", error);
}
};

useEffect(() => {
if (displayActive && zoom >= constants.PROPERTY_BOUNDARIES_ZOOM_LEVEL)
getProperties();
}, [center, zoom, displayActive]);

const onClickNewProperty = (property) => {
if (activePanel !== "Drawing Tools") {
dispatch(highlightProperties({ [property.poly_id]: property }));
}
};

const onClickHighlightedProperty = (property) => {
if (activePanel !== "Drawing Tools") {
dispatch(setActiveProperty(property.poly_id));
}
};

const propertyFeaturesWithOwnershipData = [];
const propertyFeaturesWithoutOwnershipData = [];

properties.forEach((property) => {
if (property.title_no)
propertyFeaturesWithOwnershipData.push(
<Feature
coordinates={[property.coordinates]}
key={property.coordinates[0][0]}
onClick={() => onClickNewProperty(property)}
/>
);
else
propertyFeaturesWithoutOwnershipData.push(
<Feature
coordinates={[property.coordinates]}
key={property.coordinates[0][0]}
onClick={() => onClickNewProperty(property)}
/>
);
});

const highlightedPropertyFeatures = Object.values(highlightedProperties).map(
(highlightedProperty) => (
<Feature
coordinates={[highlightedProperty.coordinates]}
key={highlightedProperty.coordinates[0][0]}
onClick={() => onClickHighlightedProperty(highlightedProperty)}
/>
)
);

// Add another polygon for the active property so it appears darker
if (activeProperty) {
highlightedPropertyFeatures.push(
<Feature
coordinates={[activeProperty.coordinates]}
key={activeProperty.coordinates[0][0]}
/>
);
}

return (
<>
{displayActive && zoom >= constants.PROPERTY_BOUNDARIES_ZOOM_LEVEL && (
<>
{loadingProperties && (
<LoadingData message={"fetching property boundaries"} />
)}
<Layer
type={"fill"}
paint={{
"fill-opacity": 0.15,
"fill-color": "green",
"fill-outline-color": "green",
}}
>
{propertyFeaturesWithOwnershipData}
</Layer>
<Layer
type={"fill"}
paint={{
"fill-opacity": 0.15,
"fill-color": "orange",
"fill-outline-color": "green",
}}
>
{propertyFeaturesWithoutOwnershipData}
</Layer>
</>
)}
<Layer
type={"fill"}
paint={{
"fill-opacity": 0.3,
"fill-color": "red",
}}
>
{highlightedPropertyFeatures}
</Layer>
</>
);
};

export default MapPendingProperties;
9 changes: 8 additions & 1 deletion src/components/map/MapboxMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Modals from "../modals/Modals";
import constants from "../../constants";
import mapSources from "../../data/mapSources";
import MapProperties from "./MapProperties";
import MapPendingProperties from "./MapPendingProperties";
import MapDataGroups from "./MapDataGroups";
import {
autoSave,
Expand Down Expand Up @@ -46,6 +47,7 @@ const MapboxMap = () => {
const mapRef = useRef();
const { currentMapId, unsavedMapUuid, lockedByOtherUserInitials } =
useSelector((state) => state.mapMeta);
const user = useSelector((state) => state.user);
const { zoom, lngLat, movingMethod } = useSelector((state) => state.map);
const { currentMarker } = useSelector((state) => state.markers);
const baseLayer = useSelector((state) => state.mapBaseLayer.layer);
Expand All @@ -55,7 +57,9 @@ const MapboxMap = () => {
(state) => state.drawings
);
const propertiesDisplay = useSelector(
(state) => state.landOwnership.displayActive
(state) =>
state.landOwnership.displayActive ||
state.landOwnership.pendingDisplayActive
);

useInterval(
Expand Down Expand Up @@ -308,6 +312,9 @@ const MapboxMap = () => {
<MapRelatedProperties />
</>
)}
{constants.LR_POLYGONS_ENABLED && user.privileged && (
<MapPendingProperties center={lngLat} map={map} />
)}
{/* Markers, including markers from data groups */}
{styleLoaded && (
<Markers
Expand Down
Loading

0 comments on commit 074cfd7

Please sign in to comment.