/** * Copyright (C) 2016 Cambridge Systematics, Inc. * * Licensed 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 org.onebusaway.android.directions.util; import org.onebusaway.android.R; import org.onebusaway.android.app.Application; import org.onebusaway.android.directions.tasks.TripRequest; import org.onebusaway.android.util.RegionUtils; import org.opentripplanner.api.ws.Request; import org.opentripplanner.routing.core.OptimizeType; import org.opentripplanner.routing.core.TraverseMode; import android.app.Activity; import android.os.Bundle; import android.text.TextUtils; import android.util.Log; import java.io.UnsupportedEncodingException; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; public class TripRequestBuilder { private static final String ENCODING = "UTF-8"; private static final String TAG = "TripRequestBuilder"; private static final String ARRIVE_BY = ".ARRIVE_BY"; private static final String FROM_ADDRESS = ".FROM_ADDRESS"; private static final String FROM_LAT = ".FROM_LAT"; private static final String FROM_LON = ".FROM_LON"; private static final String FROM_NAME = ".FROM_NAME"; private static final String TO_ADDRESS = ".TO_ADDRESS"; private static final String TO_LAT = ".TO_LAT"; private static final String TO_LON = ".TO_LON"; private static final String TO_NAME = ".TO_NAME"; private static final String OPTIMIZE_TRANSFERS = ".OPTIMIZE_TRANSFERS"; private static final String WHEELCHAIR_ACCESSIBLE = ".WHEELCHAIR_ACCESSIBLE"; private static final String MAX_WALK_DISTANCE = ".MAX_WALK_DISTANCE"; private static final String MODE_SET = ".MODE_SET"; private static final String DATE_TIME = ".DATE_TIME"; private TripRequest.Callback mListener; private Bundle mBundle; public TripRequestBuilder(Bundle bundle) { this.mBundle = bundle; } public TripRequestBuilder setDepartureTime(Calendar calendar) { mBundle.putBoolean(ARRIVE_BY, false); setDateTime(calendar.getTime()); return this; } public TripRequestBuilder setArrivalTime(Calendar calendar) { mBundle.putBoolean(ARRIVE_BY, true); setDateTime(calendar.getTime()); return this; } // default value is false public boolean getArriveBy() { Boolean b = mBundle.getBoolean(ARRIVE_BY); return b == null ? false : b; } public TripRequestBuilder setFrom(CustomAddress from) { mBundle.putParcelable(FROM_ADDRESS, from); return this; } public CustomAddress getFrom() { return (CustomAddress) mBundle.getParcelable(FROM_ADDRESS); } public CustomAddress getTo() { return (CustomAddress) mBundle.getParcelable(TO_ADDRESS); } public TripRequestBuilder setTo(CustomAddress to) { mBundle.putParcelable(TO_ADDRESS, to); return this; } public TripRequestBuilder setListener(TripRequest.Callback listener) { this.mListener = listener; return this; } public TripRequestBuilder setOptimizeTransfers(boolean set) { mBundle.putSerializable(OPTIMIZE_TRANSFERS, set ? OptimizeType.TRANSFERS : OptimizeType.QUICK); return this; } private OptimizeType getOptimizeType() { OptimizeType type = (OptimizeType) mBundle.getSerializable(OPTIMIZE_TRANSFERS); return type == null ? OptimizeType.QUICK : type; } public boolean getOptimizeTransfers() { return getOptimizeType() == OptimizeType.TRANSFERS; } public TripRequestBuilder setWheelchairAccessible(boolean wheelchair) { mBundle.putBoolean(WHEELCHAIR_ACCESSIBLE, wheelchair); return this; } public boolean getWheelchairAccessible() { return mBundle.getBoolean(WHEELCHAIR_ACCESSIBLE); } public TripRequestBuilder setMaxWalkDistance(double walkDistance) { mBundle.putDouble(MAX_WALK_DISTANCE, walkDistance); return this; } public Double getMaxWalkDistance() { Double d = mBundle.getDouble(MAX_WALK_DISTANCE); return d != 0 ? d : null; } // Built in TraverseModeSet does not work properly so we cannot use request.setMode // This is built from examining dropdown on the OTP webapp // there are also airplane, bike, bike + ride, park + ride, kiss + ride, etc options // transit -> TRANSIT,WALK // bus only -> BUSISH,WALK // rail only -> TRAINISH,WALK public TripRequestBuilder setModeSetById(int id) { List<TraverseMode> modes; switch (id) { case R.string.transit_mode_transit: modes = Arrays.asList(TraverseMode.TRANSIT, TraverseMode.WALK); break; case R.string.transit_mode_bus: modes = Arrays.asList(TraverseMode.BUSISH, TraverseMode.WALK); break; case R.string.transit_mode_rail: modes = Arrays.asList(TraverseMode.TRAINISH, TraverseMode.WALK); break; default: Log.e(TAG, "Invalid mode set ID"); modes = Arrays.asList(TraverseMode.TRANSIT, TraverseMode.WALK); } String modeString = TextUtils.join(",", modes); mBundle.putString(MODE_SET, modeString); return this; } public int getModeSetId() { List<TraverseMode> modes = getModes(); if (modes.contains(TraverseMode.BUSISH)) { return R.string.transit_mode_bus; } if (modes.contains(TraverseMode.TRAINISH)) { return R.string.transit_mode_rail; } if (modes.contains(TraverseMode.TRANSIT)) { return R.string.transit_mode_transit; } return -1; } private List<TraverseMode> getModes() { List<TraverseMode> modes = new ArrayList<>(); String modeString = mBundle.getString(MODE_SET); if (modeString == null) { return Arrays.asList(TraverseMode.TRANSIT, TraverseMode.WALK); } String[] tokens = modeString.split(","); for (String tok : tokens) { TraverseMode mode = TraverseMode.valueOf(tok); modes.add(mode); } return modes; } private String getModeString() { return mBundle.getString(MODE_SET); } public TripRequest execute(Activity activity) { String from = getAddressString(getFrom()); String to = getAddressString(getTo()); if (TextUtils.isEmpty(from) || TextUtils.isEmpty(to)) { throw new IllegalArgumentException("Must supply start and end to route between."); } Request request = new Request(); request.setArriveBy(getArriveBy()); request.setFrom(from); request.setTo(to); request.setOptimize(getOptimizeType()); request.setWheelchair(getWheelchairAccessible()); Double maxWalkDistance = getMaxWalkDistance(); if (maxWalkDistance != null) { request.setMaxWalkDistance(maxWalkDistance); } Date d = getDateTime(); // OTP expects date/time in this format String date = getFormattedDate(OTPConstants.FORMAT_OTP_SERVER_DATE_REQUEST, d); String time = getFormattedDate(OTPConstants.FORMAT_OTP_SERVER_TIME_REQUEST, d); request.setDateTime(date, time); // Request mode set does not work properly String modeString = mBundle.getString(MODE_SET); if (modeString != null) { request.getParameters().put("mode", modeString); } // Our default. This could be configurable. request.setShowIntermediateStops(true); // TripRequest will accept a null value and give a user-friendly error String otpBaseUrl; Application app = Application.get(); if (!TextUtils.isEmpty(app.getCustomOtpApiUrl())) { otpBaseUrl = app.getCustomOtpApiUrl(); Log.d(TAG, "Using custom OTP API URL set by user '" + otpBaseUrl + "'."); if (!TextUtils.isEmpty(otpBaseUrl)) { try { // URI.parse() doesn't tell us if the scheme is missing, so use URL() instead (#126) URL url = new URL(otpBaseUrl); } catch (MalformedURLException e) { // Assume HTTP scheme, since without a scheme the Uri won't parse the authority otpBaseUrl = activity.getString(R.string.http_prefix) + otpBaseUrl; } } } else { otpBaseUrl = app.getCurrentRegion().getOtpBaseUrl(); } String fmtOtpBaseUrl = otpBaseUrl != null ? RegionUtils.formatOtpBaseUrl(otpBaseUrl) : null; TripRequest tripRequest; if (activity == null) { tripRequest = new TripRequest(fmtOtpBaseUrl, mListener); } else { WeakReference<Activity> ref = new WeakReference<Activity>(activity); tripRequest = new TripRequest(fmtOtpBaseUrl, mListener); } tripRequest.execute(request); return tripRequest; } public void execute() { execute(null); } private String getAddressString(CustomAddress address) { if (address == null) { return null; } if (address.hasLatitude() && address.hasLongitude()) { double lat = address.getLatitude(); double lon = address.getLongitude(); return String.format(OTPConstants.OTP_LOCALE, "%g,%g", lat, lon); } // Not set via geocoder OR via location service. Use raw string (set in TripPlanFragment to first line of address). String line = address.getAddressLine(0); try { return URLEncoder.encode(line, ENCODING); } catch (UnsupportedEncodingException ex) { Log.e(TAG, "Error encoding address: " + ex); return ""; } } public TripRequestBuilder setDateTime(Date d) { mBundle.putLong(DATE_TIME, d.getTime()); return this; } public TripRequestBuilder setDateTime(Calendar cal) { return setDateTime(cal.getTime()); } public Date getDateTime() { Long time = mBundle.getLong(DATE_TIME); if (time == null || time == 0L) { return null; } else { return new Date(time); } } public Bundle getBundle() { return mBundle; } /** * Copy all the data from this builder's bundle into another bundle * @param target bundle */ public void copyIntoBundle(Bundle target) { target.putBoolean(ARRIVE_BY, getArriveBy()); target.putParcelable(FROM_ADDRESS, getFrom()); target.putParcelable(TO_ADDRESS, getTo()); target.putSerializable(OPTIMIZE_TRANSFERS, getOptimizeType()); target.putBoolean(WHEELCHAIR_ACCESSIBLE, getWheelchairAccessible()); if (getMaxWalkDistance() != null) { target.putDouble(MAX_WALK_DISTANCE, getMaxWalkDistance()); } target.putString(MODE_SET, getModeString()); if (getDateTime() != null) { target.putLong(DATE_TIME, getDateTime().getTime()); } } /** * Copy all the data from this builder's bundle into another bundle, but only use simple data types * @param target bundle */ public void copyIntoBundleSimple(Bundle target) { target.putBoolean(ARRIVE_BY, getArriveBy()); CustomAddress from = getFrom(), to = getTo(); target.putDouble(FROM_LAT, from.getLatitude()); target.putDouble(FROM_LON, from.getLongitude()); target.putString(FROM_NAME, from.toString()); target.putDouble(TO_LAT, to.getLatitude()); target.putDouble(TO_LON, to.getLongitude()); target.putString(TO_NAME, to.toString()); target.putString(OPTIMIZE_TRANSFERS, getOptimizeType().toString()); target.putBoolean(WHEELCHAIR_ACCESSIBLE, getWheelchairAccessible()); if (getMaxWalkDistance() != null) { target.putDouble(MAX_WALK_DISTANCE, getMaxWalkDistance()); } target.putString(MODE_SET, getModeString()); if (getDateTime() != null) { target.putLong(DATE_TIME, getDateTime().getTime()); } } /** * Initialize from a BaseBundle */ public static TripRequestBuilder initFromBundleSimple(Bundle bundle) { Bundle target = new Bundle(); target.putBoolean(ARRIVE_BY, bundle.getBoolean(ARRIVE_BY)); CustomAddress from = new CustomAddress(); from.setLatitude(bundle.getDouble(FROM_LAT)); from.setLongitude(bundle.getDouble(FROM_LON)); from.setAddressLine(0, bundle.getString(FROM_NAME)); CustomAddress to = new CustomAddress(); to.setLatitude(bundle.getDouble(TO_LAT)); to.setLongitude(bundle.getDouble(TO_LON)); to.setAddressLine(0, bundle.getString(TO_NAME)); target.putParcelable(FROM_ADDRESS, from); target.putParcelable(TO_ADDRESS, to); String optName = bundle.getString(OPTIMIZE_TRANSFERS); if (optName != null) { target.putSerializable(OPTIMIZE_TRANSFERS, OptimizeType.valueOf(optName)); } target.putBoolean(WHEELCHAIR_ACCESSIBLE, bundle.getBoolean(WHEELCHAIR_ACCESSIBLE)); target.putDouble(MAX_WALK_DISTANCE, bundle.getDouble(MAX_WALK_DISTANCE)); target.putString(MODE_SET, bundle.getString(MODE_SET)); target.putLong(DATE_TIME, bundle.getLong(DATE_TIME)); return new TripRequestBuilder(target); } /** * Determine whether this trip request can be submitted to an OTP server. * @return true if ready to submit, false otherwise */ public boolean ready() { return getFrom() != null && getFrom().isSet() && getTo() != null && getTo().isSet() && getDateTime() != null; } private static String getFormattedDate(String format, Date date) { return new SimpleDateFormat(format, OTPConstants.OTP_LOCALE).format(date); } }