Skip to content

Commit

Permalink
Merge pull request #266 from performant-software/feature/rc261_locati…
Browse files Browse the repository at this point in the history
…on_markers

RC #261 - Location markers
  • Loading branch information
dleadbetter authored Mar 12, 2024
2 parents b49d491 + cd8b078 commit 941a65b
Show file tree
Hide file tree
Showing 13 changed files with 837 additions and 86 deletions.
3 changes: 2 additions & 1 deletion packages/core-data/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@radix-ui/react-accordion": "^1.1.2",
"@radix-ui/react-dialog": "^1.0.5",
"@samvera/clover-iiif": "^2.3.2",
"@turf/turf": "^6.5.0",
"i18next": "^23.8.2",
"lucide-react": "^0.321.0",
"react-instantsearch": "^7.5.4",
Expand All @@ -44,4 +45,4 @@
"tailwindcss": "^3.4.1",
"vite": "^5.1.4"
}
}
}
59 changes: 0 additions & 59 deletions packages/core-data/src/components/PlaceMarker.js

This file was deleted.

77 changes: 77 additions & 0 deletions packages/core-data/src/components/PlaceMarkers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
// @flow

import { LocationMarkers } from '@performant-software/geospatial';
import { feature, featureCollection } from '@turf/turf';
import React, {
useCallback,
useEffect,
useMemo,
useState
} from 'react';
import _ from 'underscore';

type Props = {
animate?: boolean,

/**
* The URL of the Core Data place record.
*/
urls: Array<string>
};

/**
* This component renders a map marker for a given Core Data Place record.
*/
const PlaceMarkers = (props: Props) => {
const [places, setPlaces] = useState([]);

/**
* Converts the set of places into a FeatureCollection.
*
* @type {FeatureCollection<Geometry, Properties>}
*/
const data = useMemo(() => featureCollection(places), [places]);

/**
* Fetches the passed URLs and converts the response to JSON.
*
* @type {function(): *}
*/
const onFetch = useCallback(() => (
_.map(props.urls, (url) => fetch(url).then((res) => res.json()))
), [props.urls]);

/**
* Converts the passed list of records to Features and sets them on the state.
*
* @type {function(*): *}
*/
const onLoad = useCallback((records) => (
_.map(records, (record) => setPlaces((prevPlaces) => [
...prevPlaces,
feature(record.geometry, record.properties)
]))
), []);

/**
* Fetch the place record from the passed URL.
*/
useEffect(() => {
Promise
.all(onFetch())
.then(onLoad);
}, [onFetch, onLoad]);

if (_.isEmpty(data?.features)) {
return null;
}

return (
<LocationMarkers
animate={props.animate}
data={data}
/>
);
};

export default PlaceMarkers;
2 changes: 1 addition & 1 deletion packages/core-data/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import './index.css';
export { default as LoadAnimation } from './components/LoadAnimation';
export { default as MediaGallery } from './components/MediaGallery';
export { default as PlaceDetailsPanel } from './components/PlaceDetailsPanel';
export { default as PlaceMarker } from './components/PlaceMarker';
export { default as PlaceMarkers } from './components/PlaceMarkers';
export { default as PlaceResultsList } from './components/PlaceResultsList';
export { default as RelatedItemsList } from './components/RelatedItemsList';
export { default as RelatedList } from './components/RelatedList';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ import { DEFAULT_FILL_STYLE, DEFAULT_POINT_STYLE, DEFAULT_STROKE_STYLE } from '.
import MapUtils from '../utils/Map';

