Do you want to make a contribution? If yes, we should be doing it via GitHub and you have to send pull requests for each feature separately.
sure I will , if you can help me with that , now I have like circle with center and radius data, for exemple ;how I can make a detection of a device when OnEnter this circle (i made sure that the circle a,d all features are mapbox objects)
Now how I can deal with the geofencing events
thank youu
I don't know the answer off the top of my head.
I dont think it is going to be different than on the classic app since its only dealing with the coordinates ans some math ? right ?
Hello Ali, I'm finding a way to implement the Geofence feature in the modern UI too.
Could you send me the project with the geofence menu, please? Thank you very much.
please check my pull request here: https://github.com/traccar/traccar-web/pull/838
also you can send me your implementation to see
Thanks for you help, Ali. May I ask you one more question?
When I attempt to run your implementation, the geofences from my database are displayed! However, I haven't seen the editing menu yet; could you please tell me if I forgot to do something halfway?
Thank you very much.
did you make sure , to import the js file in the mainPage inside the <map> , a controller should be displayed in your screen
Hi Ali, I saw your pull request and was able to create the geofence drawing tool due to your instruction here. Thank you very much.
However, whenever I draw a geofence in the modern UI, it doesn't come with an ID (and a name) so I couldn't store them in the database.
I would appreciate if you could give me some instructions about that?
this is my full updated Code:
import React from 'react';
import Directions from '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions';
import '@mapbox/mapbox-gl-directions/dist/mapbox-gl-directions.css'
import MapboxDraw from 'C:/Users/ALTERNATOR/Desktop/Rapport-PFE/PFE/Traccar/nextTrac/modern/node_modules/@mapbox/mapbox-gl-draw';
import theme from '@mapbox/mapbox-gl-draw/src/lib/theme';
import { map } from './Map';
import 'mapbox-gl/dist/mapbox-gl.css';
import '@mapbox/mapbox-gl-draw/dist/mapbox-gl-draw.css';
import {useState,useEffect} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import 'font-awesome/css/font-awesome.min.css';
import {
CircleMode,
DragCircleMode,
DirectMode,
SimpleSelectMode
} from 'mapbox-gl-draw-circle';
import Button from '@material-ui/core/Button';
import Dialog from '@material-ui/core/Dialog';
import DialogActions from '@material-ui/core/DialogActions';
import DialogContent from '@material-ui/core/DialogContent';
import DialogTitle from '@material-ui/core/DialogTitle';
import Slide from '@material-ui/core/Slide';
import TextField from '@material-ui/core/TextField';
import { geofencesActions } from '../store';
import { useAttributePreference } from '../common/preferences';
import GeofenceMap from './GeofenceMap'
import { useEffectAsync } from '../reactHelper';
//const mapboxAccessToken = useAttributePreference('mapboxAccessToken');
const Transition = React.forwardRef(function Transition(props, ref) {
return <Slide direction="up" ref={ref} {...props} />;
});
function ParseJson(data){
let arrayData=[];
var dat=data["features"];
for(var i=0;i< dat.length;i++){
if(Object.keys(dat[i]["properties"]).length==0){
if(dat[i]["geometry"]["type"]=="Polygon"){
var stri="POLYGON(("
for(var j=0;j<dat[i]["geometry"]["coordinates"][0].length;j++){
stri+=dat[i]["geometry"]["coordinates"][0][j][1]+" "+dat[i]["geometry"]["coordinates"][0][j][0]+","
}
stri=stri.substring(0,stri.length-1)+"))"
arrayData.push(stri)
}
if(dat[i]["geometry"]["type"]=="LineString"){
var striline="LINESTRING (";
if(dat[i]["geometry"]["coordinates"].length==2){
for(var k=0;k<dat[i]["geometry"]["coordinates"].length;k++){
striline+=dat[i]["geometry"]["coordinates"][k][0]+" "+dat[i]["geometry"]["coordinates"][k][1]+","
}
}
else{
for(var k=0;k<dat[i]["geometry"]["coordinates"].length;k++){
striline+=dat[i]["geometry"]["coordinates"][k][1]+" "+dat[i]["geometry"]["coordinates"][k][0]+","
}
}
striline=striline.substring(0,striline.length-1)+")"
arrayData.push(striline)
}
}
else{
arrayData.push("CIRCLE ("+dat[i]["properties"]["center"][1]+" "+dat[i]["properties"]["center"][0]+","+dat[i]["properties"]["radiusInKm"]*1000+")")
}
}
return arrayData
}
class extendDrawBar {
constructor(opt) {
let ctrl = this;
ctrl.draw = opt.draw;
ctrl.buttons = opt.buttons || [];
ctrl.onAddOrig = opt.draw.onAdd;
ctrl.onRemoveOrig = opt.draw.onRemove;
}
onAdd(map) {
let ctrl = this;
ctrl.map = map;
ctrl.elContainer = ctrl.onAddOrig(map);
ctrl.buttons.forEach((b) => {
ctrl.addButton(b);
});
return ctrl.elContainer;
}
onRemove(map) {
let ctrl = this;
ctrl.buttons.forEach((b) => {
ctrl.removeButton(b);
});
ctrl.onRemoveOrig(map);
}
addButton(opt) {
let ctrl = this;
var elButton = document.createElement('button');
elButton.className = 'fa fa-circle-o';
if (opt.classes instanceof Array) {
opt.classes.forEach((c) => {
elButton.classList.add(c);
});
}
elButton.addEventListener(opt.on, opt.action);
ctrl.elContainer.appendChild(elButton);
opt.elButton = elButton;
}
removeButton(opt) {
opt.elButton.removeEventListener(opt.on, opt.action);
opt.elButton.remove();
}
}
const draw = new MapboxDraw({
displayControlsDefault: false,
controls: {
point: true,
line_string: true,
polygon: true,
trash: true,
},
styles: theme,
modes: {
...MapboxDraw.modes,
draw_circle : CircleMode,
drag_circle : DragCircleMode,
direct_select: DirectMode,
simple_select: SimpleSelectMode
},
defaultMode: "simple_select",
userProperties: true
});
var drawBar = new extendDrawBar({
draw: draw,
buttons: [
{
on: 'click',
action: function circle(){
draw.changeMode('drag_circle');
},
classes: []
}
]
});
const directions = new Directions({
accessToken: "pk.eyJ1IjoiYWxpYmVybzAwOSIsImEiOiJja240ZGZvcngwNXBqMndvZnF1MThjZHVnIn0.3HjoQt279wR8tla2b2OHiA",
unit: 'metric',
profile: 'mapbox/cycling'
});
var n=new Map();
const GeofenceEditMap = () => {
const dispatch = useDispatch();
const [geofences, setGeofences] = useState([]);
useEffectAsync(async () => {
const response = await fetch('/api/geofences');
if (response.ok) {
setGeofences(await response.json());
}
}, []);
//const geofences = useSelector(state => state.geofences.items);
var m=new Map() // The idea is to affect to every id in map , the id in data base
// the data name is saved with id and Name as a map
useEffect(() => {
map.addControl(drawBar, 'top-left');
//map.addControl(directions,'bottom-left');
return () => map.removeControl(drawBar);
}, []);
const [open, setOpen] = React.useState(false);
const [txt,settxt] = React.useState("");
const [btntxt,setbtntxt] = React.useState("");
const [id,setid] = React.useState("");
const [idmap,setidmap] = React.useState("");
const [area,setarea]=React.useState("");
const handleClickOpen = () => {
setOpen(true);
};
const handleClose = (e,methode) => {
setbtntxt(txt);
setOpen(false);
geofenceName()
};
const geofenceName=()=>{
const requestOptions = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"name": txt,
"description": "string",
"area": area,
"calendarId": 0,
"attributes": { }
})
}
setGeofences([...geofences,{
"name": txt,
"description": "string",
"area": area,
"calendarId": 0,
"attributes": { }
}])
fetch('/api/geofences/', requestOptions)
.then(response => response.json())
.then(response => {
setid(response.id);
n.set(response.id,response.name);
m.set(idmap,response.id);
})
}
map.on('draw.create', function (e) {
handleClickOpen();
setarea(ParseJson(e).toString());
setidmap(e.features[0].id);
});
map.on('draw.update',function (e) {
const requestOptions = {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"id" : m.get(e.features[0].id),
"name": n.get(m.get(e.features[0].id)),
"description": "string",
"area": ParseJson(e).toString(),
"calendarId": 0,
"attributes": { }
})
};
//geofences.forEach(i=>{if(i.id==m.get(e.features[0].id)){console.log(i)}})
setGeofences(()=>{var l=geofences.filter(i=>i.id!=m.get(e.features[0].id));
return [...l,{
"name": n.get(m.get(e.features[0].id)),
"description": "string",
"area": ParseJson(e).toString(),
"calendarId": 0,
"attributes": { }
}]
})
fetch('/api/geofences/'+(m.get(e.features[0].id)), requestOptions)
.then(response => response.json()) } );
map.on('draw.delete', function (e) {
setGeofences(geofences.filter(i=>i.id!=m.get(e.features[0].id)))
fetch('/api/geofences/'+(m.get(e.features[0].id)), {method : "DELETE"})
.then(response => response.json())
});
/* directions.on('route',function(e){
console.log(directions.getWaypoints())
}) */
return(
<>
<GeofenceMap geofences={geofences} />
<Dialog
open={open}
TransitionComponent={Transition}
keepMounted
onClose={handleClose}
aria-labelledby="alert-dialog-slide-title"
aria-describedby="alert-dialog-slide-description"
>
<DialogTitle id="alert-dialog-slide-title" align="center"> Geofence Name</DialogTitle>
<DialogContent>
<TextField
autoFocus
margin="dense"
id="name"
label="Name"
type="text"
fullWidth
onChange={e=> settxt(e.target.value)}
/>
</DialogContent>
<DialogActions style={{alignItems: "center", justifyContent: "center"}}>
<Button onClick={handleClose} color="primary">
Enter
</Button>
</DialogActions>
</Dialog>
</>
);
}
export default GeofenceEditMap;
Hi Ali, thanks for your support. I think your implementation was true, but I met these errors with it and I think I should discuss with you (in case you meet those ones too).
When I tried your code, I get this error "There is already a source with this ID."
I searched for some solutions and was able to re-enter the modern UI, right after I comment two blocks of code in the file GeofenceMaps.js, this:
useEffect(() => {
// map.addSource(id, {
// 'type': 'geojson',
// 'data': {
// type: 'FeatureCollection',
// features: []
// }
// });
and this:
// useEffect(() => {
// map.getSource(id).setData({
// type: 'FeatureCollection',
// features: geofences.map(item => [item.name, reverseCoordinates(wellknown(item.area))]).filter(([, geometry]) => !!geometry).map(([name, geometry]) => ({
// type: 'Feature',
// geometry: geometry,
// properties: { name },
// })),
// });
// }, [geofences]);
I was able to create the geofences with IDs and stored them in the database. However, the geofences weren't displayed in the modern UI.
Did you meet this error with your implementation and how could you solve it? Thank you very much.
Hi Ali, I have an update. I solved the errors by uncomment those two blocks and commented this line in your code (the GeofenceEditMap.js file):
{/*<GeofenceMap geofences={geofences} />*/}
Then, your implementation works like a charm! However, it doesn't allow me to delete, move or resize the geofence once I refresh the website. Did you meet this error?
There is a bug on traccar manager android version 3.2. You cannot delete a geofence from menu because second confirmation button is not poping up to front screen thus delete process can not be completed
If you think there's a bug, please create a ticket on GitHub and include steps to reproduce and s screen recording.
Hello Anton; I have finally added a geofence menu with circle, polygone, line and point features. their data are stored and updated when a geofence is Edited(moved, deleted ,or sized).
I think I can deal with virtual perimeter ; in interaction with the device locations since the data are features coordinates.
I want to know how I can make event and notifications with what I have now.
I can send you the project if you want to take a look .
best regards