aboutsummaryrefslogtreecommitdiff
path: root/src/main/java/org/traccar/reports
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/org/traccar/reports')
-rw-r--r--src/main/java/org/traccar/reports/CombinedReportProvider.java83
-rw-r--r--src/main/java/org/traccar/reports/CsvExportProvider.java76
-rw-r--r--src/main/java/org/traccar/reports/Events.java133
-rw-r--r--src/main/java/org/traccar/reports/EventsReportProvider.java170
-rw-r--r--src/main/java/org/traccar/reports/GpxExportProvider.java76
-rw-r--r--src/main/java/org/traccar/reports/KmlExportProvider.java80
-rw-r--r--src/main/java/org/traccar/reports/ReportUtils.java368
-rw-r--r--src/main/java/org/traccar/reports/Route.java85
-rw-r--r--src/main/java/org/traccar/reports/RouteReportProvider.java113
-rw-r--r--src/main/java/org/traccar/reports/Stops.java102
-rw-r--r--src/main/java/org/traccar/reports/StopsReportProvider.java104
-rw-r--r--src/main/java/org/traccar/reports/Summary.java151
-rw-r--r--src/main/java/org/traccar/reports/SummaryReportProvider.java192
-rw-r--r--src/main/java/org/traccar/reports/Trips.java100
-rw-r--r--src/main/java/org/traccar/reports/TripsReportProvider.java104
-rw-r--r--src/main/java/org/traccar/reports/common/ExpressionEvaluatorFactory.java58
-rw-r--r--src/main/java/org/traccar/reports/common/ReportExecutor.java25
-rw-r--r--src/main/java/org/traccar/reports/common/ReportMailer.java65
-rw-r--r--src/main/java/org/traccar/reports/common/ReportUtils.java406
-rw-r--r--src/main/java/org/traccar/reports/common/TripsConfig.java73
-rw-r--r--src/main/java/org/traccar/reports/model/BaseReportItem.java (renamed from src/main/java/org/traccar/reports/model/BaseReport.java)2
-rw-r--r--src/main/java/org/traccar/reports/model/CombinedReportItem.java65
-rw-r--r--src/main/java/org/traccar/reports/model/DeviceReportSection.java (renamed from src/main/java/org/traccar/reports/model/DeviceReport.java)2
-rw-r--r--src/main/java/org/traccar/reports/model/StopReportItem.java (renamed from src/main/java/org/traccar/reports/model/StopReport.java)2
-rw-r--r--src/main/java/org/traccar/reports/model/SummaryReportItem.java (renamed from src/main/java/org/traccar/reports/model/SummaryReport.java)2
-rw-r--r--src/main/java/org/traccar/reports/model/TripReportItem.java (renamed from src/main/java/org/traccar/reports/model/TripReport.java)2
-rw-r--r--src/main/java/org/traccar/reports/model/TripsConfig.java105
27 files changed, 1695 insertions, 1049 deletions
diff --git a/src/main/java/org/traccar/reports/CombinedReportProvider.java b/src/main/java/org/traccar/reports/CombinedReportProvider.java
new file mode 100644
index 000000000..bad3a61b3
--- /dev/null
+++ b/src/main/java/org/traccar/reports/CombinedReportProvider.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2023 Anton Tananaev (anton@traccar.org)
+ *
+ * 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.reports;
+
+import org.traccar.helper.model.DeviceUtil;
+import org.traccar.helper.model.PositionUtil;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.reports.common.ReportUtils;
+import org.traccar.reports.model.CombinedReportItem;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Order;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class CombinedReportProvider {
+
+ private static final Set<String> EXCLUDE_TYPES = Set.of(Event.TYPE_DEVICE_MOVING);
+
+ private final ReportUtils reportUtils;
+ private final Storage storage;
+
+ @Inject
+ public CombinedReportProvider(ReportUtils reportUtils, Storage storage) {
+ this.reportUtils = reportUtils;
+ this.storage = storage;
+ }
+
+ public Collection<CombinedReportItem> getObjects(
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws StorageException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<CombinedReportItem> result = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ CombinedReportItem item = new CombinedReportItem();
+ item.setDeviceId(device.getId());
+ var positions = PositionUtil.getPositions(storage, device.getId(), from, to);
+ item.setRoute(positions.stream()
+ .map(p -> new double[] {p.getLongitude(), p.getLatitude()})
+ .collect(Collectors.toList()));
+ var events = storage.getObjects(Event.class, new Request(
+ new Columns.All(),
+ new Condition.And(
+ new Condition.Equals("deviceId", device.getId()),
+ new Condition.Between("eventTime", "from", from, "to", to)),
+ new Order("eventTime")));
+ item.setEvents(events.stream()
+ .filter(e -> e.getPositionId() > 0 && !EXCLUDE_TYPES.contains(e.getType()))
+ .collect(Collectors.toList()));
+ var eventPositions = events.stream()
+ .map(Event::getPositionId)
+ .collect(Collectors.toSet());
+ item.setPositions(positions.stream()
+ .filter(p -> eventPositions.contains(p.getId()))
+ .collect(Collectors.toList()));
+ result.add(item);
+ }
+ return result;
+ }
+}
diff --git a/src/main/java/org/traccar/reports/CsvExportProvider.java b/src/main/java/org/traccar/reports/CsvExportProvider.java
new file mode 100644
index 000000000..521dc120a
--- /dev/null
+++ b/src/main/java/org/traccar/reports/CsvExportProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * 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.reports;
+
+import org.traccar.helper.DateUtil;
+import org.traccar.helper.model.PositionUtil;
+import org.traccar.model.Position;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+
+import jakarta.inject.Inject;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+public class CsvExportProvider {
+
+ private final Storage storage;
+
+ @Inject
+ public CsvExportProvider(Storage storage) {
+ this.storage = storage;
+ }
+
+ public void generate(
+ OutputStream outputStream, long deviceId, Date from, Date to) throws StorageException {
+
+ var positions = PositionUtil.getPositions(storage, deviceId, from, to);
+
+ var attributes = positions.stream()
+ .flatMap((position -> position.getAttributes().keySet().stream()))
+ .collect(Collectors.toUnmodifiableSet());
+
+ var properties = new LinkedHashMap<String, Function<Position, Object>>();
+ properties.put("id", Position::getId);
+ properties.put("deviceId", Position::getDeviceId);
+ properties.put("protocol", Position::getProtocol);
+ properties.put("serverTime", position -> DateUtil.formatDate(position.getServerTime()));
+ properties.put("deviceTime", position -> DateUtil.formatDate(position.getDeviceTime()));
+ properties.put("fixTime", position -> DateUtil.formatDate(position.getFixTime()));
+ properties.put("valid", Position::getValid);
+ properties.put("latitude", Position::getLatitude);
+ properties.put("longitude", Position::getLongitude);
+ properties.put("altitude", Position::getAltitude);
+ properties.put("speed", Position::getSpeed);
+ properties.put("course", Position::getCourse);
+ properties.put("address", Position::getAddress);
+ properties.put("accuracy", Position::getAccuracy);
+ attributes.forEach(key -> properties.put(key, position -> position.getAttributes().get(key)));
+
+ try (PrintWriter writer = new PrintWriter(outputStream)) {
+ writer.println(String.join(",", properties.keySet()));
+ positions.forEach(position -> writer.println(properties.values().stream()
+ .map(f -> Objects.toString(f.apply(position), ""))
+ .collect(Collectors.joining(","))));
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/Events.java b/src/main/java/org/traccar/reports/Events.java
deleted file mode 100644
index e4b905702..000000000
--- a/src/main/java/org/traccar/reports/Events.java
+++ /dev/null
@@ -1,133 +0,0 @@
-/*
- * Copyright 2016 - 2018 Anton Tananaev (anton@traccar.org)
- * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
- *
- * 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.reports;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.Iterator;
-
-import org.apache.poi.ss.util.WorkbookUtil;
-import org.traccar.Context;
-import org.traccar.model.Device;
-import org.traccar.model.Event;
-import org.traccar.model.Geofence;
-import org.traccar.model.Group;
-import org.traccar.model.Maintenance;
-import org.traccar.reports.model.DeviceReport;
-import org.traccar.storage.StorageException;
-
-public final class Events {
-
- private Events() {
- }
-
- public static Collection<Event> getObjects(long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Collection<String> types, Date from, Date to) throws StorageException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<Event> result = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- Collection<Event> events = Context.getDataManager().getEvents(deviceId, from, to);
- boolean all = types.isEmpty() || types.contains(Event.ALL_EVENTS);
- for (Event event : events) {
- if (all || types.contains(event.getType())) {
- long geofenceId = event.getGeofenceId();
- long maintenanceId = event.getMaintenanceId();
- if ((geofenceId == 0 || Context.getGeofenceManager().checkItemPermission(userId, geofenceId))
- && (maintenanceId == 0
- || Context.getMaintenancesManager().checkItemPermission(userId, maintenanceId))) {
- result.add(event);
- }
- }
- }
- }
- return result;
- }
-
- public static void getExcel(OutputStream outputStream,
- long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Collection<String> types, Date from, Date to) throws StorageException, IOException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<DeviceReport> devicesEvents = new ArrayList<>();
- ArrayList<String> sheetNames = new ArrayList<>();
- HashMap<Long, String> geofenceNames = new HashMap<>();
- HashMap<Long, String> maintenanceNames = new HashMap<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- Collection<Event> events = Context.getDataManager().getEvents(deviceId, from, to);
- boolean all = types.isEmpty() || types.contains(Event.ALL_EVENTS);
- for (Iterator<Event> iterator = events.iterator(); iterator.hasNext();) {
- Event event = iterator.next();
- if (all || types.contains(event.getType())) {
- long geofenceId = event.getGeofenceId();
- long maintenanceId = event.getMaintenanceId();
- if (geofenceId != 0) {
- if (Context.getGeofenceManager().checkItemPermission(userId, geofenceId)) {
- Geofence geofence = Context.getGeofenceManager().getById(geofenceId);
- if (geofence != null) {
- geofenceNames.put(geofenceId, geofence.getName());
- }
- } else {
- iterator.remove();
- }
- } else if (maintenanceId != 0) {
- if (Context.getMaintenancesManager().checkItemPermission(userId, maintenanceId)) {
- Maintenance maintenance = Context.getMaintenancesManager().getById(maintenanceId);
- if (maintenance != null) {
- maintenanceNames.put(maintenanceId, maintenance.getName());
- }
- } else {
- iterator.remove();
- }
- }
- } else {
- iterator.remove();
- }
- }
- DeviceReport deviceEvents = new DeviceReport();
- Device device = Context.getIdentityManager().getById(deviceId);
- deviceEvents.setDeviceName(device.getName());
- sheetNames.add(WorkbookUtil.createSafeSheetName(deviceEvents.getDeviceName()));
- if (device.getGroupId() != 0) {
- Group group = Context.getGroupsManager().getById(device.getGroupId());
- if (group != null) {
- deviceEvents.setGroupName(group.getName());
- }
- }
- deviceEvents.setObjects(events);
- devicesEvents.add(deviceEvents);
- }
- String templatePath = Context.getConfig().getString("report.templatesPath",
- "templates/export/");
- try (InputStream inputStream = new FileInputStream(templatePath + "/events.xlsx")) {
- org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
- jxlsContext.putVar("devices", devicesEvents);
- jxlsContext.putVar("sheetNames", sheetNames);
- jxlsContext.putVar("geofenceNames", geofenceNames);
- jxlsContext.putVar("maintenanceNames", maintenanceNames);
- jxlsContext.putVar("from", from);
- jxlsContext.putVar("to", to);
- ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
- }
- }
-}
diff --git a/src/main/java/org/traccar/reports/EventsReportProvider.java b/src/main/java/org/traccar/reports/EventsReportProvider.java
new file mode 100644
index 000000000..f252f28cc
--- /dev/null
+++ b/src/main/java/org/traccar/reports/EventsReportProvider.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2018 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.reports;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.model.DeviceUtil;
+import org.traccar.model.Device;
+import org.traccar.model.Event;
+import org.traccar.model.Geofence;
+import org.traccar.model.Group;
+import org.traccar.model.Maintenance;
+import org.traccar.model.Position;
+import org.traccar.reports.common.ReportUtils;
+import org.traccar.reports.model.DeviceReportSection;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Order;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+
+public class EventsReportProvider {
+
+ private final Config config;
+ private final ReportUtils reportUtils;
+ private final Storage storage;
+
+ @Inject
+ public EventsReportProvider(Config config, ReportUtils reportUtils, Storage storage) {
+ this.config = config;
+ this.reportUtils = reportUtils;
+ this.storage = storage;
+ }
+
+ private List<Event> getEvents(long deviceId, Date from, Date to) throws StorageException {
+ return storage.getObjects(Event.class, new Request(
+ new Columns.All(),
+ new Condition.And(
+ new Condition.Equals("deviceId", deviceId),
+ new Condition.Between("eventTime", "from", from, "to", to)),
+ new Order("eventTime")));
+ }
+
+ public Collection<Event> getObjects(
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Collection<String> types, Date from, Date to) throws StorageException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<Event> result = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ Collection<Event> events = getEvents(device.getId(), from, to);
+ boolean all = types.isEmpty() || types.contains(Event.ALL_EVENTS);
+ for (Event event : events) {
+ if (all || types.contains(event.getType())) {
+ long geofenceId = event.getGeofenceId();
+ long maintenanceId = event.getMaintenanceId();
+ if ((geofenceId == 0 || reportUtils.getObject(userId, Geofence.class, geofenceId) != null)
+ && (maintenanceId == 0
+ || reportUtils.getObject(userId, Maintenance.class, maintenanceId) != null)) {
+ result.add(event);
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ public void getExcel(
+ OutputStream outputStream, long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Collection<String> types, Date from, Date to) throws StorageException, IOException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<DeviceReportSection> devicesEvents = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ HashMap<Long, String> geofenceNames = new HashMap<>();
+ HashMap<Long, String> maintenanceNames = new HashMap<>();
+ HashMap<Long, Position> positions = new HashMap<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ Collection<Event> events = getEvents(device.getId(), from, to);
+ boolean all = types.isEmpty() || types.contains(Event.ALL_EVENTS);
+ for (Iterator<Event> iterator = events.iterator(); iterator.hasNext();) {
+ Event event = iterator.next();
+ if (all || types.contains(event.getType())) {
+ long geofenceId = event.getGeofenceId();
+ long maintenanceId = event.getMaintenanceId();
+ if (geofenceId != 0) {
+ Geofence geofence = reportUtils.getObject(userId, Geofence.class, geofenceId);
+ if (geofence != null) {
+ geofenceNames.put(geofenceId, geofence.getName());
+ } else {
+ iterator.remove();
+ }
+ } else if (maintenanceId != 0) {
+ Maintenance maintenance = reportUtils.getObject(userId, Maintenance.class, maintenanceId);
+ if (maintenance != null) {
+ maintenanceNames.put(maintenanceId, maintenance.getName());
+ } else {
+ iterator.remove();
+ }
+ }
+ } else {
+ iterator.remove();
+ }
+ }
+ for (Event event : events) {
+ long positionId = event.getPositionId();
+ if (positionId > 0) {
+ Position position = storage.getObject(Position.class, new Request(
+ new Columns.All(), new Condition.Equals("id", positionId)));
+ positions.put(positionId, position);
+ }
+ }
+ DeviceReportSection deviceEvents = new DeviceReportSection();
+ deviceEvents.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(deviceEvents.getDeviceName()));
+ if (device.getGroupId() > 0) {
+ Group group = storage.getObject(Group.class, new Request(
+ new Columns.All(), new Condition.Equals("id", device.getGroupId())));
+ if (group != null) {
+ deviceEvents.setGroupName(group.getName());
+ }
+ }
+ deviceEvents.setObjects(events);
+ devicesEvents.add(deviceEvents);
+ }
+
+ File file = Paths.get(config.getString(Keys.TEMPLATES_ROOT), "export", "events.xlsx").toFile();
+ try (InputStream inputStream = new FileInputStream(file)) {
+ var context = reportUtils.initializeContext(userId);
+ context.putVar("devices", devicesEvents);
+ context.putVar("sheetNames", sheetNames);
+ context.putVar("geofenceNames", geofenceNames);
+ context.putVar("maintenanceNames", maintenanceNames);
+ context.putVar("positions", positions);
+ context.putVar("from", from);
+ context.putVar("to", to);
+ reportUtils.processTemplateWithSheets(inputStream, outputStream, context);
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/reports/GpxExportProvider.java b/src/main/java/org/traccar/reports/GpxExportProvider.java
new file mode 100644
index 000000000..1c45b6416
--- /dev/null
+++ b/src/main/java/org/traccar/reports/GpxExportProvider.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * 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.reports;
+
+import org.traccar.helper.DateUtil;
+import org.traccar.helper.model.PositionUtil;
+import org.traccar.model.Device;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.util.Date;
+
+public class GpxExportProvider {
+
+ private final Storage storage;
+
+ @Inject
+ public GpxExportProvider(Storage storage) {
+ this.storage = storage;
+ }
+
+ public void generate(
+ OutputStream outputStream, long deviceId, Date from, Date to) throws StorageException {
+
+ var device = storage.getObject(Device.class, new Request(
+ new Columns.All(), new Condition.Equals("id", deviceId)));
+ var positions = PositionUtil.getPositions(storage, deviceId, from, to);
+
+ try (PrintWriter writer = new PrintWriter(outputStream)) {
+ writer.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ writer.print("<gpx version=\"1.0\">");
+ writer.print("<trk>");
+ writer.print("<name>");
+ writer.print(device.getName());
+ writer.print("</name>");
+ writer.print("<trkseg>");
+ positions.forEach(position -> {
+ writer.print("<trkpt lat=\"");
+ writer.print(position.getLatitude());
+ writer.print("\" lon=\"");
+ writer.print(position.getLongitude());
+ writer.print("\">");
+ writer.print("<ele>");
+ writer.print(position.getAltitude());
+ writer.print("</ele>");
+ writer.print("<time>");
+ writer.print(DateUtil.formatDate(position.getFixTime()));
+ writer.print("</time>");
+ writer.print("</trkpt>");
+ });
+ writer.print("</trkseg>");
+ writer.print("</trk>");
+ writer.print("</gpx>");
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/KmlExportProvider.java b/src/main/java/org/traccar/reports/KmlExportProvider.java
new file mode 100644
index 000000000..24dca018c
--- /dev/null
+++ b/src/main/java/org/traccar/reports/KmlExportProvider.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2022 Anton Tananaev (anton@traccar.org)
+ *
+ * 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.reports;
+
+import org.traccar.helper.model.PositionUtil;
+import org.traccar.model.Device;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.io.OutputStream;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.stream.Collectors;
+
+public class KmlExportProvider {
+
+ private final Storage storage;
+
+ @Inject
+ public KmlExportProvider(Storage storage) {
+ this.storage = storage;
+ }
+
+ public void generate(
+ OutputStream outputStream, long deviceId, Date from, Date to) throws StorageException {
+
+ var device = storage.getObject(Device.class, new Request(
+ new Columns.All(), new Condition.Equals("id", deviceId)));
+ var positions = PositionUtil.getPositions(storage, deviceId, from, to);
+
+ var dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+
+ try (PrintWriter writer = new PrintWriter(outputStream)) {
+ writer.print("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
+ writer.print("<kml xmlns=\"http://www.opengis.net/kml/2.2\">");
+ writer.print("<Document>");
+ writer.print("<name>");
+ writer.print(device.getName());
+ writer.print("</name>");
+ writer.print("<Placemark>");
+ writer.print("<name>");
+ writer.print(dateFormat.format(from));
+ writer.print(" - ");
+ writer.print(dateFormat.format(to));
+ writer.print("</name>");
+ writer.print("<LineString>");
+ writer.print("<extrude>1</extrude>");
+ writer.print("<tessellate>1</tessellate>");
+ writer.print("<altitudeMode>absolute</altitudeMode>");
+ writer.print("<coordinates>");
+ writer.print(positions.stream()
+ .map((p -> String.format("%f,%f,%f", p.getLongitude(), p.getLatitude(), p.getAltitude())))
+ .collect(Collectors.joining(" ")));
+ writer.print("</coordinates>");
+ writer.print("</LineString>");
+ writer.print("</Placemark>");
+ writer.print("</Document>");
+ writer.print("</kml>");
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/ReportUtils.java b/src/main/java/org/traccar/reports/ReportUtils.java
deleted file mode 100644
index 58674beae..000000000
--- a/src/main/java/org/traccar/reports/ReportUtils.java
+++ /dev/null
@@ -1,368 +0,0 @@
-/*
- * Copyright 2016 - 2020 Anton Tananaev (anton@traccar.org)
- * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org)
- *
- * 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.reports;
-
-import org.apache.velocity.tools.generic.DateTool;
-import org.apache.velocity.tools.generic.NumberTool;
-import org.jxls.area.Area;
-import org.jxls.builder.xls.XlsCommentAreaBuilder;
-import org.jxls.common.CellRef;
-import org.jxls.formula.StandardFormulaProcessor;
-import org.jxls.transform.Transformer;
-import org.jxls.transform.poi.PoiTransformer;
-import org.jxls.util.TransformerFactory;
-import org.traccar.Context;
-import org.traccar.config.Keys;
-import org.traccar.database.DeviceManager;
-import org.traccar.database.IdentityManager;
-import org.traccar.handler.events.MotionEventHandler;
-import org.traccar.helper.UnitsConverter;
-import org.traccar.model.DeviceState;
-import org.traccar.model.Driver;
-import org.traccar.model.Event;
-import org.traccar.model.Position;
-import org.traccar.reports.model.BaseReport;
-import org.traccar.reports.model.StopReport;
-import org.traccar.reports.model.TripReport;
-import org.traccar.reports.model.TripsConfig;
-
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.TimeZone;
-
-public final class ReportUtils {
-
- private ReportUtils() {
- }
-
- public static void checkPeriodLimit(Date from, Date to) {
- long limit = Context.getConfig().getLong(Keys.REPORT_PERIOD_LIMIT) * 1000;
- if (limit > 0 && to.getTime() - from.getTime() > limit) {
- throw new IllegalArgumentException("Time period exceeds the limit");
- }
- }
-
- public static String getDistanceUnit(long userId) {
- return (String) Context.getPermissionsManager().lookupAttribute(userId, "distanceUnit", "km");
- }
-
- public static String getSpeedUnit(long userId) {
- return (String) Context.getPermissionsManager().lookupAttribute(userId, "speedUnit", "kn");
- }
-
- public static String getVolumeUnit(long userId) {
- return (String) Context.getPermissionsManager().lookupAttribute(userId, "volumeUnit", "ltr");
- }
-
- public static TimeZone getTimezone(long userId) {
- String timezone = (String) Context.getPermissionsManager().lookupAttribute(userId, "timezone", null);
- return timezone != null ? TimeZone.getTimeZone(timezone) : TimeZone.getDefault();
- }
-
- public static Collection<Long> getDeviceList(Collection<Long> deviceIds, Collection<Long> groupIds) {
- Collection<Long> result = new ArrayList<>(deviceIds);
- for (long groupId : groupIds) {
- result.addAll(Context.getPermissionsManager().getGroupDevices(groupId));
- }
- return result;
- }
-
- public static double calculateDistance(Position firstPosition, Position lastPosition) {
- return calculateDistance(firstPosition, lastPosition, true);
- }
-
- public static double calculateDistance(Position firstPosition, Position lastPosition, boolean useOdometer) {
- double distance = 0.0;
- double firstOdometer = firstPosition.getDouble(Position.KEY_ODOMETER);
- double lastOdometer = lastPosition.getDouble(Position.KEY_ODOMETER);
-
- if (useOdometer && firstOdometer != 0.0 && lastOdometer != 0.0) {
- distance = lastOdometer - firstOdometer;
- } else if (firstPosition.getAttributes().containsKey(Position.KEY_TOTAL_DISTANCE)
- && lastPosition.getAttributes().containsKey(Position.KEY_TOTAL_DISTANCE)) {
- distance = lastPosition.getDouble(Position.KEY_TOTAL_DISTANCE)
- - firstPosition.getDouble(Position.KEY_TOTAL_DISTANCE);
- }
- return distance;
- }
-
- public static double calculateFuel(Position firstPosition, Position lastPosition) {
-
- if (firstPosition.getAttributes().get(Position.KEY_FUEL_LEVEL) != null
- && lastPosition.getAttributes().get(Position.KEY_FUEL_LEVEL) != null) {
-
- BigDecimal value = BigDecimal.valueOf(firstPosition.getDouble(Position.KEY_FUEL_LEVEL)
- - lastPosition.getDouble(Position.KEY_FUEL_LEVEL));
- return value.setScale(1, RoundingMode.HALF_EVEN).doubleValue();
- }
- return 0;
- }
-
- public static String findDriver(Position firstPosition, Position lastPosition) {
- if (firstPosition.getAttributes().containsKey(Position.KEY_DRIVER_UNIQUE_ID)) {
- return firstPosition.getString(Position.KEY_DRIVER_UNIQUE_ID);
- } else if (lastPosition.getAttributes().containsKey(Position.KEY_DRIVER_UNIQUE_ID)) {
- return lastPosition.getString(Position.KEY_DRIVER_UNIQUE_ID);
- }
- return null;
- }
-
- public static String findDriverName(String driverUniqueId) {
- if (driverUniqueId != null && Context.getDriversManager() != null) {
- Driver driver = Context.getDriversManager().getDriverByUniqueId(driverUniqueId);
- if (driver != null) {
- return driver.getName();
- }
- }
- return null;
- }
-
- public static org.jxls.common.Context initializeContext(long userId) {
- org.jxls.common.Context jxlsContext = PoiTransformer.createInitialContext();
- jxlsContext.putVar("distanceUnit", getDistanceUnit(userId));
- jxlsContext.putVar("speedUnit", getSpeedUnit(userId));
- jxlsContext.putVar("volumeUnit", getVolumeUnit(userId));
- jxlsContext.putVar("webUrl", Context.getVelocityEngine().getProperty("web.url"));
- jxlsContext.putVar("dateTool", new DateTool());
- jxlsContext.putVar("numberTool", new NumberTool());
- jxlsContext.putVar("timezone", getTimezone(userId));
- jxlsContext.putVar("locale", Locale.getDefault());
- jxlsContext.putVar("bracketsRegex", "[\\{\\}\"]");
- return jxlsContext;
- }
-
- public static void processTemplateWithSheets(
- InputStream templateStream, OutputStream targetStream,
- org.jxls.common.Context jxlsContext) throws IOException {
-
- Transformer transformer = TransformerFactory.createTransformer(templateStream, targetStream);
- List<Area> xlsAreas = new XlsCommentAreaBuilder(transformer).build();
- for (Area xlsArea : xlsAreas) {
- xlsArea.applyAt(new CellRef(xlsArea.getStartCellRef().getCellName()), jxlsContext);
- xlsArea.setFormulaProcessor(new StandardFormulaProcessor());
- xlsArea.processFormulas();
- }
- transformer.deleteSheet(xlsAreas.get(0).getStartCellRef().getSheetName());
- transformer.write();
- }
-
- private static TripReport calculateTrip(
- ArrayList<Position> positions, int startIndex, int endIndex, boolean ignoreOdometer) {
-
- Position startTrip = positions.get(startIndex);
- Position endTrip = positions.get(endIndex);
-
- double speedMax = 0;
- for (int i = startIndex; i <= endIndex; i++) {
- double speed = positions.get(i).getSpeed();
- if (speed > speedMax) {
- speedMax = speed;
- }
- }
-
- TripReport trip = new TripReport();
-
- long tripDuration = endTrip.getFixTime().getTime() - startTrip.getFixTime().getTime();
- long deviceId = startTrip.getDeviceId();
- trip.setDeviceId(deviceId);
- trip.setDeviceName(Context.getIdentityManager().getById(deviceId).getName());
-
- trip.setStartPositionId(startTrip.getId());
- trip.setStartLat(startTrip.getLatitude());
- trip.setStartLon(startTrip.getLongitude());
- trip.setStartTime(startTrip.getFixTime());
- String startAddress = startTrip.getAddress();
- if (startAddress == null && Context.getGeocoder() != null
- && Context.getConfig().getBoolean(Keys.GEOCODER_ON_REQUEST)) {
- startAddress = Context.getGeocoder().getAddress(startTrip.getLatitude(), startTrip.getLongitude(), null);
- }
- trip.setStartAddress(startAddress);
-
- trip.setEndPositionId(endTrip.getId());
- trip.setEndLat(endTrip.getLatitude());
- trip.setEndLon(endTrip.getLongitude());
- trip.setEndTime(endTrip.getFixTime());
- String endAddress = endTrip.getAddress();
- if (endAddress == null && Context.getGeocoder() != null
- && Context.getConfig().getBoolean(Keys.GEOCODER_ON_REQUEST)) {
- endAddress = Context.getGeocoder().getAddress(endTrip.getLatitude(), endTrip.getLongitude(), null);
- }
- trip.setEndAddress(endAddress);
-
- trip.setDistance(calculateDistance(startTrip, endTrip, !ignoreOdometer));
- trip.setDuration(tripDuration);
- if (tripDuration > 0) {
- trip.setAverageSpeed(UnitsConverter.knotsFromMps(trip.getDistance() * 1000 / tripDuration));
- }
- trip.setMaxSpeed(speedMax);
- trip.setSpentFuel(calculateFuel(startTrip, endTrip));
-
- trip.setDriverUniqueId(findDriver(startTrip, endTrip));
- trip.setDriverName(findDriverName(trip.getDriverUniqueId()));
-
- if (!ignoreOdometer
- && startTrip.getDouble(Position.KEY_ODOMETER) != 0
- && endTrip.getDouble(Position.KEY_ODOMETER) != 0) {
- trip.setStartOdometer(startTrip.getDouble(Position.KEY_ODOMETER));
- trip.setEndOdometer(endTrip.getDouble(Position.KEY_ODOMETER));
- } else {
- trip.setStartOdometer(startTrip.getDouble(Position.KEY_TOTAL_DISTANCE));
- trip.setEndOdometer(endTrip.getDouble(Position.KEY_TOTAL_DISTANCE));
- }
-
- return trip;
- }
-
- private static StopReport calculateStop(
- ArrayList<Position> positions, int startIndex, int endIndex, boolean ignoreOdometer) {
-
- Position startStop = positions.get(startIndex);
- Position endStop = positions.get(endIndex);
-
- StopReport stop = new StopReport();
-
- long deviceId = startStop.getDeviceId();
- stop.setDeviceId(deviceId);
- stop.setDeviceName(Context.getIdentityManager().getById(deviceId).getName());
-
- stop.setPositionId(startStop.getId());
- stop.setLatitude(startStop.getLatitude());
- stop.setLongitude(startStop.getLongitude());
- stop.setStartTime(startStop.getFixTime());
- String address = startStop.getAddress();
- if (address == null && Context.getGeocoder() != null
- && Context.getConfig().getBoolean(Keys.GEOCODER_ON_REQUEST)) {
- address = Context.getGeocoder().getAddress(stop.getLatitude(), stop.getLongitude(), null);
- }
- stop.setAddress(address);
-
- stop.setEndTime(endStop.getFixTime());
-
- long stopDuration = endStop.getFixTime().getTime() - startStop.getFixTime().getTime();
- stop.setDuration(stopDuration);
- stop.setSpentFuel(calculateFuel(startStop, endStop));
-
- if (startStop.getAttributes().containsKey(Position.KEY_HOURS)
- && endStop.getAttributes().containsKey(Position.KEY_HOURS)) {
- stop.setEngineHours(endStop.getLong(Position.KEY_HOURS) - startStop.getLong(Position.KEY_HOURS));
- }
-
- if (!ignoreOdometer
- && startStop.getDouble(Position.KEY_ODOMETER) != 0
- && endStop.getDouble(Position.KEY_ODOMETER) != 0) {
- stop.setStartOdometer(startStop.getDouble(Position.KEY_ODOMETER));
- stop.setEndOdometer(endStop.getDouble(Position.KEY_ODOMETER));
- } else {
- stop.setStartOdometer(startStop.getDouble(Position.KEY_TOTAL_DISTANCE));
- stop.setEndOdometer(endStop.getDouble(Position.KEY_TOTAL_DISTANCE));
- }
-
- return stop;
-
- }
-
- private static <T extends BaseReport> T calculateTripOrStop(
- ArrayList<Position> positions, int startIndex, int endIndex, boolean ignoreOdometer, Class<T> reportClass) {
-
- if (reportClass.equals(TripReport.class)) {
- return (T) calculateTrip(positions, startIndex, endIndex, ignoreOdometer);
- } else {
- return (T) calculateStop(positions, startIndex, endIndex, ignoreOdometer);
- }
- }
-
- private static boolean isMoving(ArrayList<Position> positions, int index, TripsConfig tripsConfig) {
- if (tripsConfig.getMinimalNoDataDuration() > 0) {
- boolean beforeGap = index < positions.size() - 1
- && positions.get(index + 1).getFixTime().getTime() - positions.get(index).getFixTime().getTime()
- >= tripsConfig.getMinimalNoDataDuration();
- boolean afterGap = index > 0
- && positions.get(index).getFixTime().getTime() - positions.get(index - 1).getFixTime().getTime()
- >= tripsConfig.getMinimalNoDataDuration();
- if (beforeGap || afterGap) {
- return false;
- }
- }
- if (positions.get(index).getAttributes().containsKey(Position.KEY_MOTION)
- && positions.get(index).getAttributes().get(Position.KEY_MOTION) instanceof Boolean) {
- return positions.get(index).getBoolean(Position.KEY_MOTION);
- } else {
- return positions.get(index).getSpeed() > tripsConfig.getSpeedThreshold();
- }
- }
-
- public static <T extends BaseReport> Collection<T> detectTripsAndStops(
- IdentityManager identityManager, DeviceManager deviceManager,
- Collection<Position> positionCollection,
- TripsConfig tripsConfig, boolean ignoreOdometer, Class<T> reportClass) {
-
- Collection<T> result = new ArrayList<>();
-
- ArrayList<Position> positions = new ArrayList<>(positionCollection);
- if (!positions.isEmpty()) {
- boolean trips = reportClass.equals(TripReport.class);
- MotionEventHandler motionHandler = new MotionEventHandler(identityManager, deviceManager, tripsConfig);
- DeviceState deviceState = new DeviceState();
- deviceState.setMotionState(isMoving(positions, 0, tripsConfig));
- int startEventIndex = trips == deviceState.getMotionState() ? 0 : -1;
- int startNoEventIndex = -1;
- for (int i = 0; i < positions.size(); i++) {
- Map<Event, Position> event = motionHandler.updateMotionState(deviceState, positions.get(i),
- isMoving(positions, i, tripsConfig));
- if (startEventIndex == -1
- && (trips != deviceState.getMotionState() && deviceState.getMotionPosition() != null
- || trips == deviceState.getMotionState() && event != null)) {
- startEventIndex = i;
- startNoEventIndex = -1;
- } else if (trips != deviceState.getMotionState() && startEventIndex != -1
- && deviceState.getMotionPosition() == null && event == null) {
- startEventIndex = -1;
- }
- if (startNoEventIndex == -1
- && (trips == deviceState.getMotionState() && deviceState.getMotionPosition() != null
- || trips != deviceState.getMotionState() && event != null)) {
- startNoEventIndex = i;
- } else if (startNoEventIndex != -1 && deviceState.getMotionPosition() == null && event == null) {
- startNoEventIndex = -1;
- }
- if (startEventIndex != -1 && startNoEventIndex != -1 && event != null
- && trips != deviceState.getMotionState()) {
- result.add(calculateTripOrStop(positions, startEventIndex, startNoEventIndex,
- ignoreOdometer, reportClass));
- startEventIndex = -1;
- }
- }
- if (startEventIndex != -1 && (startNoEventIndex != -1 || !trips)) {
- result.add(calculateTripOrStop(positions, startEventIndex,
- startNoEventIndex != -1 ? startNoEventIndex : positions.size() - 1,
- ignoreOdometer, reportClass));
- }
- }
-
- return result;
- }
-
-}
diff --git a/src/main/java/org/traccar/reports/Route.java b/src/main/java/org/traccar/reports/Route.java
deleted file mode 100644
index 4a5edd295..000000000
--- a/src/main/java/org/traccar/reports/Route.java
+++ /dev/null
@@ -1,85 +0,0 @@
-/*
- * Copyright 2016 Anton Tananaev (anton@traccar.org)
- * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
- *
- * 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.reports;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-
-import org.apache.poi.ss.util.WorkbookUtil;
-import org.traccar.Context;
-import org.traccar.model.Device;
-import org.traccar.model.Group;
-import org.traccar.model.Position;
-import org.traccar.reports.model.DeviceReport;
-import org.traccar.storage.StorageException;
-
-public final class Route {
-
- private Route() {
- }
-
- public static Collection<Position> getObjects(long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Date from, Date to) throws StorageException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<Position> result = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- result.addAll(Context.getDataManager().getPositions(deviceId, from, to));
- }
- return result;
- }
-
- public static void getExcel(OutputStream outputStream,
- long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Date from, Date to) throws StorageException, IOException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<DeviceReport> devicesRoutes = new ArrayList<>();
- ArrayList<String> sheetNames = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- Collection<Position> positions = Context.getDataManager()
- .getPositions(deviceId, from, to);
- DeviceReport deviceRoutes = new DeviceReport();
- Device device = Context.getIdentityManager().getById(deviceId);
- deviceRoutes.setDeviceName(device.getName());
- sheetNames.add(WorkbookUtil.createSafeSheetName(deviceRoutes.getDeviceName()));
- if (device.getGroupId() != 0) {
- Group group = Context.getGroupsManager().getById(device.getGroupId());
- if (group != null) {
- deviceRoutes.setGroupName(group.getName());
- }
- }
- deviceRoutes.setObjects(positions);
- devicesRoutes.add(deviceRoutes);
- }
- String templatePath = Context.getConfig().getString("report.templatesPath",
- "templates/export/");
- try (InputStream inputStream = new FileInputStream(templatePath + "/route.xlsx")) {
- org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
- jxlsContext.putVar("devices", devicesRoutes);
- jxlsContext.putVar("sheetNames", sheetNames);
- jxlsContext.putVar("from", from);
- jxlsContext.putVar("to", to);
- ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
- }
- }
-}
diff --git a/src/main/java/org/traccar/reports/RouteReportProvider.java b/src/main/java/org/traccar/reports/RouteReportProvider.java
new file mode 100644
index 000000000..d761fe1e5
--- /dev/null
+++ b/src/main/java/org/traccar/reports/RouteReportProvider.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.reports;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.model.DeviceUtil;
+import org.traccar.helper.model.PositionUtil;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.model.Position;
+import org.traccar.reports.common.ReportUtils;
+import org.traccar.reports.model.DeviceReportSection;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+import java.util.HashMap;
+
+public class RouteReportProvider {
+
+ private final Config config;
+ private final ReportUtils reportUtils;
+ private final Storage storage;
+
+ private final Map<String, Integer> namesCount = new HashMap<>();
+
+ @Inject
+ public RouteReportProvider(Config config, ReportUtils reportUtils, Storage storage) {
+ this.config = config;
+ this.reportUtils = reportUtils;
+ this.storage = storage;
+ }
+
+ public Collection<Position> getObjects(long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws StorageException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<Position> result = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ result.addAll(PositionUtil.getPositions(storage, device.getId(), from, to));
+ }
+ return result;
+ }
+
+
+ private String getUniqueSheetName(String key) {
+ namesCount.compute(key, (k, value) -> value == null ? 1 : (value + 1));
+ return namesCount.get(key) > 1 ? key + '-' + namesCount.get(key) : key;
+ }
+
+ public void getExcel(OutputStream outputStream,
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws StorageException, IOException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<DeviceReportSection> devicesRoutes = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ var positions = PositionUtil.getPositions(storage, device.getId(), from, to);
+ DeviceReportSection deviceRoutes = new DeviceReportSection();
+ deviceRoutes.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(getUniqueSheetName(deviceRoutes.getDeviceName())));
+ if (device.getGroupId() > 0) {
+ Group group = storage.getObject(Group.class, new Request(
+ new Columns.All(), new Condition.Equals("id", device.getGroupId())));
+ if (group != null) {
+ deviceRoutes.setGroupName(group.getName());
+ }
+ }
+ deviceRoutes.setObjects(positions);
+ devicesRoutes.add(deviceRoutes);
+ }
+
+ File file = Paths.get(config.getString(Keys.TEMPLATES_ROOT), "export", "route.xlsx").toFile();
+ try (InputStream inputStream = new FileInputStream(file)) {
+ var context = reportUtils.initializeContext(userId);
+ context.putVar("devices", devicesRoutes);
+ context.putVar("sheetNames", sheetNames);
+ context.putVar("from", from);
+ context.putVar("to", to);
+ reportUtils.processTemplateWithSheets(inputStream, outputStream, context);
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/reports/Stops.java b/src/main/java/org/traccar/reports/Stops.java
deleted file mode 100644
index 36a4a7549..000000000
--- a/src/main/java/org/traccar/reports/Stops.java
+++ /dev/null
@@ -1,102 +0,0 @@
-/*
- * Copyright 2017 Anton Tananaev (anton@traccar.org)
- * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
- *
- * 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.reports;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-
-import org.apache.poi.ss.util.WorkbookUtil;
-import org.traccar.Context;
-import org.traccar.Main;
-import org.traccar.database.DeviceManager;
-import org.traccar.database.IdentityManager;
-import org.traccar.model.Device;
-import org.traccar.model.Group;
-import org.traccar.reports.model.DeviceReport;
-import org.traccar.reports.model.StopReport;
-import org.traccar.storage.StorageException;
-
-public final class Stops {
-
- private Stops() {
- }
-
- private static Collection<StopReport> detectStops(long deviceId, Date from, Date to) throws StorageException {
- boolean ignoreOdometer = Context.getDeviceManager()
- .lookupAttributeBoolean(deviceId, "report.ignoreOdometer", false, false, true);
-
- IdentityManager identityManager = Main.getInjector().getInstance(IdentityManager.class);
- DeviceManager deviceManager = Main.getInjector().getInstance(DeviceManager.class);
-
- return ReportUtils.detectTripsAndStops(
- identityManager, deviceManager, Context.getDataManager().getPositions(deviceId, from, to),
- Context.getTripsConfig(), ignoreOdometer, StopReport.class);
- }
-
- public static Collection<StopReport> getObjects(
- long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Date from, Date to) throws StorageException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<StopReport> result = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- result.addAll(detectStops(deviceId, from, to));
- }
- return result;
- }
-
- public static void getExcel(
- OutputStream outputStream, long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Date from, Date to) throws StorageException, IOException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<DeviceReport> devicesStops = new ArrayList<>();
- ArrayList<String> sheetNames = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- Collection<StopReport> stops = detectStops(deviceId, from, to);
- DeviceReport deviceStops = new DeviceReport();
- Device device = Context.getIdentityManager().getById(deviceId);
- deviceStops.setDeviceName(device.getName());
- sheetNames.add(WorkbookUtil.createSafeSheetName(deviceStops.getDeviceName()));
- if (device.getGroupId() != 0) {
- Group group = Context.getGroupsManager().getById(device.getGroupId());
- if (group != null) {
- deviceStops.setGroupName(group.getName());
- }
- }
- deviceStops.setObjects(stops);
- devicesStops.add(deviceStops);
- }
- String templatePath = Context.getConfig().getString("report.templatesPath",
- "templates/export/");
- try (InputStream inputStream = new FileInputStream(templatePath + "/stops.xlsx")) {
- org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
- jxlsContext.putVar("devices", devicesStops);
- jxlsContext.putVar("sheetNames", sheetNames);
- jxlsContext.putVar("from", from);
- jxlsContext.putVar("to", to);
- ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
- }
- }
-
-}
diff --git a/src/main/java/org/traccar/reports/StopsReportProvider.java b/src/main/java/org/traccar/reports/StopsReportProvider.java
new file mode 100644
index 000000000..2160fec0e
--- /dev/null
+++ b/src/main/java/org/traccar/reports/StopsReportProvider.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.reports;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.model.DeviceUtil;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.reports.common.ReportUtils;
+import org.traccar.reports.model.DeviceReportSection;
+import org.traccar.reports.model.StopReportItem;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+
+public class StopsReportProvider {
+
+ private final Config config;
+ private final ReportUtils reportUtils;
+ private final Storage storage;
+
+ @Inject
+ public StopsReportProvider(Config config, ReportUtils reportUtils, Storage storage) {
+ this.config = config;
+ this.reportUtils = reportUtils;
+ this.storage = storage;
+ }
+
+ public Collection<StopReportItem> getObjects(
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws StorageException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<StopReportItem> result = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ result.addAll(reportUtils.detectTripsAndStops(device, from, to, StopReportItem.class));
+ }
+ return result;
+ }
+
+ public void getExcel(
+ OutputStream outputStream, long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws StorageException, IOException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<DeviceReportSection> devicesStops = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ Collection<StopReportItem> stops = reportUtils.detectTripsAndStops(device, from, to, StopReportItem.class);
+ DeviceReportSection deviceStops = new DeviceReportSection();
+ deviceStops.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(deviceStops.getDeviceName()));
+ if (device.getGroupId() > 0) {
+ Group group = storage.getObject(Group.class, new Request(
+ new Columns.All(), new Condition.Equals("id", device.getGroupId())));
+ if (group != null) {
+ deviceStops.setGroupName(group.getName());
+ }
+ }
+ deviceStops.setObjects(stops);
+ devicesStops.add(deviceStops);
+ }
+
+ File file = Paths.get(config.getString(Keys.TEMPLATES_ROOT), "export", "stops.xlsx").toFile();
+ try (InputStream inputStream = new FileInputStream(file)) {
+ var context = reportUtils.initializeContext(userId);
+ context.putVar("devices", devicesStops);
+ context.putVar("sheetNames", sheetNames);
+ context.putVar("from", from);
+ context.putVar("to", to);
+ reportUtils.processTemplateWithSheets(inputStream, outputStream, context);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/Summary.java b/src/main/java/org/traccar/reports/Summary.java
deleted file mode 100644
index 4924af062..000000000
--- a/src/main/java/org/traccar/reports/Summary.java
+++ /dev/null
@@ -1,151 +0,0 @@
-/*
- * Copyright 2016 - 2020 Anton Tananaev (anton@traccar.org)
- * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
- *
- * 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.reports;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Calendar;
-import java.util.Collection;
-import java.util.Date;
-
-import org.jxls.util.JxlsHelper;
-import org.traccar.Context;
-import org.traccar.helper.UnitsConverter;
-import org.traccar.model.Position;
-import org.traccar.reports.model.SummaryReport;
-import org.traccar.storage.StorageException;
-
-public final class Summary {
-
- private Summary() {
- }
-
- private static SummaryReport calculateSummaryResult(long deviceId, Collection<Position> positions) {
- SummaryReport result = new SummaryReport();
- result.setDeviceId(deviceId);
- result.setDeviceName(Context.getIdentityManager().getById(deviceId).getName());
- if (positions != null && !positions.isEmpty()) {
- Position firstPosition = null;
- Position previousPosition = null;
- for (Position position : positions) {
- if (firstPosition == null) {
- firstPosition = position;
- }
- previousPosition = position;
- if (position.getSpeed() > result.getMaxSpeed()) {
- result.setMaxSpeed(position.getSpeed());
- }
- }
- boolean ignoreOdometer = Context.getDeviceManager()
- .lookupAttributeBoolean(deviceId, "report.ignoreOdometer", false, false, true);
- result.setDistance(ReportUtils.calculateDistance(firstPosition, previousPosition, !ignoreOdometer));
- result.setSpentFuel(ReportUtils.calculateFuel(firstPosition, previousPosition));
-
- long durationMilliseconds;
- if (firstPosition.getAttributes().containsKey(Position.KEY_HOURS)
- && previousPosition.getAttributes().containsKey(Position.KEY_HOURS)) {
- durationMilliseconds =
- previousPosition.getLong(Position.KEY_HOURS) - firstPosition.getLong(Position.KEY_HOURS);
- result.setEngineHours(durationMilliseconds);
- } else {
- durationMilliseconds =
- previousPosition.getFixTime().getTime() - firstPosition.getFixTime().getTime();
- }
-
- if (durationMilliseconds > 0) {
- result.setAverageSpeed(
- UnitsConverter.knotsFromMps(result.getDistance() * 1000 / durationMilliseconds));
- }
-
- if (!ignoreOdometer
- && firstPosition.getDouble(Position.KEY_ODOMETER) != 0
- && previousPosition.getDouble(Position.KEY_ODOMETER) != 0) {
- result.setStartOdometer(firstPosition.getDouble(Position.KEY_ODOMETER));
- result.setEndOdometer(previousPosition.getDouble(Position.KEY_ODOMETER));
- } else {
- result.setStartOdometer(firstPosition.getDouble(Position.KEY_TOTAL_DISTANCE));
- result.setEndOdometer(previousPosition.getDouble(Position.KEY_TOTAL_DISTANCE));
- }
-
- result.setStartTime(firstPosition.getFixTime());
- result.setEndTime(previousPosition.getFixTime());
- }
- return result;
- }
-
- private static int getDay(long userId, Date date) {
- Calendar calendar = Calendar.getInstance(ReportUtils.getTimezone(userId));
- calendar.setTime(date);
- return calendar.get(Calendar.DAY_OF_MONTH);
- }
-
- private static Collection<SummaryReport> calculateSummaryResults(
- long userId, long deviceId, Date from, Date to, boolean daily) throws StorageException {
-
- ArrayList<Position> positions = new ArrayList<>(Context.getDataManager().getPositions(deviceId, from, to));
-
- ArrayList<SummaryReport> results = new ArrayList<>();
- if (daily && !positions.isEmpty()) {
- int startIndex = 0;
- int startDay = getDay(userId, positions.iterator().next().getFixTime());
- for (int i = 0; i < positions.size(); i++) {
- int currentDay = getDay(userId, positions.get(i).getFixTime());
- if (currentDay != startDay) {
- results.add(calculateSummaryResult(deviceId, positions.subList(startIndex, i)));
- startIndex = i;
- startDay = currentDay;
- }
- }
- results.add(calculateSummaryResult(deviceId, positions.subList(startIndex, positions.size())));
- } else {
- results.add(calculateSummaryResult(deviceId, positions));
- }
-
- return results;
- }
-
- public static Collection<SummaryReport> getObjects(long userId, Collection<Long> deviceIds,
- Collection<Long> groupIds, Date from, Date to, boolean daily) throws StorageException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<SummaryReport> result = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- result.addAll(calculateSummaryResults(userId, deviceId, from, to, daily));
- }
- return result;
- }
-
- public static void getExcel(OutputStream outputStream,
- long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Date from, Date to, boolean daily) throws StorageException, IOException {
- ReportUtils.checkPeriodLimit(from, to);
- Collection<SummaryReport> summaries = getObjects(userId, deviceIds, groupIds, from, to, daily);
- String templatePath = Context.getConfig().getString("report.templatesPath",
- "templates/export/");
- try (InputStream inputStream = new FileInputStream(templatePath + "/summary.xlsx")) {
- org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
- jxlsContext.putVar("summaries", summaries);
- jxlsContext.putVar("from", from);
- jxlsContext.putVar("to", to);
- JxlsHelper.getInstance().setUseFastFormulaProcessor(false)
- .processTemplate(inputStream, outputStream, jxlsContext);
- }
- }
-}
diff --git a/src/main/java/org/traccar/reports/SummaryReportProvider.java b/src/main/java/org/traccar/reports/SummaryReportProvider.java
new file mode 100644
index 000000000..ffde0b067
--- /dev/null
+++ b/src/main/java/org/traccar/reports/SummaryReportProvider.java
@@ -0,0 +1,192 @@
+/*
+ * Copyright 2016 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.reports;
+
+import org.jxls.util.JxlsHelper;
+import org.traccar.api.security.PermissionsService;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.helper.model.DeviceUtil;
+import org.traccar.helper.model.PositionUtil;
+import org.traccar.helper.model.UserUtil;
+import org.traccar.model.Device;
+import org.traccar.model.Position;
+import org.traccar.reports.common.ReportUtils;
+import org.traccar.reports.model.SummaryReportItem;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Order;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Paths;
+import java.time.Duration;
+import java.time.ZonedDateTime;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+
+public class SummaryReportProvider {
+
+ private final Config config;
+ private final ReportUtils reportUtils;
+ private final PermissionsService permissionsService;
+ private final Storage storage;
+
+ @Inject
+ public SummaryReportProvider(
+ Config config, ReportUtils reportUtils, PermissionsService permissionsService, Storage storage) {
+ this.config = config;
+ this.reportUtils = reportUtils;
+ this.permissionsService = permissionsService;
+ this.storage = storage;
+ }
+
+ private Position getEdgePosition(long deviceId, Date from, Date to, boolean end) throws StorageException {
+ return storage.getObject(Position.class, new Request(
+ new Columns.All(),
+ new Condition.And(
+ new Condition.Equals("deviceId", deviceId),
+ new Condition.Between("fixTime", "from", from, "to", to)),
+ new Order("fixTime", end, 1)));
+ }
+
+ private Collection<SummaryReportItem> calculateDeviceResult(
+ Device device, Date from, Date to, boolean fast) throws StorageException {
+
+ SummaryReportItem result = new SummaryReportItem();
+ result.setDeviceId(device.getId());
+ result.setDeviceName(device.getName());
+
+ Position first = null;
+ Position last = null;
+ if (fast) {
+ first = getEdgePosition(device.getId(), from, to, false);
+ last = getEdgePosition(device.getId(), from, to, true);
+ } else {
+ var positions = PositionUtil.getPositions(storage, device.getId(), from, to);
+ for (Position position : positions) {
+ if (first == null) {
+ first = position;
+ }
+ if (position.getSpeed() > result.getMaxSpeed()) {
+ result.setMaxSpeed(position.getSpeed());
+ }
+ last = position;
+ }
+ }
+
+ if (first != null && last != null) {
+ boolean ignoreOdometer = config.getBoolean(Keys.REPORT_IGNORE_ODOMETER);
+ result.setDistance(PositionUtil.calculateDistance(first, last, !ignoreOdometer));
+ result.setSpentFuel(reportUtils.calculateFuel(first, last));
+
+ long durationMilliseconds;
+ if (first.hasAttribute(Position.KEY_HOURS) && last.hasAttribute(Position.KEY_HOURS)) {
+ durationMilliseconds = last.getLong(Position.KEY_HOURS) - first.getLong(Position.KEY_HOURS);
+ result.setEngineHours(durationMilliseconds);
+ } else {
+ durationMilliseconds = last.getFixTime().getTime() - first.getFixTime().getTime();
+ }
+
+ if (durationMilliseconds > 0) {
+ result.setAverageSpeed(UnitsConverter.knotsFromMps(result.getDistance() * 1000 / durationMilliseconds));
+ }
+
+ if (!ignoreOdometer
+ && first.getDouble(Position.KEY_ODOMETER) != 0 && last.getDouble(Position.KEY_ODOMETER) != 0) {
+ result.setStartOdometer(first.getDouble(Position.KEY_ODOMETER));
+ result.setEndOdometer(last.getDouble(Position.KEY_ODOMETER));
+ } else {
+ result.setStartOdometer(first.getDouble(Position.KEY_TOTAL_DISTANCE));
+ result.setEndOdometer(last.getDouble(Position.KEY_TOTAL_DISTANCE));
+ }
+
+ result.setStartTime(first.getFixTime());
+ result.setEndTime(last.getFixTime());
+ return List.of(result);
+ }
+
+ return List.of();
+ }
+
+ private Collection<SummaryReportItem> calculateDeviceResults(
+ Device device, ZonedDateTime from, ZonedDateTime to, boolean daily) throws StorageException {
+
+ boolean fast = Duration.between(from, to).toSeconds() > config.getLong(Keys.REPORT_FAST_THRESHOLD);
+ var results = new ArrayList<SummaryReportItem>();
+ if (daily) {
+ while (from.truncatedTo(ChronoUnit.DAYS).isBefore(to.truncatedTo(ChronoUnit.DAYS))) {
+ ZonedDateTime fromDay = from.truncatedTo(ChronoUnit.DAYS);
+ ZonedDateTime nextDay = fromDay.plus(1, ChronoUnit.DAYS);
+ results.addAll(calculateDeviceResult(
+ device, Date.from(from.toInstant()), Date.from(nextDay.toInstant()), fast));
+ from = nextDay;
+ }
+ results.addAll(calculateDeviceResult(device, Date.from(from.toInstant()), Date.from(to.toInstant()), fast));
+ } else {
+ results.addAll(calculateDeviceResult(device, Date.from(from.toInstant()), Date.from(to.toInstant()), fast));
+ }
+ return results;
+ }
+
+ public Collection<SummaryReportItem> getObjects(
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to, boolean daily) throws StorageException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ var tz = UserUtil.getTimezone(permissionsService.getServer(), permissionsService.getUser(userId)).toZoneId();
+
+ ArrayList<SummaryReportItem> result = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ var deviceResults = calculateDeviceResults(
+ device, from.toInstant().atZone(tz), to.toInstant().atZone(tz), daily);
+ for (SummaryReportItem summaryReport : deviceResults) {
+ if (summaryReport.getStartTime() != null && summaryReport.getEndTime() != null) {
+ result.add(summaryReport);
+ }
+ }
+ }
+ return result;
+ }
+
+ public void getExcel(OutputStream outputStream,
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to, boolean daily) throws StorageException, IOException {
+ Collection<SummaryReportItem> summaries = getObjects(userId, deviceIds, groupIds, from, to, daily);
+
+ File file = Paths.get(config.getString(Keys.TEMPLATES_ROOT), "export", "summary.xlsx").toFile();
+ try (InputStream inputStream = new FileInputStream(file)) {
+ var context = reportUtils.initializeContext(userId);
+ context.putVar("summaries", summaries);
+ context.putVar("from", from);
+ context.putVar("to", to);
+ JxlsHelper.getInstance().setUseFastFormulaProcessor(false)
+ .processTemplate(inputStream, outputStream, context);
+ }
+ }
+}
diff --git a/src/main/java/org/traccar/reports/Trips.java b/src/main/java/org/traccar/reports/Trips.java
deleted file mode 100644
index 1461b869e..000000000
--- a/src/main/java/org/traccar/reports/Trips.java
+++ /dev/null
@@ -1,100 +0,0 @@
-/*
- * Copyright 2016 Anton Tananaev (anton@traccar.org)
- * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
- *
- * 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.reports;
-
-import java.io.FileInputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Date;
-
-import org.apache.poi.ss.util.WorkbookUtil;
-import org.traccar.Context;
-import org.traccar.Main;
-import org.traccar.database.DeviceManager;
-import org.traccar.database.IdentityManager;
-import org.traccar.model.Device;
-import org.traccar.model.Group;
-import org.traccar.reports.model.DeviceReport;
-import org.traccar.reports.model.TripReport;
-import org.traccar.storage.StorageException;
-
-public final class Trips {
-
- private Trips() {
- }
-
- private static Collection<TripReport> detectTrips(long deviceId, Date from, Date to) throws StorageException {
- boolean ignoreOdometer = Context.getDeviceManager()
- .lookupAttributeBoolean(deviceId, "report.ignoreOdometer", false, false, true);
-
- IdentityManager identityManager = Main.getInjector().getInstance(IdentityManager.class);
- DeviceManager deviceManager = Main.getInjector().getInstance(DeviceManager.class);
-
- return ReportUtils.detectTripsAndStops(
- identityManager, deviceManager, Context.getDataManager().getPositions(deviceId, from, to),
- Context.getTripsConfig(), ignoreOdometer, TripReport.class);
- }
-
- public static Collection<TripReport> getObjects(long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Date from, Date to) throws StorageException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<TripReport> result = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- result.addAll(detectTrips(deviceId, from, to));
- }
- return result;
- }
-
- public static void getExcel(OutputStream outputStream,
- long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
- Date from, Date to) throws StorageException, IOException {
- ReportUtils.checkPeriodLimit(from, to);
- ArrayList<DeviceReport> devicesTrips = new ArrayList<>();
- ArrayList<String> sheetNames = new ArrayList<>();
- for (long deviceId: ReportUtils.getDeviceList(deviceIds, groupIds)) {
- Context.getPermissionsManager().checkDevice(userId, deviceId);
- Collection<TripReport> trips = detectTrips(deviceId, from, to);
- DeviceReport deviceTrips = new DeviceReport();
- Device device = Context.getIdentityManager().getById(deviceId);
- deviceTrips.setDeviceName(device.getName());
- sheetNames.add(WorkbookUtil.createSafeSheetName(deviceTrips.getDeviceName()));
- if (device.getGroupId() != 0) {
- Group group = Context.getGroupsManager().getById(device.getGroupId());
- if (group != null) {
- deviceTrips.setGroupName(group.getName());
- }
- }
- deviceTrips.setObjects(trips);
- devicesTrips.add(deviceTrips);
- }
- String templatePath = Context.getConfig().getString("report.templatesPath",
- "templates/export/");
- try (InputStream inputStream = new FileInputStream(templatePath + "/trips.xlsx")) {
- org.jxls.common.Context jxlsContext = ReportUtils.initializeContext(userId);
- jxlsContext.putVar("devices", devicesTrips);
- jxlsContext.putVar("sheetNames", sheetNames);
- jxlsContext.putVar("from", from);
- jxlsContext.putVar("to", to);
- ReportUtils.processTemplateWithSheets(inputStream, outputStream, jxlsContext);
- }
- }
-
-}
diff --git a/src/main/java/org/traccar/reports/TripsReportProvider.java b/src/main/java/org/traccar/reports/TripsReportProvider.java
new file mode 100644
index 000000000..9ff7232af
--- /dev/null
+++ b/src/main/java/org/traccar/reports/TripsReportProvider.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.reports;
+
+import org.apache.poi.ss.util.WorkbookUtil;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.helper.model.DeviceUtil;
+import org.traccar.model.Device;
+import org.traccar.model.Group;
+import org.traccar.reports.common.ReportUtils;
+import org.traccar.reports.model.DeviceReportSection;
+import org.traccar.reports.model.TripReportItem;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Request;
+
+import jakarta.inject.Inject;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+
+public class TripsReportProvider {
+
+ private final Config config;
+ private final ReportUtils reportUtils;
+ private final Storage storage;
+
+ @Inject
+ public TripsReportProvider(Config config, ReportUtils reportUtils, Storage storage) {
+ this.config = config;
+ this.reportUtils = reportUtils;
+ this.storage = storage;
+ }
+
+ public Collection<TripReportItem> getObjects(
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws StorageException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<TripReportItem> result = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ result.addAll(reportUtils.detectTripsAndStops(device, from, to, TripReportItem.class));
+ }
+ return result;
+ }
+
+ public void getExcel(OutputStream outputStream,
+ long userId, Collection<Long> deviceIds, Collection<Long> groupIds,
+ Date from, Date to) throws StorageException, IOException {
+ reportUtils.checkPeriodLimit(from, to);
+
+ ArrayList<DeviceReportSection> devicesTrips = new ArrayList<>();
+ ArrayList<String> sheetNames = new ArrayList<>();
+ for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) {
+ Collection<TripReportItem> trips = reportUtils.detectTripsAndStops(device, from, to, TripReportItem.class);
+ DeviceReportSection deviceTrips = new DeviceReportSection();
+ deviceTrips.setDeviceName(device.getName());
+ sheetNames.add(WorkbookUtil.createSafeSheetName(deviceTrips.getDeviceName()));
+ if (device.getGroupId() > 0) {
+ Group group = storage.getObject(Group.class, new Request(
+ new Columns.All(), new Condition.Equals("id", device.getGroupId())));
+ if (group != null) {
+ deviceTrips.setGroupName(group.getName());
+ }
+ }
+ deviceTrips.setObjects(trips);
+ devicesTrips.add(deviceTrips);
+ }
+
+ File file = Paths.get(config.getString(Keys.TEMPLATES_ROOT), "export", "trips.xlsx").toFile();
+ try (InputStream inputStream = new FileInputStream(file)) {
+ var context = reportUtils.initializeContext(userId);
+ context.putVar("devices", devicesTrips);
+ context.putVar("sheetNames", sheetNames);
+ context.putVar("from", from);
+ context.putVar("to", to);
+ reportUtils.processTemplateWithSheets(inputStream, outputStream, context);
+ }
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/common/ExpressionEvaluatorFactory.java b/src/main/java/org/traccar/reports/common/ExpressionEvaluatorFactory.java
new file mode 100644
index 000000000..8b139a572
--- /dev/null
+++ b/src/main/java/org/traccar/reports/common/ExpressionEvaluatorFactory.java
@@ -0,0 +1,58 @@
+package org.traccar.reports.common;
+
+import org.apache.commons.jexl3.JexlBuilder;
+import org.apache.commons.jexl3.introspection.JexlPermissions;
+import org.jxls.expression.ExpressionEvaluator;
+import org.jxls.expression.JexlExpressionEvaluator;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+
+public class ExpressionEvaluatorFactory implements org.jxls.expression.ExpressionEvaluatorFactory {
+
+ private final JexlPermissions permissions = new JexlPermissions() {
+ @Override
+ public boolean allow(Package pack) {
+ return true;
+ }
+
+ @Override
+ public boolean allow(Class<?> clazz) {
+ return true;
+ }
+
+ @Override
+ public boolean allow(Constructor<?> ctor) {
+ return true;
+ }
+
+ @Override
+ public boolean allow(Method method) {
+ return true;
+ }
+
+ @Override
+ public boolean allow(Field field) {
+ return true;
+ }
+
+ @Override
+ public JexlPermissions compose(String... src) {
+ return this;
+ }
+ };
+
+ @Override
+ public ExpressionEvaluator createExpressionEvaluator(String expression) {
+ JexlExpressionEvaluator expressionEvaluator = expression == null
+ ? new JexlExpressionEvaluator()
+ : new JexlExpressionEvaluator(expression);
+ expressionEvaluator.setJexlEngine(new JexlBuilder()
+ .silent(true)
+ .strict(false)
+ .permissions(permissions)
+ .create());
+ return expressionEvaluator;
+ }
+}
diff --git a/src/main/java/org/traccar/reports/common/ReportExecutor.java b/src/main/java/org/traccar/reports/common/ReportExecutor.java
new file mode 100644
index 000000000..aed4b8c23
--- /dev/null
+++ b/src/main/java/org/traccar/reports/common/ReportExecutor.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 Anton Tananaev (anton@traccar.org)
+ *
+ * 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.reports.common;
+
+import org.traccar.storage.StorageException;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface ReportExecutor {
+ void execute(OutputStream stream) throws StorageException, IOException;
+}
diff --git a/src/main/java/org/traccar/reports/common/ReportMailer.java b/src/main/java/org/traccar/reports/common/ReportMailer.java
new file mode 100644
index 000000000..9fb30fe9f
--- /dev/null
+++ b/src/main/java/org/traccar/reports/common/ReportMailer.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 Anton Tananaev (anton@traccar.org)
+ *
+ * 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.reports.common;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.traccar.api.security.PermissionsService;
+import org.traccar.mail.MailManager;
+import org.traccar.model.User;
+import org.traccar.storage.StorageException;
+
+import jakarta.activation.DataHandler;
+import jakarta.inject.Inject;
+import jakarta.mail.MessagingException;
+import jakarta.mail.internet.MimeBodyPart;
+import jakarta.mail.util.ByteArrayDataSource;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+public class ReportMailer {
+
+ private static final Logger LOGGER = LoggerFactory.getLogger(ReportMailer.class);
+
+ private final PermissionsService permissionsService;
+ private final MailManager mailManager;
+
+ @Inject
+ public ReportMailer(PermissionsService permissionsService, MailManager mailManager) {
+ this.permissionsService = permissionsService;
+ this.mailManager = mailManager;
+ }
+
+ public void sendAsync(long userId, ReportExecutor executor) {
+ new Thread(() -> {
+ try {
+ var stream = new ByteArrayOutputStream();
+ executor.execute(stream);
+
+ MimeBodyPart attachment = new MimeBodyPart();
+ attachment.setFileName("report.xlsx");
+ attachment.setDataHandler(new DataHandler(new ByteArrayDataSource(
+ stream.toByteArray(), "application/octet-stream")));
+
+ User user = permissionsService.getUser(userId);
+ mailManager.sendMessage(user, false, "Report", "The report is in the attachment.", attachment);
+ } catch (StorageException | IOException | MessagingException e) {
+ LOGGER.warn("Email report failed", e);
+ }
+ }).start();
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/common/ReportUtils.java b/src/main/java/org/traccar/reports/common/ReportUtils.java
new file mode 100644
index 000000000..43db82708
--- /dev/null
+++ b/src/main/java/org/traccar/reports/common/ReportUtils.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org)
+ * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.reports.common;
+
+import org.apache.velocity.app.VelocityEngine;
+import org.apache.velocity.tools.generic.DateTool;
+import org.apache.velocity.tools.generic.NumberTool;
+import org.jxls.area.Area;
+import org.jxls.builder.xls.XlsCommentAreaBuilder;
+import org.jxls.common.CellRef;
+import org.jxls.formula.StandardFormulaProcessor;
+import org.jxls.transform.Transformer;
+import org.jxls.transform.poi.PoiTransformer;
+import org.jxls.util.TransformerFactory;
+import org.traccar.api.security.PermissionsService;
+import org.traccar.config.Config;
+import org.traccar.config.Keys;
+import org.traccar.geocoder.Geocoder;
+import org.traccar.helper.UnitsConverter;
+import org.traccar.helper.model.AttributeUtil;
+import org.traccar.helper.model.PositionUtil;
+import org.traccar.helper.model.UserUtil;
+import org.traccar.model.BaseModel;
+import org.traccar.model.Device;
+import org.traccar.model.Driver;
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+import org.traccar.model.User;
+import org.traccar.reports.model.BaseReportItem;
+import org.traccar.reports.model.StopReportItem;
+import org.traccar.reports.model.TripReportItem;
+import org.traccar.session.state.MotionProcessor;
+import org.traccar.session.state.MotionState;
+import org.traccar.storage.Storage;
+import org.traccar.storage.StorageException;
+import org.traccar.storage.query.Columns;
+import org.traccar.storage.query.Condition;
+import org.traccar.storage.query.Order;
+import org.traccar.storage.query.Request;
+
+import jakarta.annotation.Nullable;
+import jakarta.inject.Inject;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+public class ReportUtils {
+
+ private final Config config;
+ private final Storage storage;
+ private final PermissionsService permissionsService;
+ private final VelocityEngine velocityEngine;
+ private final Geocoder geocoder;
+
+ @Inject
+ public ReportUtils(
+ Config config, Storage storage, PermissionsService permissionsService,
+ VelocityEngine velocityEngine, @Nullable Geocoder geocoder) {
+ this.config = config;
+ this.storage = storage;
+ this.permissionsService = permissionsService;
+ this.velocityEngine = velocityEngine;
+ this.geocoder = geocoder;
+ }
+
+ public <T extends BaseModel> T getObject(
+ long userId, Class<T> clazz, long objectId) throws StorageException, SecurityException {
+ return storage.getObject(clazz, new Request(
+ new Columns.All(),
+ new Condition.And(
+ new Condition.Equals("id", objectId),
+ new Condition.Permission(User.class, userId, clazz))));
+ }
+
+ public void checkPeriodLimit(Date from, Date to) {
+ long limit = config.getLong(Keys.REPORT_PERIOD_LIMIT) * 1000;
+ if (limit > 0 && to.getTime() - from.getTime() > limit) {
+ throw new IllegalArgumentException("Time period exceeds the limit");
+ }
+ }
+
+ public double calculateFuel(Position firstPosition, Position lastPosition) {
+
+ if (firstPosition.getAttributes().get(Position.KEY_FUEL_LEVEL) != null
+ && lastPosition.getAttributes().get(Position.KEY_FUEL_LEVEL) != null) {
+
+ BigDecimal value = BigDecimal.valueOf(firstPosition.getDouble(Position.KEY_FUEL_LEVEL)
+ - lastPosition.getDouble(Position.KEY_FUEL_LEVEL));
+ return value.setScale(1, RoundingMode.HALF_EVEN).doubleValue();
+ }
+ return 0;
+ }
+
+ public String findDriver(Position firstPosition, Position lastPosition) {
+ if (firstPosition.hasAttribute(Position.KEY_DRIVER_UNIQUE_ID)) {
+ return firstPosition.getString(Position.KEY_DRIVER_UNIQUE_ID);
+ } else if (lastPosition.hasAttribute(Position.KEY_DRIVER_UNIQUE_ID)) {
+ return lastPosition.getString(Position.KEY_DRIVER_UNIQUE_ID);
+ }
+ return null;
+ }
+
+ public String findDriverName(String driverUniqueId) throws StorageException {
+ if (driverUniqueId != null) {
+ Driver driver = storage.getObject(Driver.class, new Request(
+ new Columns.All(),
+ new Condition.Equals("uniqueId", driverUniqueId)));
+ if (driver != null) {
+ return driver.getName();
+ }
+ }
+ return null;
+ }
+
+ public org.jxls.common.Context initializeContext(long userId) throws StorageException {
+ var server = permissionsService.getServer();
+ var user = permissionsService.getUser(userId);
+ var context = PoiTransformer.createInitialContext();
+ context.putVar("distanceUnit", UserUtil.getDistanceUnit(server, user));
+ context.putVar("speedUnit", UserUtil.getSpeedUnit(server, user));
+ context.putVar("volumeUnit", UserUtil.getVolumeUnit(server, user));
+ context.putVar("webUrl", velocityEngine.getProperty("web.url"));
+ context.putVar("dateTool", new DateTool());
+ context.putVar("numberTool", new NumberTool());
+ context.putVar("timezone", UserUtil.getTimezone(server, user));
+ context.putVar("locale", Locale.getDefault());
+ context.putVar("bracketsRegex", "[\\{\\}\"]");
+ return context;
+ }
+
+ public void processTemplateWithSheets(
+ InputStream templateStream, OutputStream targetStream, org.jxls.common.Context context) throws IOException {
+
+ Transformer transformer = TransformerFactory.createTransformer(templateStream, targetStream);
+ List<Area> xlsAreas = new XlsCommentAreaBuilder(transformer).build();
+ for (Area xlsArea : xlsAreas) {
+ xlsArea.applyAt(new CellRef(xlsArea.getStartCellRef().getCellName()), context);
+ xlsArea.setFormulaProcessor(new StandardFormulaProcessor());
+ xlsArea.processFormulas();
+ }
+ transformer.deleteSheet(xlsAreas.get(0).getStartCellRef().getSheetName());
+ transformer.write();
+ }
+
+ private TripReportItem calculateTrip(
+ Device device, Position startTrip, Position endTrip, double maxSpeed,
+ boolean ignoreOdometer) throws StorageException {
+
+ TripReportItem trip = new TripReportItem();
+
+ long tripDuration = endTrip.getFixTime().getTime() - startTrip.getFixTime().getTime();
+ long deviceId = startTrip.getDeviceId();
+ trip.setDeviceId(deviceId);
+ trip.setDeviceName(device.getName());
+
+ trip.setStartPositionId(startTrip.getId());
+ trip.setStartLat(startTrip.getLatitude());
+ trip.setStartLon(startTrip.getLongitude());
+ trip.setStartTime(startTrip.getFixTime());
+ String startAddress = startTrip.getAddress();
+ if (startAddress == null && geocoder != null && config.getBoolean(Keys.GEOCODER_ON_REQUEST)) {
+ startAddress = geocoder.getAddress(startTrip.getLatitude(), startTrip.getLongitude(), null);
+ }
+ trip.setStartAddress(startAddress);
+
+ trip.setEndPositionId(endTrip.getId());
+ trip.setEndLat(endTrip.getLatitude());
+ trip.setEndLon(endTrip.getLongitude());
+ trip.setEndTime(endTrip.getFixTime());
+ String endAddress = endTrip.getAddress();
+ if (endAddress == null && geocoder != null && config.getBoolean(Keys.GEOCODER_ON_REQUEST)) {
+ endAddress = geocoder.getAddress(endTrip.getLatitude(), endTrip.getLongitude(), null);
+ }
+ trip.setEndAddress(endAddress);
+
+ trip.setDistance(PositionUtil.calculateDistance(startTrip, endTrip, !ignoreOdometer));
+ trip.setDuration(tripDuration);
+ if (tripDuration > 0) {
+ trip.setAverageSpeed(UnitsConverter.knotsFromMps(trip.getDistance() * 1000 / tripDuration));
+ }
+ trip.setMaxSpeed(maxSpeed);
+ trip.setSpentFuel(calculateFuel(startTrip, endTrip));
+
+ trip.setDriverUniqueId(findDriver(startTrip, endTrip));
+ trip.setDriverName(findDriverName(trip.getDriverUniqueId()));
+
+ if (!ignoreOdometer
+ && startTrip.getDouble(Position.KEY_ODOMETER) != 0
+ && endTrip.getDouble(Position.KEY_ODOMETER) != 0) {
+ trip.setStartOdometer(startTrip.getDouble(Position.KEY_ODOMETER));
+ trip.setEndOdometer(endTrip.getDouble(Position.KEY_ODOMETER));
+ } else {
+ trip.setStartOdometer(startTrip.getDouble(Position.KEY_TOTAL_DISTANCE));
+ trip.setEndOdometer(endTrip.getDouble(Position.KEY_TOTAL_DISTANCE));
+ }
+
+ return trip;
+ }
+
+ private StopReportItem calculateStop(
+ Device device, Position startStop, Position endStop, boolean ignoreOdometer) {
+
+ StopReportItem stop = new StopReportItem();
+
+ long deviceId = startStop.getDeviceId();
+ stop.setDeviceId(deviceId);
+ stop.setDeviceName(device.getName());
+
+ stop.setPositionId(startStop.getId());
+ stop.setLatitude(startStop.getLatitude());
+ stop.setLongitude(startStop.getLongitude());
+ stop.setStartTime(startStop.getFixTime());
+ String address = startStop.getAddress();
+ if (address == null && geocoder != null && config.getBoolean(Keys.GEOCODER_ON_REQUEST)) {
+ address = geocoder.getAddress(stop.getLatitude(), stop.getLongitude(), null);
+ }
+ stop.setAddress(address);
+
+ stop.setEndTime(endStop.getFixTime());
+
+ long stopDuration = endStop.getFixTime().getTime() - startStop.getFixTime().getTime();
+ stop.setDuration(stopDuration);
+ stop.setSpentFuel(calculateFuel(startStop, endStop));
+
+ if (startStop.hasAttribute(Position.KEY_HOURS) && endStop.hasAttribute(Position.KEY_HOURS)) {
+ stop.setEngineHours(endStop.getLong(Position.KEY_HOURS) - startStop.getLong(Position.KEY_HOURS));
+ }
+
+ if (!ignoreOdometer
+ && startStop.getDouble(Position.KEY_ODOMETER) != 0
+ && endStop.getDouble(Position.KEY_ODOMETER) != 0) {
+ stop.setStartOdometer(startStop.getDouble(Position.KEY_ODOMETER));
+ stop.setEndOdometer(endStop.getDouble(Position.KEY_ODOMETER));
+ } else {
+ stop.setStartOdometer(startStop.getDouble(Position.KEY_TOTAL_DISTANCE));
+ stop.setEndOdometer(endStop.getDouble(Position.KEY_TOTAL_DISTANCE));
+ }
+
+ return stop;
+
+ }
+
+ @SuppressWarnings("unchecked")
+ private <T extends BaseReportItem> T calculateTripOrStop(
+ Device device, Position startPosition, Position endPosition, double maxSpeed,
+ boolean ignoreOdometer, Class<T> reportClass) throws StorageException {
+
+ if (reportClass.equals(TripReportItem.class)) {
+ return (T) calculateTrip(device, startPosition, endPosition, maxSpeed, ignoreOdometer);
+ } else {
+ return (T) calculateStop(device, startPosition, endPosition, ignoreOdometer);
+ }
+ }
+
+ private boolean isMoving(List<Position> positions, int index, TripsConfig tripsConfig) {
+ if (tripsConfig.getMinimalNoDataDuration() > 0) {
+ boolean beforeGap = index < positions.size() - 1
+ && positions.get(index + 1).getFixTime().getTime() - positions.get(index).getFixTime().getTime()
+ >= tripsConfig.getMinimalNoDataDuration();
+ boolean afterGap = index > 0
+ && positions.get(index).getFixTime().getTime() - positions.get(index - 1).getFixTime().getTime()
+ >= tripsConfig.getMinimalNoDataDuration();
+ if (beforeGap || afterGap) {
+ return false;
+ }
+ }
+ return positions.get(index).getBoolean(Position.KEY_MOTION);
+ }
+
+ public <T extends BaseReportItem> List<T> detectTripsAndStops(
+ Device device, Date from, Date to, Class<T> reportClass) throws StorageException {
+
+ long threshold = config.getLong(Keys.REPORT_FAST_THRESHOLD);
+ if (Duration.between(from.toInstant(), to.toInstant()).toSeconds() > threshold) {
+ return fastTripsAndStops(device, from, to, reportClass);
+ } else {
+ return slowTripsAndStops(device, from, to, reportClass);
+ }
+ }
+
+ public <T extends BaseReportItem> List<T> slowTripsAndStops(
+ Device device, Date from, Date to, Class<T> reportClass) throws StorageException {
+
+ List<T> result = new ArrayList<>();
+ TripsConfig tripsConfig = new TripsConfig(
+ new AttributeUtil.StorageProvider(config, storage, permissionsService, device));
+ boolean ignoreOdometer = config.getBoolean(Keys.REPORT_IGNORE_ODOMETER);
+
+ var positions = PositionUtil.getPositions(storage, device.getId(), from, to);
+ if (!positions.isEmpty()) {
+ boolean trips = reportClass.equals(TripReportItem.class);
+
+ MotionState motionState = new MotionState();
+ boolean initialValue = isMoving(positions, 0, tripsConfig);
+ motionState.setMotionStreak(initialValue);
+ motionState.setMotionState(initialValue);
+
+ boolean detected = trips == motionState.getMotionState();
+ double maxSpeed = 0;
+ int startEventIndex = detected ? 0 : -1;
+ int startNoEventIndex = -1;
+ for (int i = 0; i < positions.size(); i++) {
+ boolean motion = isMoving(positions, i, tripsConfig);
+ if (motionState.getMotionState() != motion) {
+ if (motion == trips) {
+ if (!detected) {
+ startEventIndex = i;
+ maxSpeed = positions.get(i).getSpeed();
+ }
+ startNoEventIndex = -1;
+ } else {
+ startNoEventIndex = i;
+ }
+ } else {
+ maxSpeed = Math.max(maxSpeed, positions.get(i).getSpeed());
+ }
+
+ MotionProcessor.updateState(motionState, positions.get(i), motion, tripsConfig);
+ if (motionState.getEvent() != null) {
+ if (motion == trips) {
+ detected = true;
+ startNoEventIndex = -1;
+ } else if (startEventIndex >= 0 && startNoEventIndex >= 0) {
+ result.add(calculateTripOrStop(
+ device, positions.get(startEventIndex), positions.get(startNoEventIndex),
+ maxSpeed, ignoreOdometer, reportClass));
+ detected = false;
+ startEventIndex = -1;
+ startNoEventIndex = -1;
+ }
+ }
+ }
+ if (detected & startEventIndex >= 0 && startEventIndex < positions.size() - 1) {
+ int endIndex = startNoEventIndex >= 0 ? startNoEventIndex : positions.size() - 1;
+ result.add(calculateTripOrStop(
+ device, positions.get(startEventIndex), positions.get(endIndex),
+ maxSpeed, ignoreOdometer, reportClass));
+ }
+ }
+
+ return result;
+ }
+
+ public <T extends BaseReportItem> List<T> fastTripsAndStops(
+ Device device, Date from, Date to, Class<T> reportClass) throws StorageException {
+
+ List<T> result = new ArrayList<>();
+ boolean ignoreOdometer = config.getBoolean(Keys.REPORT_IGNORE_ODOMETER);
+ boolean trips = reportClass.equals(TripReportItem.class);
+ Set<String> filter = Set.of(Event.TYPE_DEVICE_MOVING, Event.TYPE_DEVICE_STOPPED);
+
+ var events = storage.getObjects(Event.class, new Request(
+ new Columns.All(),
+ new Condition.And(
+ new Condition.Equals("deviceId", device.getId()),
+ new Condition.Between("eventTime", "from", from, "to", to)),
+ new Order("eventTime")));
+ var filteredEvents = events.stream()
+ .filter(event -> filter.contains(event.getType()))
+ .collect(Collectors.toList());
+
+ Event startEvent = null;
+ for (Event event : filteredEvents) {
+ boolean motion = event.getType().equals(Event.TYPE_DEVICE_MOVING);
+ if (motion == trips) {
+ startEvent = event;
+ } else if (startEvent != null) {
+ Position startPosition = storage.getObject(Position.class, new Request(
+ new Columns.All(), new Condition.Equals("id", startEvent.getPositionId())));
+ Position endPosition = storage.getObject(Position.class, new Request(
+ new Columns.All(), new Condition.Equals("id", event.getPositionId())));
+ if (startPosition != null && endPosition != null) {
+ result.add(calculateTripOrStop(
+ device, startPosition, endPosition, 0, ignoreOdometer, reportClass));
+ }
+ startEvent = null;
+ }
+ }
+
+ return result;
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/common/TripsConfig.java b/src/main/java/org/traccar/reports/common/TripsConfig.java
new file mode 100644
index 000000000..2792114d4
--- /dev/null
+++ b/src/main/java/org/traccar/reports/common/TripsConfig.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2017 - 2023 Anton Tananaev (anton@traccar.org)
+ * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
+ *
+ * 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.reports.common;
+
+import org.traccar.config.Keys;
+import org.traccar.helper.model.AttributeUtil;
+
+public class TripsConfig {
+
+ public TripsConfig(AttributeUtil.Provider attributeProvider) {
+ this(
+ AttributeUtil.lookup(attributeProvider, Keys.REPORT_TRIP_MINIMAL_TRIP_DISTANCE),
+ AttributeUtil.lookup(attributeProvider, Keys.REPORT_TRIP_MINIMAL_TRIP_DURATION) * 1000,
+ AttributeUtil.lookup(attributeProvider, Keys.REPORT_TRIP_MINIMAL_PARKING_DURATION) * 1000,
+ AttributeUtil.lookup(attributeProvider, Keys.REPORT_TRIP_MINIMAL_NO_DATA_DURATION) * 1000,
+ AttributeUtil.lookup(attributeProvider, Keys.REPORT_TRIP_USE_IGNITION));
+ }
+
+ public TripsConfig(
+ double minimalTripDistance, long minimalTripDuration, long minimalParkingDuration,
+ long minimalNoDataDuration, boolean useIgnition) {
+ this.minimalTripDistance = minimalTripDistance;
+ this.minimalTripDuration = minimalTripDuration;
+ this.minimalParkingDuration = minimalParkingDuration;
+ this.minimalNoDataDuration = minimalNoDataDuration;
+ this.useIgnition = useIgnition;
+ }
+
+ private final double minimalTripDistance;
+
+ public double getMinimalTripDistance() {
+ return minimalTripDistance;
+ }
+
+ private final long minimalTripDuration;
+
+ public long getMinimalTripDuration() {
+ return minimalTripDuration;
+ }
+
+ private final long minimalParkingDuration;
+
+ public long getMinimalParkingDuration() {
+ return minimalParkingDuration;
+ }
+
+ private final long minimalNoDataDuration;
+
+ public long getMinimalNoDataDuration() {
+ return minimalNoDataDuration;
+ }
+
+ private final boolean useIgnition;
+
+ public boolean getUseIgnition() {
+ return useIgnition;
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/model/BaseReport.java b/src/main/java/org/traccar/reports/model/BaseReportItem.java
index 928c0557d..6e270dfe3 100644
--- a/src/main/java/org/traccar/reports/model/BaseReport.java
+++ b/src/main/java/org/traccar/reports/model/BaseReportItem.java
@@ -18,7 +18,7 @@ package org.traccar.reports.model;
import java.util.Date;
-public class BaseReport {
+public class BaseReportItem {
private long deviceId;
diff --git a/src/main/java/org/traccar/reports/model/CombinedReportItem.java b/src/main/java/org/traccar/reports/model/CombinedReportItem.java
new file mode 100644
index 000000000..810e00916
--- /dev/null
+++ b/src/main/java/org/traccar/reports/model/CombinedReportItem.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2023 Anton Tananaev (anton@traccar.org)
+ *
+ * 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.reports.model;
+
+import org.traccar.model.Event;
+import org.traccar.model.Position;
+
+import java.util.List;
+
+public class CombinedReportItem {
+
+ private long deviceId;
+
+ public long getDeviceId() {
+ return deviceId;
+ }
+
+ public void setDeviceId(long deviceId) {
+ this.deviceId = deviceId;
+ }
+
+ private List<double[]> route;
+
+ public List<double[]> getRoute() {
+ return route;
+ }
+
+ public void setRoute(List<double[]> route) {
+ this.route = route;
+ }
+
+ private List<Event> events;
+
+ public List<Event> getEvents() {
+ return events;
+ }
+
+ public void setEvents(List<Event> events) {
+ this.events = events;
+ }
+
+ private List<Position> positions;
+
+ public List<Position> getPositions() {
+ return positions;
+ }
+
+ public void setPositions(List<Position> positions) {
+ this.positions = positions;
+ }
+
+}
diff --git a/src/main/java/org/traccar/reports/model/DeviceReport.java b/src/main/java/org/traccar/reports/model/DeviceReportSection.java
index 932753d15..ffc4d774f 100644
--- a/src/main/java/org/traccar/reports/model/DeviceReport.java
+++ b/src/main/java/org/traccar/reports/model/DeviceReportSection.java
@@ -20,7 +20,7 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
-public class DeviceReport {
+public class DeviceReportSection {
private String deviceName;
diff --git a/src/main/java/org/traccar/reports/model/StopReport.java b/src/main/java/org/traccar/reports/model/StopReportItem.java
index e20f2c503..3c35bdc21 100644
--- a/src/main/java/org/traccar/reports/model/StopReport.java
+++ b/src/main/java/org/traccar/reports/model/StopReportItem.java
@@ -16,7 +16,7 @@
*/
package org.traccar.reports.model;
-public class StopReport extends BaseReport {
+public class StopReportItem extends BaseReportItem {
private long positionId;
diff --git a/src/main/java/org/traccar/reports/model/SummaryReport.java b/src/main/java/org/traccar/reports/model/SummaryReportItem.java
index 886f8b9e2..44a15cf1d 100644
--- a/src/main/java/org/traccar/reports/model/SummaryReport.java
+++ b/src/main/java/org/traccar/reports/model/SummaryReportItem.java
@@ -16,7 +16,7 @@
*/
package org.traccar.reports.model;
-public class SummaryReport extends BaseReport {
+public class SummaryReportItem extends BaseReportItem {
private long engineHours; // milliseconds
diff --git a/src/main/java/org/traccar/reports/model/TripReport.java b/src/main/java/org/traccar/reports/model/TripReportItem.java
index 151c34bd5..332a34cca 100644
--- a/src/main/java/org/traccar/reports/model/TripReport.java
+++ b/src/main/java/org/traccar/reports/model/TripReportItem.java
@@ -16,7 +16,7 @@
*/
package org.traccar.reports.model;
-public class TripReport extends BaseReport {
+public class TripReportItem extends BaseReportItem {
private long startPositionId;
diff --git a/src/main/java/org/traccar/reports/model/TripsConfig.java b/src/main/java/org/traccar/reports/model/TripsConfig.java
deleted file mode 100644
index 0f0c615d3..000000000
--- a/src/main/java/org/traccar/reports/model/TripsConfig.java
+++ /dev/null
@@ -1,105 +0,0 @@
-/*
- * Copyright 2017 Anton Tananaev (anton@traccar.org)
- * Copyright 2017 Andrey Kunitsyn (andrey@traccar.org)
- *
- * 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.reports.model;
-
-public class TripsConfig {
-
- public TripsConfig() {
- }
-
- public TripsConfig(double minimalTripDistance, long minimalTripDuration, long minimalParkingDuration,
- long minimalNoDataDuration, boolean useIgnition, boolean processInvalidPositions, double speedThreshold) {
- this.minimalTripDistance = minimalTripDistance;
- this.minimalTripDuration = minimalTripDuration;
- this.minimalParkingDuration = minimalParkingDuration;
- this.minimalNoDataDuration = minimalNoDataDuration;
- this.useIgnition = useIgnition;
- this.processInvalidPositions = processInvalidPositions;
- this.speedThreshold = speedThreshold;
- }
-
- private double minimalTripDistance;
-
- public double getMinimalTripDistance() {
- return minimalTripDistance;
- }
-
- public void setMinimalTripDistance(double minimalTripDistance) {
- this.minimalTripDistance = minimalTripDistance;
- }
-
- private long minimalTripDuration;
-
- public long getMinimalTripDuration() {
- return minimalTripDuration;
- }
-
- public void setMinimalTripDuration(long minimalTripDuration) {
- this.minimalTripDuration = minimalTripDuration;
- }
-
- private long minimalParkingDuration;
-
- public long getMinimalParkingDuration() {
- return minimalParkingDuration;
- }
-
- public void setMinimalParkingDuration(long minimalParkingDuration) {
- this.minimalParkingDuration = minimalParkingDuration;
- }
-
- private long minimalNoDataDuration;
-
- public long getMinimalNoDataDuration() {
- return minimalNoDataDuration;
- }
-
- public void setMinimalNoDataDuration(long minimalNoDataDuration) {
- this.minimalNoDataDuration = minimalNoDataDuration;
- }
-
- private boolean useIgnition;
-
- public boolean getUseIgnition() {
- return useIgnition;
- }
-
- public void setUseIgnition(boolean useIgnition) {
- this.useIgnition = useIgnition;
- }
-
- private boolean processInvalidPositions;
-
- public boolean getProcessInvalidPositions() {
- return processInvalidPositions;
- }
-
- public void setProcessInvalidPositions(boolean processInvalidPositions) {
- this.processInvalidPositions = processInvalidPositions;
- }
-
- private double speedThreshold;
-
- public double getSpeedThreshold() {
- return speedThreshold;
- }
-
- public void setSpeedThreshold(double speedThreshold) {
- this.speedThreshold = speedThreshold;
- }
-
-}