type Props = {
/**
* If `true`, the point marker will display with a pulsing animation.
*/
animate?: boolean,

/**
* The number of miles to buffer the GeoJSON data.
*/
Expand Down Expand Up @@ -37,7 +42,7 @@ const DEFAULT_BUFFER = 2;
/**
* This component renders a location marker to be used in a Peripleo context.
*/
const LocationMarker = (props: Props) => {
const LocationMarkers = (props: Props) => {
const map = useMap();

/**
Expand All @@ -52,12 +57,12 @@ const LocationMarker = (props: Props) => {

return (
<>
<PulsingMarkerLayer
id='current'
data={props.data}
/>
{ props.animate && (
<PulsingMarkerLayer
data={props.data}
/>
)}
<MixedGeoJSONLayer
id='current'
data={props.data}
fillStyle={props.fillStyle}
strokeStyle={props.strokeStyle}
Expand All @@ -67,11 +72,11 @@ const LocationMarker = (props: Props) => {
);
};

LocationMarker.defaultProps = {
LocationMarkers.defaultProps = {
buffer: DEFAULT_BUFFER,
fillStyle: DEFAULT_FILL_STYLE,
pointStyle: DEFAULT_POINT_STYLE,
strokeStyle: DEFAULT_STROKE_STYLE
};

export default LocationMarker;
export default LocationMarkers;
2 changes: 1 addition & 1 deletion packages/geospatial/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
export { default as DrawControl } from './components/DrawControl';
export { default as GeoJsonLayer } from './components/GeoJsonLayer';
export { default as LayerMenu } from './components/LayerMenu';
export { default as LocationMarker } from './components/LocationMarker';
export { default as LocationMarkers } from './components/LocationMarkers';
export { default as MapControl } from './components/MapControl';
export { default as MapDraw } from './components/MapDraw';
export { default as RasterLayer } from './components/RasterLayer';
Expand Down
14 changes: 8 additions & 6 deletions packages/storybook/.storybook/middleware.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
// @flow

const bodyParser = require('body-parser');
const ControlledVocabulary = require('./routes/ControlledVocabulary');
const dotenv = require('dotenv');
const UserDefinedFields = require('./routes/UserDefinedFields');
const ZoteroTranslate = require('./routes/ZoteroTranslate');
import bodyParser from 'body-parser';
import dotenv from 'dotenv';
import ControlledVocabulary from './routes/ControlledVocabulary';
import CoreData from './routes/CoreData';
import UserDefinedFields from './routes/UserDefinedFields';
import ZoteroTranslate from './routes/ZoteroTranslate';

// Configure environment variables
dotenv.config();
Expand All @@ -14,8 +15,9 @@ const expressMiddleWare = (router) => {
router.use(bodyParser.json());

ControlledVocabulary.addRoutes(router);
CoreData.addRoutes(router);
UserDefinedFields.addRoutes(router);
ZoteroTranslate.addRoutes(router);
};

module.exports = expressMiddleWare;
export default expressMiddleWare;
132 changes: 132 additions & 0 deletions packages/storybook/.storybook/routes/CoreData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// @flow

import { faker } from '@faker-js/faker';
import _ from 'underscore';
import StateBoundaries from '../../src/data/StateBoundaries.json';

/**
* Returns the min/max latitude/longitude for the passed state. If no state is provided, we'll compute
* the boundary of all states.
*
* @param state
*
* @returns {{max_lat, min_lng, min_lat, max_lng}|*}
*/
const getBoundary = (state = null) => {
if (state) {
return StateBoundaries[state];
}

let minLatitude;
let maxLatitude;
let minLongitude;
let maxLongitude;

const keys = _.keys(StateBoundaries);
_.each(keys, (key) => {
const data = StateBoundaries[key];

if (!minLatitude || data.min_lat < minLatitude) {
minLatitude = data.min_lat;
}

if (!maxLatitude || data.max_lat > maxLatitude) {
maxLatitude = data.max_lat;
}

if (!minLongitude || data.min_lng < minLongitude) {
minLongitude = data.min_lng;
}

if (!maxLongitude || data.max_lng > maxLongitude) {
maxLongitude = data.max_lng;
}
});

return {
min_lat: minLatitude,
max_lat: maxLatitude,
min_lng: minLongitude,
max_lng: maxLongitude
};
};

/**
* Creates a sample place record in the Linked Places format.
*
* @returns {{
* names: [{
* toponym: string
* }],
* geometry: {
* coordinates: number[],
* type: string
* },
* '@id': string,
* type: string,
* properties: {
* ccode: *[],
* record_id: string,
* title: string,
* uuid: string
* }
* }}
*/
const createPlace = () => {
const uuid = faker.string.uuid();
const title = faker.location.city();

const boundary = getBoundary();
const latitude = faker.location.latitude({ min: boundary.min_lat, max: boundary.max_lat });
const longitude = faker.location.longitude({ min: boundary.min_lng, max: boundary.max_lng });

return {
'@id': `https://example.com/${uuid}`,
type: 'Place',
properties: {
ccode: [],
record_id: faker.string.numeric(7),
title,
uuid
},
geometry: {
type: 'Point',
coordinates: [longitude, latitude]
},
names: [{
toponym: title
}]
};
};

/**
* Adds the `/core_data/places` and `/core_data/places/:id` routes.
*
* @param router
*/
const addRoutes = (router) => {
router.get('/core_data/places', (request, response) => {
const { query } = request;

let number = 1;
if (query.number) {
number = query.number;
}

const places = [];

_.times(number, () => places.push(createPlace()));

response.send(places);
response.end();
});

router.get('/core_data/places/:id', (request, response) => {
response.send(createPlace());
response.end();
});
};

export default {
addRoutes
};
Loading

0 comments on commit 941a65b

Please sign in to comment.