diff options
Diffstat (limited to 'src/main/java/org/traccar/reports')
16 files changed, 542 insertions, 212 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 index df55c470e..521dc120a 100644 --- a/src/main/java/org/traccar/reports/CsvExportProvider.java +++ b/src/main/java/org/traccar/reports/CsvExportProvider.java @@ -21,7 +21,7 @@ import org.traccar.model.Position; import org.traccar.storage.Storage; import org.traccar.storage.StorageException; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Date; diff --git a/src/main/java/org/traccar/reports/DevicesReportProvider.java b/src/main/java/org/traccar/reports/DevicesReportProvider.java new file mode 100644 index 000000000..7b4294d53 --- /dev/null +++ b/src/main/java/org/traccar/reports/DevicesReportProvider.java @@ -0,0 +1,78 @@ +/* + * Copyright 2024 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 jakarta.inject.Inject; +import org.jxls.util.JxlsHelper; +import org.traccar.config.Config; +import org.traccar.config.Keys; +import org.traccar.helper.model.PositionUtil; +import org.traccar.model.Device; +import org.traccar.model.Message; +import org.traccar.model.User; +import org.traccar.reports.common.ReportUtils; +import org.traccar.reports.model.DeviceReportItem; +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 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.Collection; +import java.util.stream.Collectors; + +public class DevicesReportProvider { + + private final Config config; + private final ReportUtils reportUtils; + private final Storage storage; + + @Inject + public DevicesReportProvider(Config config, ReportUtils reportUtils, Storage storage) { + this.config = config; + this.reportUtils = reportUtils; + this.storage = storage; + } + + public Collection<DeviceReportItem> getObjects(long userId) throws StorageException { + + var positions = PositionUtil.getLatestPositions(storage, userId).stream() + .collect(Collectors.toMap(Message::getDeviceId, p -> p)); + + return storage.getObjects(Device.class, new Request( + new Columns.All(), + new Condition.Permission(User.class, userId, Device.class))).stream() + .map(device -> new DeviceReportItem(device, positions.get(device.getId()))) + .collect(Collectors.toUnmodifiableList()); + } + + public void getExcel(OutputStream outputStream, long userId) throws StorageException, IOException { + + File file = Paths.get(config.getString(Keys.TEMPLATES_ROOT), "export", "devices.xlsx").toFile(); + try (InputStream inputStream = new FileInputStream(file)) { + var context = reportUtils.initializeContext(userId); + context.putVar("items", getObjects(userId)); + JxlsHelper.getInstance().setUseFastFormulaProcessor(false) + .processTemplate(inputStream, outputStream, context); + } + } +} diff --git a/src/main/java/org/traccar/reports/EventsReportProvider.java b/src/main/java/org/traccar/reports/EventsReportProvider.java index 30f55ba80..f252f28cc 100644 --- a/src/main/java/org/traccar/reports/EventsReportProvider.java +++ b/src/main/java/org/traccar/reports/EventsReportProvider.java @@ -19,6 +19,7 @@ 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; @@ -34,7 +35,7 @@ import org.traccar.storage.query.Condition; import org.traccar.storage.query.Order; import org.traccar.storage.query.Request; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -76,7 +77,7 @@ public class EventsReportProvider { reportUtils.checkPeriodLimit(from, to); ArrayList<Event> result = new ArrayList<>(); - for (Device device: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { + 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) { @@ -104,7 +105,7 @@ public class EventsReportProvider { HashMap<Long, String> geofenceNames = new HashMap<>(); HashMap<Long, String> maintenanceNames = new HashMap<>(); HashMap<Long, Position> positions = new HashMap<>(); - for (Device device: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { + 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();) { diff --git a/src/main/java/org/traccar/reports/GpxExportProvider.java b/src/main/java/org/traccar/reports/GpxExportProvider.java index ccbd97fc3..1c45b6416 100644 --- a/src/main/java/org/traccar/reports/GpxExportProvider.java +++ b/src/main/java/org/traccar/reports/GpxExportProvider.java @@ -24,7 +24,7 @@ import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.OutputStream; import java.io.PrintWriter; import java.util.Date; diff --git a/src/main/java/org/traccar/reports/KmlExportProvider.java b/src/main/java/org/traccar/reports/KmlExportProvider.java index 24fcfb8ab..24dca018c 100644 --- a/src/main/java/org/traccar/reports/KmlExportProvider.java +++ b/src/main/java/org/traccar/reports/KmlExportProvider.java @@ -23,7 +23,7 @@ import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.OutputStream; import java.io.PrintWriter; import java.text.SimpleDateFormat; diff --git a/src/main/java/org/traccar/reports/RouteReportProvider.java b/src/main/java/org/traccar/reports/RouteReportProvider.java index 3ee651619..d761fe1e5 100644 --- a/src/main/java/org/traccar/reports/RouteReportProvider.java +++ b/src/main/java/org/traccar/reports/RouteReportProvider.java @@ -19,6 +19,7 @@ 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; @@ -31,7 +32,7 @@ import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -41,6 +42,8 @@ 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 { @@ -48,6 +51,8 @@ public class RouteReportProvider { 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; @@ -60,12 +65,18 @@ public class RouteReportProvider { reportUtils.checkPeriodLimit(from, to); ArrayList<Position> result = new ArrayList<>(); - for (Device device: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { + 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 { @@ -73,11 +84,11 @@ public class RouteReportProvider { ArrayList<DeviceReportSection> devicesRoutes = new ArrayList<>(); ArrayList<String> sheetNames = new ArrayList<>(); - for (Device device: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { + 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(deviceRoutes.getDeviceName())); + 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()))); diff --git a/src/main/java/org/traccar/reports/StopsReportProvider.java b/src/main/java/org/traccar/reports/StopsReportProvider.java index ec3fd2215..2160fec0e 100644 --- a/src/main/java/org/traccar/reports/StopsReportProvider.java +++ b/src/main/java/org/traccar/reports/StopsReportProvider.java @@ -19,7 +19,7 @@ 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.PositionUtil; +import org.traccar.helper.model.DeviceUtil; import org.traccar.model.Device; import org.traccar.model.Group; import org.traccar.reports.common.ReportUtils; @@ -31,7 +31,7 @@ import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -55,20 +55,14 @@ public class StopsReportProvider { this.storage = storage; } - private Collection<StopReportItem> detectStops(Device device, Date from, Date to) throws StorageException { - boolean ignoreOdometer = config.getBoolean(Keys.REPORT_IGNORE_ODOMETER); - var positions = PositionUtil.getPositions(storage, device.getId(), from, to); - return reportUtils.detectTripsAndStops(device, positions, ignoreOdometer, StopReportItem.class); - } - 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: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { - result.addAll(detectStops(device, from, to)); + for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) { + result.addAll(reportUtils.detectTripsAndStops(device, from, to, StopReportItem.class)); } return result; } @@ -80,8 +74,8 @@ public class StopsReportProvider { ArrayList<DeviceReportSection> devicesStops = new ArrayList<>(); ArrayList<String> sheetNames = new ArrayList<>(); - for (Device device: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { - Collection<StopReportItem> stops = detectStops(device, from, to); + 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())); diff --git a/src/main/java/org/traccar/reports/SummaryReportProvider.java b/src/main/java/org/traccar/reports/SummaryReportProvider.java index 9415f3e81..ffde0b067 100644 --- a/src/main/java/org/traccar/reports/SummaryReportProvider.java +++ b/src/main/java/org/traccar/reports/SummaryReportProvider.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org) + * 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"); @@ -21,6 +21,7 @@ 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; @@ -29,18 +30,25 @@ 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 javax.inject.Inject; +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.Calendar; import java.util.Collection; import java.util.Date; +import java.util.List; public class SummaryReportProvider { @@ -58,96 +66,105 @@ public class SummaryReportProvider { this.storage = storage; } - private SummaryReportItem calculateSummaryResult(Device device, Collection<Position> positions) { + 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()); - if (positions != null && !positions.isEmpty()) { - Position firstPosition = null; - Position previousPosition = null; + + 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 (firstPosition == null) { - firstPosition = position; + if (first == null) { + first = position; } - previousPosition = 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(firstPosition, previousPosition, !ignoreOdometer)); - result.setSpentFuel(reportUtils.calculateFuel(firstPosition, previousPosition)); + result.setDistance(PositionUtil.calculateDistance(first, last, !ignoreOdometer)); + result.setSpentFuel(reportUtils.calculateFuel(first, last)); long durationMilliseconds; - if (firstPosition.hasAttribute(Position.KEY_HOURS) && previousPosition.hasAttribute(Position.KEY_HOURS)) { - durationMilliseconds = - previousPosition.getLong(Position.KEY_HOURS) - firstPosition.getLong(Position.KEY_HOURS); + 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 = - previousPosition.getFixTime().getTime() - firstPosition.getFixTime().getTime(); + durationMilliseconds = last.getFixTime().getTime() - first.getFixTime().getTime(); } if (durationMilliseconds > 0) { - result.setAverageSpeed( - UnitsConverter.knotsFromMps(result.getDistance() * 1000 / durationMilliseconds)); + 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)); + && 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(firstPosition.getDouble(Position.KEY_TOTAL_DISTANCE)); - result.setEndOdometer(previousPosition.getDouble(Position.KEY_TOTAL_DISTANCE)); + result.setStartOdometer(first.getDouble(Position.KEY_TOTAL_DISTANCE)); + result.setEndOdometer(last.getDouble(Position.KEY_TOTAL_DISTANCE)); } - result.setStartTime(firstPosition.getFixTime()); - result.setEndTime(previousPosition.getFixTime()); + result.setStartTime(first.getFixTime()); + result.setEndTime(last.getFixTime()); + return List.of(result); } - return result; - } - private int getDay(long userId, Date date) throws StorageException { - Calendar calendar = Calendar.getInstance(UserUtil.getTimezone( - permissionsService.getServer(), permissionsService.getUser(userId))); - calendar.setTime(date); - return calendar.get(Calendar.DAY_OF_MONTH); + return List.of(); } - private Collection<SummaryReportItem> calculateSummaryResults( - long userId, Device device, Date from, Date to, boolean daily) throws StorageException { + private Collection<SummaryReportItem> calculateDeviceResults( + Device device, ZonedDateTime from, ZonedDateTime to, boolean daily) throws StorageException { - var positions = PositionUtil.getPositions(storage, device.getId(), from, to); + boolean fast = Duration.between(from, to).toSeconds() > config.getLong(Keys.REPORT_FAST_THRESHOLD); var results = new ArrayList<SummaryReportItem>(); - 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(device, positions.subList(startIndex, i))); - startIndex = i; - startDay = currentDay; - } + 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.add(calculateSummaryResult(device, positions.subList(startIndex, positions.size()))); + results.addAll(calculateDeviceResult(device, Date.from(from.toInstant()), Date.from(to.toInstant()), fast)); } else { - results.add(calculateSummaryResult(device, positions)); + 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 { + 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: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { - Collection<SummaryReportItem> deviceResults = calculateSummaryResults(userId, device, from, to, daily); + 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); diff --git a/src/main/java/org/traccar/reports/TripsReportProvider.java b/src/main/java/org/traccar/reports/TripsReportProvider.java index 265811354..9ff7232af 100644 --- a/src/main/java/org/traccar/reports/TripsReportProvider.java +++ b/src/main/java/org/traccar/reports/TripsReportProvider.java @@ -19,7 +19,7 @@ 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.PositionUtil; +import org.traccar.helper.model.DeviceUtil; import org.traccar.model.Device; import org.traccar.model.Group; import org.traccar.reports.common.ReportUtils; @@ -31,7 +31,7 @@ import org.traccar.storage.query.Columns; import org.traccar.storage.query.Condition; import org.traccar.storage.query.Request; -import javax.inject.Inject; +import jakarta.inject.Inject; import java.io.File; import java.io.FileInputStream; import java.io.IOException; @@ -55,20 +55,14 @@ public class TripsReportProvider { this.storage = storage; } - private Collection<TripReportItem> detectTrips(Device device, Date from, Date to) throws StorageException { - boolean ignoreOdometer = config.getBoolean(Keys.REPORT_IGNORE_ODOMETER); - var positions = PositionUtil.getPositions(storage, device.getId(), from, to); - return reportUtils.detectTripsAndStops(device, positions, ignoreOdometer, TripReportItem.class); - } - 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: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { - result.addAll(detectTrips(device, from, to)); + for (Device device: DeviceUtil.getAccessibleDevices(storage, userId, deviceIds, groupIds)) { + result.addAll(reportUtils.detectTripsAndStops(device, from, to, TripReportItem.class)); } return result; } @@ -80,8 +74,8 @@ public class TripsReportProvider { ArrayList<DeviceReportSection> devicesTrips = new ArrayList<>(); ArrayList<String> sheetNames = new ArrayList<>(); - for (Device device: reportUtils.getAccessibleDevices(userId, deviceIds, groupIds)) { - Collection<TripReportItem> trips = detectTrips(device, from, to); + 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())); 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/ReportMailer.java b/src/main/java/org/traccar/reports/common/ReportMailer.java index 221b35ae1..9fb30fe9f 100644 --- a/src/main/java/org/traccar/reports/common/ReportMailer.java +++ b/src/main/java/org/traccar/reports/common/ReportMailer.java @@ -22,11 +22,11 @@ import org.traccar.mail.MailManager; import org.traccar.model.User; import org.traccar.storage.StorageException; -import javax.activation.DataHandler; -import javax.inject.Inject; -import javax.mail.MessagingException; -import javax.mail.internet.MimeBodyPart; -import javax.mail.util.ByteArrayDataSource; +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; @@ -55,7 +55,7 @@ public class ReportMailer { stream.toByteArray(), "application/octet-stream"))); User user = permissionsService.getUser(userId); - mailManager.sendMessage(user, "Report", "The report is in the attachment.", attachment); + mailManager.sendMessage(user, false, "Report", "The report is in the attachment.", attachment); } catch (StorageException | IOException | MessagingException e) { LOGGER.warn("Email report failed", e); } diff --git a/src/main/java/org/traccar/reports/common/ReportUtils.java b/src/main/java/org/traccar/reports/common/ReportUtils.java index a7c420095..3b8e84887 100644 --- a/src/main/java/org/traccar/reports/common/ReportUtils.java +++ b/src/main/java/org/traccar/reports/common/ReportUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 - 2022 Anton Tananaev (anton@traccar.org) + * Copyright 2016 - 2023 Anton Tananaev (anton@traccar.org) * Copyright 2016 - 2017 Andrey Kunitsyn (andrey@traccar.org) * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -16,6 +16,8 @@ */ package org.traccar.reports.common; +import jakarta.annotation.Nullable; +import jakarta.inject.Inject; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.tools.generic.DateTool; import org.apache.velocity.tools.generic.NumberTool; @@ -31,12 +33,13 @@ 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.Group; +import org.traccar.model.Event; import org.traccar.model.Position; import org.traccar.model.User; import org.traccar.reports.model.BaseReportItem; @@ -48,44 +51,35 @@ 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 javax.annotation.Nullable; -import javax.inject.Inject; -import javax.inject.Singleton; 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.Collection; -import java.util.Collections; import java.util.Date; -import java.util.LinkedList; import java.util.List; import java.util.Locale; -import java.util.Objects; +import java.util.Set; import java.util.stream.Collectors; -@Singleton public class ReportUtils { private final Config config; private final Storage storage; private final PermissionsService permissionsService; - private final TripsConfig tripsConfig; private final VelocityEngine velocityEngine; private final Geocoder geocoder; @Inject public ReportUtils( Config config, Storage storage, PermissionsService permissionsService, - TripsConfig tripsConfig, VelocityEngine velocityEngine, @Nullable Geocoder geocoder) { + VelocityEngine velocityEngine, @Nullable Geocoder geocoder) { this.config = config; this.storage = storage; this.permissionsService = permissionsService; - this.tripsConfig = tripsConfig; this.velocityEngine = velocityEngine; this.geocoder = geocoder; } @@ -106,49 +100,11 @@ public class ReportUtils { } } - public Collection<Device> getAccessibleDevices( - long userId, Collection<Long> deviceIds, Collection<Long> groupIds) throws StorageException { - - var devices = storage.getObjects(Device.class, new Request( - new Columns.All(), - new Condition.Permission(User.class, userId, Device.class))); - var deviceById = devices.stream() - .collect(Collectors.toUnmodifiableMap(Device::getId, x -> x)); - var devicesByGroup = devices.stream() - .filter(x -> x.getGroupId() > 0) - .collect(Collectors.groupingBy(Device::getGroupId)); - - var groups = storage.getObjects(Group.class, new Request( - new Columns.All(), - new Condition.Permission(User.class, userId, Group.class))); - var groupsByGroup = groups.stream() - .filter(x -> x.getGroupId() > 0) - .collect(Collectors.groupingBy(Group::getGroupId)); - - var results = deviceIds.stream() - .map(deviceById::get) - .filter(Objects::nonNull) - .collect(Collectors.toSet()); - - var groupQueue = new LinkedList<>(groupIds); - while (!groupQueue.isEmpty()) { - long groupId = groupQueue.pop(); - results.addAll(devicesByGroup.getOrDefault(groupId, Collections.emptyList())); - groupQueue.addAll(groupsByGroup.getOrDefault(groupId, Collections.emptyList()) - .stream().map(Group::getId).collect(Collectors.toUnmodifiableList())); - } - - return results; - } - - 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(); + public double calculateFuel(Position first, Position last) { + if (first.hasAttribute(Position.KEY_FUEL_USED) && last.hasAttribute(Position.KEY_FUEL_USED)) { + return last.getDouble(Position.KEY_FUEL_USED) - first.getDouble(Position.KEY_FUEL_USED); + } else if (first.hasAttribute(Position.KEY_FUEL_LEVEL) && last.hasAttribute(Position.KEY_FUEL_LEVEL)) { + return first.getDouble(Position.KEY_FUEL_LEVEL) - last.getDouble(Position.KEY_FUEL_LEVEL); } return 0; } @@ -205,20 +161,9 @@ public class ReportUtils { } private TripReportItem calculateTrip( - Device device, ArrayList<Position> positions, int startIndex, int endIndex, + Device device, Position startTrip, Position endTrip, double maxSpeed, boolean ignoreOdometer) throws StorageException { - 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; - } - } - TripReportItem trip = new TripReportItem(); long tripDuration = endTrip.getFixTime().getTime() - startTrip.getFixTime().getTime(); @@ -251,7 +196,7 @@ public class ReportUtils { if (tripDuration > 0) { trip.setAverageSpeed(UnitsConverter.knotsFromMps(trip.getDistance() * 1000 / tripDuration)); } - trip.setMaxSpeed(speedMax); + trip.setMaxSpeed(maxSpeed); trip.setSpentFuel(calculateFuel(startTrip, endTrip)); trip.setDriverUniqueId(findDriver(startTrip, endTrip)); @@ -271,10 +216,7 @@ public class ReportUtils { } private StopReportItem calculateStop( - Device device, ArrayList<Position> positions, int startIndex, int endIndex, boolean ignoreOdometer) { - - Position startStop = positions.get(startIndex); - Position endStop = positions.get(endIndex); + Device device, Position startStop, Position endStop, boolean ignoreOdometer) { StopReportItem stop = new StopReportItem(); @@ -318,17 +260,17 @@ public class ReportUtils { @SuppressWarnings("unchecked") private <T extends BaseReportItem> T calculateTripOrStop( - Device device, ArrayList<Position> positions, int startIndex, int endIndex, + Device device, Position startPosition, Position endPosition, double maxSpeed, boolean ignoreOdometer, Class<T> reportClass) throws StorageException { if (reportClass.equals(TripReportItem.class)) { - return (T) calculateTrip(device, positions, startIndex, endIndex, ignoreOdometer); + return (T) calculateTrip(device, startPosition, endPosition, maxSpeed, ignoreOdometer); } else { - return (T) calculateStop(device, positions, startIndex, endIndex, ignoreOdometer); + return (T) calculateStop(device, startPosition, endPosition, ignoreOdometer); } } - private boolean isMoving(ArrayList<Position> positions, int index, TripsConfig tripsConfig) { + 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() @@ -343,13 +285,26 @@ public class ReportUtils { return positions.get(index).getBoolean(Position.KEY_MOTION); } - public <T extends BaseReportItem> Collection<T> detectTripsAndStops( - Device device, Collection<Position> positionCollection, boolean ignoreOdometer, - Class<T> reportClass) throws StorageException { + 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 { - Collection<T> result = new ArrayList<>(); + List<T> result = new ArrayList<>(); + TripsConfig tripsConfig = new TripsConfig( + new AttributeUtil.StorageProvider(config, storage, permissionsService, device)); + boolean ignoreOdometer = config.getBoolean(Keys.REPORT_IGNORE_ODOMETER); - ArrayList<Position> positions = new ArrayList<>(positionCollection); + var positions = PositionUtil.getPositions(storage, device.getId(), from, to); if (!positions.isEmpty()) { boolean trips = reportClass.equals(TripReportItem.class); @@ -359,17 +314,23 @@ public class ReportUtils { 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) { - startEventIndex = detected ? startEventIndex : i; + 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); @@ -379,17 +340,58 @@ public class ReportUtils { startNoEventIndex = -1; } else if (startEventIndex >= 0 && startNoEventIndex >= 0) { result.add(calculateTripOrStop( - device, positions, startEventIndex, startNoEventIndex, ignoreOdometer, reportClass)); + device, positions.get(startEventIndex), positions.get(startNoEventIndex), + maxSpeed, ignoreOdometer, reportClass)); detected = false; startEventIndex = -1; startNoEventIndex = -1; } } } - if (startEventIndex >= 0 && startEventIndex < positions.size() - 1) { + if (detected & startEventIndex >= 0 && startEventIndex < positions.size() - 1) { int endIndex = startNoEventIndex >= 0 ? startNoEventIndex : positions.size() - 1; result.add(calculateTripOrStop( - device, positions, startEventIndex, endIndex, ignoreOdometer, reportClass)); + 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; } } diff --git a/src/main/java/org/traccar/reports/common/TripsConfig.java b/src/main/java/org/traccar/reports/common/TripsConfig.java index 52db97b74..2792114d4 100644 --- a/src/main/java/org/traccar/reports/common/TripsConfig.java +++ b/src/main/java/org/traccar/reports/common/TripsConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2017 - 2022 Anton Tananaev (anton@traccar.org) + * 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"); @@ -16,37 +16,28 @@ */ package org.traccar.reports.common; -import org.traccar.config.Config; import org.traccar.config.Keys; +import org.traccar.helper.model.AttributeUtil; -import javax.inject.Inject; -import javax.inject.Singleton; - -@Singleton public class TripsConfig { - @Inject - public TripsConfig(Config config) { + public TripsConfig(AttributeUtil.Provider attributeProvider) { this( - config.getLong(Keys.REPORT_TRIP_MINIMAL_TRIP_DISTANCE), - config.getLong(Keys.REPORT_TRIP_MINIMAL_TRIP_DURATION) * 1000, - config.getLong(Keys.REPORT_TRIP_MINIMAL_PARKING_DURATION) * 1000, - config.getLong(Keys.REPORT_TRIP_MINIMAL_NO_DATA_DURATION) * 1000, - config.getBoolean(Keys.REPORT_TRIP_USE_IGNITION), - config.getBoolean(Keys.EVENT_MOTION_PROCESS_INVALID_POSITIONS), - config.getDouble(Keys.EVENT_MOTION_SPEED_THRESHOLD)); + 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, boolean processInvalidPositions, double speedThreshold) { + long minimalNoDataDuration, boolean useIgnition) { this.minimalTripDistance = minimalTripDistance; this.minimalTripDuration = minimalTripDuration; this.minimalParkingDuration = minimalParkingDuration; this.minimalNoDataDuration = minimalNoDataDuration; this.useIgnition = useIgnition; - this.processInvalidPositions = processInvalidPositions; - this.speedThreshold = speedThreshold; } private final double minimalTripDistance; @@ -79,16 +70,4 @@ public class TripsConfig { return useIgnition; } - private final boolean processInvalidPositions; - - public boolean getProcessInvalidPositions() { - return processInvalidPositions; - } - - private final double speedThreshold; - - public double getSpeedThreshold() { - return speedThreshold; - } - } 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/DeviceReportItem.java b/src/main/java/org/traccar/reports/model/DeviceReportItem.java new file mode 100644 index 000000000..74cd5f32c --- /dev/null +++ b/src/main/java/org/traccar/reports/model/DeviceReportItem.java @@ -0,0 +1,48 @@ +/* + * Copyright 2024 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.Device; +import org.traccar.model.Position; + +public class DeviceReportItem { + + public DeviceReportItem(Device device, Position position) { + this.device = device; + this.position = position; + } + + private Device device; + + public Device getDevice() { + return device; + } + + public void setDevice(Device device) { + this.device = device; + } + + private Position position; + + public Position getPosition() { + return position; + } + + public void setPosition(Position position) { + this.position = position; + } + +} |