-
Notifications
You must be signed in to change notification settings - Fork 0
/
filter-gpx.kscript
executable file
·191 lines (159 loc) · 5.88 KB
/
filter-gpx.kscript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/env kscript
@file:DependsOn("io.jenetics:jpx:1.5.2")
@file:DependsOn("commons-cli:commons-cli:1.4")
@file:DependsOn("com.javadocmd:simplelatlng:1.3.1")
@file:DependsOn("org.yaml:snakeyaml:1.24")
import com.javadocmd.simplelatlng.*
import com.javadocmd.simplelatlng.util.*
import com.javadocmd.simplelatlng.window.*
import io.jenetics.jpx.*
import java.io.File
import java.time.*
import java.time.format.*
import java.time.temporal.*
import java.time.Duration.between
import org.apache.commons.cli.*
import org.yaml.snakeyaml.*
enum class GroupBy(val lambda: (Point) -> Any) {
HOUR({ it.time.get().truncatedTo(ChronoUnit.HOURS) }),
DAY({ it.time.get().toLocalDate() }),
MONTH({ YearMonth.from(it.time.get().toLocalDate()) })
}
fun parseArguments(): CommandLine {
val parser: CommandLineParser = DefaultParser()
val center = Option.builder("c")
.longOpt("center")
.hasArg()
.required()
.type(String::class.java)
.valueSeparator()
.desc("Center coordinates of the area. E.g. 53.123,10.543")
.build()
val radius = Option.builder("r")
.longOpt("radius")
.hasArg()
.type(Double::class.java)
.valueSeparator()
.desc("Radius of the area in meters (default: 200m)")
.build()
val groupBy = Option.builder("g")
.longOpt("groupBy")
.hasArg()
.type(GroupBy::class.java)
.valueSeparator()
.desc("Group by either DAY, HOUR or MONTH (default: DAY)")
.build()
val hideZeros = Option.builder("hz")
.longOpt("hideZeros")
.desc("Hide result line if the duration within the bounds is 0secs (PT0S)")
.build()
val verbose = Option.builder("v")
.longOpt("verbose")
.desc("Show details (enter/leave area timestamps)")
.build()
val options = Options()
options.addOption(center)
options.addOption(radius)
options.addOption(groupBy)
options.addOption(hideZeros)
options.addOption(verbose)
try {
val commandLine = parser.parse(options, args)
if (commandLine.argList.size != 1) {
throw ParseException("No filename provided.")
}
return commandLine
} catch (e: ParseException) {
val formatter = HelpFormatter()
formatter.printHelp("filter-gpx.kscript [options] <gpx.file>", options)
kotlin.system.exitProcess(-1)
}
}
fun durationInArea(points: List<Point>, window: CircularWindow): List<Pair<Point,Point>> {
val sortedPoints = points.sortedBy { it.time.get() }
var entryPoint: Point? = null
val pairs = mutableListOf<Pair<Point, Point>>()
sortedPoints.forEach {
if (entryPoint == null && window.contains(it)) {
entryPoint = it
} else if (entryPoint != null && !window.contains(it)) {
pairs.add(Pair(entryPoint!!, it))
entryPoint = null
}
}
if (entryPoint != null) {
pairs.add(Pair(entryPoint!!, sortedPoints.last()))
}
return pairs
}
fun Iterable<Duration>.sum(): Duration {
return this.fold(Duration.ofSeconds(0)) { sum, element -> sum.plus(element) }
}
fun CircularWindow.contains(point: Point): Boolean {
return contains(LatLng(point.latitude.toDouble(), point.longitude.toDouble()))
}
fun latLngFrom(coordinates: String): LatLng {
val pair = coordinates.split(",").map { it.toDouble() }.zipWithNext().first()
return LatLng(pair.first, pair.second)
}
fun ZonedDateTime.format(): String {
val localizedDateTime = this.withZoneSameInstant(ZoneId.systemDefault())
return localizedDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
}
fun Duration.format(): String {
if (toDays() > 0) {
return "%dd %02dh %02dm %02ds".format(toDays(), toHours() % 24, toMinutes() % 60, seconds % 60)
}
if (toHours() > 0) {
return "%02dh %02dm %02ds".format(toHours() % 24, toMinutes() % 60, seconds % 60)
}
if (toMinutes() > 0) {
return "%02dm %02ds".format(toMinutes() % 60, seconds % 60)
}
return "%02ds".format(seconds)
}
fun getCenter(parameter: String): LatLng {
fun loadLocationFromFile(): String? {
val file = File("./locations.yaml")
if (!file.exists()) {
return null
}
val locations: Map<String, String> = file.inputStream().use { Yaml().load(it) }
if (parameter !in locations) {
return null
}
return locations[parameter]
}
val fromFile = loadLocationFromFile()
val coordinates = fromFile ?: parameter
fromFile?.let {
println("Found location ${parameter}: ${it}")
}
return latLngFrom(coordinates)
}
val commandLine = parseArguments()
val filename: String = commandLine.argList.first()
val center = getCenter(commandLine.getOptionValue("center"))
val radius = commandLine.getOptionValue("radius")?.toDouble() ?: 200.0
val groupBy = commandLine.getOptionValue("groupBy")?.let { GroupBy.valueOf(it) } ?: GroupBy.DAY
val hideZeros = commandLine.hasOption("hideZeros")
val verbose = commandLine.hasOption("verbose")
val track = GPX.reader(GPX.Version.V10, GPX.Reader.Mode.LENIENT)
.read(filename)
.tracks
.filter { it.name.isPresent && it.name.get() == "SM-G973F" }
val window = CircularWindow(center, radius, LengthUnit.METER)
val points = track.flatMap { it.segments }.flatMap { it.points }
val groupedByDate = points.groupBy(groupBy.lambda)
groupedByDate.forEach { point ->
val pairs = durationInArea(point.value, window)
val duration = pairs.map { between(it.first.time.get(), it.second.time.get()) }.sum()
if (!hideZeros || !duration.isZero) {
println("${point.key}: ${duration.format()} (${point.value.size} Points)")
if (verbose) {
pairs.forEach {
println("\tEnter: ${it.first.time.get().format()} Leave: ${it.second.time.get().format()}")
}
}
}
}