Skip to content

Commit

Permalink
Merge pull request #19 from mkrupczak3/v13
Browse files Browse the repository at this point in the history
v0.13.0
  • Loading branch information
mkrupczak3 authored Jan 26, 2023
2 parents 95e7021 + c531945 commit dfda0eb
Show file tree
Hide file tree
Showing 23 changed files with 369 additions and 85 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ OpenAthena™ allows consumer and professional drones to spot precise geodetic l

# Operation Guide

<img width="586" alt="OpenAthena Android splash screen demo" src="./assets/App_Open_Demo_landscape.png">

## Obtain a GeoTIFF Digital Elevation Model:

Use of this app requires loading a GeoTIFF Digital Elevation Model (DEM) file, stored as a GeoTIFF ".tif" file.
Expand Down Expand Up @@ -43,6 +45,14 @@ Then, press the "🧮" button to calculate the target location on the ground:
<img width="586" alt="OpenAthena™ Android Target Calculation demo using cobb.tif and DJI_0419.JPG, output mode WGS84" src="./assets/DJI_0419_Target_Res_Demo_landscape.png">


## [ATAK](https://en.wikipedia.org/wiki/Android_Team_Awareness_Kit) Cursor on Target

When the "🧮" button is pressed, OpenAthena will automatically send a multicast packet to udp://239.2.3.1:6969 . Under default settings, this will cause a marker to show up in ATAK at the target location:

<img width="586" alt="OpenAthena for Android triggers a waypoint to show in Android Team Awarness Kit at the calculated location" src="./assets/ATAK_OpenAthena_CoT_Demo_landscape.png">

Change the marker to its appropriate type (friend, suspect, hostile) then send the target to other networked users.

# Application Settings (optional) ⚙:

OpenAthena for Android supports multiple output modes for target calculation, including:
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ android {
applicationId "com.openathena"
minSdk 28
targetSdk 32
versionCode 10
versionName "0.12.3"
versionCode 11
versionName "0.13.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"
android:minSdkVersion="30" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE" />

<!-- Chromebook compatibility -->
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-feature android:name="android.hardware.type.pc" android:required="false"/>
Expand Down
12 changes: 6 additions & 6 deletions app/src/main/java/com/openathena/AboutActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@ public boolean onOptionsItemSelected(MenuItem item)
return true;
}

if (id == R.id.action_log) {
intent = new Intent(getApplicationContext(),ActivityLog.class);
intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
startActivity(intent);
return true;
}
// if (id == R.id.action_log) {
// intent = new Intent(getApplicationContext(),ActivityLog.class);
// intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
// startActivity(intent);
// return true;
// }

// don't do anything if about is selected as we are already there

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,7 @@ public class CK42_Gauss_Krüger_Translator {
double N = I+II* Math.pow(Lon-Lon0,2)+III* Math.pow(Lon-Lon0,4)+IIIA* Math.pow(Lon-Lon0,6);
double E = E0+IV*(Lon-Lon0)+V* Math.pow(Lon-Lon0,3)+VI* Math.pow(Lon-Lon0,5);

E -= zone * 1e6; // subtract the zone number back out from the Easting reference

long[] outArr = new long[]{(long) zone, (long) N, (long) E};
long[] outArr = new long[]{(long) N, (long) E};
return outArr;
}

Expand Down
233 changes: 233 additions & 0 deletions app/src/main/java/com/openathena/CursorOnTargetSender.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
package com.openathena;

import org.w3c.dom.CDATASection;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xmlpull.v1.XmlSerializer;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringWriter;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.net.UnknownHostException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;

import android.content.Context;
import android.content.SharedPreferences;
import android.net.wifi.WifiManager;
import android.preference.PreferenceManager;
import android.util.Log;
import android.util.Xml;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;

