aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAbyss777 <abyss@fox5.ru>2016-06-10 16:02:06 +0500
committerAbyss777 <abyss@fox5.ru>2016-06-10 16:02:06 +0500
commit41fe4ca770875842f4d17531506c4bc74dc90501 (patch)
treed8cdfbf19873d8dd4ae100887b0cec5134f5eb74
parente966778c43ee4a2fa12705cded8648b96ef78f61 (diff)
downloadtraccar-server-41fe4ca770875842f4d17531506c4bc74dc90501.tar.gz
traccar-server-41fe4ca770875842f4d17531506c4bc74dc90501.tar.bz2
traccar-server-41fe4ca770875842f4d17531506c4bc74dc90501.zip
Geofences
-rw-r--r--schema/changelog-3.6.xml60
-rw-r--r--setup/unix/traccar.xml71
-rw-r--r--setup/windows/traccar.xml71
-rw-r--r--src/org/traccar/BasePipelineFactory.java10
-rw-r--r--src/org/traccar/Context.java10
-rw-r--r--src/org/traccar/api/resource/DeviceGeofenceResource.java61
-rw-r--r--src/org/traccar/api/resource/DevicePermissionResource.java2
-rw-r--r--src/org/traccar/api/resource/DeviceResource.java2
-rw-r--r--src/org/traccar/api/resource/GeofencePermissionResource.java56
-rw-r--r--src/org/traccar/api/resource/GeofenceResource.java85
-rw-r--r--src/org/traccar/api/resource/GroupGeofenceResource.java56
-rw-r--r--src/org/traccar/api/resource/GroupPermissionResource.java2
-rw-r--r--src/org/traccar/api/resource/GroupResource.java2
-rw-r--r--src/org/traccar/api/resource/UserResource.java3
-rw-r--r--src/org/traccar/database/ConnectionManager.java3
-rw-r--r--src/org/traccar/database/DataManager.java92
-rw-r--r--src/org/traccar/database/GeofenceManager.java191
-rw-r--r--src/org/traccar/database/PermissionsManager.java7
-rw-r--r--src/org/traccar/events/GeofenceEventHandler.java74
-rw-r--r--src/org/traccar/events/MotionEventHandler.java3
-rw-r--r--src/org/traccar/geofence/GeofenceCircle.java82
-rw-r--r--src/org/traccar/geofence/GeofenceGeometry.java13
-rw-r--r--src/org/traccar/geofence/GeofencePolygon.java160
-rw-r--r--src/org/traccar/model/Device.java9
-rw-r--r--src/org/traccar/model/Event.java20
-rw-r--r--src/org/traccar/model/Extensible.java56
-rw-r--r--src/org/traccar/model/Geofence.java67
-rw-r--r--src/org/traccar/model/GeofencePermission.java25
-rw-r--r--src/org/traccar/model/GroupGeofence.java25
-rw-r--r--src/org/traccar/model/Message.java43
-rw-r--r--src/org/traccar/model/Position.java10
-rw-r--r--src/org/traccar/model/UserDeviceGeofence.java35
-rw-r--r--src/org/traccar/web/WebServer.java7
-rw-r--r--test/org/traccar/geofence/GeofenceCircleTest.java36
-rw-r--r--test/org/traccar/geofence/GeofencePolygonTest.java38
-rw-r--r--web/l10n/en.json2
36 files changed, 1418 insertions, 71 deletions
diff --git a/schema/changelog-3.6.xml b/schema/changelog-3.6.xml
index 378ec741f..8fe48fca6 100644
--- a/schema/changelog-3.6.xml
+++ b/schema/changelog-3.6.xml
@@ -19,6 +19,7 @@
</column>
<column name="deviceid" type="INT" />
<column name="positionid" type="INT" />
+ <column name="geofenceid" type="INT" />
<column name="attributes" type="VARCHAR(4096)">
<constraints nullable="false" />
</column>
@@ -29,6 +30,65 @@
<addColumn tableName="devices">
<column name="motion" type="VARCHAR(128)" />
</addColumn>
+ <addColumn tableName="devices">
+ <column name="geofenceId" type="INT" />
+ </addColumn>
+
+ <createTable tableName="geofences">
+ <column name="id" type="INT" autoIncrement="true">
+ <constraints primaryKey="true" />
+ </column>
+ <column name="name" type="VARCHAR(128)">
+ <constraints nullable="false" />
+ </column>
+ <column name="description" type="VARCHAR(128)" />
+ <column name="area" type="VARCHAR(4096)">
+ <constraints nullable="false" />
+ </column>
+ <column name="attributes" type="VARCHAR(4096)">
+ <constraints nullable="false" />
+ </column>
+ </createTable>
+
+ <createTable tableName="user_geofence">
+ <column name="userid" type="INT">
+ <constraints nullable="false" />
+ </column>
+ <column name="geofenceid" type="INT">
+ <constraints nullable="false" />
+ </column>
+ </createTable>
+
+ <addForeignKeyConstraint baseTableName="user_geofence" baseColumnNames="userid" constraintName="fk_user_geofence_userid" referencedTableName="users" referencedColumnNames="id" onDelete="CASCADE" />
+ <addForeignKeyConstraint baseTableName="user_geofence" baseColumnNames="geofenceid" constraintName="fk_user_geofence_geofenceid" referencedTableName="geofences" referencedColumnNames="id" onDelete="CASCADE" />
+
+ <createTable tableName="group_geofence">
+ <column name="groupid" type="INT">
+ <constraints nullable="false" />
+ </column>
+ <column name="geofenceid" type="INT">
+ <constraints nullable="false" />
+ </column>
+ </createTable>
+
+ <addForeignKeyConstraint baseTableName="group_geofence" baseColumnNames="groupid" constraintName="fk_group_geofence_groupid" referencedTableName="groups" referencedColumnNames="id" onDelete="CASCADE" />
+ <addForeignKeyConstraint baseTableName="group_geofence" baseColumnNames="geofenceid" constraintName="fk_group_geofence_geofenceid" referencedTableName="geofences" referencedColumnNames="id" onDelete="CASCADE" />
+
+ <createTable tableName="user_device_geofence">
+ <column name="userid" type="INT">
+ <constraints nullable="false" />
+ </column>
+ <column name="deviceid" type="INT">
+ <constraints nullable="false" />
+ </column>
+ <column name="geofenceid" type="INT">
+ <constraints nullable="false" />
+ </column>
+ </createTable>
+
+ <addForeignKeyConstraint baseTableName="user_device_geofence" baseColumnNames="userid" constraintName="fk_user_device_geofence_userid" referencedTableName="users" referencedColumnNames="id" onDelete="CASCADE" />
+ <addForeignKeyConstraint baseTableName="user_device_geofence" baseColumnNames="deviceid" constraintName="fk_user_device_geofence_deviceid" referencedTableName="devices" referencedColumnNames="id" onDelete="CASCADE" />
+ <addForeignKeyConstraint baseTableName="user_device_geofence" baseColumnNames="geofenceid" constraintName="fk_user_device_geofence_geofenceid" referencedTableName="geofences" referencedColumnNames="id" onDelete="CASCADE" />
</changeSet>
</databaseChangeLog>
diff --git a/setup/unix/traccar.xml b/setup/unix/traccar.xml
index d8d4a1084..37ff31bbe 100644
--- a/setup/unix/traccar.xml
+++ b/setup/unix/traccar.xml
@@ -24,6 +24,8 @@
<entry key='event.motionHandler'>true</entry>
+ <entry key='event.geofenceHandler'>true</entry>
+
<!-- DATABASE CONFIG -->
<entry key='database.driver'>org.h2.Driver</entry>
@@ -116,7 +118,7 @@
</entry>
<entry key='database.updateDeviceStatus'>
- UPDATE devices SET status = :status, lastUpdate = :lastUpdate, motion = :motion WHERE id = :id;
+ UPDATE devices SET status = :status, lastUpdate = :lastUpdate, motion = :motion, geofenceId = :geofenceId WHERE id = :id;
</entry>
<entry key='database.deleteDevice'>
@@ -177,14 +179,77 @@
</entry>
<entry key='database.insertEvent'>
- INSERT INTO events (type, serverTime, deviceId, positionId, attributes)
- VALUES (:type, :serverTime, :deviceId, :positionId, :attributes);
+ INSERT INTO events (type, serverTime, deviceId, positionId, geofenceId, attributes)
+ VALUES (:type, :serverTime, :deviceId, :positionId, :geofenceId, :attributes);
</entry>
<entry key='database.selectEvents'>
SELECT * FROM events WHERE deviceId = :deviceId AND type LIKE :type AND serverTime BETWEEN :from AND :to ORDER BY serverTime DESC;
</entry>
+ <entry key='database.selectGeofence'>
+ SELECT * FROM geofences
+ WHERE id = :id;
+ </entry>
+
+ <entry key='database.selectGeofencesAll'>
+ SELECT * FROM geofences;
+ </entry>
+
+ <entry key='database.insertGeofence'>
+ INSERT INTO geofences (name, description, area, attributes)
+ VALUES (:name, :description, :area, :attributes);
+ </entry>
+
+ <entry key='database.updateGeofence'>
+ UPDATE geofences SET
+ name = :name,
+ description = :description,
+ area = :area,
+ attributes = :attributes
+ WHERE id = :id;
+ </entry>
+
+ <entry key='database.deleteGeofence'>
+ DELETE FROM geofences WHERE id = :id;
+ </entry>
+
+ <entry key='database.selectGeofencePermissions'>
+ SELECT userId, geofenceId FROM user_geofence;
+ </entry>
+
+ <entry key='database.linkGeofence'>
+ INSERT INTO user_geofence (userId, geofenceId) VALUES (:userId, :geofenceId);
+ </entry>
+
+ <entry key='database.unlinkGeofence'>
+ DELETE FROM user_geofence WHERE userId = :userId AND geofenceId = :geofenceId;
+ </entry>
+
+ <entry key='database.selectGroupGeofences'>
+ SELECT groupId, geofenceId FROM group_geofence;
+ </entry>
+
+ <entry key='database.linkGroupGeofence'>
+ INSERT INTO group_geofence (groupId, geofenceId) VALUES (:groupId, :geofenceId);
+ </entry>
+
+ <entry key='database.unlinkGroupGeofence'>
+ DELETE FROM group_geofence WHERE groupId = :groupId AND geofenceId = :geofenceId;
+ </entry>
+
+ <entry key='database.selectUserDeviceGeofences'>
+ SELECT userId, deviceId, geofenceId FROM user_device_geofence;
+ </entry>
+
+ <entry key='database.linkUserDeviceGeofence'>
+ INSERT INTO user_device_geofence (userId, deviceId, geofenceId) VALUES (:userId, :deviceId, :geofenceId);
+ </entry>
+
+ <entry key='database.unlinkUserDeviceGeofence'>
+ DELETE FROM user_device_geofence WHERE userId = :userId AND deviceId = :deviceId AND geofenceId = :geofenceId;
+ </entry>
+
<!-- PROTOCOL CONFIG -->
<entry key='gps103.port'>5001</entry>
diff --git a/setup/windows/traccar.xml b/setup/windows/traccar.xml
index 71f56f4c7..5f86e53b0 100644
--- a/setup/windows/traccar.xml
+++ b/setup/windows/traccar.xml
@@ -24,6 +24,8 @@
<entry key='event.motionHandler'>true</entry>
+ <entry key='event.geofenceHandler'>true</entry>
+
<!-- DATABASE CONFIG -->
<entry key='database.driver'>org.h2.Driver</entry>
@@ -116,7 +118,7 @@
</entry>
<entry key='database.updateDeviceStatus'>
- UPDATE devices SET status = :status, lastUpdate = :lastUpdate, motion = :motion WHERE id = :id;
+ UPDATE devices SET status = :status, lastUpdate = :lastUpdate, motion = :motion, geofenceId = :geofenceId WHERE id = :id;
</entry>
<entry key='database.deleteDevice'>
@@ -177,14 +179,77 @@
</entry>
<entry key='database.insertEvent'>
- INSERT INTO events (type, serverTime, deviceId, positionId, attributes)
- VALUES (:type, :serverTime, :deviceId, :positionId, :attributes);
+ INSERT INTO events (type, serverTime, deviceId, positionId, geofenceId, attributes)
+ VALUES (:type, :serverTime, :deviceId, :positionId, :geofenceId, :attributes);
</entry>
<entry key='database.selectEvents'>
SELECT * FROM events WHERE deviceId = :deviceId AND type LIKE :type AND serverTime BETWEEN :from AND :to ORDER BY serverTime DESC;
</entry>
+ <entry key='database.selectGeofence'>
+ SELECT * FROM geofences
+ WHERE id = :id;
+ </entry>
+
+ <entry key='database.selectGeofencesAll'>
+ SELECT * FROM geofences;
+ </entry>
+
+ <entry key='database.insertGeofence'>
+ INSERT INTO geofences (name, description, area, attributes)
+ VALUES (:name, :description, :area, :attributes);
+ </entry>
+
+ <entry key='database.updateGeofence'>
+ UPDATE geofences SET
+ name = :name,
+ description = :description,
+ area = :area,
+ attributes = :attributes
+ WHERE id = :id;
+ </entry>
+
+ <entry key='database.deleteGeofence'>
+ DELETE FROM geofences WHERE id = :id;
+ </entry>
+
+ <entry key='database.selectGeofencePermissions'>
+ SELECT userId, geofenceId FROM user_geofence;
+ </entry>
+
+ <entry key='database.linkGeofence'>
+ INSERT INTO user_geofence (userId, geofenceId) VALUES (:userId, :geofenceId);
+ </entry>
+
+ <entry key='database.unlinkGeofence'>
+ DELETE FROM user_geofence WHERE userId = :userId AND geofenceId = :geofenceId;
+ </entry>
+
+ <entry key='database.selectGroupGeofences'>
+ SELECT groupId, geofenceId FROM group_geofence;
+ </entry>
+
+ <entry key='database.linkGroupGeofence'>
+ INSERT INTO group_geofence (groupId, geofenceId) VALUES (:groupId, :geofenceId);
+ </entry>
+
+ <entry key='database.unlinkGroupGeofence'>
+ DELETE FROM group_geofence WHERE groupId = :groupId AND geofenceId = :geofenceId;
+ </entry>
+
+ <entry key='database.selectUserDeviceGeofences'>
+ SELECT userId, deviceId, geofenceId FROM user_device_geofence;
+ </entry>
+
+ <entry key='database.linkUserDeviceGeofence'>
+ INSERT INTO user_device_geofence (userId, deviceId, geofenceId) VALUES (:userId, :deviceId, :geofenceId);
+ </entry>
+
+ <entry key='database.unlinkUserDeviceGeofence'>
+ DELETE FROM user_device_geofence WHERE userId = :userId AND deviceId = :deviceId AND geofenceId = :geofenceId;
+ </entry>
+
<!-- PROTOCOL CONFIG -->
<entry key='gps103.port'>5001</entry>
diff --git a/src/org/traccar/BasePipelineFactory.java b/src/org/traccar/BasePipelineFactory.java
index 634c6d6a4..b61d95171 100644
--- a/src/org/traccar/BasePipelineFactory.java
+++ b/src/org/traccar/BasePipelineFactory.java
@@ -30,6 +30,7 @@ import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.handler.logging.LoggingHandler;
import org.jboss.netty.handler.timeout.IdleStateHandler;
import org.traccar.events.CommandResultEventHandler;
+import org.traccar.events.GeofenceEventHandler;
import org.traccar.events.MotionEventHandler;
import org.traccar.events.OverspeedEventHandler;
import org.traccar.helper.Log;
@@ -50,6 +51,7 @@ public abstract class BasePipelineFactory implements ChannelPipelineFactory {
private CommandResultEventHandler commandResultEventHandler;
private OverspeedEventHandler overspeedEventHandler;
private MotionEventHandler motionEventHandler;
+ private GeofenceEventHandler geofenceEventHandler;
private static final class OpenChannelHandler extends SimpleChannelHandler {
@@ -140,6 +142,10 @@ public abstract class BasePipelineFactory implements ChannelPipelineFactory {
motionEventHandler = new MotionEventHandler();
}
+ if (Context.getConfig().getBoolean("event.geofenceHandler")) {
+ geofenceEventHandler = new GeofenceEventHandler();
+ }
+
}
protected abstract void addSpecificHandlers(ChannelPipeline pipeline);
@@ -197,6 +203,10 @@ public abstract class BasePipelineFactory implements ChannelPipelineFactory {
pipeline.addLast("MotionEventHandler", motionEventHandler);
}
+ if (geofenceEventHandler != null) {
+ pipeline.addLast("GeofenceEventHandler", geofenceEventHandler);
+ }
+
pipeline.addLast("mainHandler", new MainEventHandler());
return pipeline;
}
diff --git a/src/org/traccar/Context.java b/src/org/traccar/Context.java
index 1aba24b8c..b78d11e7b 100644
--- a/src/org/traccar/Context.java
+++ b/src/org/traccar/Context.java
@@ -20,6 +20,7 @@ import org.traccar.database.ConnectionManager;
import org.traccar.database.DataManager;
import org.traccar.database.IdentityManager;
import org.traccar.database.PermissionsManager;
+import org.traccar.database.GeofenceManager;
import org.traccar.geocode.BingMapsReverseGeocoder;
import org.traccar.geocode.FactualReverseGeocoder;
import org.traccar.geocode.GisgraphyReverseGeocoder;
@@ -99,6 +100,12 @@ public final class Context {
return serverManager;
}
+ private static GeofenceManager geofenceManager;
+
+ public static GeofenceManager getGeofenceManager() {
+ return geofenceManager;
+ }
+
private static final AsyncHttpClient ASYNC_HTTP_CLIENT = new AsyncHttpClient();
public static AsyncHttpClient getAsyncHttpClient() {
@@ -177,9 +184,12 @@ public final class Context {
permissionsManager = new PermissionsManager(dataManager);
+ geofenceManager = new GeofenceManager(dataManager);
+
connectionManager = new ConnectionManager(dataManager);
serverManager = new ServerManager();
+
}
public static void init(IdentityManager testIdentityManager) {
diff --git a/src/org/traccar/api/resource/DeviceGeofenceResource.java b/src/org/traccar/api/resource/DeviceGeofenceResource.java
new file mode 100644
index 000000000..dc7014d79
--- /dev/null
+++ b/src/org/traccar/api/resource/DeviceGeofenceResource.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton.tananaev@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.model.UserDeviceGeofence;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.sql.SQLException;
+
+@Path("devices/geofences")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class DeviceGeofenceResource extends BaseResource {
+
+ @POST
+ public Response add(UserDeviceGeofence entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkUser(getUserId(), entity.getUserId());
+ Context.getPermissionsManager().checkDevice(getUserId(), entity.getDeviceId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), entity.getGeofenceId());
+ Context.getDataManager().linkUserDeviceGeofence(entity.getUserId(),
+ entity.getDeviceId(), entity.getGeofenceId());
+ Context.getGeofenceManager().refresh();
+ return Response.ok(entity).build();
+ }
+
+ @DELETE
+ public Response remove(UserDeviceGeofence entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkUser(getUserId(), entity.getUserId());
+ Context.getPermissionsManager().checkDevice(getUserId(), entity.getDeviceId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), entity.getGeofenceId());
+ Context.getDataManager().unlinkUserDeviceGeofence(entity.getUserId(), entity.getDeviceId(),
+ entity.getGeofenceId());
+ Context.getGeofenceManager().refresh();
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/org/traccar/api/resource/DevicePermissionResource.java b/src/org/traccar/api/resource/DevicePermissionResource.java
index f77375cba..2ef436f11 100644
--- a/src/org/traccar/api/resource/DevicePermissionResource.java
+++ b/src/org/traccar/api/resource/DevicePermissionResource.java
@@ -38,6 +38,7 @@ public class DevicePermissionResource extends BaseResource {
Context.getPermissionsManager().checkAdmin(getUserId());
Context.getDataManager().linkDevice(entity.getUserId(), entity.getDeviceId());
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.ok(entity).build();
}
@@ -46,6 +47,7 @@ public class DevicePermissionResource extends BaseResource {
Context.getPermissionsManager().checkAdmin(getUserId());
Context.getDataManager().unlinkDevice(entity.getUserId(), entity.getDeviceId());
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.noContent().build();
}
diff --git a/src/org/traccar/api/resource/DeviceResource.java b/src/org/traccar/api/resource/DeviceResource.java
index 6c0ef32ca..26880c1f8 100644
--- a/src/org/traccar/api/resource/DeviceResource.java
+++ b/src/org/traccar/api/resource/DeviceResource.java
@@ -60,6 +60,7 @@ public class DeviceResource extends BaseResource {
Context.getDataManager().addDevice(entity);
Context.getDataManager().linkDevice(getUserId(), entity.getId());
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.ok(entity).build();
}
@@ -79,6 +80,7 @@ public class DeviceResource extends BaseResource {
Context.getPermissionsManager().checkDevice(getUserId(), id);
Context.getDataManager().removeDevice(id);
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.noContent().build();
}
diff --git a/src/org/traccar/api/resource/GeofencePermissionResource.java b/src/org/traccar/api/resource/GeofencePermissionResource.java
new file mode 100644
index 000000000..329c72b07
--- /dev/null
+++ b/src/org/traccar/api/resource/GeofencePermissionResource.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.model.GeofencePermission;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.sql.SQLException;
+
+@Path("permissions/geofences")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class GeofencePermissionResource extends BaseResource {
+
+ @POST
+ public Response add(GeofencePermission entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkUser(getUserId(), entity.getUserId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), entity.getGeofenceId());
+ Context.getDataManager().linkGeofence(entity.getUserId(), entity.getGeofenceId());
+ Context.getGeofenceManager().refresh();
+ return Response.ok(entity).build();
+ }
+
+ @DELETE
+ public Response remove(GeofencePermission entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkUser(getUserId(), entity.getUserId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), entity.getGeofenceId());
+ Context.getDataManager().unlinkGeofence(entity.getUserId(), entity.getGeofenceId());
+ Context.getGeofenceManager().refresh();
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/org/traccar/api/resource/GeofenceResource.java b/src/org/traccar/api/resource/GeofenceResource.java
new file mode 100644
index 000000000..b56e20e08
--- /dev/null
+++ b/src/org/traccar/api/resource/GeofenceResource.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015 Anton Tananaev (anton.tananaev@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.model.Geofence;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+
+import java.sql.SQLException;
+import java.util.Collection;
+
+@Path("geofences")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class GeofenceResource extends BaseResource {
+
+ @GET
+ public Collection<Geofence> get(
+ @QueryParam("all") boolean all, @QueryParam("userId") long userId) throws SQLException {
+ if (all) {
+ Context.getPermissionsManager().checkAdmin(getUserId());
+ return Context.getGeofenceManager().getAllGeofences();
+ } else {
+ if (userId == 0) {
+ userId = getUserId();
+ }
+ Context.getPermissionsManager().checkUser(getUserId(), userId);
+ return Context.getGeofenceManager().getUserGeofences(userId);
+ }
+ }
+
+ @POST
+ public Response add(Geofence entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getDataManager().addGeofence(entity);
+ Context.getDataManager().linkGeofence(getUserId(), entity.getId());
+ Context.getGeofenceManager().refresh();
+ return Response.ok(entity).build();
+ }
+
+ @Path("{id}")
+ @PUT
+ public Response update(@PathParam("id") long id, Geofence entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), id);
+ Context.getGeofenceManager().updateGeofence(entity);
+ return Response.ok(entity).build();
+ }
+
+ @Path("{id}")
+ @DELETE
+ public Response remove(@PathParam("id") long id) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), id);
+ Context.getDataManager().removeGeofence(id);
+ Context.getGeofenceManager().refresh();
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/org/traccar/api/resource/GroupGeofenceResource.java b/src/org/traccar/api/resource/GroupGeofenceResource.java
new file mode 100644
index 000000000..1ef495a86
--- /dev/null
+++ b/src/org/traccar/api/resource/GroupGeofenceResource.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2016 Anton Tananaev (anton.tananaev@gmail.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.traccar.api.resource;
+
+import org.traccar.Context;
+import org.traccar.api.BaseResource;
+import org.traccar.model.GroupGeofence;
+
+import javax.ws.rs.Consumes;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.POST;
+import javax.ws.rs.Path;
+import javax.ws.rs.Produces;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import java.sql.SQLException;
+
+@Path("groups/geofences")
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public class GroupGeofenceResource extends BaseResource {
+
+ @POST
+ public Response add(GroupGeofence entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkGroup(getUserId(), entity.getGroupId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), entity.getGeofenceId());
+ Context.getDataManager().linkGroupGeofence(entity.getGroupId(), entity.getGeofenceId());
+ Context.getGeofenceManager().refresh();
+ return Response.ok(entity).build();
+ }
+
+ @DELETE
+ public Response remove(GroupGeofence entity) throws SQLException {
+ Context.getPermissionsManager().checkReadonly(getUserId());
+ Context.getPermissionsManager().checkGroup(getUserId(), entity.getGroupId());
+ Context.getPermissionsManager().checkGeofence(getUserId(), entity.getGeofenceId());
+ Context.getDataManager().unlinkGroupGeofence(entity.getGroupId(), entity.getGeofenceId());
+ Context.getGeofenceManager().refresh();
+ return Response.noContent().build();
+ }
+
+}
diff --git a/src/org/traccar/api/resource/GroupPermissionResource.java b/src/org/traccar/api/resource/GroupPermissionResource.java
index b8ec4ae3c..564a379d2 100644
--- a/src/org/traccar/api/resource/GroupPermissionResource.java
+++ b/src/org/traccar/api/resource/GroupPermissionResource.java
@@ -38,6 +38,7 @@ public class GroupPermissionResource extends BaseResource {
Context.getPermissionsManager().checkAdmin(getUserId());
Context.getDataManager().linkGroup(entity.getUserId(), entity.getGroupId());
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.ok(entity).build();
}
@@ -46,6 +47,7 @@ public class GroupPermissionResource extends BaseResource {
Context.getPermissionsManager().checkAdmin(getUserId());
Context.getDataManager().unlinkGroup(entity.getUserId(), entity.getGroupId());
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.noContent().build();
}
diff --git a/src/org/traccar/api/resource/GroupResource.java b/src/org/traccar/api/resource/GroupResource.java
index e22796645..dda9ab03b 100644
--- a/src/org/traccar/api/resource/GroupResource.java
+++ b/src/org/traccar/api/resource/GroupResource.java
@@ -59,6 +59,7 @@ public class GroupResource extends BaseResource {
Context.getDataManager().addGroup(entity);
Context.getDataManager().linkGroup(getUserId(), entity.getId());
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.ok(entity).build();
}
@@ -78,6 +79,7 @@ public class GroupResource extends BaseResource {
Context.getPermissionsManager().checkGroup(getUserId(), id);
Context.getDataManager().removeGroup(id);
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.noContent().build();
}
diff --git a/src/org/traccar/api/resource/UserResource.java b/src/org/traccar/api/resource/UserResource.java
index 0b307ab88..7e503dcfb 100644
--- a/src/org/traccar/api/resource/UserResource.java
+++ b/src/org/traccar/api/resource/UserResource.java
@@ -52,6 +52,7 @@ public class UserResource extends BaseResource {
}
Context.getDataManager().addUser(entity);
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.ok(entity).build();
}
@@ -65,6 +66,7 @@ public class UserResource extends BaseResource {
}
Context.getDataManager().updateUser(entity);
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.ok(entity).build();
}
@@ -74,6 +76,7 @@ public class UserResource extends BaseResource {
Context.getPermissionsManager().checkUser(getUserId(), id);
Context.getDataManager().removeUser(id);
Context.getPermissionsManager().refresh();
+ Context.getGeofenceManager().refresh();
return Response.noContent().build();
}
diff --git a/src/org/traccar/database/ConnectionManager.java b/src/org/traccar/database/ConnectionManager.java
index ec5903548..6e47dfad3 100644
--- a/src/org/traccar/database/ConnectionManager.java
+++ b/src/org/traccar/database/ConnectionManager.java
@@ -155,7 +155,8 @@ public class ConnectionManager {
Log.warning(error);
}
for (long userId : Context.getPermissionsManager().getDeviceUsers(deviceId)) {
- if (listeners.containsKey(userId)) {
+ if (listeners.containsKey(userId) && (event.getGeofenceId() == 0
+ || Context.getGeofenceManager().checkGeofence(userId, event.getGeofenceId()))) {
for (UpdateListener listener : listeners.get(userId)) {
listener.onUpdateEvent(event, position);
}
diff --git a/src/org/traccar/database/DataManager.java b/src/org/traccar/database/DataManager.java
index 67b7d1e55..86c930c0b 100644
--- a/src/org/traccar/database/DataManager.java
+++ b/src/org/traccar/database/DataManager.java
@@ -48,11 +48,15 @@ import org.traccar.helper.Log;
import org.traccar.model.Device;
import org.traccar.model.DevicePermission;
import org.traccar.model.Event;
+import org.traccar.model.Geofence;
import org.traccar.model.Group;
+import org.traccar.model.GroupGeofence;
import org.traccar.model.GroupPermission;
import org.traccar.model.Position;
import org.traccar.model.Server;
import org.traccar.model.User;
+import org.traccar.model.UserDeviceGeofence;
+import org.traccar.model.GeofencePermission;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@@ -368,6 +372,7 @@ public class DataManager implements IdentityManager {
Device cachedDevice = getDeviceById(device.getId());
cachedDevice.setStatus(device.getStatus());
cachedDevice.setMotion(device.getMotion());
+ cachedDevice.setGeofenceId(device.getGeofenceId());
}
public void removeDevice(long deviceId) throws SQLException {
@@ -524,4 +529,91 @@ public class DataManager implements IdentityManager {
return getEvents(deviceId, type, new Date(), to);
}
+ public Collection<Geofence> getGeofences() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectGeofencesAll"))
+ .executeQuery(Geofence.class);
+ }
+
+ public Geofence getGeofence(long geofenceId) throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectGeofences"))
+ .setLong("id", geofenceId)
+ .executeQuerySingle(Geofence.class);
+ }
+
+ public void addGeofence(Geofence geofence) throws SQLException {
+ geofence.setId(QueryBuilder.create(dataSource, getQuery("database.insertGeofence"), true)
+ .setObject(geofence)
+ .executeUpdate());
+ }
+
+ public void updateGeofence(Geofence geofence) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.updateGeofence"))
+ .setObject(geofence)
+ .executeUpdate();
+ }
+
+ public void removeGeofence(long geofenceId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.deleteGeofence"))
+ .setLong("id", geofenceId)
+ .executeUpdate();
+ }
+
+ public Collection<GeofencePermission> getGeofencePermissions() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectGeofencePermissions"))
+ .executeQuery(GeofencePermission.class);
+ }
+
+ public void linkGeofence(long userId, long geofenceId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.linkGeofence"))
+ .setLong("userId", userId)
+ .setLong("geofenceId", geofenceId)
+ .executeUpdate();
+ }
+
+ public void unlinkGeofence(long userId, long geofenceId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.unlinkGeofence"))
+ .setLong("userId", userId)
+ .setLong("geofenceId", geofenceId)
+ .executeUpdate();
+ }
+
+ public Collection<GroupGeofence> getGroupGeofences() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectGroupGeofences"))
+ .executeQuery(GroupGeofence.class);
+ }
+
+ public void linkGroupGeofence(long groupId, long geofenceId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.linkGroupGeofence"))
+ .setLong("groupId", groupId)
+ .setLong("geofenceId", geofenceId)
+ .executeUpdate();
+ }
+
+ public void unlinkGroupGeofence(long groupId, long geofenceId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.unlinkGroupGeofence"))
+ .setLong("groupId", groupId)
+ .setLong("geofenceId", geofenceId)
+ .executeUpdate();
+ }
+
+ public Collection<UserDeviceGeofence> getUserDeviceGeofences() throws SQLException {
+ return QueryBuilder.create(dataSource, getQuery("database.selectUserDeviceGeofences"))
+ .executeQuery(UserDeviceGeofence.class);
+ }
+
+ public void linkUserDeviceGeofence(long userId, long deviceId, long geofenceId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.linkUserDeviceGeofence"))
+ .setLong("userId", userId)
+ .setLong("deviceId", deviceId)
+ .setLong("geofenceId", geofenceId)
+ .executeUpdate();
+ }
+
+ public void unlinkUserDeviceGeofence(long userId, long deviceId, long geofenceId) throws SQLException {
+ QueryBuilder.create(dataSource, getQuery("database.unlinkUserDeviceGeofence"))
+ .setLong("userId", userId)
+ .setLong("deviceId", deviceId)
+ .setLong("geofenceId", geofenceId)
+ .executeUpdate();
+ }
}
diff --git a/src/org/traccar/database/GeofenceManager.java b/src/org/traccar/database/GeofenceManager.java
new file mode 100644
index 000000000..64afc876c
--- /dev/null
+++ b/src/org/traccar/database/GeofenceManager.java
@@ -0,0 +1,191 @@
+package org.traccar.database;
+
+import java.sql.SQLException;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import org.traccar.helper.Log;
+import org.traccar.model.Device;
+import org.traccar.model.Geofence;
+import org.traccar.model.GroupGeofence;
+import org.traccar.model.UserDeviceGeofence;
+import org.traccar.model.GeofencePermission;
+
+public class GeofenceManager {
+
+ private final DataManager dataManager;
+
+ private final Map<Long, Geofence> geofences = new HashMap<>();
+ private final Map<Long, Set<Long>> userGeofences = new HashMap<>();
+ private final Map<Long, Set<Long>> groupGeofences = new HashMap<>();
+
+ private final Map<Long, Set<Long>> deviceGeofences = new HashMap<>();
+ private final Map<Long, Map<Long, Set<Long>>> userDeviceGeofences = new HashMap<>();
+
+ private final ReadWriteLock deviceGeofencesLock = new ReentrantReadWriteLock();
+ private final ReadWriteLock geofencesLock = new ReentrantReadWriteLock();
+ private final ReadWriteLock groupGeofencesLock = new ReentrantReadWriteLock();
+
+ public GeofenceManager(DataManager dataManager) {
+ this.dataManager = dataManager;
+ refresh();
+ }
+
+ public Set<Long> getUserGeofencesIds(long userId) {
+ if (!userGeofences.containsKey(userId)) {
+ userGeofences.put(userId, new HashSet<Long>());
+ }
+ return userGeofences.get(userId);
+ }
+
+ private Set<Long> getGroupGeofences(long groupId) {
+ if (!groupGeofences.containsKey(groupId)) {
+ groupGeofences.put(groupId, new HashSet<Long>());
+ }
+ return groupGeofences.get(groupId);
+ }
+
+ public Set<Long> getAllDeviceGeofences(long deviceId) {
+ deviceGeofencesLock.readLock().lock();
+ try {
+ return getDeviceGeofences(deviceGeofences, deviceId);
+ } finally {
+ deviceGeofencesLock.readLock().unlock();
+ }
+
+ }
+
+ public Set<Long> getUserDeviceGeofences(long userId, long deviceId) {
+ deviceGeofencesLock.readLock().lock();
+ try {
+ return getUserDeviceGeofencesUnlocked(userId, deviceId);
+ } finally {
+ deviceGeofencesLock.readLock().unlock();
+ }
+ }
+
+ private Set<Long> getDeviceGeofences(Map<Long, Set<Long>> deviceGeofences, long deviceId) {
+ if (!deviceGeofences.containsKey(deviceId)) {
+ deviceGeofences.put(deviceId, new HashSet<Long>());
+ }
+ return deviceGeofences.get(deviceId);
+ }
+
+ private Set<Long> getUserDeviceGeofencesUnlocked(long userId, long deviceId) {
+ if (!userDeviceGeofences.containsKey(userId)) {
+ userDeviceGeofences.put(userId, new HashMap<Long, Set<Long>>());
+ }
+ return getDeviceGeofences(userDeviceGeofences.get(userId), deviceId);
+ }
+
+ public final void refresh() {
+ if (dataManager != null) {
+ try {
+ geofencesLock.writeLock().lock();
+ groupGeofencesLock.writeLock().lock();
+ deviceGeofencesLock.writeLock().lock();
+ try {
+ geofences.clear();
+ for (Geofence geofence : dataManager.getGeofences()) {
+ geofences.put(geofence.getId(), geofence);
+ }
+
+ userGeofences.clear();
+ for (GeofencePermission geofencePermission : dataManager.getGeofencePermissions()) {
+ getUserGeofencesIds(geofencePermission.getUserId()).add(geofencePermission.getGeofenceId());
+ }
+
+ groupGeofences.clear();
+ for (GroupGeofence groupGeofence : dataManager.getGroupGeofences()) {
+ getGroupGeofences(groupGeofence.getGroupId()).add(groupGeofence.getGeofenceId());
+ }
+
+ deviceGeofences.clear();
+
+ for (Map.Entry<Long, Map<Long, Set<Long>>> deviceGeofence : userDeviceGeofences.entrySet()) {
+ deviceGeofence.getValue().clear();
+ }
+ userDeviceGeofences.clear();
+
+ for (UserDeviceGeofence userDeviceGeofence : dataManager.getUserDeviceGeofences()) {
+ getDeviceGeofences(deviceGeofences, userDeviceGeofence.getDeviceId())
+ .add(userDeviceGeofence.getGeofenceId());
+ getUserDeviceGeofencesUnlocked(userDeviceGeofence.getUserId(), userDeviceGeofence.getDeviceId())
+ .add(userDeviceGeofence.getGeofenceId());
+ }
+ for (Device device : dataManager.getAllDevices()) {
+ long groupId = device.getGroupId();
+ while (groupId != 0) {
+ getDeviceGeofences(deviceGeofences, device.getId()).addAll(getGroupGeofences(groupId));
+ groupId = dataManager.getGroupById(groupId).getGroupId();
+ }
+ }
+
+ } finally {
+ geofencesLock.writeLock().unlock();
+ groupGeofencesLock.writeLock().unlock();
+ deviceGeofencesLock.writeLock().unlock();
+ }
+
+ } catch (SQLException error) {
+ Log.warning(error);
+ }
+ }
+ }
+
+ public final Collection<Geofence> getAllGeofences() {
+ geofencesLock.readLock().lock();
+ try {
+ return geofences.values();
+ } finally {
+ geofencesLock.readLock().unlock();
+ }
+ }
+
+ public final Collection<Geofence> getUserGeofences(long userId) {
+ geofencesLock.readLock().lock();
+ try {
+ Collection<Geofence> result = new LinkedList<>();
+ for (Long geofenceId : getUserGeofencesIds(userId)) {
+ result.add(getGeofence(geofenceId));
+ }
+ return result;
+ } finally {
+ geofencesLock.readLock().unlock();
+ }
+ }
+
+ public final Geofence getGeofence(Long geofenceId) {
+ geofencesLock.readLock().lock();
+ try {
+ return geofences.get(geofenceId);
+ } finally {
+ geofencesLock.readLock().unlock();
+ }
+ }
+
+ public final void updateGeofence(Geofence geofence) {
+ geofencesLock.writeLock().lock();
+ try {
+ geofences.put(geofence.getId(), geofence);
+ } finally {
+ geofencesLock.writeLock().unlock();
+ }
+ try {
+ dataManager.updateGeofence(geofence);
+ } catch (SQLException error) {
+ Log.warning(error);
+ }
+ }
+
+ public boolean checkGeofence(long userId, long geofenceId) {
+ return getUserGeofencesIds(userId).contains(geofenceId);
+ }
+
+}
diff --git a/src/org/traccar/database/PermissionsManager.java b/src/org/traccar/database/PermissionsManager.java
index 08d44b382..96a6488ef 100644
--- a/src/org/traccar/database/PermissionsManager.java
+++ b/src/org/traccar/database/PermissionsManager.java
@@ -15,6 +15,7 @@
*/
package org.traccar.database;
+import org.traccar.Context;
import org.traccar.helper.Log;
import org.traccar.model.Device;
import org.traccar.model.DevicePermission;
@@ -145,4 +146,10 @@ public class PermissionsManager {
}
}
+ public void checkGeofence(long userId, long geofenceId) throws SecurityException {
+ if (!Context.getGeofenceManager().checkGeofence(userId, geofenceId) && !isAdmin(userId)) {
+ throw new SecurityException("Geofence access denied");
+ }
+ }
+
}
diff --git a/src/org/traccar/events/GeofenceEventHandler.java b/src/org/traccar/events/GeofenceEventHandler.java
new file mode 100644
index 000000000..bf9060ca1
--- /dev/null
+++ b/src/org/traccar/events/GeofenceEventHandler.java
@@ -0,0 +1,74 @@
+package org.traccar.events;
+
+import java.sql.SQLException;
+import java.util.Set;
+
+import org.traccar.BaseEventHandler;
+import org.traccar.Context;
+import org.traccar.database.DataManager;
+import org.traccar.database.GeofenceManager;
+import org.traccar.helper.Log;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+public class GeofenceEventHandler extends BaseEventHandler {
+
+ private int suppressRepeated;
+ private GeofenceManager geofenceManager;
+ private DataManager dataManager;
+
+ public GeofenceEventHandler() {
+ suppressRepeated = Context.getConfig().getInteger("event.suppressRepeated", 60);
+ geofenceManager = Context.getGeofenceManager();
+ dataManager = Context.getDataManager();
+ }
+
+ @Override
+ protected Event analizePosition(Position position) {
+ Event event = null;
+ if (!isLastPosition() || !position.getValid()) {
+ return event;
+ }
+
+ Device device = dataManager.getDeviceById(position.getDeviceId());
+ if (device == null) {
+ return event;
+ }
+
+ Set<Long> geofences = geofenceManager.getAllDeviceGeofences(position.getDeviceId());
+ if (geofences == null) {
+ return event;
+ }
+ long geofenceId = 0;
+ for (Long geofence : geofences) {
+ if (geofenceManager.getGeofence(geofence).getGeometry()
+ .containsPoint(position.getLatitude(), position.getLongitude())) {
+ geofenceId = geofence;
+ break;
+ }
+ }
+
+ if (device.getGeofenceId() != geofenceId) {
+ try {
+ if (geofenceId == 0) {
+ event = new Event(Event.TYPE_GEOFENCE_EXIT, position.getDeviceId(), position.getId());
+ event.setGeofenceId(device.getGeofenceId());
+ } else {
+ event = new Event(Event.TYPE_GEOFENCE_ENTER, position.getDeviceId(), position.getId());
+ event.setGeofenceId(geofenceId);
+ }
+ if (event != null && !dataManager.getLastEvents(
+ position.getDeviceId(), event.getType(), suppressRepeated).isEmpty()) {
+ event = null;
+ }
+ device.setGeofenceId(geofenceId);
+ dataManager.updateDeviceStatus(device);
+ } catch (SQLException error) {
+ Log.warning(error);
+ }
+
+ }
+ return event;
+ }
+}
diff --git a/src/org/traccar/events/MotionEventHandler.java b/src/org/traccar/events/MotionEventHandler.java
index a3b81ddc4..306fa8a4e 100644
--- a/src/org/traccar/events/MotionEventHandler.java
+++ b/src/org/traccar/events/MotionEventHandler.java
@@ -33,6 +33,9 @@ public class MotionEventHandler extends BaseEventHandler {
return event;
}
String motion = device.getMotion();
+ if (motion == null) {
+ motion = Device.STATUS_STOPPED;
+ }
if (valid && speed > SPEED_THRESHOLD && !motion.equals(Device.STATUS_MOVING)) {
Context.getConnectionManager().updateDevice(position.getDeviceId(), Device.STATUS_MOVING, null);
event = new Event(Event.TYPE_DEVICE_MOVING, position.getDeviceId(), position.getId());
diff --git a/src/org/traccar/geofence/GeofenceCircle.java b/src/org/traccar/geofence/GeofenceCircle.java
new file mode 100644
index 000000000..76d5a2816
--- /dev/null
+++ b/src/org/traccar/geofence/GeofenceCircle.java
@@ -0,0 +1,82 @@
+package org.traccar.geofence;
+
+import java.text.DecimalFormat;
+import java.text.ParseException;
+
+import org.traccar.helper.DistanceCalculator;
+
+public class GeofenceCircle extends GeofenceGeometry {
+
+ private double centerLatitude;
+ private double centerLongitude;
+ private double radius;
+
+ public GeofenceCircle() {
+ super();
+ }
+
+ public GeofenceCircle(String wkt) throws ParseException {
+ super();
+ fromWKT(wkt);
+ }
+
+ public GeofenceCircle(double latitude, double longitude, double radius) {
+ super();
+ this.centerLatitude = latitude;
+ this.centerLongitude = longitude;
+ this.radius = radius;
+ }
+
+ @Override
+ public boolean containsPoint(double latitude, double longitude) {
+ return DistanceCalculator.distance(centerLatitude, centerLongitude, latitude, longitude) <= radius;
+ }
+
+ @Override
+ public String toWKT() {
+ String wkt = "";
+ wkt = "CIRCLE (";
+ wkt += String.valueOf(centerLatitude);
+ wkt += " ";
+ wkt += String.valueOf(centerLongitude);
+ wkt += ", ";
+ DecimalFormat format = new DecimalFormat("0.#");
+ wkt += format.format(radius);
+ wkt += ")";
+ return wkt;
+ }
+
+ @Override
+ public void fromWKT(String wkt) throws ParseException {
+ if (!wkt.startsWith("CIRCLE")) {
+ throw new ParseException("Mismatch geometry type", 0);
+ }
+ String content = wkt.substring(wkt.indexOf("(") + 1, wkt.indexOf(")"));
+ if (content == null || content.equals("")) {
+ throw new ParseException("No content", 0);
+ }
+ String[] commatokens = content.split(",");
+ if (commatokens.length != 2) {
+ throw new ParseException("Not valid content", 0);
+ }
+ String[] tokens = commatokens[0].split("\\s");
+ if (tokens.length != 2) {
+ throw new ParseException("Too much or less coordinates", 0);
+ }
+ try {
+ centerLatitude = Double.parseDouble(tokens[0]);
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[0] + " is not a double", 0);
+ }
+ try {
+ centerLongitude = Double.parseDouble(tokens[1]);
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[1] + " is not a double", 0);
+ }
+ try {
+ radius = Double.parseDouble(commatokens[1]);
+ } catch (NumberFormatException e) {
+ throw new ParseException(commatokens[1] + " is not a double", 0);
+ }
+ }
+}
diff --git a/src/org/traccar/geofence/GeofenceGeometry.java b/src/org/traccar/geofence/GeofenceGeometry.java
new file mode 100644
index 000000000..c8f042413
--- /dev/null
+++ b/src/org/traccar/geofence/GeofenceGeometry.java
@@ -0,0 +1,13 @@
+package org.traccar.geofence;
+
+import java.text.ParseException;
+
+public abstract class GeofenceGeometry {
+
+ public abstract boolean containsPoint(double latitude, double longitude);
+
+ public abstract String toWKT();
+
+ public abstract void fromWKT(String wkt) throws ParseException;
+
+}
diff --git a/src/org/traccar/geofence/GeofencePolygon.java b/src/org/traccar/geofence/GeofencePolygon.java
new file mode 100644
index 000000000..08178375a
--- /dev/null
+++ b/src/org/traccar/geofence/GeofencePolygon.java
@@ -0,0 +1,160 @@
+package org.traccar.geofence;
+
+import java.text.ParseException;
+import java.util.ArrayList;
+
+public class GeofencePolygon extends GeofenceGeometry {
+
+ public GeofencePolygon() {
+ super();
+ }
+
+ public GeofencePolygon(String wkt) throws ParseException {
+ super();
+ fromWKT(wkt);
+ }
+
+ private static class Coordinate {
+
+ public static final double DEGREE360 = 360;
+
+ private double lat;
+ private double lon;
+
+ public double getLat() {
+ return lat;
+ }
+
+ public void setLat(double lat) {
+ this.lat = lat;
+ }
+
+ public double getLon() {
+ return lon;
+ }
+
+ // Need not to confuse algorithm by the abrupt reset of longitude
+ public double getLon360() {
+ return lon + DEGREE360;
+ }
+
+ public void setLon(double lon) {
+ this.lon = lon;
+ }
+ }
+
+ private ArrayList<Coordinate> coordinates;
+
+ private double[] constant;
+ private double[] multiple;
+
+ private void precalc() {
+ if (coordinates == null) {
+ return;
+ }
+ int polyCorners = coordinates.size();
+ int i;
+ int j = polyCorners - 1;
+
+ if (constant != null) {
+ constant = null;
+ }
+ if (multiple != null) {
+ multiple = null;
+ }
+
+ constant = new double[polyCorners];
+ multiple = new double[polyCorners];
+
+
+ for (i = 0; i < polyCorners; j = i++) {
+ if (coordinates.get(j).getLon360() == coordinates.get(i).getLon360()) {
+ constant[i] = coordinates.get(i).getLat();
+ multiple[i] = 0;
+ } else {
+ constant[i] = coordinates.get(i).getLat()
+ - (coordinates.get(i).getLon360() * coordinates.get(j).getLat())
+ / (coordinates.get(j).getLon360() - coordinates.get(i).getLon360())
+ + (coordinates.get(i).getLon360() * coordinates.get(i).getLat())
+ / (coordinates.get(j).getLon360() - coordinates.get(i).getLon360());
+ multiple[i] = (coordinates.get(j).getLat() - coordinates.get(i).getLat())
+ / (coordinates.get(j).getLon360() - coordinates.get(i).getLon360());
+ }
+ }
+ }
+
+ @Override
+ public boolean containsPoint(double latitude, double longitude) {
+
+ int polyCorners = coordinates.size();
+ int i;
+ int j = polyCorners - 1;
+ double longitude360 = longitude + Coordinate.DEGREE360;
+ boolean oddNodes = false;
+
+ for (i = 0; i < polyCorners; j = i++) {
+ if (coordinates.get(i).getLon360() < longitude360
+ && coordinates.get(j).getLon360() >= longitude360
+ || coordinates.get(j).getLon360() < longitude360
+ && coordinates.get(i).getLon360() >= longitude360) {
+ oddNodes ^= longitude360 * multiple[i] + constant[i] < latitude;
+ }
+ }
+ return oddNodes;
+ }
+
+ @Override
+ public String toWKT() {
+ StringBuffer buf = new StringBuffer();
+ buf.append("POLYGON (");
+ for (Coordinate coordinate : coordinates) {
+ buf.append(String.valueOf(coordinate.getLat()));
+ buf.append(" ");
+ buf.append(String.valueOf(coordinate.getLon()));
+ buf.append(", ");
+ }
+ return buf.substring(0, buf.length() - 2) + ")";
+ }
+
+ @Override
+ public void fromWKT(String wkt) throws ParseException {
+ if (coordinates == null) {
+ coordinates = new ArrayList<Coordinate>();
+ } else {
+ coordinates.clear();
+ }
+
+ if (!wkt.startsWith("POLYGON")) {
+ throw new ParseException("Mismatch geometry type", 0);
+ }
+ String content = wkt.substring(wkt.indexOf("(") + 1, wkt.indexOf(")"));
+ if (content == null || content.equals("")) {
+ throw new ParseException("No content", 0);
+ }
+ String[] commatokens = content.split(",");
+ if (commatokens.length < 3) {
+ throw new ParseException("Not valid content", 0);
+ }
+
+ for (String commatoken : commatokens) {
+ String[] tokens = commatoken.trim().split("\\s");
+ if (tokens.length != 2) {
+ throw new ParseException("Here must be two coordinates: " + commatoken, 0);
+ }
+ Coordinate coordinate = new Coordinate();
+ try {
+ coordinate.setLat(Double.parseDouble(tokens[0]));
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[0] + " is not a double", 0);
+ }
+ try {
+ coordinate.setLon(Double.parseDouble(tokens[1]));
+ } catch (NumberFormatException e) {
+ throw new ParseException(tokens[1] + " is not a double", 0);
+ }
+ coordinates.add(coordinate);
+ }
+ precalc();
+ }
+
+}
diff --git a/src/org/traccar/model/Device.java b/src/org/traccar/model/Device.java
index d32f9f851..45c3d46dc 100644
--- a/src/org/traccar/model/Device.java
+++ b/src/org/traccar/model/Device.java
@@ -114,4 +114,13 @@ public class Device {
this.motion = motion;
}
+ private long geofenceId;
+
+ public long getGeofenceId() {
+ return geofenceId;
+ }
+
+ public void setGeofenceId(long geofenceId) {
+ this.geofenceId = geofenceId;
+ }
}
diff --git a/src/org/traccar/model/Event.java b/src/org/traccar/model/Event.java
index 6de885c70..863acd621 100644
--- a/src/org/traccar/model/Event.java
+++ b/src/org/traccar/model/Event.java
@@ -20,16 +20,6 @@ public class Event extends Message {
public Event() {
}
- private long id;
-
- public long getId() {
- return id;
- }
-
- public void setId(long id) {
- this.id = id;
- }
-
public static final String TYPE_COMMAND_RESULT = "commandResult";
public static final String TYPE_DEVICE_ONLINE = "deviceOnline";
@@ -71,4 +61,14 @@ public class Event extends Message {
this.positionId = positionId;
}
+ private long geofenceId = 0;
+
+ public long getGeofenceId() {
+ return geofenceId;
+ }
+
+ public void setGeofenceId(long geofenceId) {
+ this.geofenceId = geofenceId;
+ }
+
}
diff --git a/src/org/traccar/model/Extensible.java b/src/org/traccar/model/Extensible.java
new file mode 100644
index 000000000..b4052dbda
--- /dev/null
+++ b/src/org/traccar/model/Extensible.java
@@ -0,0 +1,56 @@
+package org.traccar.model;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+public class Extensible {
+
+ private long id;
+
+ public long getId() {
+ return id;
+ }
+
+ public void setId(long id) {
+ this.id = id;
+ }
+
+ private Map<String, Object> attributes = new LinkedHashMap<>();
+
+ public Map<String, Object> getAttributes() {
+ return attributes;
+ }
+
+ public void setAttributes(Map<String, Object> attributes) {
+ this.attributes = attributes;
+ }
+
+ public void set(String key, boolean value) {
+ attributes.put(key, value);
+ }
+
+ public void set(String key, int value) {
+ attributes.put(key, value);
+ }
+
+ public void set(String key, long value) {
+ attributes.put(key, value);
+ }
+
+ public void set(String key, double value) {
+ attributes.put(key, value);
+ }
+
+ public void set(String key, String value) {
+ if (value != null && !value.isEmpty()) {
+ attributes.put(key, value);
+ }
+ }
+
+ public void add(Map.Entry<String, Object> entry) {
+ if (entry != null && entry.getValue() != null) {
+ attributes.put(entry.getKey(), entry.getValue());
+ }
+ }
+
+}
diff --git a/src/org/traccar/model/Geofence.java b/src/org/traccar/model/Geofence.java
new file mode 100644
index 000000000..0723c21e0
--- /dev/null
+++ b/src/org/traccar/model/Geofence.java
@@ -0,0 +1,67 @@
+package org.traccar.model;
+
+import java.text.ParseException;
+
+import org.traccar.geofence.GeofenceCircle;
+import org.traccar.geofence.GeofenceGeometry;
+import org.traccar.geofence.GeofencePolygon;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+public class Geofence extends Extensible {
+
+ public static final String TYPE_GEOFENCE_CILCLE = "geofenceCircle";
+ public static final String TYPE_GEOFENCE_POLYGON = "geofencePolygon";
+
+ private String name;
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ private String description;
+
+ public String getDescription() {
+ return description;
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ }
+
+ private String area;
+
+ public String getArea() {
+ return area;
+ }
+
+ public void setArea(String area) throws ParseException {
+
+ if (area.startsWith("CIRCLE")) {
+ geometry = new GeofenceCircle(area);
+ } else if (area.startsWith("POLYGON")) {
+ geometry = new GeofencePolygon(area);
+ } else {
+ throw new ParseException("Unknown geometry type", 0);
+ }
+
+ this.area = area;
+ }
+
+ private GeofenceGeometry geometry;
+
+ @JsonIgnore
+ public GeofenceGeometry getGeometry() {
+ return geometry;
+ }
+
+ public void setGeometry(GeofenceGeometry geometry) {
+ area = geometry.toWKT();
+ this.geometry = geometry;
+ }
+
+}
diff --git a/src/org/traccar/model/GeofencePermission.java b/src/org/traccar/model/GeofencePermission.java
new file mode 100644
index 000000000..38fe7b6c1
--- /dev/null
+++ b/src/org/traccar/model/GeofencePermission.java
@@ -0,0 +1,25 @@
+package org.traccar.model;
+
+public class GeofencePermission {
+
+ private long userId;
+
+ public long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(long userId) {
+ this.userId = userId;
+ }
+
+ private long geofenceId;
+
+ public long getGeofenceId() {
+ return geofenceId;
+ }
+
+ public void setGeofenceId(long geofenceId) {
+ this.geofenceId = geofenceId;
+ }
+
+}
diff --git a/src/org/traccar/model/GroupGeofence.java b/src/org/traccar/model/GroupGeofence.java
new file mode 100644
index 000000000..a8f6bd475
--- /dev/null
+++ b/src/org/traccar/model/GroupGeofence.java
@@ -0,0 +1,25 @@
+package org.traccar.model;
+
+public class GroupGeofence {
+
+ private long groupId;
+
+ public long getGroupId() {
+ return groupId;
+ }
+
+ public void setGroupId(long groupId) {
+ this.groupId = groupId;
+ }
+
+ private long geofenceId;
+
+ public long getGeofenceId() {
+ return geofenceId;
+ }
+
+ public void setGeofenceId(long geofenceId) {
+ this.geofenceId = geofenceId;
+ }
+
+}
diff --git a/src/org/traccar/model/Message.java b/src/org/traccar/model/Message.java
index 8722acc16..5015b9339 100644
--- a/src/org/traccar/model/Message.java
+++ b/src/org/traccar/model/Message.java
@@ -15,10 +15,7 @@
*/
package org.traccar.model;
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-public class Message {
+public class Message extends Extensible {
private long deviceId;
@@ -40,42 +37,4 @@ public class Message {
this.type = type;
}
- private Map<String, Object> attributes = new LinkedHashMap<>();
-
- public Map<String, Object> getAttributes() {
- return attributes;
- }
-
- public void setAttributes(Map<String, Object> attributes) {
- this.attributes = attributes;
- }
-
- public void set(String key, boolean value) {
- attributes.put(key, value);
- }
-
- public void set(String key, int value) {
- attributes.put(key, value);
- }
-
- public void set(String key, long value) {
- attributes.put(key, value);
- }
-
- public void set(String key, double value) {
- attributes.put(key, value);
- }
-
- public void set(String key, String value) {
- if (value != null && !value.isEmpty()) {
- attributes.put(key, value);
- }
- }
-
- public void add(Map.Entry<String, Object> entry) {
- if (entry != null && entry.getValue() != null) {
- attributes.put(entry.getKey(), entry.getValue());
- }
- }
-
}
diff --git a/src/org/traccar/model/Position.java b/src/org/traccar/model/Position.java
index 22d1be846..b4079dae6 100644
--- a/src/org/traccar/model/Position.java
+++ b/src/org/traccar/model/Position.java
@@ -66,16 +66,6 @@ public class Position extends Message {
public static final String PREFIX_IO = "io";
public static final String PREFIX_COUNT = "count";
- private long id;
-
- public long getId() {
- return id;
- }
-
- public void setId(long id) {
- this.id = id;
- }
-
private String protocol;
public String getProtocol() {
diff --git a/src/org/traccar/model/UserDeviceGeofence.java b/src/org/traccar/model/UserDeviceGeofence.java
new file mode 100644
index 000000000..c84aa46b8
--- /dev/null
+++ b/src/org/traccar/model/UserDeviceGeofence.java
@@ -0,0 +1,35 @@
+package org.traccar.model;
+
+public class UserDeviceGeofence {
+
+ private long userId;
+
+ public long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(long userId) {
+ this.userId = userId;
+ }
+
+ private long deviceId;
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ private long geofenceId;
+
+ public long getGeofenceId() {
+ return geofenceId;
+ }
+
+ public void setGeofenceId(long geofenceId) {
+ this.geofenceId = geofenceId;
+ }
+
+}
diff --git a/src/org/traccar/web/WebServer.java b/src/org/traccar/web/WebServer.java
index 751db7a33..c06ee5d35 100644
--- a/src/org/traccar/web/WebServer.java
+++ b/src/org/traccar/web/WebServer.java
@@ -44,7 +44,11 @@ import org.traccar.api.resource.GroupResource;
import org.traccar.api.resource.DeviceResource;
import org.traccar.api.resource.PositionResource;
import org.traccar.api.resource.CommandTypeResource;
+import org.traccar.api.resource.DeviceGeofenceResource;
import org.traccar.api.resource.EventResource;
+import org.traccar.api.resource.GeofencePermissionResource;
+import org.traccar.api.resource.GeofenceResource;
+import org.traccar.api.resource.GroupGeofenceResource;
import org.traccar.helper.Log;
import javax.naming.InitialContext;
@@ -150,7 +154,8 @@ public class WebServer {
resourceConfig.registerClasses(ServerResource.class, SessionResource.class, CommandResource.class,
GroupPermissionResource.class, DevicePermissionResource.class, UserResource.class,
GroupResource.class, DeviceResource.class, PositionResource.class,
- CommandTypeResource.class, EventResource.class);
+ CommandTypeResource.class, EventResource.class, GeofenceResource.class,
+ DeviceGeofenceResource.class, GeofencePermissionResource.class, GroupGeofenceResource.class);
servletHandler.addServlet(new ServletHolder(new ServletContainer(resourceConfig)), "/*");
handlers.addHandler(servletHandler);
diff --git a/test/org/traccar/geofence/GeofenceCircleTest.java b/test/org/traccar/geofence/GeofenceCircleTest.java
new file mode 100644
index 000000000..820f34e34
--- /dev/null
+++ b/test/org/traccar/geofence/GeofenceCircleTest.java
@@ -0,0 +1,36 @@
+package org.traccar.geofence;
+
+import java.text.ParseException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GeofenceCircleTest {
+
+ @Test
+ public void testCircleWKT() {
+ String test = "CIRCLE (55.75414 37.6204, 100)";
+ GeofenceGeometry geofenceGeometry = new GeofenceCircle();
+ try {
+ geofenceGeometry.fromWKT(test);
+ } catch (ParseException e){
+ Assert.assertTrue("ParseExceprion: " + e.getMessage(), true);
+ }
+ Assert.assertEquals(geofenceGeometry.toWKT(), test);
+ }
+
+ @Test
+ public void testContainsCircle() {
+ String test = "CIRCLE (55.75414 37.6204, 100)";
+ GeofenceGeometry geofenceGeometry = new GeofenceCircle();
+ try {
+ geofenceGeometry.fromWKT(test);
+ } catch (ParseException e){
+ Assert.assertTrue("ParseExceprion: " + e.getMessage(), true);
+ }
+
+ Assert.assertTrue(geofenceGeometry.containsPoint(55.75477, 37.62025));
+
+ Assert.assertTrue(!geofenceGeometry.containsPoint(55.75545, 37.61921));
+ }
+}
diff --git a/test/org/traccar/geofence/GeofencePolygonTest.java b/test/org/traccar/geofence/GeofencePolygonTest.java
new file mode 100644
index 000000000..cc0ed77ad
--- /dev/null
+++ b/test/org/traccar/geofence/GeofencePolygonTest.java
@@ -0,0 +1,38 @@
+package org.traccar.geofence;
+
+import java.text.ParseException;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+public class GeofencePolygonTest {
+
+ @Test
+ public void testPolygonWKT() {
+ String test = "POLYGON (55.75474 37.61823, 55.75513 37.61888, 55.7535 37.6222, 55.75315 37.62165)";
+ GeofenceGeometry geofenceGeometry = new GeofencePolygon();
+ try {
+ geofenceGeometry.fromWKT(test);
+ } catch (ParseException e){
+ Assert.assertTrue("ParseExceprion: " + e.getMessage(), true);
+ }
+ Assert.assertEquals(geofenceGeometry.toWKT(), test);
+ }
+
+ @Test
+ public void testContainsPolygon() {
+ String test = "POLYGON (55.75474 37.61823, 55.75513 37.61888, 55.7535 37.6222, 55.75315 37.62165)";
+ GeofenceGeometry geofenceGeometry = new GeofencePolygon();
+ try {
+ geofenceGeometry.fromWKT(test);
+ } catch (ParseException e){
+ Assert.assertTrue("ParseExceprion: " + e.getMessage(), true);
+ }
+
+ Assert.assertTrue(geofenceGeometry.containsPoint(55.75476, 37.61915));
+
+ Assert.assertTrue(!geofenceGeometry.containsPoint(55.75545, 37.61921));
+
+ }
+
+}
diff --git a/web/l10n/en.json b/web/l10n/en.json
index f4bd253ea..48dc6533c 100644
--- a/web/l10n/en.json
+++ b/web/l10n/en.json
@@ -105,5 +105,5 @@
"eventDeviceOverspeed": "Device exceeds the speed",
"eventCommandResult": "Command result: ",
"eventGeofenceEnter": "Device has entered geofence",
- "eventGeofenceEnter": "Device has exited geofence"
+ "eventGeofenceExit": "Device has exited geofence"
} \ No newline at end of file