/* * Copyright (C) 2013 Serdar * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package de.fub.maps.project.detector.model.pipeline.preprocessors.filters; import de.fub.agg2graph.gpseval.data.Waypoint; import de.fub.agg2graph.structs.GPSCalc; import de.fub.maps.project.api.process.ProcessState; import de.fub.maps.project.detector.model.gpx.GpxWayPoint; import de.fub.maps.project.detector.model.gpx.TrackSegment; import de.fub.maps.project.detector.model.inference.InferenceMode; import de.fub.maps.project.detector.model.pipeline.preprocessors.FilterProcess; import de.fub.maps.project.detector.model.xmls.ProcessDescriptor; import de.fub.maps.project.detector.model.xmls.Property; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; import org.openide.util.Exceptions; import org.openide.util.NbBundle; import org.openide.util.lookup.ServiceProvider; /** * Implementation of the Change-Point Segmentation algorithm of "Understanding * Transportation Modes Based on GPS Data for Web Applications" by YU ZHENG, * YUKUN CHEN, QUANNAN LI, XING XIE and WEI-YING MA * * @author Serdar */ @ServiceProvider(service = FilterProcess.class) public class ChangePointSegmentationFilterProcess extends FilterProcess { private static final String PROP_NAME_LOOSE_UPPER_VELOCITY_BOUND = "change.point.upper.velocity.bound"; private static final String PROP_NAME_LOOSE_UPPER_ACCELERATION_BOUND = "change.point.upper.acceleration.bound"; private static final String PROP_NAME_CERTAIN_SEGMENT_LENGTH_THRESHOLD = "change.point.certain.seg.length.threshold"; private static final String PROP_NAME_UNCERTAIN_SEGMENT_COUNT_THRESHOLD = "change.point.uncertain.seg.count.threshold"; private static final String PROP_NAME_MINIMAL_DISTANCE_BOUND = "change.point.minimal.distance.bound"; private static final String PROP_NAME_MINIMAL_TIME_DIFFERENCE = "change.point.minimal.time.difference"; private static final Logger LOG = Logger.getLogger(ChangePointSegmentationFilterProcess.class.getName()); private List<TrackSegment> result = new ArrayList<TrackSegment>(100); private List<TrackSegment> gpsTracks; // upper bounds for distingush walking and non-walking segment private double looseUpperBoundsVelocity = -1; private double looseUpperBoundsAcceleration = -1; // threshold that declareds that each segment's length exceeds this threshold as certain. private double certainSegmentLengthThreshold = -1; // threshold count if uncertain segment reach this count it will be declared as non-walking segment private int uncertainSegCountThreshold = -1; private long minimalTimeDifference = -1; private double minimalDistanceBound = -1; public ChangePointSegmentationFilterProcess() { } private void init() { ProcessDescriptor processDescriptor = getProcessDescriptor(); if (processDescriptor != null) { List<Property> propertyList = processDescriptor.getProperties().getPropertyList(); try { for (Property property : propertyList) { if (property.getValue() != null) { if (PROP_NAME_CERTAIN_SEGMENT_LENGTH_THRESHOLD.equals(property.getId())) { certainSegmentLengthThreshold = Double.valueOf(property.getValue()); continue; } else if (PROP_NAME_LOOSE_UPPER_ACCELERATION_BOUND.equals(property.getId())) { looseUpperBoundsAcceleration = Double.valueOf(property.getValue()); continue; } else if (PROP_NAME_LOOSE_UPPER_VELOCITY_BOUND.equals(property.getId())) { looseUpperBoundsVelocity = Double.valueOf(property.getValue()); continue; } else if (PROP_NAME_UNCERTAIN_SEGMENT_COUNT_THRESHOLD.equals(property.getId())) { uncertainSegCountThreshold = Integer.parseInt(property.getValue()); continue; } else if (PROP_NAME_MINIMAL_DISTANCE_BOUND.equals(property.getId())) { minimalDistanceBound = Double.valueOf(property.getValue()); continue; } else if (PROP_NAME_MINIMAL_TIME_DIFFERENCE.equals(property.getId())) { minimalTimeDifference = Long.valueOf(property.getValue()); continue; } else if (PROP_NAME_FILTER_SCOPE.equals(property.getId())) { try { scope = InferenceMode.fromValue(property.getValue()); } catch (IllegalArgumentException ex) { LOG.log(Level.SEVERE, ex.getMessage(), ex); } } } } } catch (NumberFormatException ex) { setProcessState(ProcessState.SETTING_ERROR); Exceptions.printStackTrace(ex); } } } @Override protected void start() { init(); if (result == null) { result = new ArrayList<TrackSegment>(); } result.clear(); if (gpsTracks != null) { for (TrackSegment trackSegment : gpsTracks) { // 1. step determine (non) walking points List<ChangePointWaypoint> waypoints = transformTo(trackSegment); // 2. step partition into walkin / non-wakling segments. List<ChangePointSegment> partition = partionWaypoints(waypoints); // 3. step make segments certain List<ChangePointSegment> certainPartition = makeCertainParition(partition); // 4. step partion original segment with the help of the change point in the // walking segment in the certainPartition list. result.addAll(partitionTrackSegment(trackSegment, certainPartition)); } gpsTracks.clear(); } } private Collection<? extends TrackSegment> partitionTrackSegment(TrackSegment trackSegment, List<ChangePointSegment> certainPartition) { List<TrackSegment> partitionedTrackSegments = new ArrayList<TrackSegment>(certainPartition.size()); for (ChangePointSegment changePointSegment : certainPartition) { if (changePointSegment.getTransportMode() == Type.WALKING) { if (!changePointSegment.getWayPointList().isEmpty()) { // the first and last point of a walking segment potencially are // change points. Waypoint startPoint = changePointSegment.getWayPointList().get(0); Waypoint endPoint = changePointSegment.getWayPointList().get(changePointSegment.getWayPointList().size() - 1); List<Waypoint> list = Arrays.asList(startPoint, endPoint); for (Waypoint changePoint : list) { TrackSegment track = new TrackSegment(); for (Waypoint waypoint : trackSegment.getWayPointList()) { track.add(waypoint); if (waypoint.equals(changePoint)) { break; } } trackSegment.getWayPointList().removeAll(track.getWayPointList()); if (!track.getWayPointList().isEmpty()) { partitionedTrackSegments.add(track); } } } } } return partitionedTrackSegments; } private List<ChangePointSegment> makeCertainParition(List<ChangePointSegment> partition) { List<ChangePointSegment> certainPartition = new LinkedList<ChangePointSegment>(); LoopOne: for (ChangePointSegment segment : partition) { double length = segment.getLength(); if (length > certainSegmentLengthThreshold) { segment.setType(SegmentType.CERTAIN); certainPartition.add(segment); } else { segment.setType(SegmentType.UNCERTAIN); List<ChangePointSegment> temp = new ArrayList<ChangePointSegment>(uncertainSegCountThreshold); // check whether the result list has the minimum count of elements if (certainPartition.size() >= uncertainSegCountThreshold) { // check whether the last <code>uncertainSegCountThreshold</code> segments are of // tyoe UNCERTAIN for (int i = certainPartition.size() - 1 - uncertainSegCountThreshold; i >= 0 && i < certainPartition.size(); i++) { ChangePointSegment cps = certainPartition.get(i); if (SegmentType.UNCERTAIN != cps.getType()) { // at least one is not of type UNCERTAIN // add the current segment to the result list and // continue the outer loop certainPartition.add(segment); continue LoopOne; } // collect the last <code>uncertainSegCountThreshold</code> segments temp.add(cps); } // check whether there are enough element in the temp list. if (!temp.isEmpty() && temp.size() == uncertainSegCountThreshold) { ChangePointSegment cps = temp.iterator().next(); // merge the uncertainSegCountThreashold + 1 segments // with the first segment and remove them from the // result list. for (int i = 1; i < temp.size(); i++) { cps.addAll(temp.get(i).getWayPointList()); certainPartition.remove(temp.get(i)); } cps.setTransportMode(Type.NON_WALKING); } } else { certainPartition.add(segment); } } } return certainPartition; } private boolean isSegmentValid(ChangePointSegment segment) { double segmentLength = segment.getLength(); long timeDifference = segment.getTimeDifference(); return segmentLength > minimalDistanceBound && timeDifference > minimalTimeDifference; } private List<ChangePointSegment> partionWaypoints(List<ChangePointWaypoint> waypointList) { List<ChangePointSegment> partition = new ArrayList<ChangePointSegment>(100); ChangePointSegment lastSegment = null; ChangePointSegment segment = new ChangePointSegment(); ChangePointWaypoint lastWaypoint = null; for (ChangePointWaypoint waypoint : waypointList) { if (lastWaypoint != null) { // check whether the last and current way point have are // of the same type (i.e. walking or non-walking if (lastWaypoint.getTransportMode() != waypoint.getTransportMode()) { // they have different types // check whether the current segment satisfies the // necessary creteria to be a stand along segment or // it has to be merged with the previous segment. if (!isSegmentValid(segment)) { // the segment does not satisfy the constrains if (lastSegment != null) { // segment has a previous segment to be merged with. lastSegment.getWayPointList().addAll(segment.getWayPointList()); // lastSegment will be not changed } else { // there is no previous segment, because it the // first segment in the partion partition.add(segment); lastSegment = segment; } } else { // the current segment satisfies the constrain and will be // added to the partition as a stand alone segment. partition.add(segment); lastSegment = segment; } // create a new segment segment = new ChangePointSegment(); } else { // both point are of the same type and the current point // will be added to the current segment. segment.add(waypoint); segment.setTransportMode(waypoint.getTransportMode()); } } lastWaypoint = waypoint; } // check whether the current segment is already in the partition // if not add to partition. if (!partition.contains(segment)) { if (!isSegmentValid(segment)) { if (lastSegment != null) { lastSegment.getWayPointList().addAll(segment.getWayPointList()); } else { partition.add(segment); } } else { partition.add(segment); } } return partition; } private List<ChangePointWaypoint> transformTo(TrackSegment trackSegment) { List<ChangePointWaypoint> waypoints = new LinkedList<ChangePointWaypoint>(); ChangePointWaypoint lastWaypoint = null; for (Waypoint waypoint : trackSegment.getWayPointList()) { ChangePointWaypoint changePointWaypoint = new ChangePointWaypoint(waypoint); if (lastWaypoint != null) { if (changePointWaypoint.getTimestamp() != null && lastWaypoint.getTimestamp() != null) { long timeDiff = (changePointWaypoint.getTimestamp().getTime() - lastWaypoint.getTimestamp().getTime()) / 1000; if (timeDiff != 0) { double speedDiff = GPSCalc.getDistVincentyFast(lastWaypoint.getLat(), lastWaypoint.getLon(), changePointWaypoint.getLat(), changePointWaypoint.getLon()) / timeDiff; // acceleration in meters / sec^2 double acceleration = speedDiff / timeDiff; if (acceleration < looseUpperBoundsAcceleration && lastWaypoint.getSpeed() < looseUpperBoundsVelocity) { changePointWaypoint.setTransportMode(Type.WALKING); } else { changePointWaypoint.setTransportMode(Type.NON_WALKING); } waypoints.add(changePointWaypoint); } } } lastWaypoint = changePointWaypoint; } return waypoints; } @NbBundle.Messages("CLT_ChangePointSequencizerFilter_Name=Change Point Sequencizer") @Override public String getName() { if (getProcessDescriptor() != null && getProcessDescriptor().getName() != null) { return getProcessDescriptor().getName(); } return Bundle.CLT_ChangePointSequencizerFilter_Name(); } @NbBundle.Messages("CLT_ChangePointSequencizerFilter_Description=Description") @Override public String getDescription() { if (getProcessDescriptor() != null && getProcessDescriptor().getDescription() != null) { return getProcessDescriptor().getDescription(); } return Bundle.CLT_ChangePointSequencizerFilter_Description(); } @Override public void setInput(List<TrackSegment> input) { this.gpsTracks = input; } @Override public List<TrackSegment> getResult() { List<TrackSegment> arrayList = this.result; this.result = null; this.gpsTracks = null; return arrayList; } @NbBundle.Messages({ "CLT_ChangePointSegmentationFilter_Property_Scope_Name=Scope", "CLT_ChangePointSegmentationFilter_Property_Scope_Description=Excecution Scope in which Phase of the Detector this filter should be applied.", "CLT_ChangePointSegmentationFilter_Property_VelocityBound_Name=Velocity Bound", "CLT_ChangePointSegmentationFilter_Property_VelocityBound_Description=GPS tracks will be segmented by change point.", "CLT_ChangePointSegmentationFilter_Property_AccelerationBound_Name=Acceleration Bound", "CLT_ChangePointSegmentationFilter_Property_AccelerationBound_Description=Specifies a loose upper bound for the acceleration value of a point to declare point as (non) wakling", "CLT_ChangePointSegmentationFilter_Property_CertainLengthThreshold_Name=Certain Length Threshold", "CLT_ChangePointSegmentationFilter_Property_CertainLengthThreshold_Description=Specifies the length that each segment must exceed to be declared certain.", "CLT_ChangePointSegmentationFilter_Property_UncertainCountThreshold_Name=Unchertain Count Threshold", "CLT_ChangePointSegmentationFilter_Property_UncertainCountThreshold_Description=Specifies how many successive uncertain segments needs to be to declare uncertain segments as one non-wakling segment", "CLT_ChangePointSegmentationFilter_Property_ChangePointMinimalDistanceBound_Name=Minimal Distance Bound", "CLT_ChangePointSegmentationFilter_Property_ChangePointMinimalDistanceBound_Description=Specifies the minimal length of an segment with consecutive (non) walking points must exceed if not it will be merged with the backward segment.", "CLT_ChangePointSegmentationFilter_Property_ChangePointMinimalTimeDifference_Name=Minimal Time Difference", "CLT_ChangePointSegmentationFilter_Property_ChangePointMinimalTimeDifference_Description=Specifies the duration time of an segment with consecutive (non) walking points must exceed" }) @Override protected ProcessDescriptor createProcessDescriptor() { ProcessDescriptor descriptor = new ProcessDescriptor(); descriptor.setJavaType(ChangePointSegmentationFilterProcess.class.getName()); descriptor.setName(Bundle.CLT_ChangePointSequencizerFilter_Name()); descriptor.setDescription(Bundle.CLT_ChangePointSequencizerFilter_Description()); Property property = new Property(); property.setId(PROP_NAME_FILTER_SCOPE); property.setJavaType(InferenceMode.class.getName()); property.setName(Bundle.CLT_ChangePointSegmentationFilter_Property_Scope_Name()); property.setDescription(Bundle.CLT_ChangePointSegmentationFilter_Property_Scope_Description()); property.setValue(InferenceMode.INFERENCE_MODE.toString()); descriptor.getProperties().getPropertyList().add(property); // <!-- velocity value in meters/sec. --> property = new Property(); property.setId(PROP_NAME_LOOSE_UPPER_VELOCITY_BOUND); property.setJavaType(Double.class.getName()); property.setName(Bundle.CLT_ChangePointSegmentationFilter_Property_VelocityBound_Name()); property.setDescription(Bundle.CLT_ChangePointSegmentationFilter_Property_VelocityBound_Description()); property.setValue("1.8"); descriptor.getProperties().getPropertyList().add(property); //<!-- acceleration in meters/sec^2 --> property = new Property(); property.setId(PROP_NAME_LOOSE_UPPER_ACCELERATION_BOUND); property.setJavaType(Double.class.getName()); property.setValue("0.6"); property.setName(Bundle.CLT_ChangePointSegmentationFilter_Property_AccelerationBound_Name()); property.setDescription(Bundle.CLT_ChangePointSegmentationFilter_Property_AccelerationBound_Description()); descriptor.getProperties().getPropertyList().add(property); //<!-- length value in meters --> property = new Property(); property.setId(PROP_NAME_CERTAIN_SEGMENT_LENGTH_THRESHOLD); property.setJavaType(Double.class.getName()); property.setValue("200"); property.setName(Bundle.CLT_ChangePointSegmentationFilter_Property_CertainLengthThreshold_Name()); property.setDescription(Bundle.CLT_ChangePointSegmentationFilter_Property_CertainLengthThreshold_Description()); descriptor.getProperties().getPropertyList().add(property); property = new Property(); property.setId(PROP_NAME_UNCERTAIN_SEGMENT_COUNT_THRESHOLD); property.setJavaType(Integer.class.getName()); property.setValue("2"); property.setName(Bundle.CLT_ChangePointSegmentationFilter_Property_UncertainCountThreshold_Name()); property.setDescription(Bundle.CLT_ChangePointSegmentationFilter_Property_UncertainCountThreshold_Description()); descriptor.getProperties().getPropertyList().add(property); // <!-- value in meters --> property = new Property(); property.setId(PROP_NAME_MINIMAL_DISTANCE_BOUND); property.setJavaType(Double.class.getName()); property.setValue("20"); property.setName(Bundle.CLT_ChangePointSegmentationFilter_Property_ChangePointMinimalDistanceBound_Name()); property.setDescription(Bundle.CLT_ChangePointSegmentationFilter_Property_ChangePointMinimalDistanceBound_Description()); descriptor.getProperties().getPropertyList().add(property); // <!-- time in seconds --> property = new Property(); property.setId(PROP_NAME_MINIMAL_TIME_DIFFERENCE); property.setJavaType(Long.class.getName()); property.setValue("10"); property.setName(PROP_NAME_PROCESS_STATE); property.setDescription(PROP_NAME_PROCESS_STATE); descriptor.getProperties().getPropertyList().add(property); return descriptor; } private static class ChangePointSegment extends TrackSegment { private SegmentType type = null; private Type transportMode = null; public Type getTransportMode() { return transportMode; } public void setTransportMode(Type transportMode) { this.transportMode = transportMode; } public SegmentType getType() { return type; } public void setType(SegmentType type) { this.type = type; } public long getTimeDifference() { long timeDiff = 0; if (getWayPointList().size() > 1) { Waypoint endPoint = getWayPointList().get(getWayPointList().size() - 1); Waypoint startPoint = getWayPointList().get(0); if (endPoint.getTimestamp() != null && startPoint.getTimestamp() != null) { timeDiff = (endPoint.getTimestamp().getTime() - startPoint.getTimestamp().getTime()) / 1000; } } return timeDiff; } public double getLength() { double length = 0; Waypoint lastPoint = null; for (int i = 0; i < getWayPointList().size(); i++) { Waypoint currentPoint = getWayPointList().get(i); if (lastPoint != null) { length += GPSCalc.getDistVincentyFast( lastPoint.getLat(), lastPoint.getLon(), currentPoint.getLat(), currentPoint.getLon()); } lastPoint = currentPoint; } return length; } } private static class ChangePointWaypoint extends GpxWayPoint { private Type type = null; private final Waypoint original; private ChangePointWaypoint(Waypoint original) { super(createPropertyMap(original)); this.original = original; } public Waypoint getOriginal() { return original; } public Type getTransportMode() { return type; } public void setTransportMode(Type type) { this.type = type; } @Override public int hashCode() { return original.hashCode(); } @SuppressWarnings("EqualsWhichDoesntCheckParameterClass") @Override public boolean equals(Object obj) { return original.equals(obj); } private static Map<String, String> createPropertyMap(Waypoint waypoint) { HashMap<String, String> map = new HashMap<String, String>(); for (String propertyName : waypoint.getPropertyList()) { map.put(propertyName, waypoint.getPropertyValue(propertyName)); } return map; } } private enum SegmentType { CERTAIN, UNCERTAIN; } private enum Type { WALKING, NON_WALKING; } }