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

RC #261 - Location markers #266

Merged
merged 2 commits into from
Mar 12, 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
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