Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Intersection of points download from authority #165

Merged
merged 1 commit into from
Mar 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions app/helpers/Intersect.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* Intersect a set of points against the user's locally stored points.
*
* v1 - Unencrypted, simpleminded (minimal optimization).
*/

import { GetStoreData, SetStoreData } from '../helpers/General';

export async function IntersectSet(concernLocationArray) {
GetStoreData('LOCATION_DATA').then(locationArrayString => {
var locationArray;
if (locationArrayString !== null) {
locationArray = JSON.parse(locationArrayString);
} else {
locationArray = [];
}

let dayBin = [
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
]; // Bins for 28 days

// Sort the concernLocationArray
let localArray = normalizeData(locationArray);
let concernArray = normalizeData(concernLocationArray);

let concernTimeWindow = 1000 * 60 * 60 * 2; // +/- 2 hours window
let concernDistWindow = 60; // distance of concern, in feet

// At 38 degrees North latitude:
let ftPerLat = 364000; // 1 deg lat equals 364,000 ft
let ftPerLon = 288200; // 1 deg of longitude equals 288,200 ft

var nowUTC = new Date().toISOString();
var timeNow = Date.parse(nowUTC);

// Save a little CPU, no need to do sqrt()
let concernDistWindowSq = concernDistWindow * concernDistWindow;

// Both locationArray and concernLocationArray should be in the
// format [ { "time": 123, "latitude": 12.34, "longitude": 34.56 }]

for (let loc of localArray) {
let timeMin = loc.time - concernTimeWindow;
let timeMax = loc.time + concernTimeWindow;

let i = binarySearchForTime(concernArray, timeMin);
if (i < 0) i = -(i + 1);

while (i < concernArray.length && concernArray[i].time <= timeMax) {
// Perform a simple Euclidian distance test
let deltaLat = (concernArray[i].latitude - loc.latitude) * ftPerLat;
let deltaLon = (concernArray[i].longitude - loc.longitude) * ftPerLon;
// TODO: Scale ftPer factors based on lat to reduce projection error

let distSq = deltaLat * deltaLat + deltaLon * deltaLon;
if (distSq < concernDistWindowSq) {
// Crossed path. Bin the count of encounters by days from today.
let longAgo = timeNow - loc.time;
let daysAgo = Math.round(longAgo / (1000 * 60 * 60 * 24));

dayBin[daysAgo] += 1;
}

i++;
}
}

// TODO: Show in the UI!
console.log('Crossing results: ', dayBin);
SetStoreData('CROSSED_PATHS', dayBin); // TODO: Store per authority?
});
}

function normalizeData(arr) {
// This fixes several issues that I found in different input data:
// * Values stored as strings instead of numbers
// * Extra info in the input
// * Improperly sorted data (can happen after an Import)
var result = [];

for (var i = 0; i < arr.length; i++) {
elem = arr[i];
if ('time' in elem && 'latitude' in elem && 'longitude' in elem) {
result.push({
time: Number(elem.time),
latitude: Number(elem.latitude),
longitude: Number(elem.longitude),
});
}
}

result.sort();
return result;
}

function binarySearchForTime(array, targetTime) {
// Binary search:
// array = sorted array
// target = search target
// Returns:
// value >= 0, index of found item
// value < 0, i where -(i+1) is the insertion point
var i = 0;
var n = array.length - 1;

while (i <= n) {
var k = (n + i) >> 1;
var cmp = targetTime - array[k].time;

if (cmp > 0) {
i = k + 1;
} else if (cmp < 0) {
n = k - 1;
} else {
// Found exact match!
// NOTE: Could be one of several if array has duplicates
return k;
}
}
return -i - 1;
}
66 changes: 65 additions & 1 deletion app/views/LocationTracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import news from './../assets/images/newspaper.png';
import kebabIcon from './../assets/images/kebabIcon.png';
import pkLogo from './../assets/images/PKLogo.png';

import { IntersectSet } from '../helpers/Intersect';
import { GetStoreData, SetStoreData } from '../helpers/General';
import languages from './../locales/languages';

Expand All @@ -35,6 +36,7 @@ class LocationTracking extends Component {
super(props);

this.state = {
timer_intersect: null,
isLogging: '',
};
}
Expand All @@ -57,8 +59,70 @@ class LocationTracking extends Component {
}
})
.catch(error => console.log(error));

let timer_intersect = setInterval(this.intersect_tick, 1000 * 60 * 60 * 12); // once every 12 hours
// DEBUG: 1000 * 10); // once every 10 seconds

this.setState({
timer_intersect,
});
}

intersect_tick = () => {
// This function is called once every 12 hours. It should do several things:

// Get the user's health authorities
GetStoreData('HEALTH_AUTHORITIES')
.then(authority_list => {
if (!authority_list) {
// DEBUG: Force a test list
// authority_list = [
// {
// name: 'Platte County Health',
// url:
// 'https://raw.githack.com/tripleblindmarket/safe-places/develop/examples/safe-paths.json',
// },
//];
return;
}

if (authority_list) {
// Pull down data from all the registered health authorities
for (let authority of authority_list) {
fetch(authority.url)
.then(response => response.json())
.then(responseJson => {
// Example response =
// { "authority_name": "Steve's Fake Testing Organization",
// "publish_date_utc": "1584924583",
// "info_website": "https://www.who.int/emergencies/diseases/novel-coronavirus-2019",
// "concern_points":
// [
// { "time": 123, "latitude": 12.34, "longitude": 12.34},
// { "time": 456, "latitude": 12.34, "longitude": 12.34}
// ]
// }

// Update cache of info about the authority
// (info_url might have changed, etc.)

// TODO: authority_list, match by authority_list.url, then re-save "authority_name", "info_website" and
// "publish_date_utc" (we should notify users if their authority is no longer functioning.)
// console.log('Received data from authority.url=', authority.url);

IntersectSet(responseJson.concern_points);
});
}
} else {
console.log('No authority list');
return;
}
})
.catch(error => console.log('Failed to load authority list', error));
};

componentWillUnmount() {
clearInterval(this.state.timer_intersect);
BackHandler.removeEventListener('hardwareBackPress', this.handleBackPress);
}

Expand Down Expand Up @@ -132,7 +196,7 @@ class LocationTracking extends Component {
alignSelf: 'flex-end',
zIndex: 10,
}}>
<MenuTrigger style={{ marginTop: 14 }}>
<MenuTrigger style={{ marginTop: 14, marginRight: -10 }}>
<Image
source={kebabIcon}
style={{
Expand Down