letsee-geofencing
Version:
letsee geofencing npm package
619 lines (584 loc) • 21.9 kB
JavaScript
import React, { useEffect, useRef, useState } from 'react'
import { MapContainer, TileLayer, useMap, Marker, useMapEvents, Popup, LayersControl, FeatureGroup,Rectangle, Circle, LayerGroup, Polyline, Polygon } from 'react-leaflet'
import axios from "axios"
import { EditControl } from 'react-leaflet-draw'
import circleToPolygon from 'circle-to-polygon'
import './style.css'
import './leaflet.contextmenu/leaflet.contextmenu.js'
import './leaflet.contextmenu/leaflet.contextmenu.css'
import delete_icon from './assets/delete_icon.jpeg'
import change_icon from './assets/change_icon.png'
// import 'leaflet-contextmenu'
export const NamePopup = ({nameOrigin, setNameOrigin ,changeLayerName, updateFenceName, tmpEvent, setShowNamepopup}) => {
const newName = useRef()
const wrapperRef = useRef()
const submit = async() => {
changeLayerName(newName.current.value)
await updateFenceName(tmpEvent.target, newName.current.value)
// 2. namePopup 안보이기
setShowNamepopup(false)
}
const handleChange = (e) => {
setNameOrigin(e.target.value)
}
const handleClickBackground = (e) => {
if (e.target !== wrapperRef.current) return
setShowNamepopup(false)
}
return (
<div className='name-popup-wrapper' onClick={handleClickBackground} ref={wrapperRef}>
<div className='name-popup'>
<p>설정하실 펜스의 이름을 입력하세요.</p>
<input ref={newName} placeholder='name' value={nameOrigin} onChange={handleChange} />
<button onClick={submit}>변경</button>
</div>
</div>
);
}
export const GeofencingMap = ({geofencingWorker, initCenter, initZoom}) => {
const [message, setMessage] = useState('')
const [tmpEvent, setTempEvent] = useState(null)
const [showNamepopup, setShowNamepopup] = useState(false)
const [nameOrigin, setNameOrigin] = useState('')
const drawnItems = useRef();
const map = useRef();
useEffect( () => { // 시작할때
// geofencingWorker.testLog()
getFences()
setTimeout(() => {
map.current.off('contextmenu')
}, 0);
// const initFunction = async() => {
// await getFences()
// await map.current.off('contextmenu')
// }
// initFunction()
return () => {
}
}, [])
const changeLayerName = (newName) => {
tmpEvent.target.geoName = newName
tmpEvent.target.setTooltipContent(newName)
}
const _onCreated = async(event) => {
addPolygon(event)
}
const _onEdited = async(event) => {
console.log(event)
const _layers = event.layers._layers
for(let key in _layers){
generateBoundaryData(_layers[key])
}
for(let key in _layers){
updateFenceGeog(_layers[key])
}
}
const _onDeleted = (event) => {
const deletedLayers = event.layers._layers;
removeFences(deletedLayers)
}
const removeFences = async(_layers) => {
for(let key in _layers){
const res = await geofencingWorker.removeFence(_layers[key].geoId)
}
}
const generate_uuidv4 = () => {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
const addPolygon = (_event) => {
const tmpId = generate_uuidv4()
const tmpName = '마우스 오른쪽 클릭하여 이름을 설정하세요.'
let tmpLayer = _event.layer
const layerType = _event.layerType
if(layerType==='circle'){
// console.log('converting Circle to Polygon...')
const circleCenter = [tmpLayer._latlng.lng, tmpLayer._latlng.lat ]
const createdPolygon = circleToPolygon(circleCenter, tmpLayer._mRadius, 32);
tmpLayer.remove()
// console.log(createdPolygon)
let lngLatArray = []
createdPolygon.coordinates[0].forEach(el => {
lngLatArray.push([el[1],el[0]])
});
let tmpPolygon = new L.Polygon([lngLatArray], { opacity: 0.5 });
tmpLayer = tmpPolygon
}
tmpLayer.geoId = tmpId
tmpLayer.geoName = tmpName
AddToolTip(tmpLayer, tmpLayer.geoName)
generateBoundaryData(tmpLayer)
addLayerClickEvent(tmpLayer)
drawnItems.current.addLayer(tmpLayer)
createFence(tmpLayer)
}
const createFence = async (_layer) => {
if(_layer.hasOwnProperty('_rings')) // polygon
{
const res = await geofencingWorker.createFence(_layer,_layer.geoId,_layer.geoName)
}
}
const updateFenceGeog = async (_layer) => {
if(_layer.hasOwnProperty('_rings')) // polygon
{
const res = await geofencingWorker.updateFenceGeog(_layer,_layer.geoId)
console.log(res)
}
}
const updateFenceName = async (_layer, newName) => {
// console.log('[ufn] layer :', _layer)
// console.log('[ufn] newName :', newName)
// console.log('[ufn] _layer.geoId :', _layer.geoId)
const res = await geofencingWorker.updateFenceName(_layer.geoId, newName)
}
const addLayerClickEvent = (_Layer) => {
// _Layer.on('click', (event) => { // 펜스 클릭 했을 때?
// _Layer.on('contextmenu', (event) => { // 펜스 Right 클릭 했을 때?
// setTempEvent(event)
// const prevName = event.target.geoName
// setNameOrigin(prevName)
// // 1. namePopup 보이기
// setShowNamepopup(true)
// })
_Layer.on('contextmenu', (event) => { // 펜스 Right 클릭 했을 때?
let cm = map.current.contextmenu
cm.event = event
cm.showAt(event.latlng) // 우클릭 메뉴 보이기
})
}
const getFences = async () => {
const res = await geofencingWorker.getFences()
if(!res.rows){console.log("Failed to load fences"); return;}
res.rows.forEach(el => {
const rowJson = JSON.parse(el.st_asgeojson)
// console.log(el.id, el.name, rowJson.coordinates[0])
DrawPolygon(el.id, el.name, rowJson.coordinates[0])
});
// map.current.off('contextmenu')
}
const DrawPolygon = (id, name, coords) => {
let tempCoords = []
for(let i=0; i<coords.length ; i++){
tempCoords.push( [coords[i][1],coords[i][0]] )
}
let tmpPolygon = new L.Polygon(tempCoords, { opacity: 0.5} );
addLayerClickEvent(tmpPolygon)
generateBoundaryData(tmpPolygon)
tmpPolygon.geoId = id
tmpPolygon.geoName = name
AddToolTip(tmpPolygon, tmpPolygon.geoName)
drawnItems.current.addLayer(tmpPolygon);
}
const generateBoundaryData = (_layer) => {
let tmpArr = []
_layer._latlngs[0].forEach(el => {
const tmpRow = [el.lng, el.lat]
tmpArr.push(tmpRow)
});
tmpArr.push([_layer._latlngs[0][0].lng, _layer._latlngs[0][0].lat])
_layer.boundaryData = {
type : 'Polygon',
coordinates : [tmpArr]
}
}
const AddToolTip = (layer, name) => {
layer.bindTooltip(name , {permanent: true, direction:"center"}).openTooltip()
}
const openNamePopup = (e) => {
const cm = map.current.contextmenu
const event = cm.event
setTempEvent(event)
const prevName = event.target.geoName
setNameOrigin(prevName)
// 1. namePopup 보이기
setShowNamepopup(true)
}
const func1 = () => {
console.log("func1")
}
const func2 = () => {
console.log("func2")
}
const func3 = () => {
console.log("func3")
}
const removeClickedFence = async() => {
console.log("4")
const cm = map.current.contextmenu
const event = cm.event
const deletedLayer = event.target;
drawnItems.current.removeLayer(deletedLayer)
const res = await geofencingWorker.removeFence(deletedLayer.geoId)
}
const test = async() => {
// console.log(map.current)
// map.current.off('contextmenu')
// map.current.contextmenu.removeAllItems()
}
return (
<>
<div className='geofencingMap-wrapper'>
{/* <button onClick={test}>PackageTest</button> */}
{
showNamepopup &&
<NamePopup setShowNamepopup={setShowNamepopup} nameOrigin={nameOrigin} setNameOrigin={setNameOrigin} changeLayerName={changeLayerName} updateFenceName={updateFenceName} tmpEvent={tmpEvent}></NamePopup>
}
<MapContainer
contextmenu="true"
contextmenuWidth= "140"
contextmenuItems= {[{
text: '이름 변경',
icon: change_icon,
callback: openNamePopup
},
'-',
{
text: '기능1',
callback: func1
}, {
text: '기능2',
callback: func2
}, {
text: '기능3',
callback: func3
},
'-',
{
text: '삭제',
icon: delete_icon,
callback: removeClickedFence
}]}
ref={map} center={ initCenter? initCenter:[37.506012, 127.058175]} zoom={initZoom?initZoom:13} scrollWheelZoom={true}
>
<TileLayer
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<LayersControl position="topright">
<LayersControl.Overlay checked="true" name="지오펜스 레이어">
<FeatureGroup ref={drawnItems}>
<EditControl
position='topleft'
onCreated={_onCreated}
onDeleted={_onDeleted}
onEdited={_onEdited}
draw={{
rectangle: false,
polyline: false,
circlemarker: false,
marker: false,
polygon:{
shapeOptions: {
stroke: true,
color: '#3388ff',
weight: 4,
opacity: 0.5,
fill: true,
clickable: true
}
},
circle:{
shapeOptions: {
stroke: true,
color: '#3388ff',
weight: 4,
opacity: 0.5,
fill: true,
clickable: true
}
}
}}
/>
</FeatureGroup>
</LayersControl.Overlay>
</LayersControl>
</MapContainer>
</div>
</>
)
}
export class GeofencingWorker{
constructor(SERVER_URL,MASTER_ID, PROJECT_ID ){
this.SERVER_URL = SERVER_URL
this.MASTER_ID = MASTER_ID
this.PROJECT_ID = PROJECT_ID
// console.log(this.SERVER_URL, this.MASTER_ID, this.PROJECT_ID)
this.createProject()
}
// setServerUrl (SERVER_URL) {
// this.SERVER_URL = SERVER_URL
// }
// setMasterId (MASTER_ID) {
// this.MASTER_ID = MASTER_ID
// }
// setProjectId (PROJECT_ID) {
// this.PROJECT_ID = PROJECT_ID
// }
getFences = async () => {
try {
const res = await axios.get(this.SERVER_URL+'/getFences', {
params: {
sn: this.MASTER_ID+'_'+this.PROJECT_ID
}
}, {withCredentials: false});
if(res.data.results.name==="error"){
res.data.results.errorcode = 1;
}else{
res.data.results.errorcode = 0;
}
return res.data.results
} catch (error) {
console.error(error)
return error
}
}
createFence = async (_layer, _fenceId, _fenceName) => {
try{
const geom = "ST_GeomFromGeoJSON('"+JSON.stringify(_layer.boundaryData)+"')";
// const geom = "ST_GeomFromGeoJSON('"+_geoJson+"')";
const res = await axios.post(this.SERVER_URL+'/createFence', {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
fenceId: _fenceId,
fenceName: _fenceName,
fenceGeometry: geom
},
{
withCredentials: false,
headers:{
"Content-Type": "application/x-www-form-urlencoded"
}
})
if(res.data.results.name==="error"){
res.data.results.errorcode = 1;
}else{
res.data.results.errorcode = 0;
}
return res.data.results
} catch (error) {
return error
}
}
updateFenceGeog = async (_layer, _fenceId) => {
try{
const geom = "ST_GeomFromGeoJSON('"+JSON.stringify(_layer.boundaryData)+"')";
// const geom = "ST_GeomFromGeoJSON('"+_geoJson+"')";
const res = await axios.post(this.SERVER_URL+'/updateFenceGeog', {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
fenceId: _fenceId,
fenceGeometry: geom
},
{
withCredentials: false,
headers:{
"Content-Type": "application/x-www-form-urlencoded"
}
})
if(res.data.results.name==="error"){
res.data.results.errorcode = 1;
}else{
res.data.results.errorcode = 0;
}
return res.data.results
} catch (error) {
return error
}
}
updateFenceName = async (_fenceId, _newFenceName) => {
try{
const res = await axios.post(this.SERVER_URL+'/updateFenceName', {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
fenceId: _fenceId,
fenceName: _newFenceName,
},
{
withCredentials: false,
headers:{
"Content-Type": "application/x-www-form-urlencoded"
}
})
if(res.data.results.name==="error"){
res.data.results.errorcode = 1;
}else{
res.data.results.errorcode = 0;
}
return res.data.results
} catch (error) {
return error
}
}
removeFence = async (_fenceId) => {
try{
const res = await axios.post(this.SERVER_URL+'/removeFence', {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
fenceId: _fenceId,
},{
withCredentials: false,
headers:{
"Content-Type": "application/x-www-form-urlencoded"
}
})
if(res.data.results.name==="error"){
res.data.results.errorcode = 1;
}else{
res.data.results.errorcode = 0;
}
return res.data.results
} catch (error) {
return error
}
}
createProject = async () => {
try{
const res = await axios.post(this.SERVER_URL+'/createProject', {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
},
{
withCredentials: false,
headers:{
"Content-Type": "application/x-www-form-urlencoded"
}
});
res.data.results.errorcode = 0
// return res.data.results
return {'errorcode':0}
} catch (error) {
console.log(error)
}
}
removeProject = async () => {
// 구현 필요
// try{
// const res = await axios.post(this.SERVER_URL+'/removeProject', {
// sn: this.MASTER_ID+'_'+this.PROJECT_ID,
// },
// {
// withCredentials: false,
// headers:{
// "Content-Type": "application/x-www-form-urlencoded"
// }
// });
// res.data.results.errorcode = 0
// // return res.data.results
// return {'errorcode':0}
// } catch (error) {
// console.log(error)
// }
}
createObject = async (_clientId) => {
try{
const res = await axios.get(this.SERVER_URL+'/createObject', {
params: {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
lat: '127',
lng: '-80',
userId: _clientId
}
}, {withCredentials: false})
if(res.data.results.name==="error"){
res.data.results.errorcode = 1;
}
return res.data.results
}catch{
}
}
updateObject = async (_clientId, _position) => {
try {
const res = await axios.get(this.SERVER_URL+'/updateObject', {
params: {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
lat: _position.coords.latitude,
lng: _position.coords.longitude,
userId: _clientId
}
}, {withCredentials: false});
if(res.data.results.name==="error"){
res.data.results.errorcode = 1;
}else{
res.data.results.errorcode = 0;
}
if(res.data.results[0].rowCount>0){ // _clientId 가 DB에 존재 한다면
if(res.data.results[1].rowCount>0){ // _clientId 의 위치가 어디엔가 속한다면
return res.data.results[1].rows
}else{
return []
}
}else{
this.createObject(_clientId)
return this.updateObject(_clientId, _position)
}
} catch (error) {
return error
}
};
getObjectHistory = async (_clientId) => {
try{
const res = await axios.get(this.SERVER_URL+'/getObjectHistory', {
params: {
sn: this.MASTER_ID+'_'+this.PROJECT_ID,
clientId: _clientId
}
}, {withCredentials: false})
return res.data.results.rows
} catch (error) {
return error
}
}
testLog(){
console.log(this.SERVER_URL,this.MASTER_ID,this.PROJECT_ID)
}
}
export class Client {
constructor(geofencingWorker, id){
this.geofencingWorker = geofencingWorker
this.id = id
this.info = {
count : 0,
fences : [],
position: null
}
this.watchId = null
}
startGPS = (insideCB, outsideCB, _options) => {
let options = {}
if(_options){
options = _options
}else{
options = {
enableHighAccuracy: true, maximumAge: 10000, timeout: 5000
}
}
if(navigator.geolocation){
this.watchId = navigator.geolocation.watchPosition( (position) => {
this.updatePosition(position, insideCB, outsideCB)
}, (err) => {
console.log(err.message)
}, options )
}else {
console.log("Geolocation is not supported by this browser.")
}
}
stopGPS = () => {
navigator.geolocation.clearWatch(this.watchId);
}
updatePosition = async(_position, _insideCB, _outsideCB) => {
// console.log(_position)
const res = await this.geofencingWorker.updateObject(this.id, _position)
this.info.count = res.length;
this.info.fences = res;
this.info.position = _position;
if(res.length > 0){
if(_insideCB){
_insideCB( {count:res.length, fences:res, position:_position} )
}
}else{
if(_outsideCB){
_outsideCB()
}
}
}
checkInfo = () => {
return this.info
}
}