Skip to content

Commit

Permalink
feat: support for bing maps and donut clusters (#404)
Browse files Browse the repository at this point in the history
* Event popup handling

* Add offset to event popups

* Event popup offset fix

* Code fix

* New server cluster format

* Event popup handling

* Popup handling

* New bounds format

* New bounds format

* New bounds format

* Event handling

* fix: popup style

* Bing maps testing

* Basemap testing

* Bing Maps base layers

* Use GIS API

* Cypress test fix

* Remove unused offset parameter for popups
  • Loading branch information
turban committed Jan 21, 2020
1 parent dbeba36 commit 9ab5ad6
Show file tree
Hide file tree
Showing 19 changed files with 133 additions and 43 deletions.
4 changes: 1 addition & 3 deletions cypress/integration/layers/thematiclayer.spec.js
Expand Up @@ -96,9 +96,7 @@ context('Thematic Layers', () => {
cy.get('[data-test="orgunitlevelselect"]').should('be.visible');

cy.get('[data-test="layeredit-addbtn"]').click();
cy.get('[data-test="thematicdialog"]')
.should('have.length', 0)
.should('not.be.visible');
cy.get('[data-test="thematicdialog"]').should('not.be.visible');

/* Disabled due to failing test (seems to work when I do the same tests manually)
cy.getReduxState(state => state.map.mapViews).should('have.length', 1);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -53,7 +53,7 @@
"@dhis2/d2-ui-interpretations": "6.2.1",
"@dhis2/d2-ui-org-unit-dialog": "5.2.10",
"@dhis2/d2-ui-org-unit-tree": "5.2.10",
"@dhis2/gis-api": "^34.0.7",
"@dhis2/gis-api": "^34.0.9",
"@dhis2/ui-core": "^4.1.1",
"@dhis2/ui-widgets": "^2.0.5",
"@material-ui/core": "^3.9.3",
Expand Down
Binary file added public/images/bingaerial.jpeg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/bingdark.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/binghybrid.jpeg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/images/bingroad.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/osm.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified public/images/osmlight.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/components/layers/basemaps/BasemapList.js
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { withStyles } from '@material-ui/core/styles';
import Basemap from './Basemap';
import { layerTypes } from '../../map/MapApi';

const styles = {
container: {
Expand All @@ -14,7 +15,7 @@ const styles = {
const BasemapList = ({ classes, selectedID, basemaps, selectBasemap }) => (
<div className={classes.container} data-test="basemaplist">
{basemaps
// .filter(basemap => map.hasLayerSupport(basemap.config.type))
.filter(basemap => layerTypes.includes(basemap.config.type))
.map((basemap, index) => (
<Basemap
key={`basemap-${index}`}
Expand Down
34 changes: 27 additions & 7 deletions src/components/map/EventLayer.js
@@ -1,12 +1,15 @@
import React from 'react';
import { getInstance as getD2 } from 'd2';
import { getAnalyticsRequest } from '../../loaders/eventLoader';
import { EVENT_COLOR, EVENT_RADIUS } from '../../constants/layers';
import Layer from './Layer';
import EventPopup from './EventPopup';
import { getDisplayPropertyUrl } from '../../util/helpers';
import { formatCount } from '../../util/numbers';
import { EVENT_COLOR, EVENT_RADIUS } from '../../constants/layers';

class EventLayer extends Layer {
clusterCount = 0;

state = {
popup: null,
dataElements: null,
Expand All @@ -28,6 +31,8 @@ class EventLayer extends Layer {
programStage,
serverCluster,
areaRadius,
styleDataItem,
legend,
} = this.props;

// Some older favorites don't have a valid color code
Expand Down Expand Up @@ -79,8 +84,14 @@ class EventLayer extends Layer {
callback(params.tileId, this.toGeoJson(clusterData));
};
} else {
config.type = 'clientCluster';
config.clusterPane = id;

if (styleDataItem && legend) {
config.type = 'donutCluster';
config.groups = legend.items;
} else {
config.type = 'clientCluster';
}
}
} else if (areaRadius) {
config.buffer = areaRadius;
Expand All @@ -98,6 +109,7 @@ class EventLayer extends Layer {

// Create and add event layer based on config object
this.layer = map.createLayer(config);

map.addLayer(this.layer);

// Fit map to layer bounds once (when first created)
Expand All @@ -123,7 +135,9 @@ class EventLayer extends Layer {
this.setState({ popup: { feature, coordinates } });
}

onPopupClose = () => this.setState({ popup: null });
onPopupClose = () => {
this.setState({ popup: null });
};

// Convert server cluster response to GeoJSON
toGeoJson(data) {
Expand All @@ -136,17 +150,23 @@ class EventLayer extends Layer {
if (Array.isArray(data.rows)) {
data.rows.forEach(row => {
const extent = row[header.extent].match(/([-\d.]+)/g);
const count = parseInt(row[header.count], 10);
const clusterId = ++this.clusterCount;

features.push({
type: 'Feature',
id: row[header.points],
id: clusterId,
geometry: JSON.parse(row[header.center]),
properties: {
count: parseInt(row[header.count], 10),
cluster: count > 1,
cluster_id: clusterId,
point_count: count,
point_count_abbreviated: formatCount(count),
bounds: [
[extent[1], extent[0]],
[extent[3], extent[2]],
[extent[0], extent[1]],
[extent[2], extent[3]],
],
id: row[header.points],
},
});
});
Expand Down
33 changes: 19 additions & 14 deletions src/components/map/EventPopup.js
Expand Up @@ -4,13 +4,21 @@ import i18n from '@dhis2/d2-i18n';
import Popup from './Popup';
import { apiFetch } from '../../util/api';
import { formatTime, formatCoordinate } from '../../util/helpers';
import { EVENT_ID_FIELD } from '../../util/geojson';

// Returns true if value is not undefined or null;
const hasValue = value => value !== undefined || value !== null;

// Loads event data for the selected feature
const loadEventData = async feature =>
feature ? apiFetch(`/events/${feature.id}`) : null;
const loadEventData = async feature => {
if (!feature) {
return null;
}

const id = feature.properties.id || feature.properties[EVENT_ID_FIELD];

return apiFetch(`/events/${id}`);
};

// Returns table rows for all display elements
const getDataRows = (displayElements, dataValues, styleDataItem, value) => {
Expand Down Expand Up @@ -86,13 +94,9 @@ const EventPopup = props => {
};
}, [feature, setEventData]);

if (!eventData) {
return null;
}

const { type, coordinates: coord } = feature.geometry;
const { value } = feature.properties;
const { eventDate, dataValues = [], orgUnitName } = eventData;
const { dataValues = [], eventDate, orgUnitName } = eventData || {};

return (
<Popup
Expand All @@ -102,20 +106,21 @@ const EventPopup = props => {
>
<table>
<tbody>
{getDataRows(
displayElements,
dataValues,
styleDataItem,
value
)}
{eventData &&
getDataRows(
displayElements,
dataValues,
styleDataItem,
value
)}
{type === 'Point' && (
<tr>
<th>
{eventCoordinateFieldName ||
i18n.t('Event location')}
</th>
<td>
{coord[0]} {coord[1]}
{coord[0].toFixed(6)} {coord[1].toFixed(6)}
</td>
</tr>
)}
Expand Down
6 changes: 4 additions & 2 deletions src/components/map/MapApi.js
@@ -1,5 +1,5 @@
import MapApi from '@dhis2/gis-api';
// import MapApi from '@dhis2/maps-gl';
import MapApi, { layerTypes } from '@dhis2/gis-api';
// import MapApi, { layerTypes } from '@dhis2/maps-gl';

// Returns a new map instance
const map = options => {
Expand All @@ -9,4 +9,6 @@ const map = options => {
return new MapApi(div, options);
};

export { layerTypes };

export default map;
1 change: 1 addition & 0 deletions src/components/map/Popup.css
@@ -1,3 +1,4 @@
.dhis2-map-popup-event {
overflow-x: auto;
width: 300px;
}
8 changes: 5 additions & 3 deletions src/components/map/Popup.js
Expand Up @@ -12,11 +12,13 @@ const Popup = (props, context) => {
useEffect(() => {
container.className = className;
map.openPopup(container, coordinates, onClose);
return () => {
map.closePopup();
};
}, [map, container, className, coordinates, onClose]);

// Close popup if component is unmounted
useEffect(() => {
return () => map.closePopup();
}, []);

return createPortal(children, container);
};

Expand Down
44 changes: 44 additions & 0 deletions src/constants/basemaps.js
Expand Up @@ -40,4 +40,48 @@ export const defaultBasemaps = [
style: 'HYBRID',
},
},
{
id: 'bingLight',
name: 'Bing Road',
img: 'images/bingroad.png',
config: {
type: 'bingLayer',
style: 'CanvasLight',
apiKey:
'AotYGLQC0RDcofHC5pWLaW7k854n-6T9mTunsev9LEFwVqGaVnG8b4KERNY9PeKA', // TODO: Read from db
},
},
{
id: 'bingDark',
name: 'Bing Dark',
img: 'images/bingdark.png',
config: {
type: 'bingLayer',
style: 'CanvasDark',
apiKey:
'AotYGLQC0RDcofHC5pWLaW7k854n-6T9mTunsev9LEFwVqGaVnG8b4KERNY9PeKA', // TODO: Read from db
},
},
{
id: 'bingAerial',
name: 'Bing Aerial',
img: 'images/bingaerial.jpeg',
config: {
type: 'bingLayer',
style: 'Aerial',
apiKey:
'AotYGLQC0RDcofHC5pWLaW7k854n-6T9mTunsev9LEFwVqGaVnG8b4KERNY9PeKA', // TODO: Read from db
},
},
{
id: 'bingHybrid',
name: 'Bing Aerial Labels',
img: 'images/binghybrid.jpeg',
config: {
type: 'bingLayer',
style: 'AerialWithLabelsOnDemand',
apiKey:
'AotYGLQC0RDcofHC5pWLaW7k854n-6T9mTunsev9LEFwVqGaVnG8b4KERNY9PeKA', // TODO: Read from db
},
},
];
2 changes: 1 addition & 1 deletion src/util/__tests__/geojson.spec.js
Expand Up @@ -345,7 +345,7 @@ describe('geojson utils', () => {
});
it('Should correctly parse a simple bounding box', () => {
const bbox = getBounds('[0][1][2][3]');
expect(bbox).toEqual([['1', '0'], ['3', '2']]);
expect(bbox).toEqual([['0', '1'], ['2', '3']]);
});
});
});
6 changes: 4 additions & 2 deletions src/util/geojson.js
Expand Up @@ -7,6 +7,8 @@ export const META_DATA_FORMAT_ID = 'ID';
export const META_DATA_FORMAT_NAME = 'Name';
export const META_DATA_FORMAT_CODE = 'Code';

export const EVENT_ID_FIELD = 'psi';

const standardizeFilename = rawName => rawName.replace(/\s+/g, '_');
export const createGeoJsonBlob = data => {
const geojson = {
Expand Down Expand Up @@ -90,7 +92,7 @@ export const createEventFeatures = (response, config = {}) => {
...config.columnNames, // TODO: Check if columnNames is still needed
};

const idColName = config.idCol || 'psi';
const idColName = config.idCol || EVENT_ID_FIELD;
const idCol = findIndex(response.headers, h => h.name === idColName);
const getGeometry = buildEventGeometryGetter(
response.headers,
Expand Down Expand Up @@ -131,7 +133,7 @@ export const getBounds = bbox => {
return null;
}
const extent = bbox.match(/([-\d.]+)/g);
return [[extent[1], extent[0]], [extent[3], extent[2]]];
return [[extent[0], extent[1]], [extent[2], extent[3]]];
};

// export const downloadStyle = name => {
Expand Down
15 changes: 15 additions & 0 deletions src/util/numbers.js
@@ -0,0 +1,15 @@
export const formatCount = count => {
let num;

if (count >= 1000 && count < 9500) {
num = (count / 1000).toFixed(1) + 'k'; // 3.3k
} else if (count >= 9500 && count < 999500) {
num = Math.round(count / 1000) + 'k'; // 33k
} else if (count >= 999500 && count < 1950000) {
num = (count / 1000000).toFixed(1) + 'M'; // 3.3M
} else if (count > 1950000) {
num = Math.round(count / 1000000) + 'M'; // 33M
}

return num || count;
};
18 changes: 9 additions & 9 deletions yarn.lock
Expand Up @@ -429,18 +429,18 @@
recompose "^0.26.0"
rxjs "^5.5.7"

"@dhis2/gis-api@^34.0.7":
version "34.0.7"
resolved "https://registry.yarnpkg.com/@dhis2/gis-api/-/gis-api-34.0.7.tgz#bf4eb804a953e741e8b0b97bd4b2fce0646f9661"
integrity sha512-kVnb6ny3Sc1i7/rVcHctFNBmYlS8hjfJHmnA1LHDk7Z9gjPg8ClhyuGEQL4MPM2JB62tqsiHwsl4OtNn1OJJQQ==
"@dhis2/gis-api@^34.0.9":
version "34.0.9"
resolved "https://registry.yarnpkg.com/@dhis2/gis-api/-/gis-api-34.0.9.tgz#3152cf4161d45b03cdb24758267edb341c5fe4f1"
integrity sha512-KWaNgZmLXgW7sVX0MdngsUszAR6FbswSRTdrdYmfuWofgzvfbfg7uv7tvJCBF4Rbu87hAss/WrpdITuHVksUJA==
dependencies:
"@google/earthengine" "^0.1.172"
"@mapbox/geojson-area" "^0.2.2"
"@mapbox/polylabel" "^1.0.2"
"@turf/buffer" "^5.1.5"
d3-color "^1.2.3"
d3-scale "^2.2.2"
leaflet "^1.5.1"
leaflet "^1.6.0"
leaflet-control-geocoder "^1.6.0"
leaflet-measure "3.1.0"
leaflet.gridlayer.googlemutant "^0.8.0"
Expand Down Expand Up @@ -8447,10 +8447,10 @@ leaflet.markercluster@^1.4.1:
resolved "https://registry.yarnpkg.com/leaflet.markercluster/-/leaflet.markercluster-1.4.1.tgz#b53f2c4f2ca7306ddab1dbb6f1861d5e8aa6c5e5"
integrity sha512-ZSEpE/EFApR0bJ1w/dUGwTSUvWlpalKqIzkaYdYB7jaftQA/Y2Jav+eT4CMtEYFj+ZK4mswP13Q2acnPBnhGOw==

leaflet@^1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.5.1.tgz#9afb9d963d66c870066b1342e7a06f92840f46bf"
integrity sha512-ekM9KAeG99tYisNBg0IzEywAlp0hYI5XRipsqRXyRTeuU8jcuntilpp+eFf5gaE0xubc9RuSNIVtByEKwqFV0w==
leaflet@^1.6.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/leaflet/-/leaflet-1.6.0.tgz#aecbb044b949ec29469eeb31c77a88e2f448f308"
integrity sha512-CPkhyqWUKZKFJ6K8umN5/D2wrJ2+/8UIpXppY7QDnUZW5bZL5+SEI2J7GBpwh4LIupOKqbNSQXgqmrEJopHVNQ==

left-pad@^1.3.0:
version "1.3.0"
Expand Down

0 comments on commit 9ab5ad6

Please sign in to comment.