fix(live): point the heading arrow at true north and keep the phone marker on top
This commit is contained in:
@@ -59,6 +59,11 @@ public class LiveRangeFinderActivity extends Activity implements LocationListene
|
|||||||
private boolean searching, calibrating, mapReady;
|
private boolean searching, calibrating, mapReady;
|
||||||
private long lastSignalTime, lastLoggedTime;
|
private long lastSignalTime, lastLoggedTime;
|
||||||
private float heading;
|
private float heading;
|
||||||
|
/** Magnetic-to-true north correction for the current position. */
|
||||||
|
private float declination;
|
||||||
|
/** Smoothed compass azimuth, kept in sin/cos form to survive the 0/360 wrap. */
|
||||||
|
private float headingSin, headingCos;
|
||||||
|
private boolean headingPrimed;
|
||||||
private long lastHeadingMapUpdate;
|
private long lastHeadingMapUpdate;
|
||||||
private String headingSource = "Compass";
|
private String headingSource = "Compass";
|
||||||
private double lastRangeDistance;
|
private double lastRangeDistance;
|
||||||
@@ -429,8 +434,12 @@ public class LiveRangeFinderActivity extends Activity implements LocationListene
|
|||||||
|
|
||||||
public void onLocationChanged(Location location) {
|
public void onLocationChanged(Location location) {
|
||||||
currentLocation = location;
|
currentLocation = location;
|
||||||
|
declination = new android.hardware.GeomagneticField(
|
||||||
|
(float) location.getLatitude(), (float) location.getLongitude(),
|
||||||
|
(float) location.getAltitude(), System.currentTimeMillis()).getDeclination();
|
||||||
if (location.hasBearing() && location.hasSpeed() && location.getSpeed() > 1.0f) {
|
if (location.hasBearing() && location.hasSpeed() && location.getSpeed() > 1.0f) {
|
||||||
heading = location.getBearing(); headingSource = "GPS movement";
|
heading = location.getBearing(); headingSource = "GPS course";
|
||||||
|
headingPrimed = false;
|
||||||
}
|
}
|
||||||
if (searching) updateMap(location, heading, lastRangeState, lastRangeDistance, false);
|
if (searching) updateMap(location, heading, lastRangeState, lastRangeDistance, false);
|
||||||
}
|
}
|
||||||
@@ -444,18 +453,41 @@ public class LiveRangeFinderActivity extends Activity implements LocationListene
|
|||||||
float[] orientation = new float[3];
|
float[] orientation = new float[3];
|
||||||
SensorManager.getRotationMatrixFromVector(rotation, event.values);
|
SensorManager.getRotationMatrixFromVector(rotation, event.values);
|
||||||
SensorManager.getOrientation(rotation, orientation);
|
SensorManager.getOrientation(rotation, orientation);
|
||||||
float compass = (float) Math.toDegrees(orientation[0]);
|
float compass = (float) Math.toDegrees(orientation[0]) + declination;
|
||||||
if (compass < 0) compass += 360;
|
while (compass < 0) compass += 360;
|
||||||
|
while (compass >= 360) compass -= 360;
|
||||||
if (currentLocation == null || !currentLocation.hasSpeed() || currentLocation.getSpeed() <= 1) {
|
if (currentLocation == null || !currentLocation.hasSpeed() || currentLocation.getSpeed() <= 1) {
|
||||||
heading = compass; headingSource = "compass / rotation sensor";
|
heading = smoothHeading(compass);
|
||||||
|
headingSource = "compass";
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
if (searching && mapReady && now - lastHeadingMapUpdate >= 100L) {
|
/*
|
||||||
|
* The arrow must follow the phone whether or not a search is
|
||||||
|
* running - it is the user's own facing, not a search result.
|
||||||
|
*/
|
||||||
|
if (mapReady && now - lastHeadingMapUpdate >= 100L) {
|
||||||
lastHeadingMapUpdate = now;
|
lastHeadingMapUpdate = now;
|
||||||
js(String.format(Locale.US, "updateHeading(%.1f)", heading));
|
js(String.format(Locale.US, "updateHeading(%.1f)", heading));
|
||||||
headingText.setText(String.format(Locale.US, "Heading: %.0f° (%s)", heading, headingSource));
|
headingText.setText(String.format(Locale.US, "Heading: %.0f° (%s)", heading, headingSource));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Averages the azimuth as a unit vector so the 359 -> 0 wrap does not
|
||||||
|
* swing the arrow the long way round.
|
||||||
|
*/
|
||||||
|
private float smoothHeading(float degrees) {
|
||||||
|
double radians = Math.toRadians(degrees);
|
||||||
|
float sin = (float) Math.sin(radians), cos = (float) Math.cos(radians);
|
||||||
|
if (!headingPrimed) { headingSin = sin; headingCos = cos; headingPrimed = true; }
|
||||||
|
else {
|
||||||
|
headingSin += (sin - headingSin) * 0.25f;
|
||||||
|
headingCos += (cos - headingCos) * 0.25f;
|
||||||
|
}
|
||||||
|
float smoothed = (float) Math.toDegrees(Math.atan2(headingSin, headingCos));
|
||||||
|
return smoothed < 0 ? smoothed + 360 : smoothed;
|
||||||
|
}
|
||||||
|
|
||||||
public void onAccuracyChanged(Sensor sensor, int accuracy) { }
|
public void onAccuracyChanged(Sensor sensor, int accuracy) { }
|
||||||
|
|
||||||
private void loadMap() {
|
private void loadMap() {
|
||||||
@@ -506,14 +538,20 @@ public class LiveRangeFinderActivity extends Activity implements LocationListene
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With no solved estimate the only honest target is the item's last
|
||||||
|
* known GPS fix. Projecting a pin along the phone's own heading - which
|
||||||
|
* is what this used to do - just parks the pin wherever the user points,
|
||||||
|
* so the arrow always appears to aim at it and it is useless for walking.
|
||||||
|
*/
|
||||||
private void showFallbackPin(Location phone, float direction, double distance) {
|
private void showFallbackPin(Location phone, float direction, double distance) {
|
||||||
double projected = Math.max(1.5, Math.min(20.0, distance));
|
if (target != null && target.hasLocation) {
|
||||||
double radians = Math.toRadians(direction);
|
js(String.format(Locale.US, "updateEstimate(%.7f,%.7f,%.1f,%.1f,true)",
|
||||||
double latitude = phone.getLatitude() + Math.cos(radians) * projected / 111319.49;
|
target.latitude, target.longitude,
|
||||||
double cosine = Math.max(0.2, Math.cos(Math.toRadians(phone.getLatitude())));
|
Math.max(1.5, target.accuracy), direction));
|
||||||
double longitude = phone.getLongitude() + Math.sin(radians) * projected / (111319.49 * cosine);
|
} else {
|
||||||
js(String.format(Locale.US, "updateEstimate(%.7f,%.7f,%.1f,%.1f,true)",
|
js("clearTarget()");
|
||||||
latitude, longitude, Math.max(1.5, distance), direction));
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String sector(float bearing) {
|
private String sector(float bearing) {
|
||||||
@@ -544,13 +582,14 @@ public class LiveRangeFinderActivity extends Activity implements LocationListene
|
|||||||
"var userIcon=L.divIcon({className:'',html:'<div id=bearing class=bearing><div class=you></div><div class=tip></div></div>',iconSize:[42,42],iconAnchor:[21,21]});" +
|
"var userIcon=L.divIcon({className:'',html:'<div id=bearing class=bearing><div class=you></div><div class=tip></div></div>',iconSize:[42,42],iconAnchor:[21,21]});" +
|
||||||
"var pinIcon=L.divIcon({className:'',html:'<div class=item-pin></div>',iconSize:[31,40],iconAnchor:[15,39]});" +
|
"var pinIcon=L.divIcon({className:'',html:'<div class=item-pin></div>',iconSize:[31,40],iconAnchor:[15,39]});" +
|
||||||
"function updateUser(a,b,h,s,d,station,signal){var p=[a,b];liveState=s;if(d>0)liveDistance=d;if(s=='HOT'&&d>0&&d<2.5)hotCount++;else if(s!='HOT')hotCount=0;" +
|
"function updateUser(a,b,h,s,d,station,signal){var p=[a,b];liveState=s;if(d>0)liveDistance=d;if(s=='HOT'&&d>0&&d<2.5)hotCount++;else if(s!='HOT')hotCount=0;" +
|
||||||
"if(!user){user=L.marker(p,{icon:userIcon,zIndexOffset:1000}).addTo(map);searchCircle=L.circle(p,{radius:Math.max(.6,liveDistance),color:'#4285F4',weight:2,fillColor:'#4285F4',fillOpacity:.16}).addTo(map);map.setView(p,21);}else user.setLatLng(p);" +
|
"if(!user){user=L.marker(p,{icon:userIcon,zIndexOffset:3000}).addTo(map);searchCircle=L.circle(p,{radius:Math.max(.6,liveDistance),color:'#4285F4',weight:2,fillColor:'#4285F4',fillOpacity:.16}).addTo(map);map.setView(p,21);}else user.setLatLng(p);" +
|
||||||
"searchCircle.setLatLng(p);smoothRadius(Math.max(.6,liveDistance));searchCircle.setStyle({color:'#4285F4',fillColor:'#4285F4',fillOpacity:.16});map.panTo(p,{animate:true});updateHeading(h);updatePhoneColor(signal)}" +
|
"searchCircle.setLatLng(p);smoothRadius(Math.max(.6,liveDistance));searchCircle.setStyle({color:'#4285F4',fillColor:'#4285F4',fillOpacity:.16});map.panTo(p,{animate:true});updateHeading(h);updatePhoneColor(signal)}" +
|
||||||
"function smoothRadius(next){radiusTarget=Math.max(.6,next);if(radiusFrame)return;function step(){if(!searchCircle){radiusFrame=0;return;}var current=searchCircle.getRadius(),change=(radiusTarget-current)*.025;if(Math.abs(radiusTarget-current)<.05){searchCircle.setRadius(radiusTarget);radiusFrame=0;return;}searchCircle.setRadius(current+change);radiusFrame=requestAnimationFrame(step);}radiusFrame=requestAnimationFrame(step);}" +
|
"function smoothRadius(next){radiusTarget=Math.max(.6,next);if(radiusFrame)return;function step(){if(!searchCircle){radiusFrame=0;return;}var current=searchCircle.getRadius(),change=(radiusTarget-current)*.025;if(Math.abs(radiusTarget-current)<.05){searchCircle.setRadius(radiusTarget);radiusFrame=0;return;}searchCircle.setRadius(current+change);radiusFrame=requestAnimationFrame(step);}radiusFrame=requestAnimationFrame(step);}" +
|
||||||
"function mix(a,b,t){return Math.round(a+(b-a)*Math.max(0,Math.min(1,t)));}function phoneColor(r){var a,b,t;if(r<=-80)return '#007AFF';if(r<-70){a=[0,122,255];b=[255,214,10];t=(r+80)/10;}else if(r<-60){a=[255,214,10];b=[255,59,48];t=(r+70)/10;}else return '#FF3B30';return 'rgb('+mix(a[0],b[0],t)+','+mix(a[1],b[1],t)+','+mix(a[2],b[2],t)+')';}function updatePhoneColor(r){var c=phoneColor(r),dot=document.querySelector('.you'),tip=document.querySelector('.tip');if(dot)dot.style.backgroundColor=c;if(tip)tip.style.borderBottomColor=c;}" +
|
"function mix(a,b,t){return Math.round(a+(b-a)*Math.max(0,Math.min(1,t)));}function phoneColor(r){var a,b,t;if(r<=-80)return '#007AFF';if(r<-70){a=[0,122,255];b=[255,214,10];t=(r+80)/10;}else if(r<-60){a=[255,214,10];b=[255,59,48];t=(r+70)/10;}else return '#FF3B30';return 'rgb('+mix(a[0],b[0],t)+','+mix(a[1],b[1],t)+','+mix(a[2],b[2],t)+')';}function updatePhoneColor(r){var c=phoneColor(r),dot=document.querySelector('.you'),tip=document.querySelector('.tip');if(dot)dot.style.backgroundColor=c;if(tip)tip.style.borderBottomColor=c;}" +
|
||||||
"function updateHeading(h){var e=document.getElementById('bearing');if(e)e.style.transform='rotate('+h+'deg)';}" +
|
"function updateHeading(h){var e=document.getElementById('bearing');if(e)e.style.transform='rotate('+h+'deg)';}" +
|
||||||
"function updateEstimate(a,b,r,g,pre){var p=[a,b];if(!target)target=L.marker(p,{icon:pinIcon,zIndexOffset:1200}).addTo(map);else target.setLatLng(p);target.bindPopup(pre?'Approximate item location':'Calculated item location');}" +
|
"function updateEstimate(a,b,r,g,pre){var p=[a,b];if(!target)target=L.marker(p,{icon:pinIcon,zIndexOffset:1200}).addTo(map);else target.setLatLng(p);target.bindPopup(pre?'Approximate item location':'Calculated item location');}" +
|
||||||
"function lockTarget(a,b,r){var p=[a,b];if(!target)target=L.marker(p,{icon:pinIcon,zIndexOffset:1200}).addTo(map);else target.setLatLng(p)}" +
|
"function lockTarget(a,b,r){var p=[a,b];if(!target)target=L.marker(p,{icon:pinIcon,zIndexOffset:1200}).addTo(map);else target.setLatLng(p)}" +
|
||||||
|
"function clearTarget(){if(target){map.removeLayer(target);target=null;}}" +
|
||||||
"function updateGradient(g,s){}function setLost(){if(searchCircle)searchCircle.setStyle({color:'#90A4AE',fillColor:'#90A4AE',fillOpacity:.10});}" +
|
"function updateGradient(g,s){}function setLost(){if(searchCircle)searchCircle.setStyle({color:'#90A4AE',fillColor:'#90A4AE',fillOpacity:.10});}" +
|
||||||
"</script></body></html>";
|
"</script></body></html>";
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user