Modern UI geofence menu

Ali Ber3 years ago

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

Anton Tananaev3 years ago

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.

Ali Ber3 years ago

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

Anton Tananaev3 years ago

I don't know the answer off the top of my head.

Ali Ber3 years ago

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 ?

Hieu Nguyen3 years ago

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.

Ali Ber3 years ago

please check my pull request here: https://github.com/traccar/traccar-web/pull/838
also you can send me your implementation to see

Hieu Nguyen3 years ago

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.

Here is my desktop

Ali Ber3 years ago

did you make sure , to import the js file in the mainPage inside the <map> , a controller should be displayed in your screen

Bill Myerson3 years ago

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?

Ali Ber3 years ago

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;
Bill Myerson3 years ago

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."

This is the error when I tried your code.

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.

Bill Myerson3 years ago

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?

irfan atatuzun2 years ago

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

Anton Tananaev2 years ago

If you think there's a bug, please create a ticket on GitHub and include steps to reproduce and s screen recording.