Browse Source

add leaflet.markercluster and leaflet-control-geocoder

pull/2/head
ardhi 2 years ago
parent
commit
ee4575d4e8
  1. 2
      package.json
  2. 23
      src/appredux/modules/map/actions.js
  3. 8
      src/appredux/modules/map/reducers.js
  4. 6
      src/components/MapLeftContent/index.js
  5. 138
      src/components/MapRightContent/index.js
  6. 14
      src/services/api/base.js
  7. 71
      src/services/api/modules/map_monitoring/index.js
  8. 14
      src/utils/MapUtils.js
  9. 10
      src/views/MapMonitoring/MapMonitoring.css
  10. 79
      src/views/MapMonitoring/index.js

2
package.json

@ -50,7 +50,9 @@
"jspdf": "^2.5.1",
"jspdf-autotable": "^3.5.25",
"leaflet": "^1.8.0",
"leaflet-control-geocoder": "^2.4.0",
"leaflet-draw": "^1.0.4",
"leaflet.markercluster": "^1.5.3",
"moment": "^2.24.0",
"node-sass": "^4.12.0",
"numeral": "^2.0.6",

23
src/appredux/modules/map/actions.js

@ -13,6 +13,7 @@ export const SET_USER_POINTS = 'SET_USER_POINTS';
export const SET_SELECTED_FEATURE = 'SET_SELECTED_FEATURE';
export const SET_ROUTINGBAR_VISIBLE = 'SET_ROUTINGBAR_VISIBLE';
export const SET_IS_SEARCHING_ROUTE = 'SET_IS_SEARCHING_ROUTE';
export const SET_SELECTED_PROJECT_IDS = 'SET_SELECTED_PROJECT_IDS';
export const setMymap = obj => dispatch => {
dispatch({
@ -84,6 +85,22 @@ export const setIsSearchingRoute = obj => dispatch => {
})
}
export const setSelectedProjectIds = obj => dispatch => {
// filter to remove 'all' key
let projectIds = obj || [];
if (obj && obj.length > 0) {
let allIdx = obj.findIndex(n => n === 'all');
let pos = allIdx + 1;
if (allIdx > -1) {
projectIds = obj.slice(pos);
}
}
dispatch({
type: SET_SELECTED_PROJECT_IDS,
payload: projectIds
})
}
export const getUserPoints = async () => {
@ -104,12 +121,14 @@ export const getUserPoints = async () => {
feature.properties = {
"user_id": n.user_id,
"Name": n.join_first_name ? n.join_first_name : '-',
"Name": n.name ? n.name : '-',
"Clock in time": n.clock_in ? moment(n.clock_in).format('YYYY-MM-DD HH:mm:ss') : '-',
"Clock in location": n.clock_in_loc ? n.clock_in_loc : '-',
"Clock out time": n.clock_out ? moment(n.clock_out).format('YYYY-MM-DD HH:mm:ss') : '-',
"Clock out location": n.clock_out_loc ? n.clock_out_loc : '-',
"image": n.image_selfie ? n.image_selfie : '' // still dummy
"image": n.image_selfie ? n.image_selfie : '',
"Projects": n.projects ? n.projects : null,
"presence_status": n.presence_status ? n.presence_status : null
}
feature.geometry = {

8
src/appredux/modules/map/reducers.js

@ -8,7 +8,8 @@ import {
SET_OPEN_LEFT,
SET_OPEN_RIGHT,
SET_ROUTINGBAR_VISIBLE,
SET_IS_SEARCHING_ROUTE
SET_IS_SEARCHING_ROUTE,
SET_SELECTED_PROJECT_IDS
} from "./actions";
const initialState = {
@ -21,7 +22,8 @@ const initialState = {
userPoints: null,
selectedFeature: null,
routingBarVisible: false,
isSearchingRoute: false
isSearchingRoute: false,
setSelectedProjectIds: []
}
function mapReducer(state = initialState, action) {
@ -46,6 +48,8 @@ function mapReducer(state = initialState, action) {
return { ...state, routingBarVisible: action.payload }
case SET_IS_SEARCHING_ROUTE:
return { ...state, isSearchingRoute: action.payload }
case SET_SELECTED_PROJECT_IDS:
return { ...state, selectedProjectIds: action.payload }
default:
return state;
}

6
src/components/MapLeftContent/index.js

@ -3,7 +3,7 @@ import L from 'leaflet';
import { Button, Col, Drawer, Input, Row, Spin, Tree } from 'antd';
import ApiProject from '../../services/api/modules/project';
import { store } from '../../appredux/store';
import { getUserPoints, setMapLoading, setOpenRight, setProjectTree, setSelectedFeature, setUserPoints } from '../../appredux/modules/map/actions';
import { getUserPoints, setMapLoading, setOpenRight, setProjectTree, setSelectedFeature, setSelectedProjectIds, setUserPoints } from '../../appredux/modules/map/actions';
import ContentLoader from 'react-content-loader';
import { useSelector } from 'react-redux';
import ApiMapMonitoring from '../../services/api/modules/map_monitoring';
@ -26,13 +26,15 @@ const MapLeftContent = () => {
};
const onCheck = (checkedKeys, info) => {
console.log('onCheck', checkedKeys, info);
if (checkedKeys.length < 1) {
console.log('clear all user points');
store.dispatch(setUserPoints(null));
store.dispatch(setOpenRight(false));
store.dispatch(setSelectedFeature(null));
store.dispatch(setSelectedProjectIds([]))
return;
}
store.dispatch(setSelectedProjectIds(checkedKeys))
getUserPoints();
};

138
src/components/MapRightContent/index.js

@ -1,89 +1,129 @@
import React, { useEffect, useMemo, useRef, useState } from 'react'
import { useSelector } from 'react-redux';
import PopupButtonActions from '../PopupButtonActions';
import DEFAULT_USER_ICON from '../../assets/img/avatars/user.png';
import './styles.css'
import { BASE_SIMPRO_LUMEN_IMAGE } from '../../const/ApiConst';
import { Button, Image } from 'antd';
import {Icon} from '@iconify/react';
import closeCircle from '@iconify/icons-mdi/close-circle';
import closeIcon from '@iconify/icons-mdi/close';
import { closePopup } from '../../utils/MapUtils';
const MapRightContent = () => {
const { mapLoading, selectedFeature } = useSelector(state => state.mapReducer);
const PopupContent = useMemo(() => {
console.log('selectedFeature', selectedFeature);
if (selectedFeature && selectedFeature.properties) {
// let content = [];
// for (let key in selectedFeature.properties) {
// content.push(<tr key={key}>
// <td>{`${key}`}</td>
// <td>:</td>
// <td>{`${selectedFeature.properties[key]}`}</td>
// </tr>)
// }
// console.log('content', content);
// const PopupContent = useMemo(() => {
// console.log('selectedFeature', selectedFeature);
// if (selectedFeature && selectedFeature.properties) {
// // let content = [];
// // for (let key in selectedFeature.properties) {
// // content.push(<tr key={key}>
// // <td>{`${key}`}</td>
// // <td>:</td>
// // <td>{`${selectedFeature.properties[key]}`}</td>
// // </tr>)
// // }
// // console.log('content', content);
// return (
// <table className="table popup-table">
// <tbody>
// <tr>
// <td>Name</td>
// <td>:</td>
// <td>{selectedFeature?.properties['Name']}</td>
// </tr>
// <tr>
// <td>Clock in time</td>
// <td>:</td>
// <td>{selectedFeature?.properties['Clock in time']}</td>
// </tr>
// <tr>
// <td>Clock in location</td>
// <td>:</td>
// <td>{selectedFeature?.properties['Clock in location']}</td>
// </tr>
// <tr>
// <td>Clock out time</td>
// <td>:</td>
// <td>{selectedFeature?.properties['Clock out time']}</td>
// </tr>
// <tr>
// <td>Clock out location</td>
// <td>:</td>
// <td>{selectedFeature?.properties['Clock out location']}</td>
// </tr>
// </tbody>
// </table>
// )
// }
// }, [selectedFeature])
const renderListProject = (projects) => {
if (projects && projects.length > 0) {
return (
<table className="table popup-table">
<tbody>
<tr>
<td>Name</td>
<td>:</td>
<td>{selectedFeature?.properties['Name']}</td>
</tr>
<tr>
<td>Clock in time</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock in time']}</td>
</tr>
<tr>
<td>Clock in location</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock in location']}</td>
</tr>
<tr>
<td>Clock out time</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock out time']}</td>
</tr>
<tr>
<td>Clock out location</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock out location']}</td>
</tr>
</tbody>
</table>
<div>
<ul>
{ projects.map(n => <li>{n.project_name}</li>) }
</ul>
</div>
)
}
}, [selectedFeature])
}
const Content = useMemo(() => {
return (
<div style={{ paddingLeft: 10 }}>
<div key={'content_right'} style={{ paddingLeft: 10 }}>
<div style={{ backgroundColor: '#f0f0f0', padding: 10, marginBottom: 5, fontWeight: 'bold', fontSize: 16 }}>
Detail Information
<Button shape="circle" color='' icon={<Icon icon={closeIcon} width={25} height={25} />} size="middle" style={{marginRight: 20, justifyContent: 'center', display: 'inline-block'}}
onClick={closePopup}
/>
<div style={{display: "inline-block", verticalAlign: 'middle'}}>Detail Information</div>
</div>
<div style={{height: '75vh', overflow: 'auto'}}>
<table className="table popup-table">
<tbody>
<tr>
<tr key={'image'}>
<td colSpan={3} style={{justifyContent: 'center'}}>{selectedFeature?.properties['image'] ?
<Image
width={`100%`}
src={`${BASE_SIMPRO_LUMEN_IMAGE}/${selectedFeature?.properties['image']}`}
/>
:
<Image
width={`100%`}
src={DEFAULT_USER_ICON}
/>
}</td>
</tr>
<tr key={'name'}>
<td className='td-popup-label'>Name</td>
<td>:</td>
<td>{selectedFeature?.properties['Name']}</td>
</tr>
<tr>
<tr key={'projects'}>
<td className='td-popup-label'>Projects</td>
<td>:</td>
<td>{renderListProject(selectedFeature?.properties['Projects'])}</td>
</tr>
<tr key={'clock_in_time'}>
<td className='td-popup-label'>Clock in time</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock in time']}</td>
</tr>
<tr>
<tr key={'clock_in_loc'}>
<td className='td-popup-label'>Clock in location</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock in location']}</td>
</tr>
<tr>
<tr key={'clock_out_time'}>
<td className='td-popup-label'>Clock out time</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock out time']}</td>
</tr>
<tr>
<tr key={'clock_out_loc'}>
<td className='td-popup-label'>Clock out location</td>
<td>:</td>
<td>{selectedFeature?.properties['Clock out location']}</td>

14
src/services/api/base.js

@ -35,24 +35,34 @@ export default class RequestApi {
axios.interceptors.response.use(
response => response,
async (error) => {
// console.log('error axios', error);
// console.log('error axios', JSON.stringify(error));
if (error) {
// console.log('stringify', JSON.stringify(error));
const err = JSON.parse(JSON.stringify(error));
console.log('error', err);
if (err.name === 'AxiosError' && err.code === 'ERR_NETWORK') {
alert(err.message);
return;
}
if (err.response) {
if (err.response.status === 307 || err.response.status === 403) {
if (err.response.status === 307 || err.response.status === 403 || err.response.status === 401) {
// console.log(err.response);
alert('Token expired, please re-login');
// clearAllState();
window.localStorage.clear();
window.location.reload();
// this.props.history.replace('/login');
return;
}
}
if (err && err.message && err.message.includes('401')) {
alert('Token expired, please re-login');
// clearAllState();
window.localStorage.clear();
window.location.reload();
// this.props.history.replace('/login');
return;
}
return Promise.reject(error);
}
}

71
src/services/api/modules/map_monitoring/index.js

@ -1,46 +1,55 @@
import moment from "moment";
import { store } from "../../../../appredux/store";
import { BASE_SIMPRO_LUMEN } from "../../../../const/ApiConst";
import RequestApi from '../../base';
export default class ApiMapMonitoring extends RequestApi {
static async search() {
const URL = `${BASE_SIMPRO_LUMEN}/presence/search`
const dateFrom = moment().subtract(7,'d').format('YYYY-MM-DD 00:00:00');
const dateTo = moment().format('YYYY-MM-DD 23:59:00');
const { selectedProjectIds } = store.getState().mapReducer;
// const URL = `${BASE_SIMPRO_LUMEN}/presence/search`
const URL = `${BASE_SIMPRO_LUMEN}/map-monitoring/search`
// const dateFrom = moment().subtract(7,'d').format('YYYY-MM-DD 00:00:00');
// const dateTo = moment().format('YYYY-MM-DD 23:59:00');
// const payload = {
// "columns": [
// {
// "logic_operator": "range",
// "name": "created_at",
// "operator": "AND",
// "value": dateFrom,
// "value1": dateTo
// }
// ],
// "joins": [
// {
// "column_join": "user_id",
// "column_results": [
// "username", "name"
// ],
// "name": "m_users"
// }
// ],
// "orders": {
// "ascending": false,
// "columns": [
// "created_at"
// ]
// },
// "paging": {
// "length": 25,
// "start": 0
// }
// }
const payload = {
"columns": [
{
"logic_operator": "range",
"name": "created_at",
"operator": "AND",
"value": dateFrom,
"value1": dateTo
}
],
"joins": [
{
"column_join": "user_id",
"column_results": [
"username", "name"
],
"name": "m_users"
}
],
"orders": {
"ascending": false,
"columns": [
"created_at"
]
},
"paging": {
"length": 25,
"start": 0
}
"project_id": selectedProjectIds
}
console.log('payload', payload);
return await RequestApi.Request().post(
URL,
payload,
RequestApi.HeaderWithToken()).then(res => {
// console.log('res map-monitoring', JSON.stringify(res));
if (res) {
if (res && res.data && res.data.data) {
// console.log('ApiPresence search', res.data.data)

14
src/utils/MapUtils.js

@ -1,3 +1,4 @@
import { setOpenRight, setSelectedFeature } from "../appredux/modules/map/actions";
import { store } from "../appredux/store";
export const removeLayerByName = (layerName) => {
@ -24,4 +25,17 @@ export const removeLayerByName = (layerName) => {
mymap.removeLayer(layerToRemove[i]);
}
}
}
export const closePopup = () => {
const { mymap, routingBarVisible } = store.getState().mapReducer;
if (!routingBarVisible) {
// only can close popup when routing mode is not visible
removeLayerByName('popupTemp');
store.dispatch(setOpenRight(false));
store.dispatch(setSelectedFeature(null));
// if (mymap) {
// mymap.invalidateSize();
// }
}
}

10
src/views/MapMonitoring/MapMonitoring.css

@ -8,7 +8,7 @@
margin-left: 45%;
}
.image-marker img {
.image-marker-green img {
height: 40px !important;
width: 40px !important;
border-radius: 50%;
@ -16,6 +16,14 @@
border-color: green;
}
.image-marker-red img {
height: 40px !important;
width: 40px !important;
border-radius: 50%;
border: solid;
border-color: red;
}
.image-marker-active img {
height: 40px !important;
width: 40px !important;

79
src/views/MapMonitoring/index.js

@ -3,12 +3,12 @@ import L from 'leaflet';
import { Button, Col, Drawer, Row, Spin, Tree } from 'antd';
import ApiProject from '../../services/api/modules/project';
import { store } from '../../appredux/store';
import { getUserPoints, setMapLoading, setMymap, setOpenLeft, setOpenRight, setProjectTree, setSelectedFeature, setUserPoints } from '../../appredux/modules/map/actions';
import { getUserPoints, setMapLoading, setMymap, setOpenLeft, setOpenRight, setProjectTree, setSelectedFeature, setSelectedProjectIds, setUserPoints } from '../../appredux/modules/map/actions';
import ContentLoader from 'react-content-loader';
import { useSelector } from 'react-redux';
import MapLeftContent from '../../components/MapLeftContent';
import MapRightContent from '../../components/MapRightContent';
import { removeLayerByName } from '../../utils/MapUtils';
import { closePopup, removeLayerByName } from '../../utils/MapUtils';
import RoutingBar from '../../components/RoutingBarV2';
import Loader from "react-loader-spinner";
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
@ -17,7 +17,11 @@ import "react-toastify/dist/ReactToastify.css";
import './MapMonitoring.css';
import { BASE_SIMPRO_LUMEN_IMAGE } from '../../const/ApiConst';
import DEFAULT_USER_ICON from '../../assets/img/avatars/user.png';
import 'leaflet.markercluster/dist/MarkerCluster.Default.css'
import 'leaflet.markercluster/dist/MarkerCluster.css'
import 'leaflet.markercluster/dist/leaflet.markercluster.js'
import 'leaflet-control-geocoder/dist/Control.Geocoder.css'
import 'leaflet-control-geocoder/dist/Control.Geocoder.js'
const MapMonitoring = () => {
@ -33,11 +37,18 @@ const MapMonitoring = () => {
lng: 120.13025155062624
}
const {userPoints, mymap, openLeft, openRight, routingBarVisible, userHistory, isSearchingRoute} = useSelector(state => state.mapReducer);
const {userPoints, mymap, openLeft, openRight, routingBarVisible, userHistory, isSearchingRoute, selectedFeature} = useSelector(state => state.mapReducer);
const [gridMiddle, setGridMiddle] = useState(GRID_MIDDLE);
const [gridLeft, setGridLeft] = useState(0);
const [gridRight, setGridRight] = useState(0);
let markerCluster = L.markerClusterGroup({
name: "userPointLayer",
// disableClusteringAtZoom: 17,
showCoverageOnHover: false,
// spiderfyOnMaxZoom: false
});
useEffect(() => {
initMap();
getMapLeftContent();
@ -58,7 +69,8 @@ const MapMonitoring = () => {
onEachFeature: onEachFeatureUserPoints,
pointToLayer: pointToLayerUserPoints
});
userPointLayer.addTo(mymap);
// userPointLayer.addTo(mymap);
mymap.addLayer(markerCluster);
mymap.fitBounds(userPointLayer.getBounds());
}
}
@ -87,14 +99,10 @@ const MapMonitoring = () => {
store.dispatch(setMymap(mymap));
// setMymap(mymap);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors' }).addTo(mymap);
// add searching location (nominatim) on map
L.Control.geocoder().addTo(mymap);
mymap.on('click', (e) => {
if (!store.getState().mapReducer.routingBarVisible) {
// only can close popup when routing mode is not visible
removeLayerByName('popupTemp');
store.dispatch(setOpenRight(false));
store.dispatch(setSelectedFeature(null));
}
closePopup();
})
}
@ -102,7 +110,7 @@ const MapMonitoring = () => {
const getMapLeftContent = async () => {
store.dispatch(setMapLoading(true));
let project = await ApiProject.list();
console.log('project', project);
// console.log('project', project);
if (project && project.status && project.data && project.data.length > 0) {
let projectData = [
{
@ -118,6 +126,9 @@ const MapMonitoring = () => {
})
store.dispatch(setProjectTree(projectData));
// console.log('projectData', projectData);
let selectedProject = project.data.map(n => n.id);
// console.log('selectedProject', selectedProject);
store.dispatch(setSelectedProjectIds(selectedProject))
getUserPoints();
store.dispatch(setMapLoading(false));
}
@ -134,18 +145,22 @@ const MapMonitoring = () => {
});
}
const pointToLayerUserPoints = (feature, latlng) => {
// console.log('feature', feature);
// create a marker style
// let logoMarkerStyle = L.Icon.extend({
// options: {
// iconSize: [85, 90],
// iconAnchor: [38, 86],
// popupAnchor: [0, -80]
// }
// });
const renderClassMarker = (feature) => {
let output = 'image-marker-red';
if (selectedFeature?.properties?.user_id === feature?.properties?.user_id) {
output = 'image-marker-active';
}
else {
if (feature?.properties.presence_status === true) {
output = 'image-marker-green'
}
}
// let logoMarker = new logoMarkerStyle({iconUrl: `${BASE_SIMPRO_LUMEN_IMAGE}/${feature.properties.image}`});
return output;
}
// styling points geojson
const pointToLayerUserPoints = (feature, latlng) => {
let imgSrc = DEFAULT_USER_ICON;
if (feature && feature.properties && feature.properties.image && feature.properties.image !== '') {
imgSrc = `${BASE_SIMPRO_LUMEN_IMAGE}/${feature.properties.image}`
@ -153,26 +168,20 @@ const MapMonitoring = () => {
let img = `<img src="${imgSrc}" />`
let logoMarker = L.divIcon({
html: img,
className: 'image-marker',
// className: feature?.properties?.presence_status ? 'image-marker-green' : 'image-marker-red',
className: renderClassMarker(feature),
iconSize: [40, 40],
iconAnchor: [20, 50],
popupAnchor: [0, -80]
});
// let logoMarkerActive = L.divIcon({
// html: img,
// className: 'image-marker-active',
// iconSize: [40, 40],
// iconAnchor: [20, 50],
// popupAnchor: [0, -80]
// });
// read the coordinates from your marker
let lat = feature.geometry.coordinates[1];
let lon = feature.geometry.coordinates[0];
// create a new marker using the icon style
let marker = new L.Marker([lat,lon],{icon: logoMarker});
markerCluster.addLayer(marker);
return marker;
}
@ -238,7 +247,7 @@ const MapMonitoring = () => {
return (
<Row>
<Col span={gridLeft}>
<MapLeftContent />
<MapLeftContent key={'map_left_content'} />
</Col>
<Col span={gridMiddle}>
<div id="map-area" style={{ height: '90vh', width: '100%' }} ref={mapRef}></div>
@ -261,7 +270,7 @@ const MapMonitoring = () => {
)}
</Col>
<Col span={gridRight}>
<MapRightContent />
<MapRightContent key={'map_right_content'} />
</Col>
<ToastContainer autoClose={5000} />
</Row>

Loading…
Cancel
Save