/*
* Licensed to the Ted Dunning under one or more contributor license
* agreements. See the NOTICE file that may be
* distributed with this work for additional information
* regarding copyright ownership. Ted Dunning licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package com.mapr.synth.drive;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.DoubleNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.mapr.synth.samplers.FieldSampler;
import java.io.IOException;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Iterator;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
/**
* Emulates a commuter who drives to work and back home and who runs errands around their home.
* <p>
* The model for deciding the next action is
* <p>
* 1) commuting to work happens mostly happens with a strong peak from 7-9, but could happen anytime
* 2) commuting back home happens mostly from 15 to 20, with a strong peak from 17-19, but could happen any time.
* 3) errands happen much more during the day than at night and only happen if the commuter is at home.
* <p>
* Once the next action is selected, the trip is planned (one-way for commuting, round-trip for errands)
* <p>
* The configuration of this sampler is a little bit fancy. The home location is specified as a sampler that
* returns an object with latitude and longitude fields (typically a zip). The work location is specified as
* one of several forms:
* <p>
* - as a number. This number is used as the mean distance and the work location is picked as a normal distribution
* around the home. This is the common case.
* <p>
* - as an object. The object is interpreted as a sampler that returns an object containing (at least) latitude
* and longitude fields. Typically this would be a zip.
* <p>
* In addition, you need to specify the start and stop time for the simulation.
*/
public class Commuter extends FieldSampler {
private static final double ERRAND_SIZE_KM = 20;
private static final double WEEKEND_COMMUTE_RATE = 0.1;
public static final double WEEKEND_ERRAND_RATE = 0.9;
private static final double WEEKDAY_COMMUTE_RATE = 2;
private static final double WEEKDAY_PEAK_COMMUTE_RATE = 10;
public static final double WEEKDAY_ERRAND_RATE = 0.5;
private static final double DAY_IN_S = 24 * 3600.0;
private static final JsonNodeFactory FACTORY = JsonNodeFactory.withExactBigDecimals(false);
@SuppressWarnings("unused")
private static final int RECORD_LIMIT = 2000;
// simulation period in seconds
private double start;
private double end;
// internal mechanics
Random rand = new Random();
DateFormat df;
final static private ThreadLocal<GregorianCalendar> cal = new ThreadLocal<GregorianCalendar>() {
@Override
protected GregorianCalendar initialValue() {
return new GregorianCalendar();
}
};
// is the commuter at home?
boolean atHome;
double sampleTime = 1;
private FieldSampler homeSampler;
private FieldSampler workSampler;
@SuppressWarnings("unused")
private BlockingQueue<JsonNode> resultBuffer = new LinkedBlockingQueue<>();
private boolean isFlat;
public Commuter() throws ParseException {
setFormat("yyyy-MM-dd HH:mm:ss");
start = df.parse("2014-01-01 00:00:00").getTime() / 1000.0;
end = df.parse("2014-01-05 00:00:00").getTime() / 1000.0;
}
public static boolean isWeekend(double t) {
GregorianCalendar c = cal.get();
c.setTimeInMillis((long) (t * 1000));
int d = c.get(GregorianCalendar.DAY_OF_WEEK);
return d == GregorianCalendar.SATURDAY || d == GregorianCalendar.SUNDAY;
}
@Override
public JsonNode sample() {
final Car car = new Car();
car.setSampleTime(sampleTime);
car.getEngine().setTime(start);
JsonNode homeLocation = homeSampler.sample();
GeoPoint home = new GeoPoint(Util.toDegrees(homeLocation, "latitude"), Util.toDegrees(homeLocation, "longitude"));
double radius = workSampler.sample().asDouble();
GeoPoint work = home.nearby(radius, rand);
//---------------
ObjectNode base = new ObjectNode(FACTORY);
base.putObject("home").setAll((ObjectNode) homeLocation);
work.asJson(base.putObject("work"));
ArrayNode trips = new ArrayNode(FACTORY);
for (double t = start; t < end; ) {
double tCommute = search(atHome, t, Util.nextExponentialTime(rand, 1));
if (atHome) {
double tErrand = t + Util.nextExponentialTime(rand, (isWeekend(t) ? WEEKEND_ERRAND_RATE : WEEKDAY_ERRAND_RATE) / DAY_IN_S);
while (tErrand < tCommute && tErrand < end) {
GeoPoint nearby = home.nearby(ERRAND_SIZE_KM, rand);
ObjectNode trip = trips.addObject();
ArrayNode data = trip.putArray("data");
t = drive(tErrand, car, home, nearby, data);
recordTrip(tErrand, t - tErrand, "errand_out", 2 * home.distance(nearby), trip);
t += rand.nextDouble() * 900 + 300;
double t0 = t;
trip = trips.addObject();
data = trip.putArray("data");
t = drive(t0, car, nearby, home, data);
recordTrip(t0, t - t0, "errand_return", 2 * home.distance(nearby), trip);
tErrand = t + Util.nextExponentialTime(rand, WEEKEND_ERRAND_RATE / DAY_IN_S);
}
if (tCommute < end) {
ObjectNode trip = trips.addObject();
ArrayNode data = trip.putArray("data");
t = drive(tCommute, car, home, work, data);
recordTrip(tCommute, t - tCommute, "to_work", home.distance(work), trip);
atHome = !atHome;
}
} else {
ObjectNode trip = trips.addObject();
ArrayNode data = trip.putArray("data");
t = drive(tCommute, car, work, home, data);
recordTrip(tCommute, t - tCommute, "to_home", home.distance(work), trip);
atHome = !atHome;
}
}
if (!isFlat) {
ObjectNode x = base.deepCopy();
x.set("trips", trips);
return x;
} else {
ArrayNode r = new ArrayNode(FACTORY);
for (JsonNode trip : trips) {
ObjectNode x = base.deepCopy();
Iterator<String> jx = trip.fieldNames();
while (jx.hasNext()) {
String field = jx.next();
if (!field.equals("data")) {
x.set(field, trip.get(field));
}
}
for (JsonNode dataPoint : trip.get("data")) {
ObjectNode y = x.deepCopy();
y.setAll((ObjectNode) dataPoint);
r.add(y);
}
}
return r;
}
}
private void recordTrip(double start, double duration, String type, double distance, ObjectNode trip) {
trip.put("t", duration);
//noinspection SynchronizeOnNonFinalField
synchronized (df) {
trip.put("start", df.format(new Date((long) (start * 1000))));
}
trip.put("timestamp", (long) start * 1000);
trip.put("type", type);
trip.put("distance_km", distance);
trip.put("duration", duration);
}
public int hourOfDay(double t) {
GregorianCalendar c = cal.get();
c.setTimeInMillis((long) (t * 1000.0));
c.setTimeZone(TimeZone.getTimeZone("US/Central"));
return c.get(GregorianCalendar.HOUR_OF_DAY);
}
public double search(boolean toWork, double t, double bound) {
while (true) {
double nextHour = Util.evenHour(t + 3600);
int hour = hourOfDay(t);
double rate = lookupRate(isWeekend(t), toWork, hour);
double step = rate * (nextHour - t);
if (step > bound) {
t = t + bound / step * (nextHour - t);
return t;
} else {
bound -= step;
t = nextHour;
}
}
}
public static double lookupRate(boolean isWeekend, boolean toWork, int hour) {
if (isWeekend) {
return WEEKEND_COMMUTE_RATE / DAY_IN_S;
} else {
if (toWork) {
if (hour >= 7 && hour < 9) {
return WEEKDAY_PEAK_COMMUTE_RATE / DAY_IN_S;
} else {
return WEEKDAY_COMMUTE_RATE / DAY_IN_S;
}
} else {
if (hour >= 16 && hour < 18) {
return WEEKDAY_PEAK_COMMUTE_RATE / DAY_IN_S;
} else {
return WEEKDAY_COMMUTE_RATE / DAY_IN_S;
}
}
}
}
private double drive(double t, Car car, GeoPoint start, GeoPoint end, ArrayNode data) {
car.getEngine().setTime(t);
return car.driveTo(rand, t, start, end, new Car.Callback() {
@Override
void call(double t, Engine car, GeoPoint position) {
ObjectNode sample = data.addObject();
position.asJson(sample);
sample.put("t", t);
//noinspection SynchronizeOnNonFinalField
synchronized (df) {
sample.put("timestamp", df.format(new Date((long) (t * 1000))));
}
sample.put("mph", car.getSpeed() * Constants.MPH);
sample.put("rpm", car.getRpm());
sample.put("throttle", car.getThrottle());
}
});
}
@SuppressWarnings("UnusedDeclaration")
public void setFormat(String format) {
df = new SimpleDateFormat(format);
}
@SuppressWarnings("UnusedDeclaration")
public void setStart(String start) throws ParseException {
this.start = df.parse(start).getTime() / 1000.0;
}
@SuppressWarnings("UnusedDeclaration")
public void setEnd(String end) throws ParseException {
this.end = df.parse(end).getTime() / 1000.0;
}
@SuppressWarnings("unused")
public void setSampleTime(double sampleTime) {
this.sampleTime = sampleTime;
}
@SuppressWarnings("unused")
public void setHome(JsonNode value) throws IOException {
if (value.isObject()) {
homeSampler = FieldSampler.newSampler(value.toString());
} else {
throw new IllegalArgumentException("Expected geo-location sampler");
}
}
@SuppressWarnings("unused")
public void setWork(JsonNode value) throws IOException {
if (value.isObject()) {
workSampler = new FieldSampler() {
FieldSampler base = FieldSampler.newSampler(value.toString());
@Override
public JsonNode sample() {
return new DoubleNode(Math.sqrt(1 / base.sample().asDouble()));
}
};
} else if (value.isNumber()) {
workSampler = constant(value.asDouble());
}
}
@SuppressWarnings("unused")
public void setFlat(boolean isFlat) {
this.isFlat = isFlat;
}
@Override
public boolean isFlat() {
return isFlat;
}
}