diff options
Diffstat (limited to 'bin/gis/match-road.sh')
-rwxr-xr-x | bin/gis/match-road.sh | 148 |
1 files changed, 148 insertions, 0 deletions
diff --git a/bin/gis/match-road.sh b/bin/gis/match-road.sh new file mode 100755 index 0000000..d9beaae --- /dev/null +++ b/bin/gis/match-road.sh | |||
@@ -0,0 +1,148 @@ | |||
1 | #!/usr/bin/env bash | ||
2 | # | ||
3 | # Author: Pham | ||
4 | # | ||
5 | # This script accepts a single GPX file as parameter and | ||
6 | # output the processed GPX body to STDOUT, using Mapbox Map Matching API v4. | ||
7 | # read doc at: https://docs.mapbox.com/api/legacy/map-matching-v4/ | ||
8 | # | ||
9 | # Example: | ||
10 | # | ||
11 | # match-road.sh raw.gpx > new.gpx | ||
12 | # | ||
13 | # Hint: | ||
14 | # | ||
15 | # Remember to put Mapbox Access Token at the top! | ||
16 | |||
17 | #set -x | ||
18 | set -e | ||
19 | |||
20 | # put yout Mapbox token here | ||
21 | ACCESS_TOKEN=$(cat ~/settings/tokens/mapbox) | ||
22 | # number of coordinates for each Mapbox Map Matching API request, Maximum value is 100 | ||
23 | LIMIT=100 | ||
24 | # define the lowest confidence of accepted matched points | ||
25 | THRESHOLD=0.0001 | ||
26 | |||
27 | if [[ -z $1 ]]; then echo "You need to give a gpx file!"; exit 1; fi | ||
28 | ORIGIN_DATA=/tmp/$(basename $1).origin | ||
29 | RESPONSES=/tmp/$(basename $1).responses && true > $RESPONSES | ||
30 | MATCHED=/tmp/$(basename $1).matched | ||
31 | |||
32 | # extract data from the given gpx file | ||
33 | # only keep first point and remove the rest which in the same "second" | ||
34 | # input: [gpx format] | ||
35 | # output: [121.0179739,14.5515336] 1984-01-01T08:00:46 | ||
36 | get_data() { | ||
37 | sed -nr '/<trkpt /, /<\/trkpt>/ {H; /<\/trkpt>/ {x; s/\n/ /g; p; s/.*//; x}}' $1 | | ||
38 | sed -nr 'h; s/.*lon="([^"]+).*/\1/; H; g | ||
39 | s/.*lat="([^"]+).*/\1/; H; g | ||
40 | # If trkpt has no time, leave it blank | ||
41 | /time/ { | ||
42 | s/.*<time>([^.]+).*<\/time>.*/\1/ | ||
43 | H; g | ||
44 | } | ||
45 | s/^[^\n]+\n//; s/\n/ /g; p' | | ||
46 | sed -r 's/^([^ ]+) ([^ ]+)/[\1,\2]/' | | ||
47 | awk '!_[$2]++' | ||
48 | } | ||
49 | |||
50 | # Output GeoJSON object for Map Matching API | ||
51 | # input: [121.0179739,14.5515336] 1984-01-01T08:00:46 | ||
52 | # output: {type: "Feature", properties: {coordTimes: [...]}, geometry: {type: "LineString", coordinates: [...]}} | ||
53 | make_geojson() { | ||
54 | # change input to format like: [[lon, lat], time] | ||
55 | awk '{printf("[%s,\"%s\"]\n", $1, $2)}' | | ||
56 | jq '[inputs] | {type: "Feature", geometry: {type: "LineString", coordinates: map(.[0])}, properties: {coordTimes: (map(.[1]))}}' | ||
57 | } | ||
58 | |||
59 | # Read GeoJSON body from STDIN, and return result from Mapbox Map Matching API | ||
60 | query_matched_points() { | ||
61 | curl -X POST -s --data @- \ | ||
62 | --header "Content-Type:application/json" \ | ||
63 | https://api.mapbox.com/matching/v4/mapbox.driving.json?access_token=$ACCESS_TOKEN | ||
64 | } | ||
65 | |||
66 | # Get valid data from Map Matching API response | ||
67 | # output format is [coordinates] [index-of-original-data], like: | ||
68 | # [121.0179739,14.5515336] 35 | ||
69 | # If the point is newly added, the index would be -1, like | ||
70 | # [121.0189339,14.5525931] -1 | ||
71 | validate_matched_points() { | ||
72 | VALID_DATA=$(jq ".features[] | if(.properties.confidence < $THRESHOLD) then .geometry.coordinates=(.properties.indices|map(.+1)) else . end") | ||
73 | |||
74 | echo $VALID_DATA | | ||
75 | jq -cr '.properties | [.matchedPoints, (.indices | map(.+1))] | transpose[] | "\(.[0]) \(.[1])"' > $MATCHED | ||
76 | |||
77 | echo $VALID_DATA | jq -c '.geometry.coordinates[]' | | ||
78 | while read point; do | ||
79 | if [[ ${point:0:1} != '[' ]]; then | ||
80 | echo $(sed -n "$point p" $ORIGIN_DATA) | while read coor time; do echo $coor $(date -d $time +%s); done | ||
81 | sed -i 1d $MATCHED | ||
82 | elif head -1 $MATCHED | grep -F $point > /dev/null; then | ||
83 | index=$(head -1 $MATCHED | cut -d' ' -f2) | ||
84 | echo $point $(sed -n "$index p" $ORIGIN_DATA | cut -d' ' -f2 | date -f - +%s) | ||
85 | sed -i 1d $MATCHED | ||
86 | else | ||
87 | echo $point -1 | ||
88 | fi | ||
89 | done | ||
90 | } | ||
91 | |||
92 | # Put existing timestamps to matched points, and interpolate new timestamps into new points | ||
93 | complete_data() { | ||
94 | # interpolate timestamps to newly added points | ||
95 | awk '{COOR[NR][0]=$1; N++; COOR[NR][1]=$2} END{for (i=1; i<=N; i++) {printf COOR[i][0]; if (COOR[i][1] != -1) {print " "COOR[i][1]; LAST=i} else {while(COOR[i+n][1] == -1) n++; print " "COOR[LAST][1]+(COOR[i+n][1]-COOR[LAST][1])*(i-LAST)/(i+n-LAST)}}}' | | ||
96 | while read coor unix_time; do | ||
97 | # Transform unix timestamp into human readable time format, like following: | ||
98 | # Transform [121.018088,14.5516] 18.50 | ||
99 | # Into [121.018088,14.5516] 1970-01-01T08:00:18.50Z | ||
100 | echo $coor $(date -d @$unix_time +'%Y-%m-%dT%H:%M:%S.%2NZ') | ||
101 | done | ||
102 | } | ||
103 | |||
104 | # Make GPX format for output | ||
105 | # Take input with format: [lon,lat] [time] | ||
106 | make_gpx() { | ||
107 | sed -E 's/\[([^,]+),([^,]+)\] (.*)/ <trkpt lon="\1" lat="\2"><time>\3<\/time><\/trkpt>/' | | ||
108 | sed "1i \ | ||
109 | <gpx version=\"1.1\" creator=\"Garmin Connect\"\n\ | ||
110 | xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd http://www.garmin.com/xmlschemas/GpxExtensions/v3 http://www.garmin.com/xmlschemas/GpxExtensionsv3.xsd http://www.garmin.com/xmlschemas/TrackPointExtension/v1 http://www.garmin.com/xmlschemas/TrackPointExtensionv1.xsd\"\n\ | ||
111 | xmlns=\"http://www.topografix.com/GPX/1/1\"\n\ | ||
112 | xmlns:gpxtpx=\"http://www.garmin.com/xmlschemas/TrackPointExtension/v1\"\n\ | ||
113 | xmlns:gpxx=\"http://www.garmin.com/xmlschemas/GpxExtensions/v3\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">\n\ | ||
114 | <trk>\n <trkseg>" | | ||
115 | sed "\$a \ \ \ \ <\/trkseg>\n <\/trk>\n<\/gpx>" | ||
116 | } | ||
117 | |||
118 | get_data $1 > $ORIGIN_DATA | ||
119 | |||
120 | #RAW_REQUEST=$(basename $1 | tr '.' '_')_request.geojson | ||
121 | #cat $ORIGIN_DATA | make_geojson | jq '.properties.stroke="#ff0000"' > $RAW_REQUEST | ||
122 | |||
123 | # Consume raw data with serveral request | ||
124 | while [ -s $ORIGIN_DATA ]; do | ||
125 | # Take original data by limited points for each time: [121.0179739,14.5515336] 1984-01-01T08:00:46 | ||
126 | # Make geojson body: {type: "Feature", properties: {coordTimes: [...]}, geometry: {type: "LineString", coordinates: [...]}} | ||
127 | # Call Mapbox Map Matching API: {geometry: {coordinates: [...]}, properties: {confidence: 0.9, matching: [...], indices: [...]}} | ||
128 | # Get valid point and index by confidence value: [121.0179739,14.5515336] 5 | ||
129 | # complete_data | ||
130 | head -$LIMIT $ORIGIN_DATA | | ||
131 | make_geojson | | ||
132 | query_matched_points | | ||
133 | tee -a $RESPONSES | | ||
134 | validate_matched_points | | ||
135 | complete_data | ||
136 | |||
137 | # Remove processed raw data | ||
138 | sed -i "1,$LIMIT d" $ORIGIN_DATA | ||
139 | done | make_gpx | ||
140 | #make_geojson > test.geojson | ||
141 | |||
142 | #RAW_RESPONSE=$(basename $1 | tr '.' '_')_response.geojson | ||
143 | #MATCHED_POINTS=$(basename $1 | tr '.' '_')_matched.geojson | ||
144 | #jq . $RESPONSES | jq -s '.[0].features=[.[]|.features[]] | .[0] | del(.code) | .features=(.features|map(.properties.stroke="#00ff00"))' > $RAW_RESPONSE | ||
145 | #jq ".features=(.features|map(select(.properties.confidence>=$THRESHOLD).geometry.coordinates=.properties.matchedPoints)|map(.properties.stroke=\"#0000ff\"))" $RAW_RESPONSE > $MATCHED_POINTS | ||
146 | # | ||
147 | #DEBUG=$(basename $1 | tr '.' '_')_total.geojson | ||
148 | #cat $RAW_REQUEST $RAW_RESPONSE $MATCHED_POINTS | jq -s '{type: "FeatureCollection", features: ([.[0]] + .[1].features + .[2].features)}' > $DEBUG | ||