public class CursorOnTargetSender {

private static long eventuid = 0;
private static Context mContext;

static TimeZone tz = TimeZone.getTimeZone("UTC");
static DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");

public static void sendCoT(Context invoker, double lat, double lon, double hae, double theta, String exif_datetime) {
if(invoker == null){
throw new IllegalArgumentException("invoker context can not be null");
} else if (!(invoker instanceof android.app.Activity)) {
throw new IllegalArgumentException("invoker context must be an Activity");
} else if (theta > 90) {
throw new IllegalArgumentException("angle of camera depression " + theta + " was invalid");
} else if (lat > 90 || lat < -90){
throw new IllegalArgumentException("latitude " + lat + " degrees is invalid");
} else if (lon > 180 || lon < -180) {
throw new IllegalArgumentException("longitude " + lon + " degrees is invalid");
} else if (exif_datetime == null) {
throw new IllegalArgumentException("exif_datetime was null pointer, expected a String");
}

mContext = invoker;
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
if (sharedPreferences != null) {
eventuid = sharedPreferences.getLong("eventuid", 0);
}

df.setTimeZone(tz);
Date now = new Date();
String nowAsISO = df.format(now);
Calendar cal = Calendar.getInstance();
cal.setTime(now);
cal.add(Calendar.MINUTE, 5);
Date fiveMinsFromNow = cal.getTime();
String fiveMinutesFromNowISO = df.format(fiveMinsFromNow);
String imageISO = df.format(convert(exif_datetime));
double linearError = 15.0d / 3.0d; // optimistic estimation of 1 sigma accuracy of altitude
double circularError = 1.0d / Math.tan(Math.toRadians(theta)) * linearError; // optimistic estimation of 1 sigma accuracy based on angle of camera depression theta

String le = Double.toString(linearError);
String ce = Double.toString(circularError);
new Thread(new Runnable() {
@Override
public void run() {
String uidString = "OpenAthena-" + getDeviceHostnameHash().substring(0,8) + "-" + Long.toString(eventuid);
// String xmlString = buildCoT(uidString, imageISO, nowAsISO, fiveMinutesFromNowISO, Double.toString(lat), Double.toString(lon), ce, Double.toString(Math.round(hae)), le);
String xmlString = buildCoT(uidString, imageISO, nowAsISO, fiveMinutesFromNowISO, Double.toString(lat), Double.toString(lon), ce, Double.toString(hae), le);
// String dumxml = "<event uid=\"41414141\" type=\"a-u-G\" how=\"h-c\" start=\"2023-01-24T22:16:53Z\" time=\"2023-01-24T22:16:53Z\" stale=\"2023-01-25T22:06:53Z\"><point le=\"0\" ce=\"0\" hae=\"0\" lon=\"0\" lat=\"0\"/></event>";
// dumxml = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>" + dumxml;
Log.d("CursorOnTargetSender", xmlString);
deliverUDP(xmlString); // increments uid upon success
}
}).start();



}

/**
* Converts an ExifInterface time and date tag into a Joda time format
*
* @param exif_tag_datetime
* @return null in case of failure, the date object otherwise
*/
public static Date convert(String exif_tag_datetime) {
// EXIF tag contains no time zone data, assume it is same as local time
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss", Locale.getDefault()); // default locale of device at application start
Date outDate;
try {
outDate = simpleDateFormat.parse(exif_tag_datetime);
} catch (ParseException e) {
outDate = null;
}
return outDate;
}

public static String getDeviceHostnameHash() {
InetAddress addr;
String hash;
try {
addr = InetAddress.getLocalHost();
String hostname = addr.getHostName();
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(hostname.getBytes());
byte[] digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest) {
sb.append(String.format("%02x", b & 0xff));
}
hash = sb.toString();
} catch (UnknownHostException e) {
hash = "unknown";
} catch (NoSuchAlgorithmException e) {
hash = "unknown";
}
return hash;
}

public static String buildCoT(String uid, String imageISO, String nowAsISO, String fiveMinutesFromNowISO, String lat, String lon, String ce, String hae, String le) {
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
DocumentBuilder db = dbf.newDocumentBuilder();
Document doc = db.newDocument();


Element root = doc.createElement("event");
root.setAttribute("version", "2.0");
root.setAttribute("uid", uid);
root.setAttribute("type", "a-p-G");
root.setAttribute("how", "h-c");
root.setAttribute("time", imageISO);
root.setAttribute("start", nowAsISO);
root.setAttribute("stale", fiveMinutesFromNowISO);
doc.appendChild(root);

Element point = doc.createElement("point");
point.setAttribute("lat", lat);
point.setAttribute("lon", lon);
point.setAttribute("ce", ce);
point.setAttribute("hae", hae);
point.setAttribute("le", le);
root.appendChild(point);

Element detail = doc.createElement("detail");
root.appendChild(detail);

Element precisionlocation = doc.createElement("precisionlocation");
precisionlocation.setAttribute("altsrc", "DTED0");
precisionlocation.setAttribute("geopointsrc", "GPS");
detail.appendChild(precisionlocation);

Element remarks = doc.createElement("remarks");
remarks.setTextContent(mContext.getString(R.string.cot_remarks));
detail.appendChild(remarks);

TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty("standalone", "yes");
DOMSource source = new DOMSource(doc);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
// Log.d("CursorOnTargetSender", writer.toString());
return writer.toString();
} catch (Exception e) {
e.printStackTrace();
return "";
}
}

private static void deliverUDP(String xml) {
new Thread(new Runnable() {
@Override
public void run() {
try {
String message = xml;
InetAddress address = InetAddress.getByName("239.2.3.1");
int port = 6969;

if (mContext == null) {
throw new NullPointerException("CoT Sender invoker context was null");
}
WifiManager wifi = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
WifiManager.MulticastLock lock = wifi.createMulticastLock("CoT multicast send lock");
lock.acquire();

MulticastSocket socket = new MulticastSocket();
DatagramPacket packet = new DatagramPacket(message.getBytes(), message.length(), address, port);
socket.send(packet);
socket.close();
eventuid++;
saveUid();
lock.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}

private static void saveUid() {
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor prefsEditor = sharedPreferences.edit();
prefsEditor.putLong("eventuid", eventuid);
prefsEditor.apply();
}
}
Loading

0 comments on commit dfda0eb

Please sign in to comment.