() {
+ @Deprecated("Deprecated in Java")
+ override fun doInBackground(vararg files: File?) {
+ files.filterNotNull().forEach {
+ Log.i(TAG, "Deleting ${it.absolutePath}")
+ it.delete()
+ }
+ }
+}
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Base64.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Base64.java
index 20472658e..a5922f624 100644
--- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Base64.java
+++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Base64.java
@@ -3,16 +3,16 @@
/**
* Encodes and decodes to and from Base64 notation.
* Homepage: http://iharder.net/base64 .
- *
+ *
* Example:
- *
+ *
* String encoded = Base64.encode( myByteArray );
*
* byte[] myByteArray = Base64.decode( encoded );
*
- * The options parameter, which appears in a few places, is used to pass
- * several pieces of information to the encoder. In the "higher level" methods such as
- * encodeBytes( bytes, options ) the options parameter can be used to indicate such
+ *
The options parameter, which appears in a few places, is used to pass
+ * several pieces of information to the encoder. In the "higher level" methods such as
+ * encodeBytes( bytes, options ) the options parameter can be used to indicate such
* things as first gzipping the bytes before encoding them, not inserting linefeeds,
* and encoding using the URL-safe and Ordered dialects.
*
@@ -21,7 +21,7 @@
* to do so. I've got Base64 set to this behavior now, although earlier versions
* broke lines by default.
*
- * The constants defined in Base64 can be OR-ed together to combine options, so you
+ *
The constants defined in Base64 can be OR-ed together to combine options, so you
* might make a call like this:
*
* String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES );
@@ -68,7 +68,7 @@
* v2.3 - This is not a drop-in replacement! This is two years of comments
* and bug fixes queued up and finally executed. Thanks to everyone who sent
* me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else.
- * Much bad coding was cleaned up including throwing exceptions where necessary
+ * Much bad coding was cleaned up including throwing exceptions where necessary
* instead of returning null values or something similar. Here are some changes
* that may affect you:
*
@@ -106,24 +106,24 @@
* Special thanks to Jim Kellerman at http://www.powerset.com/
* for contributing the new Base64 dialects.
*
- *
+ *
* v2.1 - Cleaned up javadoc comments and unused variables and methods. Added
* some convenience methods for reading and writing to and from files.
* v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems
* with other encodings (like EBCDIC).
* v2.0.1 - Fixed an error when decoding a single byte, that is, when the
* encoded data was a single byte.
- * v2.0 - I got rid of methods that used booleans to set options.
+ * v2.0 - I got rid of methods that used booleans to set options.
* Now everything is more consolidated and cleaner. The code now detects
* when data that's being decoded is gzip-compressed and will decompress it
* automatically. Generally things are cleaner. You'll probably have to
* change some method calls that you were making to support the new
* options format (int s that you "OR" together).
- * v1.5.1 - Fixed bug when decompressing and decoding to a
- * byte[] using decode( String s, boolean gzipCompressed ) .
- * Added the ability to "suspend" encoding in the Output Stream so
- * you can turn on and off the encoding if you need to embed base64
- * data in an otherwise "normal" stream (like an XML file).
+ * v1.5.1 - Fixed bug when decompressing and decoding to a
+ * byte[] using decode( String s, boolean gzipCompressed ) .
+ * Added the ability to "suspend" encoding in the Output Stream so
+ * you can turn on and off the encoding if you need to embed base64
+ * data in an otherwise "normal" stream (like an XML file).
* v1.5 - Output stream pases on flush() command but doesn't do anything itself.
* This helps when using GZIP streams.
* Added the ability to GZip-compress objects before encoding them.
@@ -149,91 +149,78 @@
*/
public class Base64
{
-
-/* ******** P U B L I C F I E L D S ******** */
-
-
+
+/* ******** P U B L I C F I E L D S ******** */
+
/** No options specified. Value is zero. */
public final static int NO_OPTIONS = 0;
-
+
/** Specify encoding in first bit. Value is one. */
public final static int ENCODE = 1;
-
-
+
/** Specify decoding in first bit. Value is zero. */
public final static int DECODE = 0;
-
/** Specify that data should be gzip-compressed in second bit. Value is two. */
public final static int GZIP = 2;
/** Specify that gzipped data should not be automatically gunzipped. */
public final static int DONT_GUNZIP = 4;
-
-
+
/** Do break lines when encoding. Value is 8. */
public final static int DO_BREAK_LINES = 8;
-
- /**
+
+ /**
* Encode using Base64-like encoding that is URL- and Filename-safe as described
- * in Section 4 of RFC3548:
+ * in Section 4 of RFC3548:
* http://www.faqs.org/rfcs/rfc3548.html .
- * It is important to note that data encoded this way is not officially valid Base64,
+ * It is important to note that data encoded this way is not officially valid Base64,
* or at the very least should not be called Base64 without also specifying that is
* was encoded using the URL- and Filename-safe dialect.
*/
public final static int URL_SAFE = 16;
-
/**
* Encode using the special "ordered" dialect of Base64 described here:
* http://www.faqs.org/qa/rfcc-1940.html .
*/
public final static int ORDERED = 32;
-
-
-/* ******** P R I V A T E F I E L D S ******** */
-
-
+
+/* ******** P R I V A T E F I E L D S ******** */
+
/** Maximum line length (76) of Base64 output. */
private final static int MAX_LINE_LENGTH = 76;
-
-
+
/** The equals sign (=) as a byte. */
private final static byte EQUALS_SIGN = (byte)'=';
-
-
+
/** The new line character (\n) as a byte. */
private final static byte NEW_LINE = (byte)'\n';
-
-
+
/** Preferred encoding. */
private final static String PREFERRED_ENCODING = "US-ASCII";
-
-
+
private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding
private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding
-
-
-/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
-
+
+/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
+
/** The 64 valid Base64 values. */
/* Host platform me be something funny like EBCDIC, so we hardcode these values. */
private final static byte[] _STANDARD_ALPHABET = {
(byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
(byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
- (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
(byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
- (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
(byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
- (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
(byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'+', (byte)'/'
};
-
-
- /**
+
+ /**
* Translates a Base64 value to either its 6-bit reconstruction value
* or a negative number indicating some other meaning.
**/
@@ -268,30 +255,29 @@ public class Base64
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
- -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
};
-
-
+
/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
-
+
/**
- * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548:
+ * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548:
* http://www.faqs.org/rfcs/rfc3548.html .
* Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash."
*/
private final static byte[] _URL_SAFE_ALPHABET = {
(byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F', (byte)'G',
(byte)'H', (byte)'I', (byte)'J', (byte)'K', (byte)'L', (byte)'M', (byte)'N',
- (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
+ (byte)'O', (byte)'P', (byte)'Q', (byte)'R', (byte)'S', (byte)'T', (byte)'U',
(byte)'V', (byte)'W', (byte)'X', (byte)'Y', (byte)'Z',
(byte)'a', (byte)'b', (byte)'c', (byte)'d', (byte)'e', (byte)'f', (byte)'g',
(byte)'h', (byte)'i', (byte)'j', (byte)'k', (byte)'l', (byte)'m', (byte)'n',
- (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
+ (byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
(byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z',
- (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
+ (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5',
(byte)'6', (byte)'7', (byte)'8', (byte)'9', (byte)'-', (byte)'_'
};
-
+
/**
* Used in decoding URL- and Filename-safe dialects of Base64.
*/
@@ -330,11 +316,9 @@ public class Base64
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
- -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
};
-
-
/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
/**
@@ -356,7 +340,7 @@ public class Base64
(byte)'o', (byte)'p', (byte)'q', (byte)'r', (byte)'s', (byte)'t', (byte)'u',
(byte)'v', (byte)'w', (byte)'x', (byte)'y', (byte)'z'
};
-
+
/**
* Used in decoding the "ordered" dialect of Base64.
*/
@@ -395,13 +379,11 @@ public class Base64
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
- -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
+ -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
};
-
/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */
-
/**
* Returns one of the _SOMETHING_ALPHABET byte arrays depending on
* the options specified.
@@ -417,8 +399,7 @@ private static byte[] getAlphabet( int options ) {
} else {
return _STANDARD_ALPHABET;
}
- } // end getAlphabet
-
+ } // end getAlphabet
/**
* Returns one of the _SOMETHING_DECODABET byte arrays depending on
@@ -428,26 +409,20 @@ private static byte[] getAlphabet( int options ) {
* no guarantee as to which one will be picked.
*/
private static byte[] getDecodabet( int options ) {
- if( (options & URL_SAFE) == URL_SAFE) {
+ if ( (options & URL_SAFE) == URL_SAFE) {
return _URL_SAFE_DECODABET;
} else if ((options & ORDERED) == ORDERED) {
return _ORDERED_DECODABET;
} else {
return _STANDARD_DECODABET;
}
- } // end getAlphabet
-
+ } // end getAlphabet
-
/** Defeats instantiation. */
private Base64(){}
-
-
-
-/* ******** E N C O D I N G M E T H O D S ******** */
-
-
+/* ******** E N C O D I N G M E T H O D S ******** */
+
/**
* Encodes up to the first three bytes of array threeBytes
* and returns a four-byte array in Base64 notation.
@@ -468,12 +443,11 @@ private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes,
return b4;
} // end encode3to4
-
/**
* Encodes up to three bytes of the array source
* and writes the resulting four Base64 bytes to destination .
* The source and destination arrays can be manipulated
- * anywhere along their length by specifying
+ * anywhere along their length by specifying
* srcOffset and destOffset .
* This method does not check to make sure your arrays
* are large enough to accomodate srcOffset + 3 for
@@ -481,8 +455,8 @@ private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes,
* the destination array.
* The actual number of significant bytes in your array is
* given by numSigBytes .
- * This is the lowest level of the encoding methods with
- * all possible parameters.
+ * This is the lowest level of the encoding methods with
+ * all possible parameters.
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
@@ -492,19 +466,19 @@ private static byte[] encode3to4( byte[] b4, byte[] threeBytes, int numSigBytes,
* @return the destination array
* @since 1.3
*/
- private static byte[] encode3to4(
+ private static byte[] encode3to4(
byte[] source, int srcOffset, int numSigBytes,
byte[] destination, int destOffset, int options ) {
-
- byte[] ALPHABET = getAlphabet( options );
-
- // 1 2 3
+
+ byte[] ALPHABET = getAlphabet( options );
+
+ // 1 2 3
// 01234567890123456789012345678901 Bit position
// --------000000001111111122222222 Array position from threeBytes
// --------| || || || | Six bit groups to index ALPHABET
// >>18 >>12 >> 6 >> 0 Right shift necessary
// 0x3f 0x3f 0x3f Additional AND
-
+
// Create buffer with zero-padding if there are only one or two
// significant bytes passed in the array.
// We have to shift left 24 in order to flush out the 1's that appear
@@ -513,36 +487,33 @@ private static byte[] encode3to4(
| ( numSigBytes > 1 ? ((source[ srcOffset + 1 ] << 24) >>> 16) : 0 )
| ( numSigBytes > 2 ? ((source[ srcOffset + 2 ] << 24) >>> 24) : 0 );
- switch( numSigBytes )
- {
+ switch( numSigBytes ) {
case 3:
destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
destination[ destOffset + 3 ] = ALPHABET[ (inBuff ) & 0x3f ];
return destination;
-
+
case 2:
destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
destination[ destOffset + 2 ] = ALPHABET[ (inBuff >>> 6) & 0x3f ];
destination[ destOffset + 3 ] = EQUALS_SIGN;
return destination;
-
+
case 1:
destination[ destOffset ] = ALPHABET[ (inBuff >>> 18) ];
destination[ destOffset + 1 ] = ALPHABET[ (inBuff >>> 12) & 0x3f ];
destination[ destOffset + 2 ] = EQUALS_SIGN;
destination[ destOffset + 3 ] = EQUALS_SIGN;
return destination;
-
+
default:
return destination;
} // end switch
} // end encode3to4
-
-
/**
* Performs Base64 encoding on the raw ByteBuffer,
* writing it to the encoded ByteBuffer.
@@ -566,7 +537,6 @@ public static void encode( java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded
} // end input remaining
}
-
/**
* Performs Base64 encoding on the raw ByteBuffer,
* writing it to the encoded CharBuffer.
@@ -592,19 +562,16 @@ public static void encode( java.nio.ByteBuffer raw, java.nio.CharBuffer encoded
} // end input remaining
}
-
-
-
/**
* Serializes an object and returns the Base64-encoded
- * version of that serialized object.
- *
+ * version of that serialized object.
+ *
* As of v 2.3, if the object
* cannot be serialized or there is another error,
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned a null value, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
* The object is not GZip-compressed before being encoded.
*
* @param serializableObject The object to encode
@@ -617,19 +584,17 @@ public static String encodeObject( java.io.Serializable serializableObject )
throws java.io.IOException {
return encodeObject( serializableObject, NO_OPTIONS );
} // end encodeObject
-
-
/**
* Serializes an object and returns the Base64-encoded
* version of that serialized object.
- *
+ *
* As of v 2.3, if the object
* cannot be serialized or there is another error,
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned a null value, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
* The object is not GZip-compressed before being encoded.
*
* Example options:
@@ -652,22 +617,21 @@ public static String encodeObject( java.io.Serializable serializableObject )
public static String encodeObject( java.io.Serializable serializableObject, int options )
throws java.io.IOException {
- if( serializableObject == null ){
+ if ( serializableObject == null ){
throw new NullPointerException( "Cannot serialize a null object." );
} // end if: null
-
+
// Streams
- java.io.ByteArrayOutputStream baos = null;
+ java.io.ByteArrayOutputStream baos = null;
java.io.OutputStream b64os = null;
java.util.zip.GZIPOutputStream gzos = null;
java.io.ObjectOutputStream oos = null;
-
-
+
try {
// ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream
baos = new java.io.ByteArrayOutputStream();
b64os = new Base64.OutputStream( baos, ENCODE | options );
- if( (options & GZIP) != 0 ){
+ if ( (options & GZIP) != 0 ){
// Gzip
gzos = new java.util.zip.GZIPOutputStream(b64os);
oos = new java.io.ObjectOutputStream( gzos );
@@ -677,18 +641,18 @@ public static String encodeObject( java.io.Serializable serializableObject, int
}
oos.writeObject( serializableObject );
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
// Catch it and then throw it immediately so that
// the finally{} block is called for cleanup.
throw e;
} // end catch
finally {
- try{ oos.close(); } catch( Exception e ){}
- try{ gzos.close(); } catch( Exception e ){}
- try{ b64os.close(); } catch( Exception e ){}
- try{ baos.close(); } catch( Exception e ){}
+ try{ oos.close(); } catch ( Exception e ){}
+ try{ gzos.close(); } catch ( Exception e ){}
+ try{ b64os.close(); } catch ( Exception e ){}
+ try{ baos.close(); } catch ( Exception e ){}
} // end finally
-
+
// Return value according to relevant encoding.
try {
return new String( baos.toByteArray(), PREFERRED_ENCODING );
@@ -697,15 +661,13 @@ public static String encodeObject( java.io.Serializable serializableObject, int
// Fall back to some Java default
return new String( baos.toByteArray() );
} // end catch
-
+
} // end encode
-
-
/**
* Encodes a byte array into Base64 notation.
* Does not GZip-compress data.
- *
+ *
* @param source The data to convert
* @return The data in Base64-encoded form
* @throws NullPointerException if source array is null
@@ -719,13 +681,9 @@ public static String encodeBytes( byte[] source ) {
try {
encoded = encodeBytes(source, 0, source.length, NO_OPTIONS);
} catch (java.io.IOException ex) {
- assert false : ex.getMessage();
} // end catch
- assert encoded != null;
return encoded;
} // end encodeBytes
-
-
/**
* Encodes a byte array into Base64 notation.
@@ -740,12 +698,12 @@ public static String encodeBytes( byte[] source ) {
*
* Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )
*
- *
+ *
*
As of v 2.3, if there is an error with the GZIP stream,
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned a null value, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
*
* @param source The data to convert
* @param options Specified options
@@ -759,17 +717,16 @@ public static String encodeBytes( byte[] source ) {
public static String encodeBytes( byte[] source, int options ) throws java.io.IOException {
return encodeBytes( source, 0, source.length, options );
} // end encodeBytes
-
-
+
/**
* Encodes a byte array into Base64 notation.
* Does not GZip-compress data.
- *
+ *
* As of v 2.3, if there is an error,
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned a null value, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
*
* @param source The data to convert
* @param off Offset in array where conversion should begin
@@ -787,13 +744,9 @@ public static String encodeBytes( byte[] source, int off, int len ) {
try {
encoded = encodeBytes( source, off, len, NO_OPTIONS );
} catch (java.io.IOException ex) {
- assert false : ex.getMessage();
} // end catch
- assert encoded != null;
return encoded;
} // end encodeBytes
-
-
/**
* Encodes a byte array into Base64 notation.
@@ -808,12 +761,12 @@ public static String encodeBytes( byte[] source, int off, int len ) {
*
* Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )
*
- *
+ *
*
As of v 2.3, if there is an error with the GZIP stream,
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned a null value, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
*
* @param source The data to convert
* @param off Offset in array where conversion should begin
@@ -837,11 +790,8 @@ public static String encodeBytes( byte[] source, int off, int len, int options )
catch (java.io.UnsupportedEncodingException uue) {
return new String( encoded );
} // end catch
-
- } // end encodeBytes
-
-
+ } // end encodeBytes
/**
* Similar to {@link #encodeBytes(byte[])} but returns
@@ -858,13 +808,11 @@ public static byte[] encodeBytesToBytes( byte[] source ) {
byte[] encoded = null;
try {
encoded = encodeBytesToBytes( source, 0, source.length, Base64.NO_OPTIONS );
- } catch( java.io.IOException ex ) {
- assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
+ } catch ( java.io.IOException ex ) {
}
return encoded;
}
-
/**
* Similar to {@link #encodeBytes(byte[], int, int, int)} but returns
* a byte array instead of instantiating a String. This is more efficient
@@ -885,27 +833,25 @@ public static byte[] encodeBytesToBytes( byte[] source ) {
*/
public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int options ) throws java.io.IOException {
- if( source == null ){
+ if ( source == null ){
throw new NullPointerException( "Cannot serialize a null array." );
} // end if: null
- if( off < 0 ){
+ if ( off < 0 ){
throw new IllegalArgumentException( "Cannot have negative offset: " + off );
} // end if: off < 0
- if( len < 0 ){
+ if ( len < 0 ){
throw new IllegalArgumentException( "Cannot have length offset: " + len );
} // end if: len < 0
- if( off + len > source.length ){
+ if ( off + len > source.length ){
throw new IllegalArgumentException(
String.format( "Cannot have offset of %d and length of %d with array of length %d", off,len,source.length));
} // end if: off < 0
-
-
// Compress?
- if( (options & GZIP) != 0 ) {
+ if ( (options & GZIP) != 0 ) {
java.io.ByteArrayOutputStream baos = null;
java.util.zip.GZIPOutputStream gzos = null;
Base64.OutputStream b64os = null;
@@ -919,15 +865,15 @@ public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int op
gzos.write( source, off, len );
gzos.close();
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
// Catch it and then throw it immediately so that
// the finally{} block is called for cleanup.
throw e;
} // end catch
finally {
- try{ gzos.close(); } catch( Exception e ){}
- try{ b64os.close(); } catch( Exception e ){}
- try{ baos.close(); } catch( Exception e ){}
+ try{ gzos.close(); } catch ( Exception e ){}
+ try{ b64os.close(); } catch ( Exception e ){}
+ try{ baos.close(); } catch ( Exception e ){}
} // end finally
return baos.toByteArray();
@@ -945,12 +891,11 @@ public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int op
// If we get it right, we don't have to do an array copy, and
// we save a bunch of memory.
int encLen = ( len / 3 ) * 4 + ( len % 3 > 0 ? 4 : 0 ); // Bytes needed for actual encoding
- if( breakLines ){
+ if ( breakLines ){
encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters
}
byte[] outBuff = new byte[ encLen ];
-
int d = 0;
int e = 0;
int len2 = len - 2;
@@ -959,22 +904,20 @@ public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int op
encode3to4( source, d+off, 3, outBuff, e, options );
lineLength += 4;
- if( breakLines && lineLength >= MAX_LINE_LENGTH )
- {
+ if ( breakLines && lineLength >= MAX_LINE_LENGTH ) {
outBuff[e+4] = NEW_LINE;
e++;
lineLength = 0;
} // end if: end of line
} // en dfor: each piece of array
- if( d < len ) {
+ if ( d < len ) {
encode3to4( source, d+off, len - d, outBuff, e, options );
e += 4;
} // end if: some padding needed
-
// Only resize array if we didn't guess it right.
- if( e <= outBuff.length - 1 ){
+ if ( e <= outBuff.length - 1 ){
// If breaking lines and the last byte falls right at
// the line length (76 bytes per line), there will be
// one extra byte, and the array will need to be resized.
@@ -987,83 +930,77 @@ public static byte[] encodeBytesToBytes( byte[] source, int off, int len, int op
//System.err.println("No need to resize array.");
return outBuff;
}
-
+
} // end else: don't compress
} // end encodeBytesToBytes
-
-
-
-
/* ******** D E C O D I N G M E T H O D S ******** */
-
-
+
/**
* Decodes four bytes from array source
* and writes the resulting bytes (up to three of them)
* to destination .
* The source and destination arrays can be manipulated
- * anywhere along their length by specifying
+ * anywhere along their length by specifying
* srcOffset and destOffset .
* This method does not check to make sure your arrays
* are large enough to accomodate srcOffset + 4 for
* the source array or destOffset + 3 for
* the destination array.
- * This method returns the actual number of bytes that
+ * This method returns the actual number of bytes that
* were converted from the Base64 encoding.
- * This is the lowest level of the decoding methods with
- * all possible parameters.
- *
+ * This is the lowest level of the decoding methods with
+ * all possible parameters.
+ *
*
* @param source the array to convert
* @param srcOffset the index where conversion begins
* @param destination the array to hold the conversion
* @param destOffset the index where output will be put
- * @param options alphabet type is pulled from this (standard, url-safe, ordered)
+ * @param options alphabet type is pulled from this (standard, url-safe, ordered)
* @return the number of decoded bytes converted
* @throws NullPointerException if source or destination arrays are null
* @throws IllegalArgumentException if srcOffset or destOffset are invalid
* or there is not enough room in the array.
* @since 1.3
*/
- private static int decode4to3(
- byte[] source, int srcOffset,
+ private static int decode4to3(
+ byte[] source, int srcOffset,
byte[] destination, int destOffset, int options ) {
-
+
// Lots of error checking and exception throwing
- if( source == null ){
+ if ( source == null ){
throw new NullPointerException( "Source array was null." );
} // end if
- if( destination == null ){
+ if ( destination == null ){
throw new NullPointerException( "Destination array was null." );
} // end if
- if( srcOffset < 0 || srcOffset + 3 >= source.length ){
+ if ( srcOffset < 0 || srcOffset + 3 >= source.length ){
throw new IllegalArgumentException( String.format(
"Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset ) );
} // end if
- if( destOffset < 0 || destOffset +2 >= destination.length ){
+ if ( destOffset < 0 || destOffset +2 >= destination.length ){
throw new IllegalArgumentException( String.format(
"Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset ) );
} // end if
-
-
- byte[] DECODABET = getDecodabet( options );
-
+
+ byte[] DECODABET = getDecodabet( options );
+
// Example: Dk==
- if( source[ srcOffset + 2] == EQUALS_SIGN ) {
+ if ( source[ srcOffset + 2] == EQUALS_SIGN ) {
// Two ways to do the same thing. Don't know which way I like best.
//int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 );
int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
| ( ( DECODABET[ source[ srcOffset + 1] ] & 0xFF ) << 12 );
-
+
destination[ destOffset ] = (byte)( outBuff >>> 16 );
return 1;
}
-
+
// Example: DkL=
- else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
+ else if ( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
// Two ways to do the same thing. Don't know which way I like best.
//int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 )
// | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 )
@@ -1071,12 +1008,12 @@ else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
int outBuff = ( ( DECODABET[ source[ srcOffset ] ] & 0xFF ) << 18 )
| ( ( DECODABET[ source[ srcOffset + 1 ] ] & 0xFF ) << 12 )
| ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6 );
-
+
destination[ destOffset ] = (byte)( outBuff >>> 16 );
destination[ destOffset + 1 ] = (byte)( outBuff >>> 8 );
return 2;
}
-
+
// Example: DkLE
else {
// Two ways to do the same thing. Don't know which way I like best.
@@ -1089,7 +1026,6 @@ else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
| ( ( DECODABET[ source[ srcOffset + 2 ] ] & 0xFF ) << 6)
| ( ( DECODABET[ source[ srcOffset + 3 ] ] & 0xFF ) );
-
destination[ destOffset ] = (byte)( outBuff >> 16 );
destination[ destOffset + 1 ] = (byte)( outBuff >> 8 );
destination[ destOffset + 2 ] = (byte)( outBuff );
@@ -1097,10 +1033,6 @@ else if( source[ srcOffset + 3 ] == EQUALS_SIGN ) {
return 3;
}
} // end decodeToBytes
-
-
-
-
/**
* Low-level access to decoding ASCII characters in
@@ -1120,14 +1052,12 @@ public static byte[] decode( byte[] source )
byte[] decoded = null;
// try {
decoded = decode( source, 0, source.length, Base64.NO_OPTIONS );
-// } catch( java.io.IOException ex ) {
+// } catch ( java.io.IOException ex ) {
// assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
// }
return decoded;
}
-
-
/**
* Low-level access to decoding ASCII characters in
* the form of a byte array. Ignores GUNZIP option, if
@@ -1147,50 +1077,50 @@ public static byte[] decode( byte[] source )
*/
public static byte[] decode( byte[] source, int off, int len, int options )
throws java.io.IOException {
-
+
// Lots of error checking and exception throwing
- if( source == null ){
+ if ( source == null ){
throw new NullPointerException( "Cannot decode null source array." );
} // end if
- if( off < 0 || off + len > source.length ){
+ if ( off < 0 || off + len > source.length ){
throw new IllegalArgumentException( String.format(
"Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len ) );
} // end if
-
- if( len == 0 ){
+
+ if ( len == 0 ){
return new byte[0];
- }else if( len < 4 ){
- throw new IllegalArgumentException(
+ }else if ( len < 4 ){
+ throw new IllegalArgumentException(
"Base64-encoded string must have at least four characters, but length specified was " + len );
} // end if
-
+
byte[] DECODABET = getDecodabet( options );
-
+
int len34 = len * 3 / 4; // Estimate on array size
byte[] outBuff = new byte[ len34 ]; // Upper limit on size of output
int outBuffPosn = 0; // Keep track of where we're writing
-
+
byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space
int b4Posn = 0; // Keep track of four byte input buffer
int i = 0; // Source array counter
byte sbiDecode = 0; // Special value from DECODABET
-
+
for( i = off; i < off+len; i++ ) { // Loop through source
-
+
sbiDecode = DECODABET[ source[i]&0xFF ];
-
+
// White space, Equals sign, or legit Base64 character
// Note the values such as -5 and -9 in the
// DECODABETs at the top of the file.
- if( sbiDecode >= WHITE_SPACE_ENC ) {
- if( sbiDecode >= EQUALS_SIGN_ENC ) {
+ if ( sbiDecode >= WHITE_SPACE_ENC ) {
+ if ( sbiDecode >= EQUALS_SIGN_ENC ) {
b4[ b4Posn++ ] = source[i]; // Save non-whitespace
- if( b4Posn > 3 ) { // Time to decode?
+ if ( b4Posn > 3 ) { // Time to decode?
outBuffPosn += decode4to3( b4, 0, outBuff, outBuffPosn, options );
b4Posn = 0;
-
+
// If that was the equals sign, break out of 'for' loop
- if( source[i] == EQUALS_SIGN ) {
+ if ( source[i] == EQUALS_SIGN ) {
break;
} // end if: equals sign
} // end if: quartet built
@@ -1200,17 +1130,14 @@ public static byte[] decode( byte[] source, int off, int len, int options )
// There's a bad input character in the Base64 stream.
throw new java.io.IOException( String.format(
"Bad Base64 input character decimal %d in array position %d", ((int)source[i])&0xFF, i ) );
- } // end else:
+ } // end else:
} // each input character
-
+
byte[] out = new byte[ outBuffPosn ];
- System.arraycopy( outBuff, 0, out, 0, outBuffPosn );
+ System.arraycopy( outBuff, 0, out, 0, outBuffPosn );
return out;
} // end decode
-
-
-
-
+
/**
* Decodes data from Base64 notation, automatically
* detecting gzip-compressed data and decompressing it.
@@ -1224,8 +1151,6 @@ public static byte[] decode( String s ) throws java.io.IOException {
return decode( s, NO_OPTIONS );
}
-
-
/**
* Decodes data from Base64 notation, automatically
* detecting gzip-compressed data and decompressing it.
@@ -1238,30 +1163,30 @@ public static byte[] decode( String s ) throws java.io.IOException {
* @since 1.4
*/
public static byte[] decode( String s, int options ) throws java.io.IOException {
-
- if( s == null ){
+
+ if ( s == null ){
throw new NullPointerException( "Input string was null." );
} // end if
-
+
byte[] bytes;
try {
bytes = s.getBytes( PREFERRED_ENCODING );
} // end try
- catch( java.io.UnsupportedEncodingException uee ) {
+ catch ( java.io.UnsupportedEncodingException uee ) {
bytes = s.getBytes();
} // end catch
- //
-
+ //
+
// Decode
bytes = decode( bytes, 0, bytes.length, options );
-
+
// Check to see if it's gzip-compressed
// GZIP Magic Two-Byte Number: 0x8b1f (35615)
boolean dontGunzip = (options & DONT_GUNZIP) != 0;
- if( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) {
-
+ if ( (bytes != null) && (bytes.length >= 4) && (!dontGunzip) ) {
+
int head = ((int)bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00);
- if( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) {
+ if ( java.util.zip.GZIPInputStream.GZIP_MAGIC == head ) {
java.io.ByteArrayInputStream bais = null;
java.util.zip.GZIPInputStream gzis = null;
java.io.ByteArrayOutputStream baos = null;
@@ -1281,24 +1206,22 @@ public static byte[] decode( String s, int options ) throws java.io.IOException
bytes = baos.toByteArray();
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
e.printStackTrace();
// Just return originally-decoded bytes
} // end catch
finally {
- try{ baos.close(); } catch( Exception e ){}
- try{ gzis.close(); } catch( Exception e ){}
- try{ bais.close(); } catch( Exception e ){}
+ try{ baos.close(); } catch ( Exception e ){}
+ try{ gzis.close(); } catch ( Exception e ){}
+ try{ bais.close(); } catch ( Exception e ){}
} // end finally
} // end if: gzipped
} // end if: bytes.length >= 2
-
+
return bytes;
} // end decode
-
-
/**
* Attempts to decode Base64 data and deserialize a Java
* Object within. Returns null if there was an error.
@@ -1315,7 +1238,6 @@ public static Object decodeToObject( String encodedObject )
throws java.io.IOException, java.lang.ClassNotFoundException {
return decodeToObject(encodedObject,NO_OPTIONS,null);
}
-
/**
* Attempts to decode Base64 data and deserialize a Java
@@ -1329,26 +1251,26 @@ public static Object decodeToObject( String encodedObject )
* @return The decoded and deserialized object
* @throws NullPointerException if encodedObject is null
* @throws java.io.IOException if there is a general error
- * @throws ClassNotFoundException if the decoded object is of a
+ * @throws ClassNotFoundException if the decoded object is of a
* class that cannot be found by the JVM
* @since 2.3.4
*/
- public static Object decodeToObject(
+ public static Object decodeToObject(
String encodedObject, int options, final ClassLoader loader )
throws java.io.IOException, java.lang.ClassNotFoundException {
-
+
// Decode and gunzip if necessary
byte[] objBytes = decode( encodedObject, options );
-
+
java.io.ByteArrayInputStream bais = null;
java.io.ObjectInputStream ois = null;
Object obj = null;
-
+
try {
bais = new java.io.ByteArrayInputStream( objBytes );
// If no custom class loader is provided, use Java's builtin OIS.
- if( loader == null ){
+ if ( loader == null ){
ois = new java.io.ObjectInputStream( bais );
} // end if: no loader provided
@@ -1360,7 +1282,7 @@ public static Object decodeToObject(
public Class> resolveClass(java.io.ObjectStreamClass streamClass)
throws java.io.IOException, ClassNotFoundException {
Class> c = Class.forName(streamClass.getName(), false, loader);
- if( c == null ){
+ if ( c == null ){
return super.resolveClass(streamClass);
} else {
return c; // Class loader knows of this class.
@@ -1368,23 +1290,21 @@ public Class> resolveClass(java.io.ObjectStreamClass streamClass)
} // end resolveClass
}; // end ois
} // end else: no custom class loader
-
+
obj = ois.readObject();
} // end try
- catch( java.io.IOException | ClassNotFoundException e ) {
+ catch ( java.io.IOException | ClassNotFoundException e ) {
throw e; // Catch and throw in order to execute finally{}
} // end catch
// end catch
finally {
- try{ bais.close(); } catch( Exception e ){}
- try{ ois.close(); } catch( Exception e ){}
+ try{ bais.close(); } catch ( Exception e ){}
+ try{ ois.close(); } catch ( Exception e ){}
} // end finally
-
+
return obj;
} // end decodeObject
-
-
-
+
/**
* Convenience method for encoding data to a file.
*
@@ -1392,7 +1312,7 @@ public Class> resolveClass(java.io.ObjectStreamClass streamClass)
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned false, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
* @param dataToEncode byte array of data to encode in base64 form
* @param filename Filename for saving encoded data
* @throws java.io.IOException if there is an error
@@ -1401,27 +1321,26 @@ public Class> resolveClass(java.io.ObjectStreamClass streamClass)
*/
public static void encodeToFile( byte[] dataToEncode, String filename )
throws java.io.IOException {
-
- if( dataToEncode == null ){
+
+ if ( dataToEncode == null ){
throw new NullPointerException( "Data to encode was null." );
} // end iff
-
+
Base64.OutputStream bos = null;
try {
- bos = new Base64.OutputStream(
+ bos = new Base64.OutputStream(
new java.io.FileOutputStream( filename ), Base64.ENCODE );
bos.write( dataToEncode );
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
throw e; // Catch and throw to execute finally{} block
} // end catch: java.io.IOException
finally {
- try{ bos.close(); } catch( Exception e ){}
+ try{ bos.close(); } catch ( Exception e ){}
} // end finally
-
+
} // end encodeToFile
-
-
+
/**
* Convenience method for decoding data to a file.
*
@@ -1429,7 +1348,7 @@ public static void encodeToFile( byte[] dataToEncode, String filename )
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned false, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
* @param dataToDecode Base64-encoded data as a string
* @param filename Filename for saving decoded data
* @throws java.io.IOException if there is an error
@@ -1437,25 +1356,22 @@ public static void encodeToFile( byte[] dataToEncode, String filename )
*/
public static void decodeToFile( String dataToDecode, String filename )
throws java.io.IOException {
-
+
Base64.OutputStream bos = null;
try{
- bos = new Base64.OutputStream(
+ bos = new Base64.OutputStream(
new java.io.FileOutputStream( filename ), Base64.DECODE );
bos.write( dataToDecode.getBytes( PREFERRED_ENCODING ) );
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
throw e; // Catch and throw to execute finally{} block
} // end catch: java.io.IOException
finally {
- try{ bos.close(); } catch( Exception e ){}
+ try{ bos.close(); } catch ( Exception e ){}
} // end finally
-
+
} // end decodeToFile
-
-
-
-
+
/**
* Convenience method for reading a base64-encoded
* file and decoding it.
@@ -1464,7 +1380,7 @@ public static void decodeToFile( String dataToDecode, String filename )
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned false, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
* @param filename Filename for reading encoded data
* @return decoded byte array
* @throws java.io.IOException if there is an error
@@ -1472,51 +1388,47 @@ public static void decodeToFile( String dataToDecode, String filename )
*/
public static byte[] decodeFromFile( String filename )
throws java.io.IOException {
-
+
byte[] decodedData = null;
Base64.InputStream bis = null;
- try
- {
+ try {
// Set up some useful variables
java.io.File file = new java.io.File( filename );
byte[] buffer = null;
int length = 0;
int numBytes = 0;
-
+
// Check for size of file
- if( file.length() > Integer.MAX_VALUE )
- {
+ if ( file.length() > Integer.MAX_VALUE ) {
throw new java.io.IOException( "File is too big for this convenience method (" + file.length() + " bytes)." );
} // end if: file too big for int index
buffer = new byte[ (int)file.length() ];
-
+
// Open a stream
- bis = new Base64.InputStream(
- new java.io.BufferedInputStream(
+ bis = new Base64.InputStream(
+ new java.io.BufferedInputStream(
new java.io.FileInputStream( file ) ), Base64.DECODE );
-
+
// Read until done
while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) {
length += numBytes;
} // end while
-
+
// Save in a variable to return
decodedData = new byte[ length ];
System.arraycopy( buffer, 0, decodedData, 0, length );
-
+
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
throw e; // Catch and release to execute finally{}
} // end catch: java.io.IOException
finally {
- try{ bis.close(); } catch( Exception e) {}
+ try{ bis.close(); } catch ( Exception e) {}
} // end finally
-
+
return decodedData;
} // end decodeFromFile
-
-
-
+
/**
* Convenience method for reading a binary file
* and base64-encoding it.
@@ -1525,7 +1437,7 @@ public static byte[] decodeFromFile( String filename )
* the method will throw an java.io.IOException. This is new to v2.3!
* In earlier versions, it just returned false, but
* in retrospect that's a pretty poor way to handle it.
- *
+ *
* @param filename Filename for reading binary data
* @return base64-encoded string
* @throws java.io.IOException if there is an error
@@ -1533,41 +1445,40 @@ public static byte[] decodeFromFile( String filename )
*/
public static String encodeFromFile( String filename )
throws java.io.IOException {
-
+
String encodedData = null;
Base64.InputStream bis = null;
- try
- {
+ try {
// Set up some useful variables
java.io.File file = new java.io.File( filename );
byte[] buffer = new byte[ Math.max((int)(file.length() * 1.4+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5)
int length = 0;
int numBytes = 0;
-
+
// Open a stream
- bis = new Base64.InputStream(
- new java.io.BufferedInputStream(
+ bis = new Base64.InputStream(
+ new java.io.BufferedInputStream(
new java.io.FileInputStream( file ) ), Base64.ENCODE );
-
+
// Read until done
while( ( numBytes = bis.read( buffer, length, 4096 ) ) >= 0 ) {
length += numBytes;
} // end while
-
+
// Save in a variable to return
encodedData = new String( buffer, 0, length, Base64.PREFERRED_ENCODING );
-
+
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
throw e; // Catch and release to execute finally{}
} // end catch: java.io.IOException
finally {
- try{ bis.close(); } catch( Exception e) {}
+ try{ bis.close(); } catch ( Exception e) {}
} // end finally
-
+
return encodedData;
} // end encodeFromFile
-
+
/**
* Reads infile and encodes it to outfile .
*
@@ -1578,7 +1489,7 @@ public static String encodeFromFile( String filename )
*/
public static void encodeFileToFile( String infile, String outfile )
throws java.io.IOException {
-
+
String encoded = Base64.encodeFromFile( infile );
java.io.OutputStream out = null;
try{
@@ -1586,16 +1497,15 @@ public static void encodeFileToFile( String infile, String outfile )
new java.io.FileOutputStream( outfile ) );
out.write( encoded.getBytes("US-ASCII") ); // Strict, 7-bit output.
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
throw e; // Catch and release to execute finally{}
} // end catch
finally {
try { out.close(); }
- catch( Exception ex ){}
- } // end finally
+ catch ( Exception ex ){}
+ } // end finally
} // end encodeFileToFile
-
/**
* Reads infile and decodes it to outfile .
*
@@ -1606,7 +1516,7 @@ public static void encodeFileToFile( String infile, String outfile )
*/
public static void decodeFileToFile( String infile, String outfile )
throws java.io.IOException {
-
+
byte[] decoded = Base64.decodeFromFile( infile );
java.io.OutputStream out = null;
try{
@@ -1614,20 +1524,17 @@ public static void decodeFileToFile( String infile, String outfile )
new java.io.FileOutputStream( outfile ) );
out.write( decoded );
} // end try
- catch( java.io.IOException e ) {
+ catch ( java.io.IOException e ) {
throw e; // Catch and release to execute finally{}
} // end catch
finally {
try { out.close(); }
- catch( Exception ex ){}
- } // end finally
+ catch ( Exception ex ){}
+ } // end finally
} // end decodeFileToFile
-
-
+
/* ******** I N N E R C L A S S I N P U T S T R E A M ******** */
-
-
-
+
/**
* A {@link Base64.InputStream} will read data from another
* java.io.InputStream , given in the constructor,
@@ -1637,7 +1544,7 @@ public static void decodeFileToFile( String infile, String outfile )
* @since 1.3
*/
public static class InputStream extends java.io.FilterInputStream {
-
+
private boolean encode; // Encoding or decoding
private int position; // Current position in the buffer
private byte[] buffer; // Small buffer holding converted data
@@ -1647,8 +1554,7 @@ public static class InputStream extends java.io.FilterInputStream {
private boolean breakLines; // Break lines at less than 80 characters
private int options; // Record options used to create the stream.
private byte[] decodabet; // Local copies to avoid extra method calls
-
-
+
/**
* Constructs a {@link Base64.InputStream} in DECODE mode.
*
@@ -1658,8 +1564,7 @@ public static class InputStream extends java.io.FilterInputStream {
public InputStream( java.io.InputStream in ) {
this( in, DECODE );
} // end constructor
-
-
+
/**
* Constructs a {@link Base64.InputStream} in
* either ENCODE or DECODE mode.
@@ -1681,7 +1586,7 @@ public InputStream( java.io.InputStream in ) {
* @since 2.0
*/
public InputStream( java.io.InputStream in, int options ) {
-
+
super( in );
this.options = options; // Record for later
this.breakLines = (options & DO_BREAK_LINES) > 0;
@@ -1692,7 +1597,7 @@ public InputStream( java.io.InputStream in, int options ) {
this.lineLength = 0;
this.decodabet = getDecodabet(options);
} // end constructor
-
+
/**
* Reads enough of the input stream to convert
* to/from Base64 and returns the next byte.
@@ -1702,26 +1607,26 @@ public InputStream( java.io.InputStream in, int options ) {
*/
@Override
public int read() throws java.io.IOException {
-
+
// Do we need to get data?
- if( position < 0 ) {
- if( encode ) {
+ if ( position < 0 ) {
+ if ( encode ) {
byte[] b3 = new byte[3];
int numBinaryBytes = 0;
for( int i = 0; i < 3; i++ ) {
int b = in.read();
// If end of stream, b is -1.
- if( b >= 0 ) {
+ if ( b >= 0 ) {
b3[i] = (byte)b;
numBinaryBytes++;
} else {
break; // out of for loop
} // end else: end of stream
-
+
} // end for: each needed input byte
-
- if( numBinaryBytes > 0 ) {
+
+ if ( numBinaryBytes > 0 ) {
encode3to4( b3, 0, numBinaryBytes, buffer, 0, options );
position = 0;
numSigBytes = 4;
@@ -1730,7 +1635,7 @@ public int read() throws java.io.IOException {
return -1; // Must be end of stream
} // end else
} // end if: encoding
-
+
// Else decoding
else {
byte[] b4 = new byte[4];
@@ -1740,37 +1645,37 @@ public int read() throws java.io.IOException {
int b = 0;
do{ b = in.read(); }
while( b >= 0 && decodabet[ b & 0x7f ] <= WHITE_SPACE_ENC );
-
- if( b < 0 ) {
+
+ if ( b < 0 ) {
break; // Reads a -1 if end of stream
} // end if: end of stream
-
+
b4[i] = (byte)b;
} // end for: each needed input byte
-
- if( i == 4 ) {
+
+ if ( i == 4 ) {
numSigBytes = decode4to3( b4, 0, buffer, 0, options );
position = 0;
} // end if: got four characters
- else if( i == 0 ){
+ else if ( i == 0 ){
return -1;
} // end else if: also padded correctly
else {
// Must have broken out from above.
throw new java.io.IOException( "Improperly padded Base64 input." );
- } // end
-
+ } // end
+
} // end else: decode
} // end else: get data
-
+
// Got data?
- if( position >= 0 ) {
+ if ( position >= 0 ) {
// End of relevant data?
- if( /*!encode &&*/ position >= numSigBytes ){
+ if ( /*!encode &&*/ position >= numSigBytes ){
return -1;
} // end if: got data
-
- if( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) {
+
+ if ( encode && breakLines && lineLength >= MAX_LINE_LENGTH ) {
lineLength = 0;
return '\n';
} // end if
@@ -1778,10 +1683,10 @@ else if( i == 0 ){
lineLength++; // This isn't important when decoding
// but throwing an extra "if" seems
// just as wasteful.
-
+
int b = buffer[ position++ ];
- if( position >= bufferLength ) {
+ if ( position >= bufferLength ) {
position = -1;
} // end if: end
@@ -1789,14 +1694,13 @@ else if( i == 0 ){
// intended to be unsigned.
} // end else
} // end if: position >= 0
-
+
// Else error
else {
throw new java.io.IOException( "Error in Base64 code reading stream." );
} // end else
} // end read
-
-
+
/**
* Calls {@link #read()} repeatedly until the end of stream
* is reached or len bytes are read.
@@ -1810,37 +1714,30 @@ else if( i == 0 ){
* @since 1.3
*/
@Override
- public int read( byte[] dest, int off, int len )
+ public int read( byte[] dest, int off, int len )
throws java.io.IOException {
int i;
int b;
for( i = 0; i < len; i++ ) {
b = read();
-
- if( b >= 0 ) {
+
+ if ( b >= 0 ) {
dest[off + i] = (byte) b;
}
- else if( i == 0 ) {
+ else if ( i == 0 ) {
return -1;
}
else {
break; // Out of 'for' loop
- } // Out of 'for' loop
+ }
} // end for: each byte read
return i;
} // end read
-
+
} // end inner class InputStream
-
-
-
-
-
-
+
/* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */
-
-
-
+
/**
* A {@link Base64.OutputStream} will write data to another
* java.io.OutputStream , given in the constructor,
@@ -1850,7 +1747,7 @@ else if( i == 0 ) {
* @since 1.3
*/
public static class OutputStream extends java.io.FilterOutputStream {
-
+
private boolean encode;
private int position;
private byte[] buffer;
@@ -1861,7 +1758,7 @@ public static class OutputStream extends java.io.FilterOutputStream {
private boolean suspendEncoding;
private int options; // Record for later
private byte[] decodabet; // Local copies to avoid extra method calls
-
+
/**
* Constructs a {@link Base64.OutputStream} in ENCODE mode.
*
@@ -1871,8 +1768,7 @@ public static class OutputStream extends java.io.FilterOutputStream {
public OutputStream( java.io.OutputStream out ) {
this( out, ENCODE );
} // end constructor
-
-
+
/**
* Constructs a {@link Base64.OutputStream} in
* either ENCODE or DECODE mode.
@@ -1905,8 +1801,7 @@ public OutputStream( java.io.OutputStream out, int options ) {
this.options = options;
this.decodabet = getDecodabet(options);
} // end constructor
-
-
+
/**
* Writes the byte to the output stream after
* converting to/from Base64 notation.
@@ -1920,23 +1815,23 @@ public OutputStream( java.io.OutputStream out, int options ) {
* @since 1.3
*/
@Override
- public void write(int theByte)
+ public void write(int theByte)
throws java.io.IOException {
// Encoding suspended?
- if( suspendEncoding ) {
+ if ( suspendEncoding ) {
this.out.write( theByte );
return;
} // end if: supsended
-
+
// Encode?
- if( encode ) {
+ if ( encode ) {
buffer[ position++ ] = (byte)theByte;
- if( position >= bufferLength ) { // Enough to encode.
-
+ if ( position >= bufferLength ) { // Enough to encode.
+
this.out.write( encode3to4( b4, buffer, bufferLength, options ) );
lineLength += 4;
- if( breakLines && lineLength >= MAX_LINE_LENGTH ) {
+ if ( breakLines && lineLength >= MAX_LINE_LENGTH ) {
this.out.write( NEW_LINE );
lineLength = 0;
} // end if: end of line
@@ -1948,25 +1843,23 @@ public void write(int theByte)
// Else, Decoding
else {
// Meaningful Base64 character?
- if( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) {
+ if ( decodabet[ theByte & 0x7f ] > WHITE_SPACE_ENC ) {
buffer[ position++ ] = (byte)theByte;
- if( position >= bufferLength ) { // Enough to output.
-
+ if ( position >= bufferLength ) { // Enough to output.
+
int len = Base64.decode4to3( buffer, 0, b4, 0, options );
out.write( b4, 0, len );
position = 0;
} // end if: enough to output
} // end if: meaningful base64 character
- else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) {
+ else if ( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) {
throw new java.io.IOException( "Invalid character in Base64 data." );
} // end else: not white space either
} // end else: decoding
} // end write
-
-
-
+
/**
- * Calls {@link #write(int)} repeatedly until len
+ * Calls {@link #write(int)} repeatedly until len
* bytes are written.
*
* @param theBytes array from which to read bytes
@@ -1975,30 +1868,28 @@ else if( decodabet[ theByte & 0x7f ] != WHITE_SPACE_ENC ) {
* @since 1.3
*/
@Override
- public void write( byte[] theBytes, int off, int len )
+ public void write( byte[] theBytes, int off, int len )
throws java.io.IOException {
// Encoding suspended?
- if( suspendEncoding ) {
+ if ( suspendEncoding ) {
this.out.write( theBytes, off, len );
return;
} // end if: supsended
-
+
for( int i = 0; i < len; i++ ) {
write( theBytes[ off + i ] );
} // end for: each byte written
-
+
} // end write
-
-
-
+
/**
* Method added by PHIL. [Thanks, PHIL. -Rob]
* This pads the buffer without closing the stream.
* @throws java.io.IOException if there's an error.
*/
public void flushBase64() throws java.io.IOException {
- if( position > 0 ) {
- if( encode ) {
+ if ( position > 0 ) {
+ if ( encode ) {
out.write( encode3to4( b4, buffer, position, options ) );
position = 0;
} // end if: encoding
@@ -2009,9 +1900,8 @@ public void flushBase64() throws java.io.IOException {
} // end flush
-
- /**
- * Flushes and closes (I think, in the superclass) the stream.
+ /**
+ * Flushes and closes (I think, in the superclass) the stream.
*
* @since 1.3
*/
@@ -2023,13 +1913,11 @@ public void close() throws java.io.IOException {
// 2. Actually close the stream
// Base class both flushes and closes.
super.close();
-
+
buffer = null;
out = null;
} // end close
-
-
-
+
/**
* Suspends encoding of the stream.
* May be helpful if you need to embed a piece of
@@ -2042,8 +1930,7 @@ public void suspendEncoding() throws java.io.IOException {
flushBase64();
this.suspendEncoding = true;
} // end suspendEncoding
-
-
+
/**
* Resumes encoding of the stream.
* May be helpful if you need to embed a piece of
@@ -2054,10 +1941,7 @@ public void suspendEncoding() throws java.io.IOException {
public void resumeEncoding() {
this.suspendEncoding = false;
} // end resumeEncoding
-
-
-
+
} // end inner class OutputStream
-
-
+
} // end class Base64
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Bitmaps.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Bitmaps.java
index 3cc29c2a7..d92d09280 100644
--- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Bitmaps.java
+++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Bitmaps.java
@@ -1,136 +1,128 @@
package net.cyclestreets.util;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
+import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
+import android.net.Uri;
-public class Bitmaps
+public class Bitmaps
{
- static private BitmapFactory.Options decodeOptions()
- {
- final BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
- decodeOptions.inPurgeable = true;
- decodeOptions.inSampleSize = 4;
- return decodeOptions;
- } // decodeOptions
-
- static public Bitmap loadFile(final String fileName)
- {
- return BitmapFactory.decodeFile(fileName, decodeOptions());
- } // loadFile
-
- static public Bitmap loadStream(final InputStream stream)
- {
- Bitmap bm = null;
- try {
- // return BitmapFactory.decodeStream(inputStream);
- // Bug on slow connections, fixed in future release.
- bm = BitmapFactory.decodeStream(new FlushedInputStream(stream));
- } // try
- catch(Exception e) {
- // no matter
- } // catch
- finally {
- try {
- stream.close();
- } // try
- catch(IOException e) {
- // ah, well
- } // catch
- } // finally
- return bm;
- } // loadStream
-
- static public String resizePhoto(final String fileName)
- {
+ private static BitmapFactory.Options decodeOptions() {
+ final BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
+ decodeOptions.inSampleSize = 4;
+ return decodeOptions;
+ }
+
+ public static Bitmap LoadUri(final Context context, final Uri uri) throws FileNotFoundException {
+ final InputStream str = context.getContentResolver().openInputStream(uri);
+ return loadStream(str);
+ }
+
+ public static Bitmap loadStream(final InputStream stream) {
+ Bitmap bm = null;
+ try {
+ // return BitmapFactory.decodeStream(inputStream);
+ // Bug on slow connections, fixed in future release.
+ bm = BitmapFactory.decodeStream(new FlushedInputStream(stream));
+ }
+ catch (Exception e) {
+ // no matter
+ }
+ finally {
+ try {
+ stream.close();
+ }
+ catch (IOException e) {
+ // ah, well
+ }
+ }
+ return bm;
+ }
+
+ public static String resizePhoto(final String fileName) {
if (fileName == null)
return null;
-
- final BitmapFactory.Options options = bitmapBounds(fileName);
-
- int srcWidth = options.outWidth;
-
- final int desiredWidth = Math.min(640, srcWidth);
-
- // Calculate the correct inSampleSize/scale value. This helps
- // reduce memory use. It should be a power of 2
- // from: http://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/823966#823966
- int inSampleSize = 1;
- while(srcWidth / 2 > desiredWidth) {
- srcWidth /= 2;
- inSampleSize *= 2;
- } // while
-
- float desiredScale = (float)desiredWidth/srcWidth;
-
- // Decode with inSampleSize
- options.inJustDecodeBounds = false;
- options.inDither = false;
- options.inSampleSize = inSampleSize;
- options.inScaled = false;
- options.inPreferredConfig = Bitmap.Config.ARGB_8888;
-
- Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(fileName, options);
-
- // Resize
- final Matrix matrix = new Matrix();
- matrix.postScale(desiredScale, desiredScale);
- Bitmap scaledBitmap = Bitmap.createBitmap(sampledSrcBitmap, 0, 0, sampledSrcBitmap.getWidth(), sampledSrcBitmap.getHeight(), matrix, true);
- sampledSrcBitmap.recycle();
- sampledSrcBitmap = null;
-
- // Save
- try {
- final String smallFileName = fileName + "-small";
- final FileOutputStream out = new FileOutputStream(smallFileName);
- scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
- scaledBitmap.recycle();
- scaledBitmap = null;
- return smallFileName;
- } // try
- catch(IOException e) {
- return null;
- } // catch
- } // resizePhoto
-
- static private BitmapFactory.Options bitmapBounds(final String fileName)
- {
- final BitmapFactory.Options o = new BitmapFactory.Options();
- o.inJustDecodeBounds = true;
- BitmapFactory.decodeFile(fileName, o);
- return o;
- } // bitmapBounds
-
- static private class FlushedInputStream extends FilterInputStream
- {
- public FlushedInputStream(final InputStream inputStream)
- {
- super(inputStream);
- } // FlushedInputStream
-
- @Override
- public long skip(long n) throws IOException
- {
- long totalBytesSkipped = 0L;
- while (totalBytesSkipped < n)
- {
- long bytesSkipped = in.skip(n - totalBytesSkipped);
- if (bytesSkipped == 0L)
- {
- int b = read();
- if (b < 0)
- break; // we reached EOF
- else
- bytesSkipped = 1; // we read one byte
- } // if ...
- totalBytesSkipped += bytesSkipped;
- } // while ...
- return totalBytesSkipped;
- } // skip
- } // FlushedInputStream
-} // class Bitmaps
+
+ final BitmapFactory.Options options = bitmapBounds(fileName);
+
+ int srcWidth = options.outWidth;
+
+ final int desiredWidth = Math.min(640, srcWidth);
+
+ // Calculate the correct inSampleSize/scale value. This helps
+ // reduce memory use. It should be a power of 2
+ // from: http://stackoverflow.com/questions/477572/android-strange-out-of-memory-issue/823966#823966
+ int inSampleSize = 1;
+ while (srcWidth / 2 > desiredWidth) {
+ srcWidth /= 2;
+ inSampleSize *= 2;
+ }
+
+ float desiredScale = (float)desiredWidth/srcWidth;
+
+ // Decode with inSampleSize
+ options.inJustDecodeBounds = false;
+ options.inSampleSize = inSampleSize;
+ options.inScaled = false;
+ options.inPreferredConfig = Bitmap.Config.ARGB_8888;
+
+ Bitmap sampledSrcBitmap = BitmapFactory.decodeFile(fileName, options);
+
+ // Resize
+ final Matrix matrix = new Matrix();
+ matrix.postScale(desiredScale, desiredScale);
+ Bitmap scaledBitmap = Bitmap.createBitmap(sampledSrcBitmap, 0, 0, sampledSrcBitmap.getWidth(), sampledSrcBitmap.getHeight(), matrix, true);
+ sampledSrcBitmap.recycle();
+ sampledSrcBitmap = null;
+
+ // Save
+ try {
+ final String smallFileName = fileName + "-small";
+ final FileOutputStream out = new FileOutputStream(smallFileName);
+ scaledBitmap.compress(Bitmap.CompressFormat.JPEG, 100, out);
+ scaledBitmap.recycle();
+ scaledBitmap = null;
+ return smallFileName;
+ }
+ catch (IOException e) {
+ return null;
+ }
+ }
+
+ private static BitmapFactory.Options bitmapBounds(final String fileName) {
+ final BitmapFactory.Options o = new BitmapFactory.Options();
+ o.inJustDecodeBounds = true;
+ BitmapFactory.decodeFile(fileName, o);
+ return o;
+ }
+
+ private static class FlushedInputStream extends FilterInputStream {
+ public FlushedInputStream(final InputStream inputStream) {
+ super(inputStream);
+ }
+
+ @Override
+ public long skip(long n) throws IOException {
+ long totalBytesSkipped = 0L;
+ while (totalBytesSkipped < n) {
+ long bytesSkipped = in.skip(n - totalBytesSkipped);
+ if (bytesSkipped == 0L) {
+ int b = read();
+ if (b < 0)
+ break; // we reached EOF
+ else
+ bytesSkipped = 1; // we read one byte
+ }
+ totalBytesSkipped += bytesSkipped;
+ }
+ return totalBytesSkipped;
+ }
+ }
+}
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Collections.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Collections.java
index 08df2507d..0c2304f41 100644
--- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Collections.java
+++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Collections.java
@@ -1,43 +1,17 @@
package net.cyclestreets.util;
-import java.util.Map;
+import java.util.ArrayList;
import java.util.List;
import java.util.Collection;
-import java.util.Iterator;
public class Collections
{
- public interface MapBuilder extends Map
- {
- MapBuilder map(K key, V v);
- } // interface MapBuilder
-
- static public MapBuilder map(K key, V value)
- {
- return MapFactory.map(key, value);
- } // map
-
- static public List list(final T... values)
- {
- return ListFactory.list(values);
- } // list
-
- static public List list(final Iterator values)
- {
- return ListFactory.list(values);
- } // list
-
- static public List list(final Collection values)
- {
- return list(values.iterator());
- } // list
-
- static public List concatenate(final Collection v1, final Collection v2)
- {
- final List l = list(v1);
+ public static List concatenate(final Collection v1, final Collection v2) {
+ final List l = new ArrayList<>(v1.size() + v2.size());
+ l.addAll(v1);
l.addAll(v2);
return l;
- } // concatenate
+ }
private Collections() { }
-} // Collections}
+}
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Html.kt b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Html.kt
new file mode 100644
index 000000000..1a822830e
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Html.kt
@@ -0,0 +1,17 @@
+package net.cyclestreets.util
+
+import android.os.Build
+import android.text.Html
+import android.text.Spanned
+
+fun fromHtml(string: String): Spanned {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+ return Html.fromHtml(string, Html.FROM_HTML_MODE_LEGACY)
+ else
+ return fromHtmlPreNougat(string)
+}
+
+@Suppress("deprecation")
+private fun fromHtmlPreNougat(string: String): Spanned {
+ return Html.fromHtml(string)
+}
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/ListFactory.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/ListFactory.java
deleted file mode 100644
index 2cfcb1cb1..000000000
--- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/ListFactory.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package net.cyclestreets.util;
-
-import java.util.Collections;
-import java.util.Iterator;
-import java.util.List;
-import java.util.ArrayList;
-
-public class ListFactory
-{
- static public List list(final T... values)
- {
- final List l = new ArrayList<>();
- Collections.addAll(l, values);
-
- return l;
- } // list
-
- static public List list(final Iterator values)
- {
- final List l = new ArrayList<>();
-
- while(values.hasNext())
- l.add(values.next());
-
- return l;
- } // list
-} // ListFactory
\ No newline at end of file
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Logging.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Logging.java
new file mode 100644
index 000000000..57c1626d8
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/Logging.java
@@ -0,0 +1,9 @@
+package net.cyclestreets.util;
+
+public class Logging {
+
+ public static String getTag(Class clazz) {
+ return clazz.getCanonicalName().replace("net.cyclestreets.", "");
+ }
+
+}
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/MapFactory.java b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/MapFactory.java
deleted file mode 100644
index ec5cef158..000000000
--- a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/MapFactory.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package net.cyclestreets.util;
-
-import java.util.Map;
-import java.util.HashMap;
-import java.util.Set;
-import java.util.Collection;
-
-import net.cyclestreets.util.Collections.MapBuilder;
-
-public class MapFactory
-{
- static public MapBuilder map(K key, V value)
- {
- final Builder builder = new Builder<>();
- return builder.map(key, value);
- } // map
-
- static public class Builder implements MapBuilder
- {
- private final Map backing_;
-
- private Builder()
- {
- backing_ = new HashMap<>();
- } // Builder
-
- public MapBuilder map(K key, V value)
- {
- backing_.put(key, value);
- return this;
- } // map
-
- public void clear() { backing_.clear(); }
- public boolean containsKey(Object key) { return backing_.containsKey(key); }
- public boolean containsValue(Object value) { return backing_.containsKey(value); }
- public Set> entrySet() { return backing_.entrySet(); }
- public boolean equals(Object o) { return backing_.equals(o); }
- public V get(Object key) { return backing_.get(key); }
- public int hashCode() { return backing_.hashCode(); }
- public boolean isEmpty() { return backing_.isEmpty(); }
- public Set keySet() { return backing_.keySet(); }
- public V put(K key, V value) { return backing_.put(key, value); }
- public void putAll(Map extends K, ? extends V> m) { backing_.putAll(m); }
- public V remove(Object key) { return backing_.remove(key); }
- public int size() { return backing_.size(); }
- public Collection values() { return backing_.values(); }
- } // class Builder
-} // MapFactory
diff --git a/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/ProgressDialog.kt b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/ProgressDialog.kt
new file mode 100644
index 000000000..fc2c8fcb5
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/java/net/cyclestreets/util/ProgressDialog.kt
@@ -0,0 +1,8 @@
+package net.cyclestreets.util
+
+import android.content.Context
+
+open class ProgressDialog : android.app.ProgressDialog {
+ constructor(context: Context?) : super(context)
+ constructor(context: Context?, theme: Int) : super(context, theme)
+}
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/general_neutral.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/general_neutral.png
deleted file mode 100644
index 2551fec80..000000000
Binary files a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/general_neutral.png and /dev/null differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/icon.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/icon.png
deleted file mode 100644
index 3f121013f..000000000
Binary files a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/icon.png and /dev/null differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_atms.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_atms.png
new file mode 100644
index 000000000..1e37c9d91
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_atms.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_attractions.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_attractions.png
new file mode 100644
index 000000000..337a9c1b2
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_attractions.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bandbs.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bandbs.png
new file mode 100644
index 000000000..07edb3f35
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bandbs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bikerepairstations.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bikerepairstations.png
new file mode 100644
index 000000000..7b78d195e
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bikerepairstations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bikeshops.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bikeshops.png
new file mode 100644
index 000000000..e21c40105
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_bikeshops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_busstops.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_busstops.png
new file mode 100644
index 000000000..58619720a
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_busstops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cafes.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cafes.png
new file mode 100644
index 000000000..237b5a8a1
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cafes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_campsites.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_campsites.png
new file mode 100644
index 000000000..f91abfa31
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_campsites.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cinemas.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cinemas.png
new file mode 100644
index 000000000..cf624e9e8
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cinemas.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_conveniencestores.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_conveniencestores.png
new file mode 100644
index 000000000..d9d167121
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_conveniencestores.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cycleparking.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cycleparking.png
new file mode 100644
index 000000000..7eee70412
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cycleparking.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cyclesport.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cyclesport.png
new file mode 100644
index 000000000..f1349e437
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_cyclesport.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_drinkingwater.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_drinkingwater.png
new file mode 100644
index 000000000..d69ee51d6
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_drinkingwater.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_hospitals.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_hospitals.png
new file mode 100644
index 000000000..e1c114b22
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_hospitals.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_libraries.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_libraries.png
new file mode 100644
index 000000000..e85eb06de
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_libraries.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_londoncyclehire.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_londoncyclehire.png
new file mode 100644
index 000000000..f9081b0c2
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_londoncyclehire.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_museums.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_museums.png
new file mode 100644
index 000000000..dc83a5d68
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_museums.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_parks.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_parks.png
new file mode 100644
index 000000000..f619d8bc0
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_parks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_playgrounds.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_playgrounds.png
new file mode 100644
index 000000000..a4d9f369f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_playgrounds.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_policestations.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_policestations.png
new file mode 100644
index 000000000..086a5f603
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_policestations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_postboxes.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_postboxes.png
new file mode 100644
index 000000000..be8776b65
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_postboxes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_publicart.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_publicart.png
new file mode 100644
index 000000000..182207c1d
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_publicart.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_pubs.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_pubs.png
new file mode 100644
index 000000000..f8c2fdf70
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_pubs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_racetracks.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_racetracks.png
new file mode 100644
index 000000000..105c7c6d9
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_racetracks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_railwaystations.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_railwaystations.png
new file mode 100644
index 000000000..0c4e69817
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_railwaystations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_recycling.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_recycling.png
new file mode 100644
index 000000000..c8f9146ee
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_recycling.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_restaurants.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_restaurants.png
new file mode 100644
index 000000000..781c64b59
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_restaurants.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_schools.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_schools.png
new file mode 100644
index 000000000..7f115b7d4
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_schools.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_supermarkets.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_supermarkets.png
new file mode 100644
index 000000000..3e155e0b5
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_supermarkets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_taxiranks.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_taxiranks.png
new file mode 100644
index 000000000..445163a17
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_taxiranks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_telephones.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_telephones.png
new file mode 100644
index 000000000..b117aa5ff
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_telephones.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_theatres.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_theatres.png
new file mode 100644
index 000000000..bf3a5e636
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_theatres.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_toilets.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_toilets.png
new file mode 100644
index 000000000..3d967e43c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_toilets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_touristinformation.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_touristinformation.png
new file mode 100644
index 000000000..f7a6fc995
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_touristinformation.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_viewpoints.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_viewpoints.png
new file mode 100644
index 000000000..197737c8f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_viewpoints.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_worship.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_worship.png
new file mode 100644
index 000000000..c1eeb4256
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_worship.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_youthhostels.png b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_youthhostels.png
new file mode 100644
index 000000000..e5c76906c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-hdpi/poi_youthhostels.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-ldpi/icon.png b/libraries/cyclestreets-core/src/main/res/drawable-ldpi/icon.png
deleted file mode 100644
index 80ea5a971..000000000
Binary files a/libraries/cyclestreets-core/src/main/res/drawable-ldpi/icon.png and /dev/null differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/icon.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/icon.png
deleted file mode 100644
index 80ea5a971..000000000
Binary files a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/icon.png and /dev/null differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_atms.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_atms.png
new file mode 100644
index 000000000..086e331fa
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_atms.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_attractions.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_attractions.png
new file mode 100644
index 000000000..8e2de4fd1
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_attractions.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bandbs.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bandbs.png
new file mode 100644
index 000000000..35e40a4b5
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bandbs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bikerepairstations.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bikerepairstations.png
new file mode 100644
index 000000000..d72f131fb
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bikerepairstations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bikeshops.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bikeshops.png
new file mode 100644
index 000000000..0b8977995
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_bikeshops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_busstops.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_busstops.png
new file mode 100644
index 000000000..479a8ec61
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_busstops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cafes.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cafes.png
new file mode 100644
index 000000000..7ff1a01f7
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cafes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_campsites.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_campsites.png
new file mode 100644
index 000000000..15586d2cb
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_campsites.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cinemas.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cinemas.png
new file mode 100644
index 000000000..fb409357e
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cinemas.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_conveniencestores.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_conveniencestores.png
new file mode 100644
index 000000000..022f2fed7
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_conveniencestores.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cycleparking.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cycleparking.png
new file mode 100644
index 000000000..b834d6e0f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cycleparking.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cyclesport.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cyclesport.png
new file mode 100644
index 000000000..32400418c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_cyclesport.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_drinkingwater.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_drinkingwater.png
new file mode 100644
index 000000000..5c4c4ed96
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_drinkingwater.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_hospitals.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_hospitals.png
new file mode 100644
index 000000000..a9e663db3
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_hospitals.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_libraries.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_libraries.png
new file mode 100644
index 000000000..4becbe297
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_libraries.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_londoncyclehire.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_londoncyclehire.png
new file mode 100644
index 000000000..e29efc88e
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_londoncyclehire.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_museums.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_museums.png
new file mode 100644
index 000000000..6890d7d90
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_museums.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_parks.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_parks.png
new file mode 100644
index 000000000..6d3c0ac5d
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_parks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_playgrounds.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_playgrounds.png
new file mode 100644
index 000000000..f510cec8a
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_playgrounds.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_policestations.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_policestations.png
new file mode 100644
index 000000000..1c89c0a82
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_policestations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_postboxes.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_postboxes.png
new file mode 100644
index 000000000..582659af5
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_postboxes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_publicart.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_publicart.png
new file mode 100644
index 000000000..b715a363f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_publicart.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_pubs.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_pubs.png
new file mode 100644
index 000000000..5939738c0
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_pubs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_racetracks.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_racetracks.png
new file mode 100644
index 000000000..2bf9e4898
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_racetracks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_railwaystations.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_railwaystations.png
new file mode 100644
index 000000000..313acd686
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_railwaystations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_recycling.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_recycling.png
new file mode 100644
index 000000000..cbddb2d8a
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_recycling.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_restaurants.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_restaurants.png
new file mode 100644
index 000000000..17afb2333
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_restaurants.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_schools.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_schools.png
new file mode 100644
index 000000000..f7d6ee484
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_schools.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_supermarkets.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_supermarkets.png
new file mode 100644
index 000000000..0be167594
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_supermarkets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_taxiranks.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_taxiranks.png
new file mode 100644
index 000000000..83dbdf4c5
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_taxiranks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_telephones.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_telephones.png
new file mode 100644
index 000000000..7ee2e18f4
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_telephones.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_theatres.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_theatres.png
new file mode 100644
index 000000000..f8d00fd20
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_theatres.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_toilets.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_toilets.png
new file mode 100644
index 000000000..fa848fe7d
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_toilets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_touristinformation.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_touristinformation.png
new file mode 100644
index 000000000..a422f0a17
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_touristinformation.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_viewpoints.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_viewpoints.png
new file mode 100644
index 000000000..0f306696e
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_viewpoints.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_worship.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_worship.png
new file mode 100644
index 000000000..74027da82
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_worship.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_youthhostels.png b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_youthhostels.png
new file mode 100644
index 000000000..d1ffb229a
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-mdpi/poi_youthhostels.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_atms.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_atms.png
new file mode 100644
index 000000000..13c33a1e6
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_atms.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_attractions.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_attractions.png
new file mode 100644
index 000000000..ed49c7862
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_attractions.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bandbs.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bandbs.png
new file mode 100644
index 000000000..4ff1ee7f0
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bandbs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bikerepairstations.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bikerepairstations.png
new file mode 100644
index 000000000..2efc6ded8
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bikerepairstations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bikeshops.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bikeshops.png
new file mode 100644
index 000000000..2043f0112
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_bikeshops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_busstops.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_busstops.png
new file mode 100644
index 000000000..23e7879cc
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_busstops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cafes.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cafes.png
new file mode 100644
index 000000000..c4fd81a6e
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cafes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_campsites.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_campsites.png
new file mode 100644
index 000000000..f5657e632
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_campsites.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cinemas.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cinemas.png
new file mode 100644
index 000000000..d1c68a504
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cinemas.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_conveniencestores.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_conveniencestores.png
new file mode 100644
index 000000000..b5c37b00e
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_conveniencestores.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cycleparking.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cycleparking.png
new file mode 100644
index 000000000..61790a656
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cycleparking.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cyclesport.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cyclesport.png
new file mode 100644
index 000000000..11d020cd3
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_cyclesport.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_drinkingwater.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_drinkingwater.png
new file mode 100644
index 000000000..6a40eb946
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_drinkingwater.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_hospitals.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_hospitals.png
new file mode 100644
index 000000000..8f3812d6c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_hospitals.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_libraries.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_libraries.png
new file mode 100644
index 000000000..065bb2780
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_libraries.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_londoncyclehire.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_londoncyclehire.png
new file mode 100644
index 000000000..7d26e7a20
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_londoncyclehire.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_museums.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_museums.png
new file mode 100644
index 000000000..9a1c9692d
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_museums.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_parks.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_parks.png
new file mode 100644
index 000000000..cebdb61c1
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_parks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_playgrounds.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_playgrounds.png
new file mode 100644
index 000000000..24f7fb07c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_playgrounds.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_policestations.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_policestations.png
new file mode 100644
index 000000000..c75d78f2c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_policestations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_postboxes.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_postboxes.png
new file mode 100644
index 000000000..d3f66444f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_postboxes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_publicart.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_publicart.png
new file mode 100644
index 000000000..5141450e1
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_publicart.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_pubs.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_pubs.png
new file mode 100644
index 000000000..143a3b9a3
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_pubs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_racetracks.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_racetracks.png
new file mode 100644
index 000000000..4612e1b00
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_racetracks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_railwaystations.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_railwaystations.png
new file mode 100644
index 000000000..0652aabab
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_railwaystations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_recycling.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_recycling.png
new file mode 100644
index 000000000..0c6b77d70
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_recycling.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_restaurants.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_restaurants.png
new file mode 100644
index 000000000..c9805c830
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_restaurants.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_schools.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_schools.png
new file mode 100644
index 000000000..3cb0f84e8
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_schools.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_supermarkets.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_supermarkets.png
new file mode 100644
index 000000000..6ee4c60b6
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_supermarkets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_taxiranks.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_taxiranks.png
new file mode 100644
index 000000000..aa8fad7d9
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_taxiranks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_telephones.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_telephones.png
new file mode 100644
index 000000000..466fc7fe7
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_telephones.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_theatres.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_theatres.png
new file mode 100644
index 000000000..33ad0af42
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_theatres.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_toilets.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_toilets.png
new file mode 100644
index 000000000..11d7c3487
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_toilets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_touristinformation.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_touristinformation.png
new file mode 100644
index 000000000..233c5318c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_touristinformation.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_viewpoints.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_viewpoints.png
new file mode 100644
index 000000000..5dd88def9
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_viewpoints.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_worship.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_worship.png
new file mode 100644
index 000000000..7f12ad379
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_worship.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_youthhostels.png b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_youthhostels.png
new file mode 100644
index 000000000..289f366cf
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xhdpi/poi_youthhostels.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_atms.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_atms.png
new file mode 100644
index 000000000..83c18c0a7
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_atms.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_attractions.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_attractions.png
new file mode 100644
index 000000000..16bfe3a62
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_attractions.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bandbs.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bandbs.png
new file mode 100644
index 000000000..5f88de1bd
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bandbs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bikerepairstations.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bikerepairstations.png
new file mode 100644
index 000000000..3437344e0
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bikerepairstations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bikeshops.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bikeshops.png
new file mode 100644
index 000000000..4ce1d4551
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_bikeshops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_busstops.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_busstops.png
new file mode 100644
index 000000000..15383672c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_busstops.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cafes.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cafes.png
new file mode 100644
index 000000000..7c2f9d1b6
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cafes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_campsites.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_campsites.png
new file mode 100644
index 000000000..39e1cd2ff
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_campsites.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cinemas.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cinemas.png
new file mode 100644
index 000000000..26f1e99cb
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cinemas.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_conveniencestores.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_conveniencestores.png
new file mode 100644
index 000000000..4e475ee66
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_conveniencestores.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cycleparking.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cycleparking.png
new file mode 100644
index 000000000..eb31def0c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cycleparking.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cyclesport.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cyclesport.png
new file mode 100644
index 000000000..d2948d3c5
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_cyclesport.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_drinkingwater.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_drinkingwater.png
new file mode 100644
index 000000000..f207907a2
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_drinkingwater.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_hospitals.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_hospitals.png
new file mode 100644
index 000000000..2a220cc6f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_hospitals.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_libraries.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_libraries.png
new file mode 100644
index 000000000..65301f96a
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_libraries.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_londoncyclehire.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_londoncyclehire.png
new file mode 100644
index 000000000..8aaf43636
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_londoncyclehire.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_museums.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_museums.png
new file mode 100644
index 000000000..69a379d0f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_museums.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_parks.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_parks.png
new file mode 100644
index 000000000..41ba926fd
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_parks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_playgrounds.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_playgrounds.png
new file mode 100644
index 000000000..7f9428302
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_playgrounds.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_policestations.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_policestations.png
new file mode 100644
index 000000000..87776a2da
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_policestations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_postboxes.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_postboxes.png
new file mode 100644
index 000000000..66a14f499
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_postboxes.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_publicart.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_publicart.png
new file mode 100644
index 000000000..dd180850f
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_publicart.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_pubs.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_pubs.png
new file mode 100644
index 000000000..71245ea2c
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_pubs.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_racetracks.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_racetracks.png
new file mode 100644
index 000000000..253f37cb9
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_racetracks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_railwaystations.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_railwaystations.png
new file mode 100644
index 000000000..f892749b0
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_railwaystations.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_recycling.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_recycling.png
new file mode 100644
index 000000000..c05261278
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_recycling.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_restaurants.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_restaurants.png
new file mode 100644
index 000000000..bfea739de
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_restaurants.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_schools.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_schools.png
new file mode 100644
index 000000000..e612914ec
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_schools.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_supermarkets.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_supermarkets.png
new file mode 100644
index 000000000..4d74fbd46
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_supermarkets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_taxiranks.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_taxiranks.png
new file mode 100644
index 000000000..23ebead8d
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_taxiranks.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_telephones.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_telephones.png
new file mode 100644
index 000000000..e5be22d41
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_telephones.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_theatres.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_theatres.png
new file mode 100644
index 000000000..c075788df
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_theatres.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_toilets.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_toilets.png
new file mode 100644
index 000000000..1768e82d4
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_toilets.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_touristinformation.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_touristinformation.png
new file mode 100644
index 000000000..f7136db23
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_touristinformation.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_viewpoints.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_viewpoints.png
new file mode 100644
index 000000000..7294d9849
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_viewpoints.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_worship.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_worship.png
new file mode 100644
index 000000000..9219f1fdf
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_worship.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_youthhostels.png b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_youthhostels.png
new file mode 100644
index 000000000..e90ee0577
Binary files /dev/null and b/libraries/cyclestreets-core/src/main/res/drawable-xxxhdpi/poi_youthhostels.png differ
diff --git a/libraries/cyclestreets-core/src/main/res/drawable/icon.png b/libraries/cyclestreets-core/src/main/res/drawable/icon.png
deleted file mode 100644
index 7e6cf90b1..000000000
Binary files a/libraries/cyclestreets-core/src/main/res/drawable/icon.png and /dev/null differ
diff --git a/libraries/cyclestreets-core/src/main/res/layout/geo_item_2line.xml b/libraries/cyclestreets-core/src/main/res/layout/geo_item_2line.xml
index db81270a3..3aaa55392 100644
--- a/libraries/cyclestreets-core/src/main/res/layout/geo_item_2line.xml
+++ b/libraries/cyclestreets-core/src/main/res/layout/geo_item_2line.xml
@@ -6,9 +6,9 @@
android:layout_height="?android:listPreferredItemHeight"
android:baselineAligned="false">
@@ -28,7 +28,7 @@
android:layout_height="wrap_content"
android:singleLine="true"
android:layout_below="@android:id/text1"
- android:layout_alignLeft="@android:id/text1"
+ android:layout_alignStart="@android:id/text1"
style="?android:dropDownItemStyle" />
diff --git a/libraries/cyclestreets-core/src/main/res/values-de/arrays.xml b/libraries/cyclestreets-core/src/main/res/values-de/arrays.xml
new file mode 100644
index 000000000..d70776c93
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-de/arrays.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ - Kilometer
+ - Meilen
+
+
+ - gemütlich 16km/h (10mph)
+ - entspannt 20km/h (12mph)
+ - zügig 24 km/h (15mph)
+
+
+ - ruhigste
+ - ausgewogen
+ - schnellste
+ - kürzeste
+
+
+ - immer auf 640 Pixel Breite skalieren
+ - volle Größe, wenn WLAN, 3,5G oder 4G verfügbar
+ - volle Größe
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-de/strings.xml b/libraries/cyclestreets-core/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..4fa3c5e9b
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-de/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+ Zeige Blog-Update-Benachrichtigungen
+
+ Danke für die Übermittlung des Feedbacks. Wir werden auf dich zurück kommen, wenn wir es uns angeschaut haben.
+ Dein Feedback konnte nicht gesendet werden.\n\n
+
+ Dein Account wurde registriert.\n\nEine E-Mail wurde an die angegebene Adresse gesendet.\n\nWenn die E-Mail ankommt, folge den enthaltenen Anweisungen, um die Registrierung abzuschließen.
+ Dein Account konnte nicht registriert werden.\n\n
+
+ Du hast dich erfolgreich bei CycleStreets eingeloggt.
+ Fehler:
+ Anmeldung bei CycleStreets nicht erfolgreich. Bitte überprüfe deinen Nutzernamen und dein Passwort.
+
+ Dein Bild wurde erfolgreich hochgeladen.
+ Es gab ein Problem beim Hochladen deines Bildes: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-es/arrays.xml b/libraries/cyclestreets-core/src/main/res/values-es/arrays.xml
new file mode 100644
index 000000000..ab2539c5c
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-es/arrays.xml
@@ -0,0 +1,31 @@
+
+
+
+
+ - Kilometros
+ - Millas
+
+
+
+
+ - Lento 10mph (16km/h)
+ - Paseo 12mph (20km/h)
+ - Rápido 15mph (24km/h)
+
+
+
+
+ - Lento
+ - Equilibrado
+ - Rápido
+ - Corto
+
+
+
+ - Siempre redimensiona a 640 pixels de ancho
+ - Resolución completa cuando se esté conectado a WiFi, 3.5G or 4G
+ - Resolución completa
+
+
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-es/strings.xml b/libraries/cyclestreets-core/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..3bbd9b4d1
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-es/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+ Mostrar notificaciones de las actualizaciones del Blog
+
+ Gracias por enviarnos tu opinión. Nos pondremos en contacto cuando lo hayamos comprobado.
+ Tu opinión no puede enviarse.\n\n
+
+ Tu cuenta no puede registrarse.\n\n
+ Tu cuenta se ha registrado.\n\n Un email se ha enviado a tu cuenta. \n\n Cuando el email llegue, siguie las instrucciones que contiene y contiene para completar el registro.\n\n
+
+ Te has registrado con éxito en CycleStreets.
+ Error :
+ No se puede acceder a CycleStreets. Por favor comprueba tu nombre de usuario y contraseña.
+
+ Tu foto se ha actualizado con éxito.
+ Hubo un problema al actualizar tu foto: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-fr/arrays.xml b/libraries/cyclestreets-core/src/main/res/values-fr/arrays.xml
new file mode 100644
index 000000000..5762bfae3
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-fr/arrays.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ - kilomètres
+ - Miles
+
+
+
+ - Sans hâte 10mph (16km/h)
+ - Croisière 12mph (20km/h)
+ - Rapide 15mph (24km/h)
+
+
+
+ - Quietest
+ - Équilibré
+ - Le plus rapide
+ - Shortest
+
+
+
+ - Redimensionner toujours à 640 pixels de large
+ - Taille réelle quand le WiFi, 3.5G ou 4G disponible
+ - Taille réelle
+
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-fr/strings.xml b/libraries/cyclestreets-core/src/main/res/values-fr/strings.xml
new file mode 100644
index 000000000..3037dae2f
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-fr/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+ Afficher les notifications de mise à jour du Blog
+
+ Merci d\'avoir envoyé ce feedback. Nous reviendrons vers vous quand nous l\'aurons vérifié.
+ Votre feedback n\'a pas pu être envoyé.\n\n
+
+ Votre compte a été enregistré.\n\nUn e-mail a été envoyé à l\'adresse que vous avez donné.\n\nLorsque l\'e-mail arrive, suivez les instructions qu\'il contient pour terminer l\'enregistrement.
+ Votre compte n\'a pas pu être enregistré.\n\n
+
+ vous vous êtes connecté avec succès sur CycleStreets.
+ Erreur :
+ Impossible de se connecter à CycleStreets. S\'il vous plaît vérifier votre nom d\'utilisateur et mot de passe.
+
+ Votre photo a été chargé avec succès.
+ Il y avait un problème de chargement de votre photo: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-it/arrays.xml b/libraries/cyclestreets-core/src/main/res/values-it/arrays.xml
new file mode 100644
index 000000000..d46502dbc
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-it/arrays.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ - Chilometri
+ - Miglia
+
+
+
+ - Rilassata 10mph (16km/h)
+ - Regolare 12mph (20km/h)
+ - Veloce 15mph (24km/h)
+
+
+
+ - Tranquillo
+ - Bilanciato
+ - Veloce
+ - Corto
+
+
+
+ - Ridimensiona sempre a 640 pixel di larghezza
+ - Dimensioni massime se WiFi, 3.5G o 4G disponibili
+ - Dimensioni massime
+
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-it/strings.xml b/libraries/cyclestreets-core/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..d6375e178
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-it/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+ Mostra notifiche blog
+
+ Grazie per averci inviato la tua opinione. Ti risponderemo appena la leggeremo.
+ La tua opinione non è stata inviata.\n\n
+
+ Il tuo account è stato registrato.\n\nAbbiamo inviato una email all\'indirizzo che ci hai fornito.\n\nQuando riceverai la mail, segui le istruzioni contenute al suo interno per completare la registrazione.
+ Il tuo account non è stato creato.\n\n
+
+ Connessione con CycleStreets riuscita.
+ "Errore : "
+ Login in CycleStreets non riuscito. Controlla l\'username e la password.
+
+ Upload della foto completato con successo.
+ C\'è stato un problema con l\'upload della tua foto: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-pt/arrays.xml b/libraries/cyclestreets-core/src/main/res/values-pt/arrays.xml
new file mode 100644
index 000000000..66bbf9376
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-pt/arrays.xml
@@ -0,0 +1,26 @@
+
+
+
+
+ - Quilômetros
+ - Milhas
+
+
+ - Sem pressa 16km/h (10mph)
+ - Normal 20km/h (12mph)
+ - Rápido 24km/h (15mph)
+
+
+
+ - Mais tranquila
+ - Balanceada
+ - Mais rápida
+ - Mais curta
+
+
+ - Sempre redimensionar para 640 pixels de largura
+ - Tamanho completo quando conectado a uma raede WiFi, 3.5G ou 4G
+ - Tamanho completo
+
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-pt/strings.xml b/libraries/cyclestreets-core/src/main/res/values-pt/strings.xml
new file mode 100644
index 000000000..e7e988bfb
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-pt/strings.xml
@@ -0,0 +1,21 @@
+
+
+ Mostrar notificações de atualização do Blog
+
+ Obrigado por enviar seus comentários. Iremos retornar a você quando tivermos um resultado.
+ Seu comentário não pode ser enviado.\n\n
+
+ Sua conta foi cadastrada.\n\nUm email de confirmação vai ser enviado ao endereço fornecido\n\nQuando o email chegar, siga as instruções nele para completar o registro.
+ Sua conta não pode ser cadastrada.\n\n
+
+ Você entrou com sucesso em CycleStreets.
+ Erro :
+ Não foi possÃvel entrar em CycleStreets. Favor checar seu usuário e senha.
+
+ Sua foto foi enviada com sucesso.
+ Houve um erro enviando sua foto: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-ru/arrays.xml b/libraries/cyclestreets-core/src/main/res/values-ru/arrays.xml
new file mode 100644
index 000000000..12bc3e3c7
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-ru/arrays.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ - Километры
+ - Мили
+
+
+
+ - Ðеторопливый 10милÑ/ч (16км / ч)
+ - Ð—Ð°Ð¿Ð°Ñ Ñ…Ð¾Ð´Ð° 12милÑ/ч (20 км / ч)
+ - БыÑтрый 15милÑ/ч (24км / ч)
+
+
+
+ - БеÑшумный
+ - Уравновешенный
+ - БыÑтрый
+ - Короткий
+
+
+
+ - Ð’Ñегда изменÑть размер в ширину 640 пикÑелей
+ - Полный размер, когда WiFi, 3.5G или 4G доÑтупен
+ - Полный размер
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-ru/strings.xml b/libraries/cyclestreets-core/src/main/res/values-ru/strings.xml
new file mode 100644
index 000000000..9ecb43d88
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-ru/strings.xml
@@ -0,0 +1,22 @@
+
+
+
+ Показать ÑƒÐ²ÐµÐ´Ð¾Ð¼Ð»ÐµÐ½Ð¸Ñ Ð¾Ð± обновлениÑÑ… в блоге
+
+ Благодарим Ð’Ð°Ñ Ð·Ð° отправку обратную ÑвÑзь. Мы ÑвÑжемÑÑ Ñ Ð²Ð°Ð¼Ð¸, когда мы проверим ее.
+ Ваша Ð¾Ð±Ñ€Ð°Ñ‚Ð½Ð°Ñ ÑвÑзь не может быть отправлено.\n\n
+
+ Ваш аккаунт был зарегиÑтрирован.\n\n Ðлектронное Ñообщение было отправлено по адреÑу, который вы дали.\n Когда почта придет, Ñледуйте инÑтрукциÑм, которые оно Ñодержит, чтобы завершить региÑтрацию.
+ Ваша ÑƒÑ‡ÐµÑ‚Ð½Ð°Ñ Ð·Ð°Ð¿Ð¸ÑÑŒ не может быть зарегиÑтрировано.\n\n
+
+ Ð’Ñ‹ уÑпешно вошли в CycleStreets.
+ Ошибка :
+ Ðе удалоÑÑŒ войти в CycleStreets. ПожалуйÑта, проверьте Ð¸Ð¼Ñ Ð¿Ð¾Ð»ÑŒÐ·Ð¾Ð²Ð°Ñ‚ÐµÐ»Ñ Ð¸ пароль.
+
+ Ваше фото загружено уÑпешно.
+ Произошла ошибка при загрузке вашей фотографии: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-tr/arrays.xml b/libraries/cyclestreets-core/src/main/res/values-tr/arrays.xml
new file mode 100644
index 000000000..48c92cb80
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-tr/arrays.xml
@@ -0,0 +1,28 @@
+
+
+
+
+ - Kilometre
+ - Mil
+
+
+
+ - 10mph (16km/h) Sakin
+ - 12mph (20km/h) Sabit Hız
+ - 15mph (24km/h) Hız
+
+
+
+ - En sessiz
+ - Dengeli
+ - En hızlı
+ - En kısa
+
+
+
+ - Her zaman 640 pixel genişliğe boyutlandır
+ - WiFi, 3.5G ya da 4G iken tam boyut
+ - Tam boyut
+
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values-tr/strings.xml b/libraries/cyclestreets-core/src/main/res/values-tr/strings.xml
new file mode 100644
index 000000000..9678572cb
--- /dev/null
+++ b/libraries/cyclestreets-core/src/main/res/values-tr/strings.xml
@@ -0,0 +1,23 @@
+
+
+
+ Blog güncelleme bildirimlerini göster
+
+ Geri dönüşünüzü yayınladığınız için teşekkürler. Kontrol ettikten sonra geri dönüş yapacağız.
+ Geri dönüşünüz gönderilemedi.\n\n
+
+ Hesabınız kaydedildi.\n\n Mail verdiğiniz adrese gönderildi.\n\n
+ E-posta geldiğinde, bu kaydı tamamlamak için içerdiği yönergeleri izleyin.
+ Hesabınız kaydedilemedi.\n\n
+
+ CycleStreets uygulamasına başarıyla giriş yapıldı.
+ Hata :
+ CycleStreets uygulamasına giriş yapılamadı. Lütfen kullanıcı adı ve parolanızı kontrol ediniz.
+
+ Fotoğrafınız başarıyla yüklendi.
+ Fotoğrafınız yüklenirken bir problem oluştu: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
+
diff --git a/libraries/cyclestreets-core/src/main/res/values/arrays.xml b/libraries/cyclestreets-core/src/main/res/values/arrays.xml
index 4c8beb89a..bd3d4f5fe 100644
--- a/libraries/cyclestreets-core/src/main/res/values/arrays.xml
+++ b/libraries/cyclestreets-core/src/main/res/values/arrays.xml
@@ -1,47 +1,51 @@
+
- Kilometres
- Miles
-
+
- km
- miles
+
- Unhurried 10mph (16km/h)
- Cruising 12mph (20km/h)
- Quick 15mph (24km/h)
-
+
- 16
- 20
- 24
+
- Quietest
- Balanced
- Fastest
- Shortest
-
+
- quietest
- balanced
- fastest
- shortest
+
- Always resize to 640 pixels wide
- Full size when WiFi, 3.5G or 4G available
- Full size
-
+
- 640px
- bigIfWifi
- big
-
+
- 15 m
- 20 m
- 25 m
@@ -81,7 +85,7 @@
- 195 m
- 200 m
-
+
- 15
- 20
- 25
diff --git a/libraries/cyclestreets-core/src/main/res/values/strings.xml b/libraries/cyclestreets-core/src/main/res/values/strings.xml
index fe1617bdb..e9a2d9767 100644
--- a/libraries/cyclestreets-core/src/main/res/values/strings.xml
+++ b/libraries/cyclestreets-core/src/main/res/values/strings.xml
@@ -1,4 +1,22 @@
+
Show Blog update notifications
+
+ Thank you for submitting this feedback. We will get back to you when we have checked this out.
+ Your feedback could not be sent.\n\n
+
+ Your account has been registered.\n\nAn email has been sent to the address you gave.\n\nWhen the email arrives, follow the instructions it contains to complete the registration.
+ Your account could not be registered.\n\n
+
+ You have successfully signed into CycleStreets.
+ Error :
+ Could not sign into CycleStreets. Please check your username and password.
+
+ Your photo was uploaded successfully.
+ There was a problem uploading your photo: \n
+
+ LiveRide
+ Turn-by-turn instructions for navigation
+
diff --git a/libraries/cyclestreets-core/src/test/java/net/cyclestreets/TestUtils.java b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/TestUtils.java
new file mode 100644
index 000000000..2183cfcda
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/TestUtils.java
@@ -0,0 +1,21 @@
+package net.cyclestreets;
+
+import android.content.res.Resources;
+
+import org.apache.commons.io.IOUtils;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class TestUtils {
+
+ public static String fromResourceFile(String resourceFileName) throws IOException {
+ InputStream in = TestUtils.class.getClassLoader().getResourceAsStream(resourceFileName);
+ if (in != null) {
+ String output = IOUtils.toString(in, "UTF-8").trim();
+ in.close();
+ return output;
+ }
+ throw new Resources.NotFoundException(resourceFileName);
+ }
+}
diff --git a/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/JourneyStringTransformerTest.java b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/JourneyStringTransformerTest.java
new file mode 100644
index 000000000..16fa53283
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/JourneyStringTransformerTest.java
@@ -0,0 +1,75 @@
+package net.cyclestreets.api.client;
+
+import net.cyclestreets.TestUtils;
+
+import org.json.JSONException;
+import org.junit.BeforeClass;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.skyscreamer.jsonassert.JSONAssert;
+import org.skyscreamer.jsonassert.JSONCompareMode;
+
+import java.io.IOException;
+
+
+@Config(manifest = Config.NONE, sdk = 33)
+@RunWith(RobolectricTestRunner.class)
+public class JourneyStringTransformerTest {
+
+ private static String expectedJson;
+ private static String expectedSingleSegmentJson;
+
+ @BeforeClass
+ public static void setup() throws IOException {
+ // We want to validate that transforming from either V1 API (XML or JSON) results in the
+ // same domain JSON.
+ expectedJson = TestUtils.fromResourceFile("journey-domain.json");
+ expectedSingleSegmentJson = TestUtils.fromResourceFile("journey-single-segment-domain.json");
+ }
+
+ @Test
+ public void fromV1ApiXmlTest() throws IOException, JSONException {
+ String inputXml = TestUtils.fromResourceFile("__files/journey-v1api.xml");
+ String outputJson = JourneyStringTransformerKt.fromV1ApiXml(inputXml);
+ JSONAssert.assertEquals(expectedJson, outputJson, JSONCompareMode.STRICT);
+ }
+
+ @Test
+ public void fromV1ApiJsonTest() throws IOException, JSONException {
+ String inputJson = TestUtils.fromResourceFile("__files/journey-v1api.json");
+ String outputJson = JourneyStringTransformerKt.fromV1ApiJson(inputJson);
+ JSONAssert.assertEquals(expectedJson, outputJson, JSONCompareMode.STRICT);
+ }
+
+ @Test
+ public void singleSegmentXmlTransformation() throws IOException, JSONException {
+ String inputJson = TestUtils.fromResourceFile("__files/journey-v1api-single-segment.xml");
+ String outputJson = JourneyStringTransformerKt.fromV1ApiXml(inputJson);
+ JSONAssert.assertEquals(expectedSingleSegmentJson, outputJson, JSONCompareMode.STRICT);
+ }
+
+ @Test
+ public void singleSegmentJsonTransformation() throws IOException, JSONException {
+ String inputJson = TestUtils.fromResourceFile("__files/journey-v1api-single-segment.json");
+ String outputJson = JourneyStringTransformerKt.fromV1ApiJson(inputJson);
+ JSONAssert.assertEquals(expectedSingleSegmentJson, outputJson, JSONCompareMode.STRICT);
+ }
+
+ /**
+ * To get the domain JSON for a particular journey:
+ *
+ * 1. Hit https://www.cyclestreets.net/api/journey.json?plan=balanced&itinerary=itineraryId&key=redacted
+ * 2. Copy and paste the JSON output into the inputJson variable below
+ * 3. Run the test, then copy the output into e.g. https://jsonformatter.org/json-pretty-print
+ */
+ @Ignore
+ @Test
+ public void getDomainJson() throws IOException, JSONException {
+ String inputJson = "";
+ String outputJson = JourneyStringTransformerKt.fromV1ApiJson(inputJson);
+ System.out.println(outputJson);
+ }
+}
diff --git a/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientIntegrationTest.java b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientIntegrationTest.java
index 186d1c9e8..302345cf6 100644
--- a/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientIntegrationTest.java
+++ b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientIntegrationTest.java
@@ -1,42 +1,36 @@
package net.cyclestreets.api.client;
import android.content.Context;
+import android.net.Uri;
-import net.cyclestreets.api.Blog;
-import net.cyclestreets.api.Feedback;
-import net.cyclestreets.api.GeoPlace;
-import net.cyclestreets.api.GeoPlaces;
-import net.cyclestreets.api.POI;
-import net.cyclestreets.api.POICategories;
-import net.cyclestreets.api.POICategory;
-import net.cyclestreets.api.Photo;
-import net.cyclestreets.api.PhotomapCategories;
-import net.cyclestreets.api.Photos;
-import net.cyclestreets.api.Registration;
-import net.cyclestreets.api.Signin;
-import net.cyclestreets.api.Upload;
-import net.cyclestreets.api.UserJourney;
-import net.cyclestreets.api.UserJourneys;
+import net.cyclestreets.api.*;
+import net.cyclestreets.core.R;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
+import java.lang.reflect.Field;
import java.util.List;
import java.util.Random;
import static org.hamcrest.CoreMatchers.is;
-import static org.junit.Assert.assertThat;
+import static org.hamcrest.MatcherAssert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
+
// Useful for manual testing that operations do work with the real API, and not just WireMock.
-// If we assigned an appropriate api key, these tests could be expanded and un-ignored.
-@Ignore
+@Ignore("Only meant for manual testing")
+@Config(manifest = Config.NONE, sdk = 33)
+@RunWith(RobolectricTestRunner.class)
public class RetrofitApiClientIntegrationTest {
RetrofitApiClient apiClient;
@@ -50,20 +44,36 @@ public void setUp() throws Exception {
.withContext(testContext)
.withV1Host("https://www.cyclestreets.net")
.withV2Host("https://api.cyclestreets.net")
+ .withBlogHost("https://www.cyclestreets.org")
.build();
+
+ when(testContext.getString(R.string.feedback_ok)).thenReturn("Thank you for submitting this feedback. We will get back to you when we have checked this out.");
+ when(testContext.getString(R.string.feedback_error_prefix)).thenReturn("Your feedback could not be sent.\n\n");
+ when(testContext.getString(R.string.registration_ok)).thenReturn("Your account has been registered.\n\nAn email has been sent to the address you gave.\n\nWhen the email arrives, follow the instructions it contains to complete the registration.");
+ when(testContext.getString(R.string.registration_error_prefix)).thenReturn("Your account could not be registered.\n\n");
+ when(testContext.getString(R.string.signin_ok)).thenReturn("You have successfully signed into CycleStreets.");
+ when(testContext.getString(R.string.signin_error_prefix)).thenReturn("Error : ");
+ when(testContext.getString(R.string.signin_default_error)).thenReturn("Could not sign into CycleStreets. Please check your username and password.");
+ when(testContext.getString(R.string.upload_ok)).thenReturn("Your photo was uploaded successfully.");
+ when(testContext.getString(R.string.upload_error_prefix)).thenReturn("There was a problem uploading your photo: \n");
+ ApiClient.INSTANCE.initialiseForTests(testContext, new ApiClientImpl(apiClient));
}
private String getApiKey() throws IOException {
String apiKey = "apiKeyRedacted";
- InputStream in = RetrofitApiClientIntegrationTest.class.getClassLoader().getResourceAsStream("api.key");
+ InputStream in = RetrofitApiClientIntegrationTest.class.getClassLoader().getResourceAsStream("cyclestreets-api.key");
if (in != null) {
try {
- apiKey = IOUtils.toString(in, "UTF-8");
+ apiKey = IOUtils.toString(in, "UTF-8").trim();
+ System.out.println("Loaded api Key '" + apiKey + "' from api.key");
} catch (IOException e) {
// Give up and use default
+ System.out.println("Failed to load API key from api.key - use default");
} finally {
in.close();
}
+ } else {
+ System.out.println("No api.key found to run integration test - use default");
}
return apiKey;
}
@@ -78,9 +88,9 @@ public void hitGeoCoderApi() throws Exception {
@Test
public void hitGetPOICategoriesApi() throws Exception {
- POICategories poiCategories = apiClient.getPOICategories(16);
+ POICategories poiCategories = apiClient.getPOICategories();
for (POICategory category : poiCategories) {
- System.out.println(category.name() + ": " + category);
+ System.out.println(category.getName() + ": " + category);
}
}
@@ -96,6 +106,14 @@ public void hitGetPOIsByRadiusApi() throws Exception {
System.out.println(pois);
}
+ @Test
+ public void hitGetPhotoApi() throws Exception {
+ Photos photos = apiClient.getPhoto(93348);
+ for (Photo photo : photos) {
+ System.out.println(photo);
+ }
+ }
+
@Test
public void hitGetPhotosApi() throws Exception {
Photos photos = apiClient.getPhotos(0.1, 52.2, 0.2, 52.3);
@@ -117,7 +135,7 @@ public void hitRegistrationApi() throws Exception {
// Apologies for the test users that this method generates - we should probably delete them...
String random = String.valueOf(new Random().nextInt(100000));
System.out.println("Registering user test" + random);
- Registration.Result result = apiClient.register("test" + random, "pwd1234", "friendlyname", "test" + random + "@nosuchdomain.com");
+ Result result = apiClient.register("test" + random, "pwd1234", "friendlyname", "test" + random + "@nosuchdomain.com");
System.out.println(result.ok());
System.out.println(result.message());
assertThat(result.ok(), is(true));
@@ -129,13 +147,13 @@ public void hitAuthenticateApi() throws Exception {
System.out.println(result.ok());
System.out.println(result.name());
System.out.println(result.email());
- System.out.println(result.error());
+ System.out.println(result.message());
assertThat(result.ok(), is(true));
}
@Test
public void hitSendFeedbackApi() throws Exception {
- Feedback.Result result = apiClient.sendFeedback(1234, "test comment", "test", "test@nosuchdomain.com");
+ Result result = apiClient.sendFeedback(1234, "test comment", "test", "test@nosuchdomain.com");
System.out.println(result.ok());
System.out.println(result.message());
assertThat(result.ok(), is(true));
@@ -154,32 +172,37 @@ public void hitUploadPhotoApiWithoutPhoto() throws Exception {
"cycleparking", "good", "Caption: THIS IS TEST DATA and should not be on the map", null);
System.out.println(result.ok());
System.out.println(result.url());
- System.out.println(result.error());
+ System.out.println(result.message());
// Important - remove the test data from the map, otherwise we look pretty unprofessional!
System.out.println("Don't forgot to log on as this user and delete the photo afterwards...");
}
@Test
public void hitUploadPhotoApiWithPhoto() throws Exception {
- Upload.Result result = apiClient.uploadPhoto("test66137", "pwd1234", 0, 52, 1467394411,
- "cycleparking", "good", "Caption: THIS IS TEST DATA and should not be on the map", "/tmp/test-image.png");
+ Upload.Result result = apiClient.uploadPhoto(
+ "test66137", "pwd1234",
+ 0, 52,
+ 1467394411,
+ "cycleparking", "good",
+ "Caption: THIS IS TEST DATA and should not be on the map",
+ Uri.parse("/tmp/test-image.png"));
System.out.println(result.ok());
System.out.println(result.url());
- System.out.println(result.error());
+ System.out.println(result.message());
// Important - remove the test data from the map, otherwise we look pretty unprofessional!
System.out.println("Don't forgot to log on as this user and delete the photo afterwards...");
}
@Test
- public void hitGetJourneyXmlApi() throws Exception {
- String xml = apiClient.getJourneyXml("quietest", "0.117950,52.205302,City+Centre|0.131402,52.221046,Mulberry+Close|0.147324,52.199650,Thoday+Street", null, null, 24);
- System.out.println(xml);
+ public void hitGetJourneyJsonApi() throws Exception {
+ String json = apiClient.getJourneyJson("quietest", "0.117950,52.205302,City+Centre|0.131402,52.221046,Mulberry+Close|0.147324,52.199650,Thoday+Street", null, null, 24);
+ System.out.println(json);
}
@Test
- public void hitRetrievePreviousJourneyXmlApi() throws Exception {
- String xml = apiClient.retrievePreviousJourneyXml("fastest", 53135357);
- System.out.println(xml);
+ public void hitRetrievePreviousJourneyJsonApi() throws Exception {
+ String json = apiClient.retrievePreviousJourneyJson("fastest", 53135357);
+ System.out.println(json);
}
@Test
diff --git a/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientTest.java b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientTest.java
index 69bbea781..8e1aee102 100644
--- a/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientTest.java
+++ b/libraries/cyclestreets-core/src/test/java/net/cyclestreets/api/client/RetrofitApiClientTest.java
@@ -1,12 +1,14 @@
package net.cyclestreets.api.client;
import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
import com.github.tomakehurst.wiremock.junit.WireMockRule;
import com.github.tomakehurst.wiremock.verification.LoggedRequest;
+import net.cyclestreets.api.ApiClient;
import net.cyclestreets.api.Blog;
-import net.cyclestreets.api.Feedback;
import net.cyclestreets.api.GeoPlace;
import net.cyclestreets.api.GeoPlaces;
import net.cyclestreets.api.POI;
@@ -16,18 +18,18 @@
import net.cyclestreets.api.PhotomapCategories;
import net.cyclestreets.api.PhotomapCategory;
import net.cyclestreets.api.Photos;
-import net.cyclestreets.api.Registration;
+import net.cyclestreets.api.Result;
import net.cyclestreets.api.Signin;
import net.cyclestreets.api.Upload;
import net.cyclestreets.api.UserJourney;
import net.cyclestreets.api.UserJourneys;
+import net.cyclestreets.core.R;
import org.apache.commons.io.FileUtils;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
-import org.osmdroid.api.IGeoPoint;
import org.osmdroid.util.GeoPoint;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
@@ -49,22 +51,24 @@
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo;
import static com.github.tomakehurst.wiremock.client.WireMock.verify;
-import static org.hamcrest.CoreMatchers.containsString;
-import static org.hamcrest.CoreMatchers.is;
-import static org.hamcrest.CoreMatchers.notNullValue;
-import static org.hamcrest.Matchers.hasSize;
-import static org.junit.Assert.assertThat;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-@Config(manifest=Config.NONE)
+
+@Config(manifest = Config.NONE, sdk = 33)
@RunWith(RobolectricTestRunner.class)
public class RetrofitApiClientTest {
@Rule
public WireMockRule wireMockRule = new WireMockRule(8089);
- RetrofitApiClient apiClient;
+ private RetrofitApiClient apiClient;
+
+ private Context testContext;
+ private Resources testResources;
@Before
public void setUp() throws Exception {
@@ -72,14 +76,29 @@ public void setUp() throws Exception {
File cacheDirFile = new File("/tmp/RetrofitApiClientCache");
FileUtils.deleteDirectory(cacheDirFile);
- Context testContext = mock(Context.class);
+ testContext = mock(Context.class);
+ testResources = mock(Resources.class);
+ when(testContext.getResources()).thenReturn(testResources);
when(testContext.getCacheDir()).thenReturn(new File("/tmp"));
apiClient = new RetrofitApiClient.Builder()
.withApiKey("myApiKey")
.withContext(testContext)
.withV1Host("http://localhost:8089")
.withV2Host("http://localhost:8089")
+ .withBlogHost("http://localhost:8089")
.build();
+
+ when(testContext.getString(R.string.feedback_ok)).thenReturn("Thank you for submitting this feedback. We will get back to you when we have checked this out.");
+ when(testContext.getString(R.string.feedback_error_prefix)).thenReturn("Your feedback could not be sent.\n\n");
+ when(testContext.getString(R.string.registration_ok)).thenReturn("Your account has been registered.\n\nAn email has been sent to the address you gave.\n\nWhen the email arrives, follow the instructions it contains to complete the registration.");
+ when(testContext.getString(R.string.registration_error_prefix)).thenReturn("Your account could not be registered.\n\n");
+ when(testContext.getString(R.string.signin_ok)).thenReturn("You have successfully signed into CycleStreets.");
+ when(testContext.getString(R.string.signin_error_prefix)).thenReturn("Error : ");
+ when(testContext.getString(R.string.signin_default_error)).thenReturn("Could not sign into CycleStreets. Please check your username and password.");
+ when(testContext.getString(R.string.upload_ok)).thenReturn("Your photo was uploaded successfully.");
+ when(testContext.getString(R.string.upload_error_prefix)).thenReturn("There was a problem uploading your photo: \n");
+ // Initialise API Client messages without doing full initialise
+ ApiClient.INSTANCE.initMessages(testContext);
}
@Test
@@ -89,28 +108,30 @@ public void testGetPoiCategories() throws Exception {
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
+ .withHeader("Cache-Control", "public, max-age=604800")
.withBodyFile("pois-types.json")));
+ when(testResources.getResourcePackageName(R.drawable.poi_attractions)).thenReturn("drawable-xxhdpi");
+ when(testResources.getDrawable(anyInt(), eq(null))).thenReturn(mock(Drawable.class));
// when
- POICategories poiCategories = apiClient.getPOICategories(16);
+ POICategories poiCategories = apiClient.getPOICategories();
// call the endpoint 5 more times
for (int ii = 0; ii < 5; ii++) {
- apiClient.getPOICategories(16);
+ apiClient.getPOICategories();
}
// then
verify(getRequestedFor(urlPathEqualTo("/v2/pois.types"))
- .withQueryParam("icons", equalTo("16"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(poiCategories.count(), is(52));
+ assertThat(poiCategories.count()).isEqualTo(52);
POICategory category = poiCategories.get(37);
- assertThat(category.name(), is("Supermarkets"));
- assertThat(category.icon(), is(notNullValue()));
+ assertThat(category.getName()).isEqualTo("Supermarkets");
+ assertThat(category.getIcon()).isNotNull();
// caching should mean the REST request is only made once
List requests = findAll(getRequestedFor(urlPathEqualTo("/v2/pois.types")));
- assertThat(requests, hasSize(1));
+ assertThat(requests).hasSize(1);
}
@Test
@@ -129,7 +150,7 @@ public void testGetPoisByBbox() throws Exception {
verify(getRequestedFor(urlPathEqualTo("/v2/pois.locations"))
.withQueryParam("type", equalTo("bikeshops"))
.withQueryParam("bbox", equalTo("0.1,52.2,0.2,52.3"))
- .withQueryParam("fields", equalTo("id,name,notes,website,latitude,longitude"))
+ .withQueryParam("fields", equalTo("id,latitude,longitude,name,notes,osmTags,website"))
.withQueryParam("key", equalTo("myApiKey")));
validatePois(pois);
}
@@ -158,24 +179,37 @@ public void testGetPoisByRadius() throws Exception {
.withQueryParam("latitude", equalTo("52.25"))
.withQueryParam("radius", equalTo("100"))
.withQueryParam("limit", equalTo("150"))
- .withQueryParam("fields", equalTo("id,name,notes,website,latitude,longitude"))
+ .withQueryParam("fields", equalTo("id,latitude,longitude,name,notes,osmTags,website"))
.withQueryParam("key", equalTo("myApiKey")));
validatePois(pois);
// not cached - REST request will be made 6 times
List requests = findAll(getRequestedFor(urlPathEqualTo("/v2/pois.locations")));
- assertThat(requests, hasSize(6));
+ assertThat(requests).hasSize(6);
}
private static void validatePois(List pois) {
- assertThat(pois.size(), is(7));
- POI poi = pois.get(0);
+ assertThat(pois.size()).isEqualTo(7);
- assertThat(poi.name(), is("Chris's Bikes"));
- assertThat(poi.id(), is(101399));
- assertThat(poi.notes(), is("The notes section"));
- assertThat(poi.url(), is("http://www.madeup.com"));
- assertThat(poi.position(), is(new GeoPoint(52.225338, 0.091919)));
+ // happy path
+ POI poi = pois.get(0);
+ assertThat(poi.id()).isEqualTo("alpha_101399");
+ assertThat(poi.name()).isEqualTo("Chris's Bikes");
+ assertThat(poi.notes()).isEqualTo("The notes section");
+ assertThat(poi.phone()).isEqualTo("01234 567890");
+ assertThat(poi.openingHours()).isEqualTo("Mo-Fr 09:00-17:00\nSa 10:00-18:00");
+ assertThat(poi.url()).isEqualTo("http://www.madeup.com");
+ assertThat(poi.position()).isEqualTo(new GeoPoint(52.225338, 0.091919));
+
+ // website provided within `osmTags.url`, but not in `website`
+ poi = pois.get(6);
+ assertThat(poi.id()).isEqualTo("some-other-characters-113267");
+ assertThat(poi.name()).isEqualTo("Bicycle Ambulance");
+ assertThat(poi.notes()).isEqualTo("");
+ assertThat(poi.phone()).isEqualTo("");
+ assertThat(poi.openingHours()).isEqualTo("Tu-Fr 08:30-18:00\nSa 10:00-18:00");
+ assertThat(poi.url()).isEqualTo("http://bicycleambulance.com");
+ assertThat(poi.position()).isEqualTo(new GeoPoint(52.209179, 0.120061));
}
@Test
@@ -197,11 +231,11 @@ public void testGetUserJourneys() throws Exception {
.withQueryParam("datetime", equalTo("friendly"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(journeys.size(), is(3));
+ assertThat(journeys.size()).isEqualTo(3);
UserJourney journey = journeys.get(2);
- assertThat(journey.name(), is("Hedingham Close to Old Montague Street"));
- assertThat(journey.id(), is(43089395));
+ assertThat(journey.name()).isEqualTo("Hedingham Close to Old Montague Street");
+ assertThat(journey.id()).isEqualTo(43089395);
}
@Test
@@ -219,7 +253,7 @@ public void testGetPhotos() throws Exception {
// then
verify(getRequestedFor(urlPathEqualTo("/v2/photomap.locations"))
.withQueryParam("bbox", equalTo("0.1,52.2,0.2,52.3"))
- .withQueryParam("fields", equalTo("id,caption,categoryId,metacategoryId,hasVideo,videoFormats,thumbnailUrl,shortlink"))
+ .withQueryParam("fields", equalTo("id,caption,datetime,categoryId,metacategoryId,hasVideo,videoFormats,thumbnailUrl,shortlink"))
.withQueryParam("thumbnailsize", equalTo("640"))
.withQueryParam("limit", equalTo("45"))
.withQueryParam("key", equalTo("myApiKey")));
@@ -230,22 +264,23 @@ public void testGetPhotos() throws Exception {
iterator.next();
Photo photo4 = iterator.next();
- assertThat(iterator.hasNext(), is(false));
-
- assertThat(photo4.id(), is(82169));
- assertThat(photo4.caption(), is("Link from Clerk Maxwell Road to the West Cambridge site"));
- assertThat(photo4.category(), is("cycleways"));
- assertThat(photo4.metacategory(), is("other"));
- assertThat(photo4.thumbnailUrl(), is("https://www.cyclestreets.net/location/82169/cyclestreets82169-size640.jpg"));
- assertThat(photo4.url(), is("http://cycle.st/p82169"));
- assertThat(photo4.position(), is(new GeoPoint(52.209908, 0.094543)));
- assertThat(photo4.isPlaceholder(), is(false));
- assertThat(photo4.hasVideos(), is(true));
+ assertThat(iterator.hasNext()).isFalse();
+
+ assertThat(photo4.id()).isEqualTo(82169);
+ assertThat(photo4.caption()).isEqualTo("Link from Clerk Maxwell Road to the West Cambridge site");
+ assertThat(photo4.datetime()).isEqualTo(1466693269L);
+ assertThat(photo4.category()).isEqualTo("cycleways");
+ assertThat(photo4.metacategory()).isEqualTo("other");
+ assertThat(photo4.thumbnailUrl()).isEqualTo("https://www.cyclestreets.net/location/82169/cyclestreets82169-size640.jpg");
+ assertThat(photo4.url()).isEqualTo("https://cycle.st/p82169");
+ assertThat(photo4.position()).isEqualTo(new GeoPoint(52.209908, 0.094543));
+ assertThat(photo4.isPlaceholder()).isFalse();
+ assertThat(photo4.hasVideos()).isTrue();
List videos = (List)photo4.videos();
- assertThat(videos.size(), is(2));
+ assertThat(videos.size()).isEqualTo(2);
Video video = videos.get(1);
- assertThat(video.url(), is("http://www.cyclestreets.net/location/20588/cyclestreets20588.flv"));
- assertThat(video.format(), is("flv"));
+ assertThat(video.url()).isEqualTo("https://www.cyclestreets.net/location/20588/cyclestreets20588.flv");
+ assertThat(video.format()).isEqualTo("flv");
}
@Test
@@ -271,15 +306,15 @@ public void testGeoCoder() throws Exception {
.withQueryParam("q", equalTo("High"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(geoPlaces.size(), is(5));
+ assertThat(geoPlaces.size()).isEqualTo(5);
GeoPlace place = geoPlaces.get(1);
- assertThat(place.name(), is("The High"));
- assertThat(place.near(), is("Essex, East of England"));
- assertThat(place.coord(), is((IGeoPoint)new GeoPoint(51.769678, 0.0939271)));
+ assertThat(place.name()).isEqualTo("The High");
+ assertThat(place.near()).isEqualTo("Essex, East of England");
+ assertThat(place.coord()).isEqualTo(new GeoPoint(51.769678, 0.0939271));
// not cached - REST request will be made 6 times
List requests = findAll(getRequestedFor(urlPathEqualTo("/v2/geocoder")));
- assertThat(requests, hasSize(6));
+ assertThat(requests).hasSize(6);
}
@Test
@@ -292,7 +327,7 @@ public void testRegisterReturnsOk() throws Exception {
.withBodyFile("registration-ok.json")));
// when
- Registration.Result result = apiClient.register("arnold", "cyberdyne101", "The Terminator", "101@skynet.com");
+ Result result = apiClient.register("arnold", "cyberdyne101", "The Terminator", "101@skynet.com");
// then
verify(postRequestedFor(urlPathEqualTo("/v2/user.create"))
@@ -300,8 +335,8 @@ public void testRegisterReturnsOk() throws Exception {
.withRequestBody(equalTo("username=arnold&password=cyberdyne101&name=The%20Terminator&email=101%40skynet.com"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(result.ok(), is(true));
- assertThat(result.message(), containsString("Your account has been registered"));
+ assertThat(result.ok()).isTrue();
+ assertThat(result.message()).contains("Your account has been registered");
}
@Test
@@ -314,15 +349,15 @@ public void testRegisterReturnsError() throws Exception {
.withBodyFile("api-error.json")));
// when
- Registration.Result result = apiClient.register("username", "pwd", "name", "email@bob.com");
+ Result result = apiClient.register("username", "pwd", "name", "email@bob.com");
// then
verify(postRequestedFor(urlPathEqualTo("/v2/user.create"))
.withHeader("Content-Type", equalTo("application/x-www-form-urlencoded"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(result.ok(), is(false));
- assertThat(result.message(), containsString("Your account could not be registered."));
+ assertThat(result.ok()).isFalse();
+ assertThat(result.message()).contains("Your account could not be registered.");
}
@Test
@@ -343,9 +378,9 @@ public void testAuthenticateReturnsOk() throws Exception {
.withRequestBody(equalTo("identifier=precious&password=9nazgul"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(result.ok(), is(true));
- assertThat(result.name(), is("Bilbo Baggins"));
- assertThat(result.email(), is("bilbo@bag-end.com"));
+ assertThat(result.ok()).isTrue();
+ assertThat(result.name()).isEqualTo("Bilbo Baggins");
+ assertThat(result.email()).isEqualTo("bilbo@bag-end.com");
}
@Test
@@ -358,7 +393,7 @@ public void testSendFeedbackReturnsOk() throws Exception {
.withBodyFile("feedback-ok.json")));
// when
- Feedback.Result result = apiClient.sendFeedback(1234, "Comments I want to make", "My Name", "ballboy@wimbledon.com");
+ Result result = apiClient.sendFeedback(1234, "Comments I want to make", "My Name", "ballboy@wimbledon.com");
// then
verify(postRequestedFor(urlPathEqualTo("/v2/feedback.add"))
@@ -366,8 +401,8 @@ public void testSendFeedbackReturnsOk() throws Exception {
.withRequestBody(matching("type=routing&itinerary=1234&comments=Comments%20I%20want%20to%20make&name=My%20Name&email=ballboy%40wimbledon.com"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(result.ok(), is(true));
- assertThat(result.message(), containsString("Thank you for submitting this feedback"));
+ assertThat(result.ok()).isTrue();
+ assertThat(result.message()).contains("Thank you for submitting this feedback");
}
@Test
@@ -377,6 +412,7 @@ public void testGetPhotomapCategories() throws Exception {
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
+ .withHeader("Cache-Control", "public, max-age=1209600")
.withBodyFile("photomap-categories.json")));
// when
@@ -390,20 +426,20 @@ public void testGetPhotomapCategories() throws Exception {
// then
verify(getRequestedFor(urlPathEqualTo("/v2/photomap.categories"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(categories.categories().size(), is(18));
- assertThat(categories.metaCategories().size(), is(5));
+ assertThat(categories.categories().size()).isEqualTo(18);
+ assertThat(categories.metaCategories().size()).isEqualTo(5);
PhotomapCategory category = categories.categories().get(12);
- assertThat(category.getTag(), is("destinations"));
- assertThat(category.getName(), is("Destination"));
- assertThat(category.getDescription(), is("A place where you might want to visit."));
+ assertThat(category.getTag()).isEqualTo("destinations");
+ assertThat(category.getName()).isEqualTo("Destination");
+ assertThat(category.getDescription()).isEqualTo("A place where you might want to visit.");
PhotomapCategory metaCategory = categories.metaCategories().get(3);
- assertThat(metaCategory.getTag(), is("any"));
- assertThat(metaCategory.getName(), is("Misc"));
- assertThat(metaCategory.getDescription(), is("Non-specific"));
+ assertThat(metaCategory.getTag()).isEqualTo("any");
+ assertThat(metaCategory.getName()).isEqualTo("Misc");
+ assertThat(metaCategory.getDescription()).isEqualTo("Non-specific");
// caching should mean the REST request is only made once
List requests = findAll(getRequestedFor(urlPathEqualTo("/v2/photomap.categories")));
- assertThat(requests, hasSize(1));
+ assertThat(requests).hasSize(1);
}
@Test
@@ -426,44 +462,44 @@ public void testUploadPhotoReturnsOk() throws Exception {
.withRequestBody(matching(".*username.*arnold.*password.*cyberdyne101.*longitude.*-0.5.*latitude.*53.*datetime.*12345678.*category.*scifi.*metacategory.*evilrobots.*caption.*The Cyberdyne Model 101.*"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(result.ok(), is(true));
- assertThat(result.url(), is("https://www.cyclestreets.net/location/64001/"));
+ assertThat(result.ok()).isTrue();
+ assertThat(result.url()).isEqualTo("https://www.cyclestreets.net/location/64001/");
}
@Test
- public void testGetJourneyXml() throws Exception {
+ public void testGetJourneyJson() throws Exception {
// given
- stubFor(get(urlPathEqualTo("/api/journey.xml"))
+ stubFor(get(urlPathEqualTo("/api/journey.json"))
.willReturn(aResponse()
.withStatus(200)
- .withHeader("Content-Type", "text/xml")
- .withBodyFile("journey.xml")));
+ .withHeader("Content-Type", "text/json")
+ .withBodyFile("journey-v1api.json")));
// when
- String journeyXml = apiClient.getJourneyXml("balanced",
- "mySetOfItineraryPoints",
- "2016-07-03 07:51:12",
- null,
- 24);
+ String journeyJson = apiClient.getJourneyJson("balanced",
+ "mySetOfItineraryPoints",
+ "2016-07-03 07:51:12",
+ null,
+ 24);
// N.B. if you try putting a realistic set of itinerary points, Wiremock barfs at the presence
// of the unencoded pipe character (see https://github.com/square/retrofit/issues/1891).
// then
- verify(getRequestedFor(urlPathEqualTo("/api/journey.xml"))
+ verify(getRequestedFor(urlPathEqualTo("/api/journey.json"))
.withQueryParam("plan", equalTo("balanced"))
.withQueryParam("itinerarypoints", equalTo("mySetOfItineraryPoints"))
.withQueryParam("leaving", equalTo("2016-07-03 07:51:12"))
.withQueryParam("speed", equalTo("24"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(journeyXml, is(notNullValue()));
- assertThat(journeyXml, containsString("xml"));
+ assertThat(journeyJson).isNotNull();
+ assertThat(journeyJson).contains("{");
}
@Test
public void testGetBlogEntries() throws Exception {
// given
- stubFor(get(urlPathEqualTo("/blog/feed/"))
+ stubFor(get(urlPathEqualTo("/news/feed/"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/rss+xml; charset=UTF-8")
@@ -478,16 +514,16 @@ public void testGetBlogEntries() throws Exception {
}
// then
- verify(getRequestedFor(urlPathEqualTo("/blog/feed/"))
+ verify(getRequestedFor(urlPathEqualTo("/news/feed/"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(blog, is(notNullValue()));
- assertThat(blog.mostRecentTitle(), is("CycleHack Cambridge 2016"));
- assertThat(blog.mostRecent(), is("Sun, 10 Apr 2016 18:39:49 +0000"));
+ assertThat(blog).isNotNull();
+ assertThat(blog.mostRecentTitle()).isEqualTo("Cyclescape website redesign coming soon");
+ assertThat(blog.mostRecent()).isEqualTo("Thu, 02 Jan 2020 20:25:56 +0000");
// caching should mean the REST request is only made once
- List requests = findAll(getRequestedFor(urlPathEqualTo("/blog/feed/"))
+ List requests = findAll(getRequestedFor(urlPathEqualTo("/news/feed/"))
.withQueryParam("key", equalTo("myApiKey")));
- assertThat(requests, hasSize(1));
+ assertThat(requests).hasSize(1);
}
}
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/blogfeed.xml b/libraries/cyclestreets-core/src/test/resources/__files/blogfeed.xml
index a3f2415dd..d6aec3c77 100644
--- a/libraries/cyclestreets-core/src/test/resources/__files/blogfeed.xml
+++ b/libraries/cyclestreets-core/src/test/resources/__files/blogfeed.xml
@@ -1,300 +1,440 @@
- CycleStreets blog
-
- https://www.cyclestreets.net/blog
- News from CycleStreets
- Sun, 24 Apr 2016 08:00:54 +0000
+ News – CycleStreets
+
+ https://www.cyclestreets.org
+
+ Wed, 15 Apr 2020 12:12:24 +0000
en-GB
- hourly
- 1
- https://wordpress.org/?v=4.5.3
+
+ hourly
+
+ 1
+ https://wordpress.org/?v=5.4.1
+
+
+ https://www.cyclestreets.org/wp-content/uploads/favicon.ico
+ News – CycleStreets
+ https://www.cyclestreets.org
+ 32
+ 32
+
-
-
CycleHack Cambridge 2016
- https://www.cyclestreets.net/blog/2016/04/10/cyclehack-cambridge-2016/
- Sun, 10 Apr 2016 18:39:49 +0000
+ Cyclescape website redesign coming soon
+ https://www.cyclestreets.org/news/2020/01/02/cyclescape-website-redesign/
+ https://www.cyclestreets.org/news/2020/01/02/cyclescape-website-redesign/#respond
+
-
-
-
-
-
-
- http://www.cyclestreets.net/blog/?p=3094
-
- CycleHack is a 48-hour event aiming to make cities cycle-friendly through reducing the barriers to cycling and prototyping new ideas to improve the cycling experience and encourage more and safer cycling. More than 40 other cities around the world have signed up to host CycleHack events in their communities over the weekend of 24 to 25 June, 2016.
-
-Cambridge, home of CycleStreets, will be joining cities around the world for a weekend-long CycleHack event, which will be held at Anglia Ruskin University and other locations where specialist equipment may be required. Participants will be encouraged to test their ideas and prototypes around town during the event.
-CycleHack was launched in 2014 in Glasgow and has since grown to a global event. In 2015 CycleHack had more than 600 participants from over 25 countries across five continents. 67% of participants were inspired to cycle more. In 2016 the event is set to be even bigger with more than 40 cities already registered.
- CycleHack Cambridge is hoping to attract a whole range of people from developers, makers and data scientists to non-technical artists, designers and those who are interested in cycling and have some great ideas. We also want to include representation from all corners of our diverse cycling community and want to see lots of students and young people taking part. This event will bring together the key elements of our Cambridge culture: cycling, innovation and technology.
-The event encourages participants to prototype and test their ideas during the weekend to see how they will work in their intended context. Solutions can fall into one of the five CycleHack categories; digital, physical, policy, local plan, event/campaign. Hacks will be loaded to the online open source catalogue to show how the ideas can be replicated. Prizes will be awarded to the best hacks.
-CycleStreets is one of the partners organising the event. We’ll be on hand to help out and give advice to people considering doing a digital hack. Perhaps you’ve never used an API (a data interface – like the CycleStreets API for instance) before or don’t know what it is? We can help you get started.
-Cambridge Cycling Campaign is the main organiser of the event. Other partners include the Smart Cambridge Programme (Cambridge County Council) and CoDE Research Institute at Anglia Ruskin University.
-There are more details on the Facebook event page about the event , and you can register online .
+ Thu, 02 Jan 2020 20:25:56 +0000
+
+
+ https://www.cyclestreets.net/blog/?p=3435
+
+
+ Cross-posted from our Cyclescape blog :
+Over the last five years, we’ve received lots of feedback about our Cyclescape website, which started in 2012, as well as much experience ourselves as users.
+As a result of funding from our new project StreetFocus , we’re pleased to announce that we will shortly be starting work on a full-scale redesign of the Cyclescape site. We expect the design work to be finalised in February and will then be rolling out as developer time permits shortly after that.
+This will be a full-scale revamp. As well as a fresh visual look and a new mobile-friendly interface, this will tackle seven key areas of usability:
+
+Generally, reorganising screens to reduce confusion and make the site concepts much clearer and easier to understand;
+User onboarding process is poor – currently users have to go through far too many screens to get set up;
+Lightweight issue reporting model, so that many more issues can be reported in a more lightweight way;
+The issues map is unusable when many area-wide issues present – we want the map to be a stronger focus, and much nicer;
+Subscription by theme , e.g. ‘cycle parking’ rather than purely by geographical area;
+Dealing with general chat discussions that are neither geographical nor administrative, which has always never really worked;
+Planning application integration to add new features such as automatically linking to key documents.
+
+
+Please do take the opportunity to fill in our survey to give us your thoughts on the site.
+
+An unusable map – one of the many problems we plan to tackle
]]>
+
+ https://www.cyclestreets.org/news/2020/01/02/cyclescape-website-redesign/feed/
+ 0
+
+
-
-
Urban Cycle Parking website goes live across London
- https://www.cyclestreets.net/blog/2016/03/21/urban-cycle-parking/
- https://www.cyclestreets.net/blog/2016/03/21/urban-cycle-parking/#comments
- Mon, 21 Mar 2016 10:31:30 +0000
+ London Cycling Data
+ https://www.cyclestreets.org/news/2019/10/01/london-cycling-data/
+ https://www.cyclestreets.org/news/2019/10/01/london-cycling-data/#respond
+
-
+ Tue, 01 Oct 2019 12:00:32 +0000
+
-
-
- http://www.cyclestreets.net/blog/?p=3074
-
- A new website, Urban Cycle Parking , built by CycleStreets, has been launched by London Cycling Campaign and Transport for London , aiming to outline where existing bike parking facilities are available in and around the capital and invites people to highlight existing facilities as well as outlining where more is needed.
-The site builds on the crowdsourcing components of our data API , which had various enhancements made to enable auditing facilities.
-
-Image credit: Primary Image
-London Cycling Campaign’s Chief Executive Ashok Sinha commented:
-“Substantially more high quality cycle parking at stations and on streets is vital to sustain the welcome growth in cycle use.
-“The launch of this interactive Urban Cycle Parking website is a great opportunity for London cyclists to play an active role in improving cycling provision and to suggest the right places to install cycle stands.”
-Cyclists just need to click on the map, or take a photo (which will auto-locate the image on a modern phone), and add a few details, such as the number of stands which would be useful. After agreeing to the open data license, the location is added, so that TfL can consider the suggestions.
-
-The site has been in public beta for several boroughs. Various improvements have been made to the site during this period to enable a wider rollout.
-The visual design of the site, which is mobile-friendly, was created by Mike from Primary Image , who was a pleasure to work with. We then worked with this design to implement the functionality.
-The Urban Cycle Parking site replaces the previous Cycle Parking 4 London site that we created several years ago for LCC.
+ https://www.cyclestreets.org/?p=3502
+
+
+ Transport for London recently released a major new dataset detailing every piece of cycle infrastructure in Greater London.
+We’re proud to announce that we’re working with London Cycling Campaign (LCC) on a new project to help maintain that dataset, for TfL.
+This project will use LCC’s active volunteers and borough groups – the people who know how cycling infrastructure in London changes – to correct and update information for all London Boroughs. The database was surveyed during 2017 and 2018 and thus now needs updating to include all the improvements to cycling put in place since then (and to flag up things that have been removed or that need repair). This is a pilot project to see if large numbers of volunteers can be activated to feed data into this project alongside other partners.
+We’ve created a new website, London Cycling Data , for LCC to enable just that, and this is now live.
+
+The site interacts with a new suite of data interfaces in the CycleStreets API , for infrastructure auditing.
+The visual design for the site is by Mike of Primary Image.
+You can read more about the site and the project as a whole on LCC’s website.
+
]]>
- https://www.cyclestreets.net/blog/2016/03/21/urban-cycle-parking/feed/
- 1
+
+ https://www.cyclestreets.org/news/2019/10/01/london-cycling-data/feed/
+ 0
+
+
-
-
Students: Get paid to write code for CycleStreets/OpenStreetMap this summer with GSOC 2016
- https://www.cyclestreets.net/blog/2016/03/19/gsoc-2016-projects/
- Sat, 19 Mar 2016 18:14:27 +0000
+ Our talk at State of the Map 2019: Is the OSM data model creaking?
+ https://www.cyclestreets.org/news/2019/09/22/sotm2019/
+ https://www.cyclestreets.org/news/2019/09/22/sotm2019/#respond
+
+ Sun, 22 Sep 2019 13:48:29 +0000
+
+
+ https://www.cyclestreets.net/blog/?p=3423
- http://www.cyclestreets.net/blog/?p=3068
-
- Each year, Google runs a summer programme called Google Summer of Code (GSOC) , where students get involved in coding for open source projects like ours and are paid for their time.
-We (CycleStreets) have created a project ideas list , which would be undertaken under the umbrella of the OpenStreetMap organisation, and are willing to mentor a student or students on these.
- By way of background, CycleStreets is a social enterprise based in Cambridge, UK. We run the cycle journey planner website at www.cyclestreets.net , provide a leading bicycle routing API data interface used by a range of third-party apps and websites , and run a number of projects in the field of cycling advocacy. Many of our projects are open source .
- We use open data from OpenStreetMap (sometimes referred to as a kind of ‘wikipedia for maps’). OSM is a community/ecosystem doing a lot of leading-edge work in the area of open geo data, and there are many fields within it that could interest computer science students, engineering students, and others.
-As well as our own project list , there are are other project ideas from elsewhere within the OpenStreetMap community that we’d also strongly encourage students to consider too.
-Students must apply via the GSOC website by Friday 25th March, 7pm GMT, but it’s very important to work via the process given in the OpenStreetMap GSOC 2016 page .
-Please note that GSOC is a highly competitive process, and only those students who come up with the best proposals that will assist OpenStreetMap are likely to be accepted.
-We’d be very happy to meet in central Cambridge (UK) (or via Skype if you’re not based here) to discuss potential projects, and/or to provide some background to OpenStreetMap, whether you are thinking about one of our (CycleStreets) projects or another project listed on the OSM project ideas page. Please do get in touch with us in the first instance.
+
+ We gave a talk at the State of the Map 2019 conference, the main annual meeting for OpenStreetMap, whose data we use for our cycle routing.
+Our talk was entitled “Is the OSM data model creaking?” .
+A video of the talk is now online:
+
+Here are the slides from the talk:
+Is the OSM data model creaking? (PDF, 16MB)
+
+OpenStreetMap was designed to enable ordinary people to create open geodata that anyone can use and maintain easily. Traditional GIS concepts such as layers are dispensed with in order to make editing simple and accessible. In the same way that the web would never have taken off if HTML were not so accessible and tolerant of mistakes, this simplicity in OSM has meant a low barrier to involvement.
+However, as OSM is becoming more widely used in the mainstream, the need for accuracy and quality is becoming more and more important. Cyclists need detailed turn data to enable high-quality routing that takes full account of safety. Satnav companies, need lane data, which is difficult to represent accurately. Pedestrian routing is barely in its infancy and high quality routing for people walking or using wheelchairs is hard to achieve.
+At its root, OSM tries to represent spaces as flows (lines). This results in fundamental compromises and inaccuracies. What is good for routing is not always good for cartography, and vice-versa.
+For instance, a street containing cycleways with pavements either side is usually represented as a single line with attributes. However, it is extremely challenging to represent properly all the parts of the street, and in general people simply don’t bother: a single line with large numbers of attributes is unwieldy to edit (even when hidden by editor GUIs), and just as challenging for a router to interpret. Continual changes in the width of a street cannot be easily represented without segmenting the street heavily and creating a mess. Temporary disappearance of lanes makes editing complex. Routing ultimately ends up as a lowest common denominator result.
+The alternative method of representing this same street is as a series of individual lines. But this is equally problematic. In this model, the street loses its coherence as a single entity – humans think of it as a street with multiple uses (walking, cycling, driving, trees). Where people have done this, attributes such as street names need to be kept in sync, and in practice separate pavements often fail to have names attached. Concepts such as the ability to cross from one side of the road to the other (or even switch lanes) are not modelled, with the result that a router may take the user to the end of the street then back down. And cartography ends up showing a series of parallel lines which looks messy and does not match the human perception of a street.
+The bicycle tagging page on OSM provides a perfect demonstration of the current problem: https://wiki.openstreetmap.org/wiki/Bicycle It shows the complexity of representing many common scenarios, with increasingly incomprehensible tagging combinations. No router implements anything like all of this, and even expert OSM contributors would shy away at bothering to add this data.
+Cycleways indeed are a good general example of inconsistent tagging. Cycleways separate to a road are sometimes tagged as an attribute of the street and sometimes as a separate geometry. What about a hybrid/stepped cycle lane of the kind seen in Copenhagen – is that a cycle lane or a cycleway? Do lane counts include cycle lanes or not? How is obstructive car parking represented? Is the one-way indication applicable to the cycle lane on the road? And so on.
+Another example is junctions. Should traffic signals be treated spatially (i.e. represent the location of the traffic heads), or should they be treated linearly so that routing works properly? How should the linear model work accurately when there is only a single geometry for multiple directions? Have a look at the roads around the Arc De Triomphe in Paris – it is completely impossible for a routing engine to work out exactly how many signal delays should actually be attributed based on the presence of the marking of traffic signals in the data: https://www.openstreetmap.org/#map=17/48.87391/2.29536
+This talk will discuss these cases, and provide a starting point for discussion on what should be done to improve the situation. As people are ever keener to add more detail to the map, and as more and more mainstream users look to OSM, we have to ask whether the current model is arguably creaking too heavily. Is there a way that we can represent spaces as a set of interconnected flows in some way?
+The speaker, Martin Lucas-Smith, is one of the developers of CycleStreets, one of the earliest and most established dedicated cycle routing engines. As such, he has spent many years considering the kinds of tradeoffs represented by the current OSM data model.
]]>
+
+ https://www.cyclestreets.org/news/2019/09/22/sotm2019/feed/
+ 0
+
+
-
-
TransportHack @ Smarter Travel LIVE!
- https://www.cyclestreets.net/blog/2016/03/14/transporthack-smarter-travel-live/
- Mon, 14 Mar 2016 16:13:32 +0000
+ TfL Cycling Infrastructure Database
+ https://www.cyclestreets.org/news/2019/05/10/tfl-cid/
+ https://www.cyclestreets.org/news/2019/05/10/tfl-cid/#comments
+
-
+ Fri, 10 May 2019 16:01:11 +0000
-
+ https://www.cyclestreets.net/blog/?p=3413
- http://www.cyclestreets.net/blog/?p=3054
-
- Matt Whittle writes:
-This weekend we attended the Smarter Travel Live hack weekend . The aim of the hack event was to produce an output to support one of five challenges set by various organisations. We chose to tackle the Carplus challenge which was to try and find a way to reduce the use of cars as the primary transport means around the Lake District.
-To begin the challenge we brainstormed several possible ways in which provisions could be made to facilitate the ease of switching peoples mode of transport, these included:
-
-Increasing cycling space on trains and buses
-Car sharing schemes
-Bike hire and sharing scheme
-
-It soon became apparent that as a group we felt that a hire and sharing scheme would work best for the area, therefore we set off to try and gather evidence of how popular the scheme could potentially be as well as providing evidence to where the scheme would best be suited.
-We were provided with a travel survey which listed the origins and destinations of c. 8,000 visitors to the park, this listed the mode of transport as well as the number of people making the journey. From this data we were able to visualize the flows of people to the park as shown below.
-
-From this data we then began to summarize what the most popular locations were in the region. For this we set out a criteria that popular locations had to attract more than 100 visitors from the data. The results can be seen below.
-
-This then lead to the question, where do current cyclists cycle in the Lake district? By using the flow data and some clever use of CycleStreets API we were then able to allocate all of the current cycle flows to the route network.
-The approach, using origin-destination data routed to the on and off road travel network using CycleStreets.net, is similar to that used in the Propensity to Cycle Tool (PCT) . An early draft of a report describing the methods in more detail is available.
-
-The analysis showed the the current popular cycle network had one main entry point to the lake the district, the thick red line flowing from Milnthorpe, through Kendal and out to Windermere. Beyond the corridor the data supported evidence that flows up to Ambleside, Grasmere, Coniston and Hawkshead were also popular.
-The next question to answer we decided to answer involved trying to discover which car journeys could potentially be replaced by cycle journeys. Using the flow data and R code we managed to find all of the car journeys in the data set that were under 10km. Once again, using the CycleStreets API these were allocated to the road network and then visualized.
-
-This visualisation supports the idea developed in from the cycling data that cycling could be popular in the north Windermere area. A 10km journey would take an estimated 30 mins when travelling at a reasonable cycling speed of 12mph (19kmh).
-These two visualizations therefore supported out idea that cycling could be a popular activity in the north Windermere/ Kendal corridor area. However what we had overseen was where should this system be implemented e.g. hire locations and how should it be carried out e.g. new infrastructure or rework existing infrastructure. Some research into cycle hire in the Lake District was carried out and we discovered that there was already a fairly large economy in the region, however the system does not support A to B trips, it is primarily for users to hire bikes from a location and drop off at the same location. Plans have already been suggested for cycle hire in Kendal . What we therefore propose is that a cycle hire system could work by working with the current bicycle hire network (see below, these are current e-bike cycle hire locations from electric bicycle network ) to support A to B transportation by bicycle.
-
-Using all of this analysis we then created a ‘core’ cycle network based on the popular destinations, current cycling, car journeys less than 10km and the existing hire locations. This is where we suggest cycling infrastructure should be placed initially. Once this is built extentsions could be built to Grasmere, Coniston, Troutbeck and Grizedale in order to link up to other popular locations.
-
-Our hack has therefore provided evidence to support a cycle hire network in the Lake District. The analysis has suggested that cycle journeys could replace a large amount of car journeys in the region, therefore reducing congestion. The initial brief stated that people wanted to get out of their cars when they were visiting the Lake District, this has provided a potential solution to that need.
-We put all of our data, code and visualisations on Github.
-You can view the map of all the spatial data created for the project .
-Thanks to Landor and Transport API for organising such a great event.
+
+
+Transport for London (TfL) have created a new database of cycling infrastructure, containing 240,000 assets, covering all of Greater London. This is proposed to be released as open data.
+This groundbreaking database contains every cycle infrastructure asset within Greater London, including assets on and off-carriageway. The assets surveyed are: cycle parking; signals; signage; traffic calming measures; restricted points (e.g. steps); advanced stop lines; crossings; cycle lanes/tracks; and restricted routes (e.g. pedestrian only routes).
+“The world’s first Cycling Infrastructure Database will be the most comprehensive database of cycling infrastructure ever collected in London. Over the past 18 months, TfL has amassed data on every street in London, cataloguing almost 146,000 cycle parking spaces, 2,000 km of cycle lanes and more than 58,000 cycle signs and street markings. This information will be released as open data alongside a new digital map of cycle routes, will make journey planning and cycle parking much easier, as well as offering valuable information to TfL and the boroughs for planning future investment in cycling.â€
+TfL is keen to make this available to the OpenStreetMap community under a compatible open license, to ensure maximum use of the CID. TfL is also potentially willing to consider tool development to help facilitate sensitive merging in of this data. OpenStreetMap is the street data on which CycleStreets is built, so the better data available to OSM, the better our routing can become.
+Demonstrator map
+We’ve created a demonstrator map , for the purposes only of evaluation by the OSM community at this stage.
+This demonstrator map contains only one of the 25 areas that have been surveyed.
+We are specifically seeking comments on data quality and usefulness of this data from the OSM community. Initial analysis by CycleStreets is that the data is of excellent quality, and very suitable for conflation into OSM, to increase both comprehensiveness and metadata quality.
+
+Usage notes: The controls on the right of the map allow the different feature types to be selected. The OSM layer (available at zoom level 19+) also provides a live feed from the OSM API, to enable quick comparisons. The two photos of each asset are in the process of being supplied; those already available and cleared in GDPR terms are included in the popup.
+It is stressed that at this point, no permission is given for re-use of the data in any way, but TfL strongly intends to make this available in future. All 25 areas would be covered in the final data release, not merely the one shown currently in the demonstrator map.
+Feedback
+Feedback is very strongly encouraged, as soon as possible.
+Please do discuss the data and related aspects noted above on the talk-gb mailing list .
+Feedback and questions can also be e-mailed us.
+We are happy to provide any clarifications, which will be added to this page, as a central repository of information about the project.
+More detail
+We’ve set up a new TfL CID project wiki page on the OpenStreetMap Wiki.
]]>
+
+ https://www.cyclestreets.org/news/2019/05/10/tfl-cid/feed/
+ 2
+
+
-
-
Cycle North Staffs app created by CycleStreets
- https://www.cyclestreets.net/blog/2016/02/06/cycle-north-staffs-app/
- https://www.cyclestreets.net/blog/2016/02/06/cycle-north-staffs-app/#comments
- Sat, 06 Feb 2016 23:47:30 +0000
+ Android app beta programme
+ https://www.cyclestreets.org/news/2018/08/31/android-app-beta-programme/
+ https://www.cyclestreets.org/news/2018/08/31/android-app-beta-programme/#respond
+
-
-
+ Fri, 31 Aug 2018 16:04:21 +0000
+
+ https://www.cyclestreets.net/blog/?p=3391
- http://www.cyclestreets.net/blog/?p=3039
-
- We’ve created a new cycling app for Stoke-on-Trent City Council.
-The Cycle North Staffs app, developed by CycleStreets, is here to help you to get the most out of your cycling. The app is packed with routes across Stoke-on-Trent and Newcastle-under-Lyme to get you to school, to work, or for leisure. It’s suitable for all ages and abilities.
-The app is available for both iPhone and Android , and is available free of charge:
-
-The app has a wide range of features:
-
-Plan cycle routes from A-B
-Gives a choice of routes: fastest, quietest, and balanced option
-Browse leisure routes – fancy a pleasant hour’s ride somewhere nice?
-Browse points of interest, such as bike shops or tourist attractions
-Shows time, distance and quietness level
-Shows how many calories you would use
-Avoids hills automatically where possible, and shows the elevation profile
-Calculates CO2 saving compared to a car
-Browse photos and videos of cycle facilities in the area
-Save your favourite places for easy access
-
-
-
-The app makes use of our open-source iOS and Android codebases, helping lower costs to the Council and providing a well-tested codebase.
-We are able to create custom cycle apps and embedded cycle journey planner websites for Local Authorities, companies and others. Do get in touch if this might be of interest to your organisation.
-The apps have been possible thanks to the great work of our developers Neil and Jez.
+
+ You can now sign up as a beta-tester for our Android app, simply by opting into the beta programme in the Google Play Store. This enables you to get the latest improvements and give feedback before they are released officially.
+We’re currently making lots of changes to the app (thanks to the great work of our devs Oliver Lockwood and Jez Higgins), with lots of bug-fixes, improvements, and a new design coming.
+The app is open source and we very much welcome new contributions of code, bug reports, etc.
+Install our Android beta app
+1. On your phone, go to the CycleStreets page in the Google Play Store .
+
+
+
+2. Scroll down to the bottom of the page, and find the “Become a beta tester” box, and click on “I’m in”:
+
+
+
+3. This will then show “Beta sign-up in progress…”:
+
+
+
+4. After, when you click “Install” at the top of the page, that will install the beta app:
+
+
+
+
+
+Note that this will replace the released version of the app with the latest beta version.
+
]]>
- https://www.cyclestreets.net/blog/2016/02/06/cycle-north-staffs-app/feed/
- 1
+
+ https://www.cyclestreets.org/news/2018/08/31/android-app-beta-programme/feed/
+ 0
+
+
-
-
Cycle commuting analysis of Bristol
- https://www.cyclestreets.net/blog/2015/12/19/bristol-msc-research/
- Sat, 19 Dec 2015 21:03:18 +0000
+ PhD studentship with University of Leeds: Towards data-driven policy development: the case of London’s built cycling infrastructure
+ https://www.cyclestreets.org/news/2018/05/08/phd-studentship-with-leeds/
+ https://www.cyclestreets.org/news/2018/05/08/phd-studentship-with-leeds/#respond
+
-
+ Tue, 08 May 2018 12:00:16 +0000
+
+ https://www.cyclestreets.net/blog/?p=3363
- http://www.cyclestreets.net/blog/?p=3029
-
- We love it when our API comes in useful for academic purposes. This is a guest post by Richard Thomas .
-
-
-
Bristol: Typical cycle commute time
-
-For my MSc dissertation, I investigated determinants of the proportion of people who choose to cycle for their daily commute. Specifically, I wanted to see whether an analysis of realistic cycling routes of a representatively large sample of a city’s population could give improved predictors over existing models.
-From 2011 Census data, I extracted commuting origin/destination data for everyone in the Bristol built-up area in its most detailed form of aggregation (typically accurate to within 500m). I wanted to generate plausible cycling routes for these commutes, then for each of these routes to evaluate metrics (distance, hills, cycle paths, traffic). As census data is available giving the proportion of commuters living in each small area who cycle, multi-variate correlation could then be used to estimate the influence of these routing metrics, together with other known influential population measures taken from the census.
-So how best to perform this cycle routing and evaluate suitable metrics? On both these counts the CycleStreets Journey Planner API proved invaluable (and made my MSc dissertation a feasible proposition!) I had considered using an existing open source routing engine (such as pgRouting or Graphhopper) operating on an extract of the OpenStreetMap database as this would allow me to directly query tags on each node of a route. However the complexity in interpreting OpenStreetMap cycle-related tags is quite daunting (as documented here on CycleStreets.net ).
-Because the API returned not just the route, but details of routed distance, duration, “quietness”, estimated calories required and spot heights, useful metrics could be derived quickly from the JSON data using just Python scripts. It would have been good to more directly quantify dedicated cycle infrastructure along routes: although the “quietness” measure included this, it also included road traffic expectations. Given more time, this could have been done by using the actual route coordinates to interrogate the OpenStreetMap or CycleStreets databases, though this was complicated by API-returned points being only in latitude/longitude format rather than database node/segment numbers. In order to limit the amount of data to be processed (and the load on the CycleStreets API server, routing was limited to the 4 most popular routes from each area, although this still required nearly 16,000 routes to be generated and analyzed!
-
-
-
Summed cycle commute routes (Overview)
-
-The most notable results of these new routing-based metrics (i.e. beyond the key predictor of crow-fly distance) were as follows:
-
-Directness (Crow-fly / Routed Distance): strong indication that cycling was less popular if a reasonable (“balanced”) cycling route was particularly circuitous.
-Max Height Increase (Maximum of sum of all hill climbs for outward or return direction): strong indication (as might be expected) that hills were a strong detractor. This metric was only developed after the MSc was completed; interestingly, in the MSc analysis, the related metric of Effort Ratio (calories / distance) was not a statistically significant indicator.
-Traffic Exposure (Inverse of “Quietness”): Although this metric visually gives a good indication of cycling routes along busy roads and/or away from dedicated cycle infrastructure it was not a statistically significant predictor of cycling. Although not conclusive, this supports other research showing that cyclists are more sensitive to time taken than to pleasantness or safety when it concerns their daily commute (priorities may be different for a leisure ride).
-
-
-
-
Street level detail (OpenCycleMap)
-
-
-More details of the analysis are available in the full dissertation (or short synopsis) . Detailed 2011 census origin/destination data (table WF02 for OA/WZ) was only made available after the end of my MSc (and then only to academics for specific projects). Thus for the MSc, synthetic data was generated based on (publicly available) census data. However, a later reworking of the full analysis using the new WF02 census data gave very similar results showing that lack of public access to detailed statistics need not be a serious impediment to analysis.
-Beyond the key MSc analysis, an interesting spin-off of all the cycle routing was the development of maps (see right and below) that sums the 4 most popular commute routes from the centroid of each census Output Area , giving a good indication of the number of cyclists along individual streets if all these people were to commute by bicycle.
-Thanks again to CycleStreets for making the API available to enable this research project. Data processing was done in Python and SPSS with additional processing and map rendering in the open source QGIS package.
-Richard Thomas
-Editor’s note: We now have a batch routing system available which we’re keen to encourage for academic use like this. It can handle millions of combinations happily – not just the 16,000 combinations noted above!
+
+ An exciting PhD studentship opportunity which we are involved in, cross-posted from the Data Analytics and Society Centre for Doctoral Training website. Deadline: 3rd June 2018.
+
+Towards data-driven policy development: the case of London’s built cycling infrastructure
+In 2013, £913m of funds was allocated over 10 years for investment in London’s cycling infrastructure. Much of this — including guided quietways, protected cycle superhighways and London’s crossrail for the bike — opened in summer 2016. The chief objective: to make cycling ‘a normal part of everyday life […] something people hardly think about […and] something everyone feels comfortable doing’ (Greater London Authority 2013).
+Traditionally, attempts to evaluate such interventions might rely on survey data describing changes in *claimed* behaviour or high-level data from Automatic Traffic Counters describing infrastructure occupancy. The former are often expensive to collect and suffer from numerous (well-documented) biases and the latter are too high-level to capture more subtle changes in behaviour.
+This project will instead use new, large-scale observational datasets – from London’s bikeshare, underground and bus network, from route planning services (CycleStreets.net), user-contributed and social media data — to describe changes in city-wide cycling behaviours pre- and post- the intervention. Crucially, it will identify rich detail around the impact of current investment on behaviour and contribute quantified estimates, under uncertainty, around the impact of future investment.
+Applications are welcomed from those wishing to develop expertise in statistical model building, geospatial data and information visualisation.
+Start Date: October 2018
+Lead Supervisor : Roger Beecham (University of Leeds)
+Other Supervisors : Robert Aykroyd, Robin Lovelace, Stuart Barber
+Partners : University of Leeds
+External partners : CycleStreets.net
+Read more and apply online here.
+
]]>
+
+ https://www.cyclestreets.org/news/2018/05/08/phd-studentship-with-leeds/feed/
+ 0
+
+
-
-
New features added to Cyclescape
- https://www.cyclestreets.net/blog/2015/12/16/new-cyclescape-features/
- Wed, 16 Dec 2015 19:21:28 +0000
+ DfT cycling data for research/OpenStreetMap use – as GeoJSON
+ https://www.cyclestreets.org/news/2018/05/07/dft-cycling-data-for-osm/
+ https://www.cyclestreets.org/news/2018/05/07/dft-cycling-data-for-osm/#respond
+
-
+ Mon, 07 May 2018 21:11:41 +0000
+
+
+ https://www.cyclestreets.net/blog/?p=3384
- http://www.cyclestreets.net/blog/?p=3024
-
- Lots of new features have been added to Cyclescape , our toolkit for cycling advocacy groups.
-Our developer, Nikolai, has been busy, working on piles of improvements and bugfixes.
-As featured on the Cyclescape blog , the latest updates include:
-Street View message replies : While it’s of course possible to navigate off-site, get a Street View link, and return, we’ve taken out that extra stage. The Street View button also tries to find a sensible default location. In a thread, just click on Street View in the reply box, position the map where you want, add a comment and press submit.
-
-Privacy improvements : Some groups have told us that it is important to them that they are able to operate on the basis of member discussions using real names, so that members know who they are talking to. However, we recognise that this could be in conflict with the entirely reasonable desire not to have one’s name on the public internet if wished. Accordingly, we have worked to implement a solution to this, whereby you can set your real name which people in your groups will see, but set a display name for everyone else. Previously the display name was always used.
-Improvements for groups : If you didn’t already know, cycling groups are able to create their own Cyclescape space, giving a custom web address and various personalisations. You can now create a group using the ‘Request new group’ form in the top-right of the groups gallery. Groups can now add a photo to help personalise their page, alongside information about the group.
-
-Search system overhauled to give much better results : We’ve replaced the search result system with a completely new engine that gives much better results. After a week of tuning the results, we think this now seems to find what you’re looking for pretty consistently. Secondly, we’ve added pagination, so you’re no longer limited to one page of results. Another long-awaited improvement is that searches within a group’s area will only return results from that area. So if you’re in, say, Camden Cyclists’ Cyclescape area, you won’t get results polluted with issues from Cambridge, Sheffield, Leeds, or wherever.
-
-Getting discussions by e-mail now better : Did you know that you can also get Cyclescape discussions by e-mail? In fact, Cyclescape can be used like lots of mini e-mail lists, which you can choose to subscribe/unsubscribe to on a per-thread basis. We’ve made a number of improvements. Firstly, you can now enable digests, so you can read what’s happening by getting a single e-mail a day. Next, e-mails are now properly threaded. Also, we’ve added deadline reminders, so you’ll get a reminder a day or two before a date in a thread you’re subscribed to. So fewer excuses for missing deadlines now! A further improvement is that new users, and users newly-subscribed to a group, now receive a welcome e-mail to confirm each of these.
-
-Deadline management improvements : In case you didn’t know, your ‘My Cyclescape’ page has a listing of all the deadlines/dates in the threads you are subscribed to. So it’s easy to get an overview of what’s coming up. We’ve added an iCal feed, so that if you use a calendaring system like Apple Calendar, Google Calendar, Outlook, etc., you can have these events appear automatically in your calendar. Also, if you have e-mail enabled, each deadline/date e-mail will now include an iCal attachment for that deadline/date. Another improvement is that you can now specify a time, rather than purely just a day, when replying in a thread.
-
+
+ A techie post, about some cycling data which may be of use to people researching cycling.
+Back in 2011, we were involved in a project to convert some newly-collected cycling data from the UK’s Department for Transport for use in OpenStreetMap.
+The data was originally collected for the DfT’s Transport Direct project (which has since ceased operation), which was an early attempt to create a government cycle journey planner, launching on almost the same day as CycleStreets itself. In order to ensure that taxpayer value for this expensive (£2.4m) dataset was not lost as Transport Direct fell into disuse, CycleStreets successfully encouraged the DfT to release the data openly and to do so in a way which would encourage use in OpenStreetMap. We helped with that process, and data for cities started to be merged in from 2011.
+The data consists of the cycle network as of 2011 in each city of over 30,000 people. This does however not represent all cycle infrastructure – only where signage is present. Additionally, collection included the Sustrans NCN network.
+Since that time, things have moved on. OpenStreetMap has become the go-to datasource for cycling data internationally. GeoJSON has become the de-facto format for open geographical data. Similarly, Github has become the de-facto location to distribute open data like this.
+As part of a project we have been working on with Leeds University, the Cycling Infrastructure Prioritisation Toolkit (CyIPT) (which we will report on soon), we needed historical data from 2011, and this DfT data was a perfect candidate.
+Accordingly, we have taken the opportunity to recover the data from 2011, and convert it to GeoJSON and republish it, in the hope it might be useful for some people. It is an OpenStreetMap-orientated version of the data published on data.gov.uk. As such it is subject to OSM licensing conditions.
+DfT England Cycling Data 2011
+We would stress that, for almost all uses, CycleStreets instead strongly recommends downloading data from OpenStreetMap , which is topographically routable and is maintained and has far greater geographical coverage. Moreover, the cycle network has changed from 2011-18. However, the data in this repository contains attributes on each geometry which remain often more detailed than OSM. Accordingly, the data is most useful for research purposes and for manual merging into OSM , which is encouraged.
+Full details about the data are given in the README which can be found on the main repository page.
+Coverage:
+
+Example area – Cambridge:
+
]]>
+
+ https://www.cyclestreets.org/news/2018/05/07/dft-cycling-data-for-osm/feed/
+ 0
+
+
-
-
Patrick reports back
- https://www.cyclestreets.net/blog/2015/10/01/patrick-reports-back/
- Thu, 01 Oct 2015 12:00:43 +0000
+ Intern vacancies Summer 2018
+ https://www.cyclestreets.org/news/2018/04/11/intern-2018/
+ https://www.cyclestreets.org/news/2018/04/11/intern-2018/#comments
+
+ Wed, 11 Apr 2018 12:06:15 +0000
+
+ https://www.cyclestreets.net/blog/?p=3299
- http://www.cyclestreets.net/blog/?p=3103
-
- Back in July we introduced our summer intern, Patrick. Here, he reports back on his achievements over the three-month period.
-
-This summer I worked for Cyclestreets as a paid intern for three months. During this time, I became acquainted with the large codebase the site runs on, and made several structural changes to the organisation of the code. After my work at Cyclestreets I feel much more comfortable working with sizeable codebases, and the focus on refactoring and identifying optimisable code has made a huge change in the way I approach coding my own projects.
-One of my main tasks as intern was to refactor the existing codebase into an MVC-style layout. Building on a similar structure designed into Ruby on Rails , we set out to create classes with distinct personalities; views, models and controllers. Establishing rules that govern class content contributes to building a strong framework that isolates code into functioning categories. Once an MVC structure has been implemented, crosstalk between classes can easily be analysed and anomalies can be identified.
-Cyclestreets runs two main environments (contexts) – a GUI environment and an API environment. These two systems had become merged over time due to feature development, so re-establishing the functional dichotomy between these entities became a priority. This was a long process, as a long period of development had led to a proliferation of technical debt in the codebase.
-The concept of refactoring, or changing code structure without altering the code function, reverses this process. It is risky, as refactoring can introduce subtle bugs. It is also very easy, as I found out on several occasions, to dig yourself into a hole as one alteration leads to another, and another, until it is extremely hard to finalise the series of sequential refactorings. To avoid this, refactoring must be done systematically.
-Design patterns can be identified that provide targets for refactoring, and over the summer I became familiar with identifying duplicated code, contrived complexity, over-large classes, and long methods, to name a few of the ‘bad smells’ I learned to identify in the code.
-Rolling out my changes to the main servers was always a nail-biting moment. I didn’t want to be “that guy” that brought down the system while users in London were relying on the excellent CycleStreets app to get to work during the Underground strikes that went on during summer. [Ed.: Don’t worry, we deploy in dev first :) ]
-The team at CycleStreets were extremely supportive, and I thank them for the time they took to broaden my knowledge of PHP and good coding practices. It was a very enjoyable summer, one I will look back on fondly as in the years to come I design my own apps and come across the issues I learned to rectify during this internship. Thank you Martin and Simon!
-Martin and Simon thank you back, Patrick – the codebase has moved forward significantly over the summer, with several long-running problems resolved. You were a pleasure to work with, arguing your case well when there were difficult decisions to take in the code, and taking on feedback.
+
+ CycleStreets is seeking one or more paid interns to work on a variety of projects this summer.
+About CycleStreets
+
+CycleStreets is a social enterprise based in Cambridge, working to help get more people cycling by the provision of information on cycle-friendly routes, and various tools for use by cycle advocacy groups.
+We are best known for our journey planner , aimed at finding practical cycle routes in urban environments. To do that effectively requires collating data from a variety of sources and configuring a routing engine to make the same decisions a knowledgeable cyclist would make to find a route to their destination. We are aiming to provide the highest quality cycle routing in the world that tries to ‘think like a cyclist’.
+Our bike routing is used by our own website and apps, as well as in a range of third-party apps (such as Citymapper , Bike Hub , London cycle hire apps, etc.), as well as a variety of transport companies (SDG, Virgin East Coast, mxdata, Traveline Wales/Scotland, etc.). It is also being used for academic research and transport planning, e.g. the Propensity to Cycle Tool and CyIPT projects.
+The website also includes our Photomap – around 80,000 user-contributed photos of cycling related infrastructure from around the UK and beyond. These are used by activists to promote good practice and highlight problems to avoid in future. The Photomap also powers Local Authority websites such as the Urban Cycle Parking website for TfL. Further tools that present transport data that affects cycling are also in development.
+CycleStreets also manages Cyclescape , a geographically-based discussion forum increasingly used by cycling campaign groups across the UK.
+With our limited resources given over to focussing on keeping the core services up-to-date and responsive several areas have been left lagging and with work to do. We have a powerful API suite, but the front-end website and apps do not currently reflect its abilities in design and UI terms.
+A wide range of potential development areas
+Although improvements to our main codebase – the website – is our ideal focus, we’re happy to see work on any of our projects – routing engine, mobile apps, Cyclescape, etc.
+The codebase for the main website is a rich collection of page-generation code, database procedures and webpage classes, system configuration scripts, and a low-level routing engine implementation – all written in commonly used mainstream languages. The codebase has been reorganised thanks to help from a previous year’s intern. This codebase primarily consists of over 200 PHP classes, using traditional inheritance/loading techniques, arranged as a fairly purist MVC structure. Much of the core functionality has been converted to a public API (with over 40 calls in total) that powers the website and third-party apps/sites.
+Much of our code is on Github and we are trying to get the main website there too.
+Some specific areas which we would welcome help with (though this is not exhaustive) include:
+1. System coding and refactoring
+Ours is a large codebase, with large amounts of functionality. There is always plenty of development that can be done throughout the system. Our previous intern’s work on refactoring was particularly beneficial and further work can be done; new functionality is also very welcome.
+2. Overall website design, including integration with mobile
+There’s an opportunity to update how CycleStreets presents itself to give the service a ‘personality’ – particularly on mobile. Here we’re mainly thinking about the look and feel but also what can be done with it. The codebase can serve webpages based on templates, and these are ready to be used by responsive stylesheets that will work on a variety of screen widths. Developments in this area could provide a major benefit to users on mobile In general, we are aiming to unify our various interfaces into a single implementation.
+3. Usability
+Interaction with the journey planner works smoothest on desktop but there is a lot of scope for improving mobile interactivity. Interaction of the journey planner with the map is one area that needs work, but there a lot of small things that would help such as, for instance prompting users’ frequently used locations when they search.
+4. Cycle route quality
+Changes to the cycle routing engine are perhaps beyond the scope of a summer intern, unless you are starting from a more advanced base of knowledge. A major challenge is how to know if the suggested cycle route is good – and whether changes to input data or configuration parameters produce a better route or not. Ideas and developments of the testing regime could make a significant difference to route quality.
+5. Elevation data
+The routing engine takes into account the cost of hill climbing and that depends on accurate elevation data. There’s scope for adding to our library of data provided by countries such as Australia and Finland.
+6. Geocoding
+Maintaining an up-to-date way of translating text into a location has proven quite a distraction over the years. We’d really like to get on top of this issue and resolve it once and for all. It’s quite a well-defined isolated project, primarily involving sysadmin and code integration work.
+7. Work on our mobile apps
+If you have skills in Java for Android or Objective-C/Swift, we would also be willing to consider work on these also. The apps have been created by colleagues rather than the two of us who will be running the internship; accordingly we can’t provide training on the actual programming languages, but things like code structure and UI would be possible to be covered. A new design has been created and is almost ready to be implemented.
+8. Bikedata site
+We’ve been working to get lots of public data on our new Bikedata site. There are many ideas for development here – new layers, new features, graphing, visualisations, API improvements, etc.
+About the internship
+This is an opportunity to get involved with a live and dynamic project that faces continual resource challenges. The successful candidate will have a choice of which projects to work on based on their own preferences and ideas. We’ll provide supervision and work together to define goals and help solve problems.
+As an intern, you will be a proper part of the CycleStreets team, as a fully-paid employee over the summer period, with daily training and co-working.
+Read about how our previous intern, Patrick, found the process.
+The intern will be hired as an proper salaried employee. Our intern(s) will earn £400 per week, are regarded as part of the team from day one, and the internships last for 10 weeks. We will also come to a flexible arrangement regarding working locations and/or expenses for public area working to ensure that the successful employee is never out-of-pocket.
+The paid internship will be based in Cambridge (UK). This is because we feel it is important that there is daily contact as this is aimed to be a two-way process providing lots of training.
+As our two current employees work mainly from home, we’ll expect the intern to find their own workspace as we cannot provide an office environment. We’ll meet with you to discuss and plan your work schedule and ideas at internet cafés or meeting rooms in Cambridge, as well as online.
+We are not expecting someone with many years of development experience, as such a person would be in a stable job, and the salary level is not intended to reflect this. What is more important to us is someone with the right mindset, a fast learner, who can work at a good rate. Being an internship, this will be a two-way arrangement, with us helping give the student knowledge of working in a large codebase and the challenges this brings – though we do want someone who is a self-starter.
+How to apply
+To apply, all we need is your CV and a covering letter, sent via e-mail by the end of Monday 30th April 2018. Your covering letter should explain your interests, and include some thoughts on our site (such as a critical analysis, max 1 page), and point us to any code you have written (public code on Github is always a good sign). Feel free to contact us if you have any questions.
+We will contact all applicants by the end of Tuesday 1st May.
+Interviews will take place on either 2nd/3rd May, according to your availability.
]]>
+
+ https://www.cyclestreets.org/news/2018/04/11/intern-2018/feed/
+ 1
+
+
-
-
Cambridge recognised in upcoming cycle planning awards
- https://www.cyclestreets.net/blog/2015/09/10/cycle-planning-awards-finalist/
- Thu, 10 Sep 2015 23:07:11 +0000
+ Every GB road collision – mapped
+ https://www.cyclestreets.org/news/2017/10/01/every-road-collision-mapped/
+ https://www.cyclestreets.org/news/2017/10/01/every-road-collision-mapped/#respond
+
-
-
- http://www.cyclestreets.net/blog/?p=3012
-
- The efforts of several local organisations and companies to get more people cycling in the Cambridge, the HQ of CycleStreets, has been recognised in the inaugural Cycle Planning Awards .
-The awards ceremony is being held in Walthamstow, London on 14th September.
-CycleStreets is a finalist in the ‘Best Innovation’ category for its free-to-use journey planning website. CycleStreets runs the UK-wide cycle journey planning website and provides data feeds for a wide variety of journey planning websites and apps , as well as crowd-sourced data collection and collision data viewing systems. They aim to help encourage new people to cycling, by giving them information on where it is convenient and pleasant to cycle, as well as helping existing cyclists find good routes that improve on their existing journeys or help them through unfamiliar areas.
-
-Simon Nuttall from CycleStreets said:
-“We are very pleased indeed to be shortlisted for this award. It validates the work we have put in to build a system that helps find effective cycle routes and to inform the debate about what constitutes practical infrastructure that will encourage more people to consider cycling as a viable option for some of their journeys.â€
-Outspoken Training based in Cambridge are a finalist in the ‘Best Behaviour Change’ category for their work on a project called Bikeability Plus, which has operated in both Peterborough and in Cambridge. The overall aim of the project was to help build a better cycling culture within five primary schools and to encourage children, parents and teachers to cycle more often. The overall target was to increase those cycling to school at least once per week by 20%. The actual result was a 263% increase with more than 200 more children cycling to school each week.
- Rob King, Director of Outspoken said “To have transformed the cycling culture of a school in such a short period of time is amazing. We were particularly proud of our team of staff who made this happen through a whole host of exciting activities and challenges†One of the teachers commented: “Over the six weeks, all of our reception children learned to balance and then ride a bike. Some reduced their mums to tears having struggled previously.â€
-Finally, Cambridgeshire County Council’s Mike Davies is a finalist in the ‘Cycling Champion of the Year’ category, for his work on leading innovative cycling projects. He was nominated for the award by Cambridge Cycling Campaign . He shares the final in this category with the Mayor of Leicester, the Deputy Leader of Waltham Forest Council and the Regional Development Minister from the Northern Ireland Assembly. Mike said “I’m delighted to be a finalist in this award category. The work of many people including Council officers, Councillors, campaign groups, local businesses, cycle shops, schools and colleges, has all contributed to the success of cycling in and around Cambridge, and this is vital to ease traffic congestion and improve people’s health, independence, and ability to access employment and training in a growing city areaâ€.
+ Sun, 01 Oct 2017 00:06:14 +0000
+
+
+
+ https://www.cyclestreets.net/blog/?p=3280
+
+
+
+As campaigners for getting more people cycling, a crucial issue for us is safety of our streets. Safer streets means more people cycling – as places like the Netherlands and other European cities show.
+Every year, the Department for Transport issues a massive data release, detailing every reported road collision in Great Britain, what vehicles were involved, and the outcome in injury terms. Known as STATS19, the data contains around 60 pieces of information for every collision – whether slight, serious or fatal. This is excellent work by the DfT who collate this.
+We’ve plotted every collision , and made available the full details, on a map on our new Bikedata website.
+The data for 2016 has just been released – we got it online within a few hours of its release.
+The site is a beta – the two things we want to improve are removing the jumpiness of the icons, and dealing with the question of how to show large numbers of collisions when zoomed out – currently a limit is applied. Zooming in shows all.
+
+Since the data was first made live, we’ve fixed a few issues. The latitude/longitude values in the original data were incorrect, so we’ve reprojected these from the northings/eastings values which are the original data. Some data in London was also misnumbered, which the DfT have corrected after we pointed this out. Our interface also was not filtering correctly for car occupants – now fixed.
+For every collision, you can click to get full details – available openly without charge.
+ .
+Behind every one of these is a human story:
+https://twitter.com/KirstyLewin/status/914219899538591746
+Users of the site are finding that the visual display enables patterns to be spotted – such as the way that the typical British roundabout design fails cyclists:
+
+
+as do junctions more generally:
+
+
+We’re now working to add new comparison facilities – we want to bring out the policy implications hidden in this data.
+For instance, how do different Local Authorities compare? What happens when streets are upgraded to add safe, segregated infrastructure? How can we most easily demonstrate that allowing two-way cycling in one-way streets is a perfectly safe improvement.
+We’ve also wanted for a long time to link these to newspaper reports and are considering methods to enable people to do that.
+Let us know what you’d find useful.
]]>
+
+ https://www.cyclestreets.org/news/2017/10/01/every-road-collision-mapped/feed/
+ 0
+
+
-
-
Beautiful new galleries page unveiled
- https://www.cyclestreets.net/blog/2015/07/27/galleries-upgrade/
- Mon, 27 Jul 2015 17:00:19 +0000
-
+ Developers – need cycle parking in your app? Use our Cycle parking API
+ https://www.cyclestreets.org/news/2017/08/29/cycle-parking-api/
+ https://www.cyclestreets.org/news/2017/08/29/cycle-parking-api/#respond
+
+
+ Tue, 29 Aug 2017 14:05:24 +0000
-
+
-
-
- http://www.cyclestreets.net/blog/?p=2996
-
- We are pleased to unveil the new Galleries front page , which brings your beautiful photos and content to the front and centre. Galleries is a really neat feature to group cycling-related media for presentation or campaigning.
-There is also a lot more flexibility available while adding a new gallery – you can now navigate away from the Create Gallery form to find more photos to add, and when you return all the fields will be exactly as you left them. You can even close your browser window and come back later, and the gallery creation form will still show your data as you left it.
-As well as the graphical front end, our intern Patrick has been busy developing a new Galleries API for developers, which enables API calls to list and show the content of Galleries, and create and update Galleries.
-We hope you enjoy browsing and adding to the Galleries.
-
+
+
+ https://www.cyclestreets.net/blog/?p=3262
+
+
+ This is a post for app developers. So it contains some techy stuff which probably won’t be of interest to our cycle routing users.
+Knowing where you can find cycle parking as a cyclist, especially in cities, is helpful in avoiding theft, as well as helping keeping busy streets tidy.
+By providing information on where cycle parking exists, developers of apps can easily help people find cycle parking before they even reach their destination.
+CycleStreets provides a Cycle parking API as one of the points of interest (POI) types that can be retrieved in our extensive cycling API suite . Developers can embed this in our app – either by making realtime calls or obtaining the data en-batch using the CSV export mode.
+You can see an example implementation on our new Bikedata website:
+
+The API provides the following data:
+
+Locations of all cycle parking in the UK and other areas that we support (much of northern Europe and various cities around the world – contact us if you need other areas)
+Whether the parking is public or private
+Number of bikes that can be parked (where data is available)
+Details of whether the cycle parking is covered, what type of stands, etc. (where data is available)
+The location of the entrance point rather than the centre point (for larger installations, where data available)
+(Coming soon) Large areas of parking to be available as areas rather than (entrance) point
+
+By default, all locations (whether public or private) come through, but you can specify a filtering option (as seen in the Bikedata example ) if you wish.
+We perform a range of pre-processing to enhance the raw data:
+
+If the location is on private land, but the parking itself is not marked as such, we pre-process the data to mark it as private
+For larger installations such as cycle parks , we use the entrance point as its location, rather than the centre-point
+We convert data defined as either points or areas into a unified set of points
+
+
+This data is all possible thanks to the power of OpenStreetMap. We regularly import OSM data, perform a range of processing on it (e.g. convert locations on private land into private POIs, determining entrance points, etc.), and turn this into an indexed API.
+Our API is a flexible, robust and well-maintained interface (it has been running since 2010). If you need a Service Level Agreement, or are likely to require high volumes of requests, we can also provide the API on a contractual basis.
+To get started, just obtain an API key , and set your application to make API calls to the POIs API and render the GeoJSON response on your map.
+If you would like to request any enhancements to the API, to cover your use-case, do get in touch .
+Also, if you have any existing cycle parking data, we are happy to provide advice on how to make it available.
]]>
+
+ https://www.cyclestreets.org/news/2017/08/29/cycle-parking-api/feed/
+ 0
+
+
-
\ No newline at end of file
+
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api-single-segment.json b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api-single-segment.json
new file mode 100644
index 000000000..82d98bf3f
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api-single-segment.json
@@ -0,0 +1,82 @@
+{
+ "marker": [
+ {
+ "@attributes": {
+ "start": "Thoday Street",
+ "finish": "Thoday Street",
+ "startBearing": "0",
+ "startSpeed": "0",
+ "start_longitude": "0.14771",
+ "start_latitude": "52.20004",
+ "finish_longitude": "0.14682",
+ "finish_latitude": "52.19870",
+ "crow_fly_distance": "161",
+ "event": "depart",
+ "whence": "1535615311",
+ "speed": "20",
+ "itinerary": "63123653",
+ "clientRouteId": "0",
+ "plan": "balanced",
+ "note": "",
+ "length": "161",
+ "time": "45",
+ "busynance": "218",
+ "quietness": "74",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "west": "0.14682",
+ "south": "52.19870",
+ "east": "0.14771",
+ "north": "52.20004",
+ "name": "Thoday Street to Thoday Street",
+ "walk": "0",
+ "leaving": "2018-08-30 08:48:31",
+ "arriving": "2018-08-30 08:49:16",
+ "coordinates": "0.14771,52.20004 0.14748,52.19967 0.14714,52.19915 0.14707,52.19908 0.14704,52.19904 0.14682,52.19870",
+ "elevations": "14,15,15,15,15,16",
+ "distances": "44,62,9,5,41",
+ "grammesCO2saved": "30",
+ "calories": "4",
+ "edition": "routing180820",
+ "type": "route"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Thoday Street",
+ "legNumber": "1",
+ "distance": "161",
+ "time": "45",
+ "busynance": "218",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "",
+ "startBearing": "201",
+ "color": "#000000",
+ "points": "0.14771,52.20004 0.14748,52.19967 0.14714,52.19915 0.14707,52.19908 0.14704,52.19904 0.14682,52.19870",
+ "distances": "0,44,62,9,5,41",
+ "elevations": "14,15,15,15,15,16",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ }
+ ],
+ "waypoint": [
+ {
+ "@attributes": {
+ "sequenceId": "1",
+ "longitude": "0.14771",
+ "latitude": "52.20004"
+ }
+ },
+ {
+ "@attributes": {
+ "sequenceId": "2",
+ "longitude": "0.14682",
+ "latitude": "52.19870"
+ }
+ }
+ ]
+}
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api-single-segment.xml b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api-single-segment.xml
new file mode 100644
index 000000000..5b072215a
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api-single-segment.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api.json b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api.json
new file mode 100644
index 000000000..a2e303e62
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api.json
@@ -0,0 +1,1433 @@
+{
+ "marker": [
+ {
+ "@attributes": {
+ "start": "City+Centre",
+ "finish": "Thoday+Street",
+ "startBearing": "0",
+ "startSpeed": "0",
+ "start_longitude": "0.11783",
+ "start_latitude": "52.20530",
+ "finish_longitude": "0.14744",
+ "finish_latitude": "52.19962",
+ "crow_fly_distance": "4603",
+ "event": "depart",
+ "whence": "1533118269",
+ "speed": "24",
+ "itinerary": "62909947",
+ "clientRouteId": "0",
+ "plan": "quietest",
+ "note": "",
+ "length": "6257",
+ "time": "1584",
+ "busynance": "8471",
+ "quietness": "74",
+ "signalledJunctions": "3",
+ "signalledCrossings": "1",
+ "west": "0.11781",
+ "south": "52.19962",
+ "east": "0.15055",
+ "north": "52.22105",
+ "name": "City+Centre to Thoday+Street",
+ "walk": "1",
+ "leaving": "2018-08-01 11:11:09",
+ "arriving": "2018-08-01 11:37:33",
+ "coordinates": "0.11783,52.20530 0.11781,52.20545 0.11786,52.20549 0.11793,52.20550 0.11844,52.20551 0.11858,52.20553 0.11871,52.20556 0.11890,52.20561 0.11909,52.20570 0.11923,52.20575 0.11960,52.20590 0.12000,52.20603 0.12030,52.20611 0.12044,52.20612 0.12055,52.20614 0.12061,52.20616 0.12062,52.20619 0.12060,52.20628 0.12048,52.20650 0.12047,52.20651 0.12004,52.20699 0.11953,52.20752 0.11936,52.20776 0.11932,52.20776 0.11928,52.20778 0.11918,52.20785 0.11864,52.20833 0.11857,52.20838 0.11851,52.20841 0.11848,52.20844 0.11848,52.20846 0.11849,52.20848 0.11852,52.20849 0.11859,52.20852 0.11871,52.20858 0.11892,52.20869 0.11916,52.20884 0.11927,52.20889 0.11956,52.20901 0.11970,52.20917 0.11963,52.20928 0.11959,52.20940 0.11957,52.20953 0.11956,52.20964 0.11957,52.20970 0.11958,52.20974 0.11975,52.20981 0.11989,52.20984 0.12008,52.20988 0.12024,52.20991 0.12051,52.20997 0.12070,52.21004 0.12097,52.21018 0.12104,52.21021 0.12105,52.21022 0.12155,52.21057 0.12193,52.21086 0.12219,52.21098 0.12360,52.21128 0.12448,52.21152 0.12515,52.21172 0.12634,52.21207 0.12643,52.21210 0.12769,52.21224 0.12774,52.21222 0.12777,52.21224 0.12785,52.21221 0.12802,52.21215 0.12814,52.21224 0.12859,52.21259 0.12867,52.21270 0.12869,52.21275 0.12870,52.21279 0.12871,52.21290 0.12872,52.21312 0.12870,52.21431 0.12841,52.21428 0.12819,52.21427 0.12807,52.21427 0.12806,52.21436 0.12808,52.21468 0.12807,52.21476 0.12801,52.21485 0.12839,52.21494 0.12836,52.21498 0.12872,52.21506 0.13059,52.21547 0.13055,52.21556 0.13036,52.21597 0.13030,52.21618 0.13029,52.21627 0.13031,52.21638 0.13034,52.21659 0.13031,52.21675 0.13027,52.21689 0.13013,52.21705 0.12954,52.21750 0.12946,52.21751 0.12919,52.21772 0.12935,52.21780 0.12927,52.21787 0.12921,52.21792 0.12951,52.21807 0.13028,52.21846 0.13036,52.21850 0.13065,52.21864 0.13145,52.21900 0.13157,52.21909 0.13160,52.21937 0.13173,52.21942 0.13185,52.21946 0.13217,52.21944 0.13234,52.21945 0.13247,52.21941 0.13261,52.21948 0.13284,52.21958 0.13313,52.21975 0.13324,52.21981 0.13281,52.22013 0.13261,52.22024 0.13217,52.22059 0.13197,52.22071 0.13140,52.22105 0.13197,52.22071 0.13217,52.22059 0.13261,52.22024 0.13281,52.22013 0.13324,52.21981 0.13356,52.22009 0.13365,52.22006 0.13374,52.22003 0.13398,52.21991 0.13416,52.21978 0.13472,52.21939 0.13473,52.21937 0.13496,52.21921 0.13507,52.21913 0.13508,52.21910 0.13545,52.21909 0.13556,52.21908 0.13559,52.21906 0.13565,52.21901 0.13567,52.21895 0.13572,52.21890 0.13583,52.21886 0.13592,52.21884 0.13631,52.21866 0.13647,52.21878 0.13680,52.21894 0.13691,52.21887 0.13707,52.21876 0.13739,52.21859 0.13747,52.21855 0.13756,52.21850 0.13773,52.21842 0.13810,52.21827 0.13841,52.21814 0.13868,52.21800 0.13935,52.21756 0.13958,52.21761 0.13977,52.21765 0.14022,52.21729 0.14052,52.21702 0.14059,52.21692 0.14058,52.21685 0.14052,52.21663 0.14047,52.21651 0.14042,52.21641 0.14025,52.21621 0.14010,52.21606 0.13986,52.21581 0.14013,52.21565 0.14044,52.21545 0.14048,52.21543 0.14056,52.21538 0.14062,52.21532 0.14067,52.21522 0.14069,52.21513 0.14067,52.21505 0.14097,52.21504 0.14154,52.21472 0.14158,52.21467 0.14159,52.21464 0.14159,52.21458 0.14158,52.21452 0.14159,52.21445 0.14163,52.21441 0.14194,52.21419 0.14205,52.21412 0.14256,52.21388 0.14268,52.21381 0.14281,52.21377 0.14286,52.21376 0.14295,52.21372 0.14303,52.21366 0.14306,52.21361 0.14308,52.21355 0.14307,52.21349 0.14301,52.21339 0.14294,52.21326 0.14285,52.21310 0.14279,52.21298 0.14270,52.21282 0.14269,52.21274 0.14253,52.21251 0.14258,52.21247 0.14260,52.21238 0.14268,52.21238 0.14275,52.21236 0.14291,52.21231 0.14311,52.21224 0.14327,52.21221 0.14357,52.21216 0.14251,52.21125 0.14319,52.21094 0.14337,52.21085 0.14339,52.21082 0.14341,52.21079 0.14340,52.21074 0.14341,52.21067 0.14350,52.21050 0.14363,52.21044 0.14382,52.21035 0.14412,52.21024 0.14456,52.20990 0.14472,52.20997 0.14481,52.20990 0.14500,52.20976 0.14515,52.20966 0.14524,52.20962 0.14531,52.20959 0.14546,52.20955 0.14578,52.20948 0.14590,52.20945 0.14598,52.20943 0.14595,52.20934 0.14592,52.20924 0.14585,52.20913 0.14577,52.20900 0.14569,52.20886 0.14565,52.20880 0.14561,52.20873 0.14557,52.20866 0.14556,52.20863 0.14553,52.20858 0.14545,52.20845 0.14541,52.20839 0.14539,52.20833 0.14536,52.20820 0.14529,52.20808 0.14512,52.20794 0.14520,52.20791 0.14518,52.20786 0.14521,52.20782 0.14527,52.20777 0.14578,52.20760 0.14574,52.20757 0.14569,52.20753 0.14589,52.20744 0.14619,52.20733 0.14637,52.20727 0.14784,52.20686 0.14807,52.20680 0.14845,52.20674 0.14873,52.20671 0.14906,52.20668 0.14926,52.20664 0.14955,52.20654 0.14960,52.20649 0.14963,52.20637 0.14970,52.20636 0.14980,52.20636 0.14983,52.20634 0.14989,52.20636 0.14998,52.20628 0.15003,52.20620 0.15055,52.20526 0.15046,52.20525 0.15028,52.20521 0.15009,52.20516 0.14999,52.20509 0.14991,52.20502 0.14987,52.20497 0.14840,52.20293 0.14830,52.20277 0.14827,52.20272 0.14826,52.20264 0.14828,52.20255 0.14833,52.20240 0.14839,52.20219 0.14837,52.20212 0.14858,52.20203 0.14852,52.20198 0.14849,52.20193 0.14849,52.20187 0.14856,52.20163 0.14856,52.20148 0.14854,52.20140 0.14850,52.20133 0.14824,52.20091 0.14812,52.20072 0.14786,52.20029 0.14771,52.20004 0.14748,52.19967 0.14744,52.19962",
+ "elevations": "9,9,9,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,8,8,8,8,8,7,7,7,7,7,7,7,7,6,6,6,6,6,6,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,5,5,5,5,5,5,7,6,6,6,6,7,7,7,7,7,7,7,8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,10,10,10,9,9,10,10,10,10,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,9,9,9,9,9,9,8,8,8,8,8,8,8,8,8,8,8,8,8,7,7,8,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,6,6,6,8,9,9,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,10,10,10,9,9,9,9,8,8,8,9,8,8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,9,10,10,10,10,10,9,8,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,12,12,13,13,13,14",
+ "distances": "17,6,5,35,10,9,14,16,11,30,31,22,10,8,5,3,10,26,1,61,68,29,3,4,10,65,7,5,4,2,2,2,6,11,19,23,9,24,20,13,14,15,12,7,5,14,10,14,11,20,15,24,6,1,52,41,22,102,66,51,90,7,87,4,3,6,13,13,50,13,6,5,12,24,132,20,15,8,10,36,9,11,28,5,26,135,10,47,24,10,12,23,18,16,20,64,6,30,14,10,7,26,68,7,25,68,13,31,10,9,22,12,10,12,19,27,10,46,18,49,19,54,54,19,49,18,46,38,7,7,21,19,58,2,24,12,3,25,8,3,7,7,7,9,7,33,17,29,11,16,29,7,8,15,30,26,24,67,17,14,50,36,12,8,25,14,12,25,20,32,26,31,4,8,8,12,10,9,20,53,6,3,7,7,8,5,32,11,44,11,10,4,8,9,6,7,7,12,15,19,14,19,9,28,6,10,5,5,12,16,11,21,124,58,16,4,4,6,8,20,11,16,24,48,13,10,20,15,8,6,11,23,9,6,10,11,13,15,16,7,8,8,3,6,15,7,7,15,14,19,6,6,5,7,40,4,6,17,24,14,110,17,27,19,23,14,23,7,14,5,7,3,5,11,10,110,6,13,14,10,10,6,248,19,6,9,10,17,24,8,17,7,6,7,27,17,9,8,50,23,51,30,44,6",
+ "grammesCO2saved": "1166",
+ "calories": "116",
+ "edition": "routing180716",
+ "type": "route"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Senate House Hill, NCN 11",
+ "legNumber": "1",
+ "distance": "23",
+ "time": "14",
+ "busynance": "26",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "",
+ "startBearing": "357",
+ "color": "#aaaacc",
+ "points": "0.11783,52.20530 0.11781,52.20545 0.11786,52.20549",
+ "distances": "0,17,6",
+ "elevations": "9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Senate House Hill, NCN 11",
+ "legNumber": "1",
+ "distance": "5",
+ "time": "5",
+ "busynance": "6",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "77",
+ "color": "#aaaacc",
+ "points": "0.11786,52.20549 0.11793,52.20550",
+ "distances": "0,5",
+ "elevations": "9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "St Mary's Street, NCN 11",
+ "legNumber": "1",
+ "distance": "54",
+ "time": "52",
+ "busynance": "206",
+ "flow": "against",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "88",
+ "color": "#aaaacc",
+ "points": "0.11793,52.20550 0.11844,52.20551 0.11858,52.20553 0.11871,52.20556",
+ "distances": "0,35,10,9",
+ "elevations": "9,9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Market Hill, NCN 11",
+ "legNumber": "1",
+ "distance": "41",
+ "time": "17",
+ "busynance": "51",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "67",
+ "color": "#aaaacc",
+ "points": "0.11871,52.20556 0.11890,52.20561 0.11909,52.20570 0.11923,52.20575",
+ "distances": "0,14,16,11",
+ "elevations": "9,9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Market Street, NCN 11",
+ "legNumber": "1",
+ "distance": "106",
+ "time": "41",
+ "busynance": "131",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "57",
+ "color": "#aaaacc",
+ "points": "0.11923,52.20575 0.11960,52.20590 0.12000,52.20603 0.12030,52.20611 0.12044,52.20612 0.12055,52.20614 0.12061,52.20616",
+ "distances": "0,30,31,22,10,8,5",
+ "elevations": "9,9,8,8,8,8,8",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Sidney Street, NCN 11",
+ "legNumber": "1",
+ "distance": "198",
+ "time": "83",
+ "busynance": "289",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "12",
+ "color": "#aaaacc",
+ "points": "0.12061,52.20616 0.12062,52.20619 0.12060,52.20628 0.12048,52.20650 0.12047,52.20651 0.12004,52.20699 0.11953,52.20752 0.11936,52.20776",
+ "distances": "0,3,10,26,1,61,68,29",
+ "elevations": "8,8,8,9,9,9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Bridge Street, NCN 11",
+ "legNumber": "1",
+ "distance": "94",
+ "time": "15",
+ "busynance": "87",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "270",
+ "color": "#000000",
+ "points": "0.11936,52.20776 0.11932,52.20776 0.11928,52.20778 0.11918,52.20785 0.11864,52.20833 0.11857,52.20838 0.11851,52.20841",
+ "distances": "0,3,4,10,65,7,5",
+ "elevations": "9,9,9,9,9,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Bridge Street, NCN 11;51",
+ "legNumber": "1",
+ "distance": "6",
+ "time": "1",
+ "busynance": "6",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "329",
+ "color": "#000000",
+ "points": "0.11851,52.20841 0.11848,52.20844 0.11848,52.20846",
+ "distances": "0,4,2",
+ "elevations": "8,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Round Church Street",
+ "legNumber": "1",
+ "distance": "96",
+ "time": "14",
+ "busynance": "112",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "17",
+ "color": "#000000",
+ "points": "0.11848,52.20846 0.11849,52.20848 0.11852,52.20849 0.11859,52.20852 0.11871,52.20858 0.11892,52.20869 0.11916,52.20884 0.11927,52.20889 0.11956,52.20901",
+ "distances": "0,2,2,6,11,19,23,9,24",
+ "elevations": "8,8,8,8,8,8,8,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "20",
+ "time": "3",
+ "busynance": "21",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "28",
+ "color": "#ff0000",
+ "points": "0.11956,52.20901 0.11970,52.20917",
+ "distances": "0,20",
+ "elevations": "8,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Park Street",
+ "legNumber": "1",
+ "distance": "66",
+ "time": "13",
+ "busynance": "80",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "339",
+ "color": "#000000",
+ "points": "0.11970,52.20917 0.11963,52.20928 0.11959,52.20940 0.11957,52.20953 0.11956,52.20964 0.11957,52.20970 0.11958,52.20974",
+ "distances": "0,13,14,15,12,7,5",
+ "elevations": "7,7,7,7,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Lower Park Street",
+ "legNumber": "1",
+ "distance": "115",
+ "time": "18",
+ "busynance": "132",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "56",
+ "color": "#000000",
+ "points": "0.11958,52.20974 0.11975,52.20981 0.11989,52.20984 0.12008,52.20988 0.12024,52.20991 0.12051,52.20997 0.12070,52.21004 0.12097,52.21018 0.12104,52.21021 0.12105,52.21022",
+ "distances": "0,14,10,14,11,20,15,24,6,1",
+ "elevations": "7,7,6,6,6,6,6,6,5,5",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "lcn (unknown cycle network)",
+ "legNumber": "1",
+ "distance": "424",
+ "time": "69",
+ "busynance": "427",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "41",
+ "color": "#ff0000",
+ "points": "0.12105,52.21022 0.12155,52.21057 0.12193,52.21086 0.12219,52.21098 0.12360,52.21128 0.12448,52.21152 0.12515,52.21172 0.12634,52.21207",
+ "distances": "0,52,41,22,102,66,51,90",
+ "elevations": "5,5,5,5,5,5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "lcn (unknown cycle network) continuation",
+ "legNumber": "1",
+ "distance": "7",
+ "time": "3",
+ "busynance": "7",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "61",
+ "color": "#ff0000",
+ "points": "0.12634,52.21207 0.12643,52.21210",
+ "distances": "0,7",
+ "elevations": "5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "lcn (unknown cycle network)",
+ "legNumber": "1",
+ "distance": "87",
+ "time": "13",
+ "busynance": "84",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "80",
+ "color": "#ff0000",
+ "points": "0.12643,52.21210 0.12769,52.21224",
+ "distances": "0,87",
+ "elevations": "5,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "NCN 11",
+ "legNumber": "1",
+ "distance": "4",
+ "time": "1",
+ "busynance": "4",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "123",
+ "color": "#ff0000",
+ "points": "0.12769,52.21224 0.12774,52.21222",
+ "distances": "0,4",
+ "elevations": "4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "3",
+ "time": "13",
+ "busynance": "5",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "43",
+ "color": "#ff0000",
+ "points": "0.12774,52.21222 0.12777,52.21224",
+ "distances": "0,3",
+ "elevations": "4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "32",
+ "time": "6",
+ "busynance": "38",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "121",
+ "color": "#ff0000",
+ "points": "0.12777,52.21224 0.12785,52.21221 0.12802,52.21215 0.12814,52.21224",
+ "distances": "0,6,13,13",
+ "elevations": "4,4,4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Fort St George Bridge",
+ "legNumber": "1",
+ "distance": "86",
+ "time": "18",
+ "busynance": "112",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "38",
+ "color": "#ff0000",
+ "points": "0.12814,52.21224 0.12859,52.21259 0.12867,52.21270 0.12869,52.21275 0.12870,52.21279 0.12871,52.21290",
+ "distances": "0,50,13,6,5,12",
+ "elevations": "4,5,5,5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Pretoria Road",
+ "legNumber": "1",
+ "distance": "156",
+ "time": "34",
+ "busynance": "217",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "2",
+ "color": "#000000",
+ "points": "0.12871,52.21290 0.12872,52.21312 0.12870,52.21431",
+ "distances": "0,24,132",
+ "elevations": "5,5,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Hamilton Road",
+ "legNumber": "1",
+ "distance": "43",
+ "time": "6",
+ "busynance": "38",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "260",
+ "color": "#000000",
+ "points": "0.12870,52.21431 0.12841,52.21428 0.12819,52.21427 0.12807,52.21427",
+ "distances": "0,20,15,8",
+ "elevations": "7,6,6,6",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Ferry Path",
+ "legNumber": "1",
+ "distance": "66",
+ "time": "14",
+ "busynance": "90",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "356",
+ "color": "#000000",
+ "points": "0.12807,52.21427 0.12806,52.21436 0.12808,52.21468 0.12807,52.21476 0.12801,52.21485",
+ "distances": "0,10,36,9,11",
+ "elevations": "6,6,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Chesterton Road, A1303",
+ "legNumber": "1",
+ "distance": "28",
+ "time": "9",
+ "busynance": "56",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "69",
+ "color": "#3333aa",
+ "points": "0.12801,52.21485 0.12839,52.21494",
+ "distances": "0,28",
+ "elevations": "7,7",
+ "provisionName": "Major road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Along the side of Chesterton Hall Crescent",
+ "legNumber": "1",
+ "distance": "166",
+ "time": "29",
+ "busynance": "204",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "335",
+ "color": "#ff0000",
+ "points": "0.12839,52.21494 0.12836,52.21498 0.12872,52.21506 0.13059,52.21547",
+ "distances": "0,5,26,135",
+ "elevations": "7,7,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Chesterton Hall Crescent",
+ "legNumber": "1",
+ "distance": "250",
+ "time": "51",
+ "busynance": "321",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "345",
+ "color": "#000000",
+ "points": "0.13059,52.21547 0.13055,52.21556 0.13036,52.21597 0.13030,52.21618 0.13029,52.21627 0.13031,52.21638 0.13034,52.21659 0.13031,52.21675 0.13027,52.21689 0.13013,52.21705 0.12954,52.21750 0.12946,52.21751",
+ "distances": "0,10,47,24,10,12,23,18,16,20,64,6",
+ "elevations": "7,8,8,8,8,8,8,9,9,9,9,9",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Chesterton Hall Crescent continuation",
+ "legNumber": "1",
+ "distance": "54",
+ "time": "17",
+ "busynance": "74",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "1",
+ "turn": "bear right",
+ "startBearing": "322",
+ "color": "#ff0000",
+ "points": "0.12946,52.21751 0.12919,52.21772 0.12935,52.21780 0.12927,52.21787",
+ "distances": "0,30,14,10",
+ "elevations": "9,10,10,10",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "7",
+ "time": "3",
+ "busynance": "15",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "324",
+ "color": "#ff0000",
+ "points": "0.12927,52.21787 0.12921,52.21792",
+ "distances": "0,7",
+ "elevations": "10,10",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Link joining Hurst Park Avenue, Highworth Avenue, Milton Road, A1309, Ascham Road, Milton Road, A1134",
+ "legNumber": "1",
+ "distance": "301",
+ "time": "48",
+ "busynance": "339",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "51",
+ "color": "#ff0000",
+ "points": "0.12921,52.21792 0.12951,52.21807 0.13028,52.21846 0.13036,52.21850 0.13065,52.21864 0.13145,52.21900 0.13157,52.21909 0.13160,52.21937 0.13173,52.21942 0.13185,52.21946 0.13217,52.21944 0.13234,52.21945 0.13247,52.21941",
+ "distances": "0,26,68,7,25,68,13,31,10,9,22,12,10",
+ "elevations": "10,10,10,10,10,9,9,10,10,10,10,9,9",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Milton Road, A1309",
+ "legNumber": "1",
+ "distance": "68",
+ "time": "14",
+ "busynance": "247",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "51",
+ "color": "#3333aa",
+ "points": "0.13247,52.21941 0.13261,52.21948 0.13284,52.21958 0.13313,52.21975 0.13324,52.21981",
+ "distances": "0,12,19,27,10",
+ "elevations": "9,9,9,9,9",
+ "provisionName": "Major road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "46",
+ "time": "12",
+ "busynance": "103",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "321",
+ "color": "#7777cc",
+ "points": "0.13324,52.21981 0.13281,52.22013",
+ "distances": "0,46",
+ "elevations": "9,10",
+ "provisionName": "Service Road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Pye Alley",
+ "legNumber": "1",
+ "distance": "67",
+ "time": "64",
+ "busynance": "171",
+ "flow": "against",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "312",
+ "color": "#008800",
+ "points": "0.13281,52.22013 0.13261,52.22024 0.13217,52.22059",
+ "distances": "0,18,49",
+ "elevations": "10,10,10",
+ "provisionName": "Footpath",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Mulberry Close",
+ "legNumber": "1",
+ "distance": "73",
+ "time": "12",
+ "busynance": "92",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "314",
+ "color": "#000000",
+ "points": "0.13217,52.22059 0.13197,52.22071 0.13140,52.22105",
+ "distances": "0,19,54",
+ "elevations": "10,10,10",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Mulberry Close",
+ "legNumber": "2",
+ "distance": "73",
+ "time": "12",
+ "busynance": "93",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "double-back",
+ "startBearing": "135",
+ "color": "#000000",
+ "points": "0.13140,52.22105 0.13197,52.22071 0.13217,52.22059",
+ "distances": "0,54,19",
+ "elevations": "10,10,10",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Pye Alley",
+ "legNumber": "2",
+ "distance": "67",
+ "time": "60",
+ "busynance": "124",
+ "flow": "with",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "142",
+ "color": "#008800",
+ "points": "0.13217,52.22059 0.13261,52.22024 0.13281,52.22013",
+ "distances": "0,49,18",
+ "elevations": "10,10,10",
+ "provisionName": "Footpath",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "46",
+ "time": "8",
+ "busynance": "70",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "141",
+ "color": "#7777cc",
+ "points": "0.13281,52.22013 0.13324,52.21981",
+ "distances": "0,46",
+ "elevations": "10,9",
+ "provisionName": "Service Road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Milton Road, A1309",
+ "legNumber": "2",
+ "distance": "38",
+ "time": "6",
+ "busynance": "104",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "35",
+ "color": "#3333aa",
+ "points": "0.13324,52.21981 0.13356,52.22009",
+ "distances": "0,38",
+ "elevations": "9,9",
+ "provisionName": "Major road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Oak Tree Avenue",
+ "legNumber": "2",
+ "distance": "153",
+ "time": "25",
+ "busynance": "144",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "119",
+ "color": "#000000",
+ "points": "0.13356,52.22009 0.13365,52.22006 0.13374,52.22003 0.13398,52.21991 0.13416,52.21978 0.13472,52.21939 0.13473,52.21937 0.13496,52.21921 0.13507,52.21913 0.13508,52.21910",
+ "distances": "0,7,7,21,19,58,2,24,12,3",
+ "elevations": "9,9,9,9,9,8,8,8,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "66",
+ "time": "59",
+ "busynance": "124",
+ "flow": "against",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "93",
+ "color": "#008800",
+ "points": "0.13508,52.21910 0.13545,52.21909 0.13556,52.21908 0.13559,52.21906 0.13565,52.21901 0.13567,52.21895 0.13572,52.21890 0.13583,52.21886",
+ "distances": "0,25,8,3,7,7,7,9",
+ "elevations": "8,8,8,8,8,8,8,8",
+ "provisionName": "Footpath",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "40",
+ "time": "6",
+ "busynance": "47",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "110",
+ "color": "#000000",
+ "points": "0.13583,52.21886 0.13592,52.21884 0.13631,52.21866",
+ "distances": "0,7,33",
+ "elevations": "8,8,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Pearl Close",
+ "legNumber": "2",
+ "distance": "46",
+ "time": "8",
+ "busynance": "65",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "39",
+ "color": "#000000",
+ "points": "0.13631,52.21866 0.13647,52.21878 0.13680,52.21894",
+ "distances": "0,17,29",
+ "elevations": "7,7,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Union Lane",
+ "legNumber": "2",
+ "distance": "233",
+ "time": "40",
+ "busynance": "309",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "136",
+ "color": "#33aa33",
+ "points": "0.13680,52.21894 0.13691,52.21887 0.13707,52.21876 0.13739,52.21859 0.13747,52.21855 0.13756,52.21850 0.13773,52.21842 0.13810,52.21827 0.13841,52.21814 0.13868,52.21800 0.13935,52.21756",
+ "distances": "0,11,16,29,7,8,15,30,26,24,67",
+ "elevations": "8,7,7,7,7,7,7,7,7,7,7",
+ "provisionName": "Minor road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "High Street",
+ "legNumber": "2",
+ "distance": "31",
+ "time": "8",
+ "busynance": "39",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "70",
+ "color": "#33aa33",
+ "points": "0.13935,52.21756 0.13958,52.21761 0.13977,52.21765",
+ "distances": "0,17,14",
+ "elevations": "7,7,7",
+ "provisionName": "Minor road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Chapel Street",
+ "legNumber": "2",
+ "distance": "98",
+ "time": "18",
+ "busynance": "138",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "143",
+ "color": "#000000",
+ "points": "0.13977,52.21765 0.14022,52.21729 0.14052,52.21702 0.14059,52.21692",
+ "distances": "0,50,36,12",
+ "elevations": "7,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Church Street, NCN 11;51",
+ "legNumber": "2",
+ "distance": "136",
+ "time": "21",
+ "busynance": "139",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "185",
+ "color": "#000000",
+ "points": "0.14059,52.21692 0.14058,52.21685 0.14052,52.21663 0.14047,52.21651 0.14042,52.21641 0.14025,52.21621 0.14010,52.21606 0.13986,52.21581",
+ "distances": "0,8,25,14,12,25,20,32",
+ "elevations": "7,7,7,7,7,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "St Andrew's Road, NCN 11;51",
+ "legNumber": "2",
+ "distance": "108",
+ "time": "17",
+ "busynance": "97",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "134",
+ "color": "#000000",
+ "points": "0.13986,52.21581 0.14013,52.21565 0.14044,52.21545 0.14048,52.21543 0.14056,52.21538 0.14062,52.21532 0.14067,52.21522 0.14069,52.21513 0.14067,52.21505",
+ "distances": "0,26,31,4,8,8,12,10,9",
+ "elevations": "7,7,6,6,6,6,6,6,6",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "NCN 11;51",
+ "legNumber": "2",
+ "distance": "109",
+ "time": "15",
+ "busynance": "92",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "93",
+ "color": "#ff0000",
+ "points": "0.14067,52.21505 0.14097,52.21504 0.14154,52.21472 0.14158,52.21467 0.14159,52.21464 0.14159,52.21458 0.14158,52.21452 0.14159,52.21445 0.14163,52.21441",
+ "distances": "0,20,53,6,3,7,7,8,5",
+ "elevations": "6,5,4,4,4,4,4,4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Riverside Bridge, NCN 11;51",
+ "legNumber": "2",
+ "distance": "209",
+ "time": "42",
+ "busynance": "260",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "139",
+ "color": "#ff0000",
+ "points": "0.14163,52.21441 0.14194,52.21419 0.14205,52.21412 0.14256,52.21388 0.14268,52.21381 0.14281,52.21377 0.14286,52.21376 0.14295,52.21372 0.14303,52.21366 0.14306,52.21361 0.14308,52.21355 0.14307,52.21349 0.14301,52.21339 0.14294,52.21326 0.14285,52.21310 0.14279,52.21298",
+ "distances": "0,32,11,44,11,10,4,8,9,6,7,7,12,15,19,14",
+ "elevations": "4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "NCN 11;51",
+ "legNumber": "2",
+ "distance": "28",
+ "time": "8",
+ "busynance": "37",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "199",
+ "color": "#ff0000",
+ "points": "0.14279,52.21298 0.14270,52.21282 0.14269,52.21274",
+ "distances": "0,19,9",
+ "elevations": "5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Riverside, NCN 11",
+ "legNumber": "2",
+ "distance": "28",
+ "time": "5",
+ "busynance": "30",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "203",
+ "color": "#000000",
+ "points": "0.14269,52.21274 0.14253,52.21251",
+ "distances": "0,28",
+ "elevations": "5,5",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "16",
+ "time": "7",
+ "busynance": "46",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "143",
+ "color": "#ff0000",
+ "points": "0.14253,52.21251 0.14258,52.21247 0.14260,52.21238",
+ "distances": "0,6,10",
+ "elevations": "5,5,6",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Link between Riverside, NCN 11 and Cheddars Lane",
+ "legNumber": "2",
+ "distance": "70",
+ "time": "46",
+ "busynance": "249",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "90",
+ "color": "#ff0000",
+ "points": "0.14260,52.21238 0.14268,52.21238 0.14275,52.21236 0.14291,52.21231 0.14311,52.21224 0.14327,52.21221 0.14357,52.21216",
+ "distances": "0,5,5,12,16,11,21",
+ "elevations": "6,6,6,8,9,9,11",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Un-named link",
+ "legNumber": "2",
+ "distance": "202",
+ "time": "36",
+ "busynance": "230",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "216",
+ "color": "#ff0000",
+ "points": "0.14357,52.21216 0.14251,52.21125 0.14319,52.21094 0.14337,52.21085 0.14339,52.21082",
+ "distances": "0,124,58,16,4",
+ "elevations": "11,10,11,11,11",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Link with Newmarket Road, A1134",
+ "legNumber": "2",
+ "distance": "137",
+ "time": "22",
+ "busynance": "156",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "158",
+ "color": "#ff0000",
+ "points": "0.14339,52.21082 0.14341,52.21079 0.14340,52.21074 0.14341,52.21067 0.14350,52.21050 0.14363,52.21044 0.14382,52.21035 0.14412,52.21024 0.14456,52.20990",
+ "distances": "0,4,6,8,20,11,16,24,48",
+ "elevations": "11,11,11,11,11,11,11,11,11",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Newmarket Road, A1134",
+ "legNumber": "2",
+ "distance": "13",
+ "time": "22",
+ "busynance": "50",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "1",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "54",
+ "color": "#3333aa",
+ "points": "0.14456,52.20990 0.14472,52.20997",
+ "distances": "0,13",
+ "elevations": "11,11",
+ "provisionName": "Major road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Newmarket Road, A1134",
+ "legNumber": "2",
+ "distance": "10",
+ "time": "22",
+ "busynance": "16",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "1",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "142",
+ "color": "#7777cc",
+ "points": "0.14472,52.20997 0.14481,52.20990",
+ "distances": "0,10",
+ "elevations": "11,11",
+ "provisionName": "Service Road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "98",
+ "time": "16",
+ "busynance": "129",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "140",
+ "color": "#7777cc",
+ "points": "0.14481,52.20990 0.14500,52.20976 0.14515,52.20966 0.14524,52.20962 0.14531,52.20959 0.14546,52.20955 0.14578,52.20948 0.14590,52.20945 0.14598,52.20943",
+ "distances": "0,20,15,8,6,11,23,9,6",
+ "elevations": "11,10,10,10,9,9,9,9,8",
+ "provisionName": "Service Road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Cambridge Retail Park",
+ "legNumber": "2",
+ "distance": "174",
+ "time": "65",
+ "busynance": "288",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "192",
+ "color": "#7777cc",
+ "points": "0.14598,52.20943 0.14595,52.20934 0.14592,52.20924 0.14585,52.20913 0.14577,52.20900 0.14569,52.20886 0.14565,52.20880 0.14561,52.20873 0.14557,52.20866 0.14556,52.20863 0.14553,52.20858 0.14545,52.20845 0.14541,52.20839 0.14539,52.20833 0.14536,52.20820 0.14529,52.20808 0.14512,52.20794",
+ "distances": "0,10,11,13,15,16,7,8,8,3,6,15,7,7,15,14,19",
+ "elevations": "8,8,8,9,8,8,8,8,8,8,8,9,9,9,9,9,10",
+ "provisionName": "Service Road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "74",
+ "time": "18",
+ "busynance": "86",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "121",
+ "color": "#ff0000",
+ "points": "0.14512,52.20794 0.14520,52.20791 0.14518,52.20786 0.14521,52.20782 0.14527,52.20777 0.14578,52.20760 0.14574,52.20757 0.14569,52.20753",
+ "distances": "0,6,6,5,7,40,4,6",
+ "elevations": "10,10,10,10,10,9,10,10",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Coldhams Lane Cycle Bridge",
+ "legNumber": "2",
+ "distance": "228",
+ "time": "34",
+ "busynance": "223",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "126",
+ "color": "#ff0000",
+ "points": "0.14569,52.20753 0.14589,52.20744 0.14619,52.20733 0.14637,52.20727 0.14784,52.20686 0.14807,52.20680 0.14845,52.20674 0.14873,52.20671",
+ "distances": "0,17,24,14,110,17,27,19",
+ "elevations": "10,10,10,10,9,8,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Coldhams Lane (cycleway)",
+ "legNumber": "2",
+ "distance": "81",
+ "time": "12",
+ "busynance": "76",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "98",
+ "color": "#ff0000",
+ "points": "0.14873,52.20671 0.14906,52.20668 0.14926,52.20664 0.14955,52.20654 0.14960,52.20649 0.14963,52.20637",
+ "distances": "0,23,14,23,7,14",
+ "elevations": "7,7,7,7,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Coldhams Lane (cycleway)",
+ "legNumber": "2",
+ "distance": "20",
+ "time": "12",
+ "busynance": "19",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "103",
+ "color": "#ff0000",
+ "points": "0.14963,52.20637 0.14970,52.20636 0.14980,52.20636 0.14983,52.20634 0.14989,52.20636",
+ "distances": "0,5,7,3,5",
+ "elevations": "7,7,7,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Coldhams Lane",
+ "legNumber": "2",
+ "distance": "131",
+ "time": "43",
+ "busynance": "195",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "1",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "145",
+ "color": "#33aa33",
+ "points": "0.14989,52.20636 0.14998,52.20628 0.15003,52.20620 0.15055,52.20526",
+ "distances": "0,11,10,110",
+ "elevations": "7,7,7,7",
+ "provisionName": "Minor road",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Brampton Road",
+ "legNumber": "2",
+ "distance": "400",
+ "time": "84",
+ "busynance": "587",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "260",
+ "color": "#000000",
+ "points": "0.15055,52.20526 0.15046,52.20525 0.15028,52.20521 0.15009,52.20516 0.14999,52.20509 0.14991,52.20502 0.14987,52.20497 0.14840,52.20293 0.14830,52.20277 0.14827,52.20272 0.14826,52.20264 0.14828,52.20255 0.14833,52.20240 0.14839,52.20219 0.14837,52.20212",
+ "distances": "0,6,13,14,10,10,6,248,19,6,9,10,17,24,8",
+ "elevations": "7,7,7,7,7,7,7,10,10,10,10,10,11,11,11",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Fairfax Road",
+ "legNumber": "2",
+ "distance": "17",
+ "time": "4",
+ "busynance": "27",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "125",
+ "color": "#000000",
+ "points": "0.14837,52.20212 0.14858,52.20203",
+ "distances": "0,17",
+ "elevations": "11,11",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ },
+ {
+ "@attributes": {
+ "name": "Thoday Street",
+ "legNumber": "2",
+ "distance": "285",
+ "time": "71",
+ "busynance": "391",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "216",
+ "color": "#000000",
+ "points": "0.14858,52.20203 0.14852,52.20198 0.14849,52.20193 0.14849,52.20187 0.14856,52.20163 0.14856,52.20148 0.14854,52.20140 0.14850,52.20133 0.14824,52.20091 0.14812,52.20072 0.14786,52.20029 0.14771,52.20004 0.14748,52.19967 0.14744,52.19962",
+ "distances": "0,7,6,7,27,17,9,8,50,23,51,30,44,6",
+ "elevations": "11,11,11,11,11,11,11,11,12,12,13,13,13,14",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ }
+ ],
+ "waypoint": [
+ {
+ "@attributes": {
+ "sequenceId": "1",
+ "longitude": "0.11783",
+ "latitude": "52.20530"
+ }
+ },
+ {
+ "@attributes": {
+ "sequenceId": "2",
+ "longitude": "0.13140",
+ "latitude": "52.22105"
+ }
+ },
+ {
+ "@attributes": {
+ "sequenceId": "3",
+ "longitude": "0.14744",
+ "latitude": "52.19962"
+ }
+ }
+ ]
+}
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api.xml b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api.xml
new file mode 100644
index 000000000..686d92e9d
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/resources/__files/journey-v1api.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/journey.xml b/libraries/cyclestreets-core/src/test/resources/__files/journey.xml
deleted file mode 100644
index ac8473078..000000000
--- a/libraries/cyclestreets-core/src/test/resources/__files/journey.xml
+++ /dev/null
@@ -1,69 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/journeys.json b/libraries/cyclestreets-core/src/test/resources/__files/journeys.json
index 291b7746c..c9b0e9cea 100644
--- a/libraries/cyclestreets-core/src/test/resources/__files/journeys.json
+++ b/libraries/cyclestreets-core/src/test/resources/__files/journeys.json
@@ -31,7 +31,7 @@
},
"datetime": "2:46pm, 20th December 2014",
"idFormatted": "43,106,958",
- "url": "http://www.cyclestreets.net/journey/43106958/"
+ "url": "https://www.cyclestreets.net/journey/43106958/"
},
"43106946": {
"id": "43106946",
@@ -52,7 +52,7 @@
},
"datetime": "2:42pm, 20th December 2014",
"idFormatted": "43,106,946",
- "url": "http://www.cyclestreets.net/journey/43106946/"
+ "url": "https://www.cyclestreets.net/journey/43106946/"
},
"43089395": {
"id": "43089395",
@@ -73,7 +73,7 @@
},
"datetime": "4:08pm, 16th December 2014",
"idFormatted": "43,089,395",
- "url": "http://www.cyclestreets.net/journey/43089395/"
+ "url": "https://www.cyclestreets.net/journey/43089395/"
}
}
}
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/photos.json b/libraries/cyclestreets-core/src/test/resources/__files/photos.json
index 5d656d2b8..72cdb8bf9 100644
--- a/libraries/cyclestreets-core/src/test/resources/__files/photos.json
+++ b/libraries/cyclestreets-core/src/test/resources/__files/photos.json
@@ -10,12 +10,13 @@
"properties": {
"id": 60141,
"caption": "'One of the closest near misses'\n\nhttp://www.bbc.co.uk/news/uk-england-cambridgeshire-24562951\n\nI wonder if one reason this happened was that she regularly took a risk here and became de-sensitized to the danger?",
+ "datetime": 123,
"categoryId": "road",
"metacategoryId": "other",
"hasVideo": false,
"videoFormats": null,
"thumbnailUrl": "https://www.cyclestreets.net/location/60141/cyclestreets60141-size640.jpg",
- "shortlink": "http://cycle.st/p60141"
+ "shortlink": "https://cycle.st/p60141"
},
"geometry": {
"type": "Point",
@@ -30,12 +31,13 @@
"properties": {
"id": 80565,
"caption": "More cycle parking for another 60 bikes added at Longstanton Park & Ride",
+ "datetime": 123,
"categoryId": "general",
"metacategoryId": "other",
"hasVideo": false,
"videoFormats": null,
"thumbnailUrl": "https://www.cyclestreets.net/location/80565/cyclestreets80565-size640.jpg",
- "shortlink": "http://cycle.st/p80565"
+ "shortlink": "https://cycle.st/p80565"
},
"geometry": {
"type": "Point",
@@ -50,12 +52,13 @@
"properties": {
"id": 81980,
"caption": "These branches need trimming before they knock someone off. When wet they hang lower than shoulder height across most of the path.\n\nUPDATE: Path cleared within 24 hours of making this report on the Cambs website!",
+ "datetime": 123,
"categoryId": "cycleways",
"metacategoryId": "bad",
"hasVideo": false,
"videoFormats": null,
"thumbnailUrl": "https://www.cyclestreets.net/location/81980/cyclestreets81980-size640.jpg",
- "shortlink": "http://cycle.st/p81980"
+ "shortlink": "https://cycle.st/p81980"
},
"geometry": {
"type": "Point",
@@ -70,25 +73,26 @@
"properties": {
"id": 82169,
"caption": "Link from Clerk Maxwell Road to the West Cambridge site",
+ "datetime": 1466693269,
"categoryId": "cycleways",
"metacategoryId": "other",
"hasVideo": true,
"videoFormats": {
"mov": {
- "url": "http://www.cyclestreets.net/location/20588/cyclestreets20588.mov",
+ "url": "https://www.cyclestreets.net/location/20588/cyclestreets20588.mov",
"location": "/location/20588/cyclestreets20588.mov",
"sizeBytes": 25152682,
"sizeBytesFormatted": "24MB"
},
"flv": {
- "url": "http://www.cyclestreets.net/location/20588/cyclestreets20588.flv",
+ "url": "https://www.cyclestreets.net/location/20588/cyclestreets20588.flv",
"location": "/location/20588/cyclestreets20588.flv",
"sizeBytes": 1382820,
"sizeBytesFormatted": "1MB"
}
},
"thumbnailUrl": "https://www.cyclestreets.net/location/82169/cyclestreets82169-size640.jpg",
- "shortlink": "http://cycle.st/p82169"
+ "shortlink": "https://cycle.st/p82169"
},
"geometry": {
"type": "Point",
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/pois-types.json b/libraries/cyclestreets-core/src/test/resources/__files/pois-types.json
index 6af07e1fb..d38d70cb0 100644
--- a/libraries/cyclestreets-core/src/test/resources/__files/pois-types.json
+++ b/libraries/cyclestreets-core/src/test/resources/__files/pois-types.json
@@ -1,317 +1,265 @@
{
- "validuntil": 1468531926,
+ "validuntil": 1591689876,
"types": {
"archaeologicalsites": {
"id": "archaeologicalsites",
"name": "Archaeological sites",
- "total": "17776",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAByUlE\nQVQ4jZWTS0hUURyHv3u8yty5aWI4GeVEC2vKaDPVonY9QGwRBK6CamGrwEUaIbULXCTpKtr0gKIY\nBloVUQQTusmooRdh9oDIhlCHcGzGuY73nn+LmGHserN+q8PvnO87B845Rt8h6ywivQLN/E8MsmiG\njd7O0BQQKfcnzidZG41h169BmWZlvTM/Ry6b4fKZ/WjtASAwZRpgC7Cvq49IawzLXk3z+jbfhqFw\nAz9/fOf4uQTv0494+uAqhmArgNo6i+17DpMY6ubds3uBp578+JIbF7rYdeAYqub36RSA1h4hq55I\na4wde48ECqKb47REt1Jj1qI9FwATwHNLuG6JnksjhOyGQMGGtjinBlNMf5uodKo8KBXzf4XLsVY1\nIlr7BVr0ssByqb6dimBy4sU/CzKfXvsFYw+v4xRyK8Lu4gLp1G2/YCbzgSd3h1cUPH98iy/jY34B\nQCp5kVejyUD489sR7l/rX9ItEYgIiaGTpFN3/PCbUW4OHGXByQcLAGI7O9gSP+gTtGxsZ9vuTl+v\nxKBYXTiFOfK5bOXDlFMszCIif/LzxumO8IBSuluQuuqZpnWbVNhuMgBKiw4zX8c9qX4rAhjqyi+f\nUZ3U8mIvBAAAAABJRU5ErkJggg==\n"
+ "total": "60259"
},
"bandbs": {
"id": "bandbs",
"name": "B&Bs",
- "total": "356",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB30lE\nQVQ4ja2SvWtTYRTGf++H997cNCXJTUIrtIKgBWmjpFVRF7cOLi5OToKj4uBQFcFRJwUFF0Fw81+I\nCH7s4qBgrB0MTcCU1oS2yU1yc/M6aEI+WkTwmQ7nec6Pc3hfwePCCoibINKMaNpVhAY2/HDUAsMm\nQjwSPFmtYMiM+tmUzcXDUaQQvPy2Q+Fney9IRWOIjvaXZ12OJizel30ALhyKMu0q3pT84aAUUTk6\nfHkuxkxM82mz1e992GgyF7e4dGRibIkhwLVsHIFgrRaMBQvVNo6SXD8eHwcckIK7J5OU6x1Ku+PD\nPa3vBJR3O9xeSqCl+A1wLcWDsx6ft1osz7rcO+1xazEJwMpigivHJgF4ej7DnaUkWc/iazXg/hkP\nRwtkRCJerfsspGwmLYmrBTMxjecochmHKVeTjigyriJmSZKOYt6zeV3yiSgp5FYzNPlinSCEVmgA\nUAIWUhZrtYB5zyKXsWkEhlZoSDqKZtglX6xTbYVGD9744sv20M1vB54tX2z061NTTr8eAgzq3MEI\nwZ+NIlryrtzYM7cvwO90efixBsCNE/H9Yox9pH/VfwAY/L/H9pExDY0UzzDdqxU/mMilbdHzvm+H\nIdABWK22dS5tq573o94xQBsjnv8CvjGXEYbFUo8AAAAASUVORK5CYII=\n"
+ "total": "258"
},
"bikerepairstations": {
"id": "bikerepairstations",
"name": "Bike repair stations",
- "total": "404",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACd0lE\nQVQ4jXWSW0hUURiFv33OjMM4M5niLS9ldrEHoYjwpYgYIaO8USThkwRREEnUQxAGPQYFkRCISgiC\nFESIVpho2EvZiw+GZSaKeMnJmRydmTO3c87uwbJpnBbsh73+vdb+/8UvaJ28BeImiByScDjHxthK\nNJnegMSLEA8VhHIjlRjg0fEc7lRkpTYQZCNls4LEkVyzqYKa3Q52OC20HMmipyofp1XZaqIIxxb2\n5M50huoLKd1mxasZeCMG/qhJf00BjftdWzwsiRd3UTrXDm7nVN8SobhJbamDqyMr9M4EsSiCDncu\nqgLdk4GESVqngoDDpgoG6wqpfrlEIGYC4LAqhOLm398UwUBtARfeLOMNGyBEcHOEhn0uBue1TXF9\nqZO5phKCV/ZwuTwDAN2UtE+s0bDXmdB361SwpGtGejRdfvJFpKttWorWKenRdPkHMcOUGW3TcnQ5\nLFc0XWpxUy4E4vLisCeslGakiZGzxeTaVcqzbHw4X4zDIrAqIiFsgaoI3C8WmFyNIQS0jPp48nld\nt6xFDc69XsJdlE6uXeXptwBxCc3vftBZmYdFEdx+7+VnxACgfWKdxZBB15d1EOJviHbLRoiVvYvE\nDAmA06pgUcAfNX8nDgN1hTQNefge0v8NMaxL7o+t0unO22w/Ykh6qvIpy0xDAPeOZtM3G9oQp9qD\nvtkQaargVW0Bz6aCNJa5OFFo5/QujQfHsnk7r/F43P//RQJ4Ph1kaF7j+qFMKvJsCAG5dpVLwx6W\nNSP5OQqScDLpj5rc/eijun+JhaBO99dASjFSaipnmjNBHgChA9HEMxfQo+O+qDGzpkc0XUaS60jR\n8Qvt0BYFBoLx3QAAAABJRU5ErkJggg==\n"
+ "total": "2729"
},
"bikeshops": {
"id": "bikeshops",
- "name": "Bike shops around the UK",
- "total": "2503",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB/klE\nQVQ4jZWRP2hTURTGf+e+96IviQkhaUKiobTSJEOL0EApttLJwdWhoKA4ODk4VLDo5uBaxUnU6izt\n5Owk1EEoNKVNhgoFKW3aBEraJDfkz3sOwYBGqfnG737fj3vOkZXplUWER4IMMYBc1y0rR70wRWQB\nGKgMICIRx3AeKlx8g5Z7EMSn/ido2iZ2zEaU9L31AaJTUTL3MiTmEj3Pd9FHdjHL6M3RswHH+WPi\n1+LokiaUCRHKhFCmonna5PDbYf/v/jSsCxaCkJhNUPle6fmNUoPIlQjVH9V+gHHeIHU7RXgijOkz\nWX++Tvpumu032wCEJ8LYMZvY1Rjx2Ti6qMm/y9M8bWLMX5p/Ovl40lPOlTlYO8Cf9BMYCSCWEBgJ\nMHxjGKftIKbgjXpZW1ijVW0x/mCcvc97TQVgR232v+yTvJ4EIDGXIDgWJHUrxc7HHXY/7RK8HGRj\naYP0nTTlXBkAw2N0RxBDMG2T3Msclt9i6tkUxa9F9JHmZPcEgHa9jT7S5JfzGOcMLL9Fp9npAgrL\nBWaWZqjsVAiOBdl8tUmr1kIXdW9ZW6+3yD7JUi/V8ca8FD4UAJDV6dUqgk9ZCnvIRpc0TsvpO1c3\n3R23UW7gdlyAau+MTsuhtl/7e/GXXNCH+jdLiRL9j/iZEpG6SYe3KO4L4hmk7LouLu77n9ErqMOs\nMm5wAAAAAElFTkSuQmCC\n"
+ "name": "Bike shops",
+ "total": "22829"
},
"britishcyclingclubs": {
"id": "britishcyclingclubs",
"name": "British Cycling clubs",
- "total": "1631",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAALHRFWHRDcmVhdGlvbiBUaW1lAFN1\nbiAyNiBKYW4gMjAxNCAxNzo1OToxOCAtMDAwMBsy/20AAAAHdElNRQfeARoSCy+xo9o4AAAACXBI\nWXMAABwgAAAcIAHND5ueAAAABGdBTUEAALGPC/xhBQAAAfxJREFUeNplUs9P01AcX7tm6ey6Sda6\nxUiisCiYGKM3PZp4NfGCVxJv/g8ePRu4GDgRMCHRgxyAGyEuQcSJmYJLCgyQLu3YOtayvbSvr6V8\nx1u2Ct/T930/n++vz/syQRBE/jeMMXU8zxME4RLKhBPK5XI0GpVlOR6PwxMhZBiG7/u5XK6fEVwY\nFNN13UXId92zK6aqKhAos5tQr9ehErDtjU1guK6HHYIxKRU1mqNpGmWy0KTRaKRSKQaM4+zVvG9a\nytbx70JlZmI9czNJB8lms0ADp5NACOE4jgLCq5fG+Js71b93peDRk8EB6VpveFgPOjDOhSWTyR7g\nN83m23cR4klT78P6gAadDqZpiqIYBkhJITt70Yx8SVCQGGaJVKtV2Dasyd62vjj300Noc+3fqWWH\noVqtxiYSCcuyaA3HJupBU9fapdLJwmdlcGjgy2wx/KE8z7PQCHagoV/fjm7dvp6+Idx/mN34ekBc\nf3hU/pE/7JZzHKjeUSkWi9FQjOcOdxv3HmSU7ePnL0Z01Xr6bKj4vbKvGDSh/9OwCYxIiDc/XVhd\nVpY+baE2XvhYhODR/snM5DrcAawKzK78kiTBv6TT6bHXj9unWEzxEITZ/hQqUkbELgKIZdl+B2qt\nVguEC8uytrIz9yHfsuweh7l63vTOqA8XABcTRs8Bp7OFToxkZycAAAAASUVORK5CYII=\n"
+ "total": "1631"
},
"busstops": {
"id": "busstops",
"name": "Bus stops",
- "total": "942597",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABvUlE\nQVQ4jZWQTWsTURSGn3Pno0lmkhG1qZI2tnan3Ug30m4KShH9A4JLCy5cunBVQQRpV0qKuNKFK5eC\nCIIbl0Ep2IISEvxA8aNq2knotJiJMy5Cph2cJvrChXvPfe9z33OEUuUqyBWQQf5HIT8RuSUsVdcI\nyQPkTMXFYzlmixYTB0zyaQ0l8GP7NzXX59mnLe69afLVa3cha0KpuglYWVPx+sIRRmyd0orL4/de\n7MPJ/AALUwdptgJOPPzIu4YPIpuqazg5lCJrKG68XOeL12YyPxBbAPPlOo1WwJliJgLr3c1Rx+DJ\nB49r5XrP1jXpeLuKEow7BlW31Xd2VddnPAkwljOouX5fQM31GcvtAKIWCrZOSpNYvCRlTWHYjp7t\nADSB+6eH+iYAcH8F0V718P2TYoDLz79z/um3Pc3z5Tqzjz7Havruw9lRC88P9wScGsn8NaMY4Nyo\n1TPuTCHNTCEdm0EEuLPaYHhKJ6MrFpbXEwFzxx2Kts71F/VdgJBtBOtBpcmlCYf9KcXi8kYiYPpw\nGsdU3H7ldgphuCUs1W4SBnMgZtHWlKGEt812kAQ4ZOmyzxSpbPid+1Du/gG184EgbPO+EQAAAABJ\nRU5ErkJggg==\n"
+ "total": "1694084"
},
"cafes": {
"id": "cafes",
"name": "Cafés",
- "total": "80105",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABk0lE\nQVQ4jZ2SPWhTURiGn3PuTZM06lC0SAZBJbhI0cGhTmKEinHO4lip4NqQ4uCioEGQurQUCk4SDIKL\nICohOimImqmpUCUGjCF6W5Kb3PyY5J4O15QEbELybh+H5zkv3zkiEvIuodSigiOMEoGBzbI2G9Cf\nA9MjwU4mleC0FOAbA3ZKKHx6d1i4+wLN5R4I/Nj8wNtnD2m3mrRbTQB0ACEEh/0nmTp6fKDAf2KG\nYDhKpfSbtVuXKeY2kY5AIqQ2tHL7b4OVaJBiLsPctdsAjkApG2XbQwU/v6XJbrzny7sEx06d6xUo\nmg1rqKBZrwLg9h7Y24HsHjas0lCBkd9Cd7k5H1rg66dX/YLtQnYg3KhVqJb/cPNBEteElzfxe/2C\nZCKGuVPYV+CZPMjV+fuUjTyrS5eoVXaAf88IYPz6zlY6xdkLYaTm6oMtc5vXT+7wORXf20M3eu+Q\neHQDyzQInLmIx3cIu2NjmQbJpzEyH1/+t5mIXPFU1bjfWVGVSlAfC3ZS01VHrktpX1eoiRFvByEf\n7wL4sJFUGu4kQQAAAABJRU5ErkJggg==\n"
+ "total": "188332"
},
"cairns": {
"id": "cairns",
"name": "Cairns",
- "total": "1455",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABaklE\nQVQ4jaVSvWoCQRic3TtsrkohyHdiocLFwtJSEMQXsPAlRKw0GHwEK7GRgPgCFlZX2CgIYqtgbZkf\nsElILhBv0kQTPQmoAwPLMjPFzAcAdwCeAfBMvgC4B4CnC8w7PmoAFi6Hpa8wAwCuDjBPfUYiEVQq\nFWSzWWw2G4TDYSyXS5TLZWy324D+DX+KKRaLXK1WrNVqTCaTFBGKCNvtNrvd7nGJrwcB+Xyek8mE\njuNQRJjL5fZvEWGv12Oz2TwdkEqluFgsmE6nGY/HORqNSJKe57HT6TAWizEajXI4HLJUKh0GhEIh\nzmYzFgoF2rZN13V5jMFgQNu2mUgkOJ1O6TjOb0Cr1WK1WqWIsF6vB8w77DSZTIau61Ip9WoqpTAe\njzGfz2EYBjzPQ7/fh2keDuT7PizLAkms12s0Gg1oraF+Othfo9YaSqnAVCTh+35gwcAdnBD9Cw3g\n4yzHId4NADcAbgF8Afg8kw/fExn9txqvRk8AAAAASUVORK5CYII=\n"
+ "total": "3633"
},
"campsites": {
"id": "campsites",
"name": "Campsites",
- "total": "9764",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABuUlE\nQVQ4jZWSu2tUURCHv7mPfbjGTZZkzWLEB5GIVoIEQRBsRDEY/BvUXrAIpLC0sAlkIYJdQjobsRQF\nIYi1iLioRQqRxL1L9kG8N3v3nLHYJRJzryFTzsz55jdzfsJibQ7kEcgYhwklQGRBqH7dRCn/Wz9+\nxCXvORirNLuWTtcmQTY9lELSgCHf4enVURwRHq7VkwGOFLw0hd9bMeeGMzR3DOvtOHWTVIAjMDXi\no+qnPgZw0gqL18p4juA7wuPp0uEAeU94cPEYxipGlfnLpdRJifnVG+NYhUZkaUT94y1d3/dRyYBS\nzuXOmaPkPOFXaAgiQ9YV7l0oknXlYMCrmQo9q3SNsvylzWqtTTxY5cWtyv8BJ4c8roznyXlCPTI8\n/9xi6VOLethXcft0gWLGSQe8mZ3AWEWBdz9COl1Lu2tZ+xmigFHl7d2JZMB0OcfksE/GFTa2e8x/\nCHab5t4HbGz38B3h0liWs8W/3tg10suZCqDEFoLIcvPUXoc3IstoXnFFeD17gsmV9QFACREKy7UO\n2YGe2lbM1o7ZA6h+bHJ+xEeAVqz9pOpvofrtCWrvg2T2nfigUHn2B6q5pQUMeEanAAAAAElFTkSu\nQmCC\n"
+ "total": "14290"
},
"carshare": {
"id": "carshare",
"name": "Car sharing",
- "total": "4970",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACL0lE\nQVQ4jXWTTWsTURSGnzuTZNKkTWqTJpTGltYPtGJQKrroqlRcVBSrLlwqdae4UNCVv0FsFy5U6kZQ\nV6JSQUREaJGuVKTWuGhD6ceYREuSyUdnknERJ81tdGDg3Hfe97n3nJkRTCzcAnEDRKdbEVw+EGRm\nrciXdJnG63SfH4AXi0ZNsEkjxB3BZELHJgJwcX+AqeNR5vQSx54t18NdfhfLl/oQQM+jRVbylgPR\nXdj4HeOH1SKvFg1eJw1p91TR4uF8FkWAXrC2HijC73LqwYjGu7EYAY/CiR4fqiKY/LyBW4HZ8zs5\nEvUCENJUzk6vbjGc4mBII+CpLT2qYKirpRbwqvUwwHCsRTpdHVC1JR3h6LKMIratnSKZM6k0QJZy\nJgAbpQrrxlbfS1lz20YTiTzUBjna6yPW5iakKcz/2sSwasSeNhdBj4phVZldK/I1s/k3LfKuRtp0\nssDuoJsHI1Hp6IoQjPb6GHm+wmZF7lUCAFyJt3NzJs2cXpJ0w6wy1t/K0x+5/wMOd2pc2NuGzyUY\nHwhIxg6vSn/QzfuVAnqh8u8ZvDnTzePvOa4faice1iTAk0SOZM7CrNrc/pipz0BpNLkVwak+P92t\nTZ0RD2sMdXnxqPJ7bHKe29XaFAYY6PAAMLsuz0ZwN5FCEIbaR2Jd3cP4W52pb1nJeG84wmCnxtGG\nnwz4qXLy2g6w94GwbChnShX75VKhmDerJaDs3KlixfqULlsLv81CXbfF/T9GRsFwjWSJRwAAAABJ\nRU5ErkJggg==\n"
+ "total": "7281"
},
"atms": {
"id": "atms",
"name": "Cashpoints (ATMs)",
- "total": "35652",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACYElE\nQVQ4jZWSv0tbURzFPy/vJhcT5QWxJRgN+GPKI0NQhwwa/AucBAfBQcgQQcGCxYqDIFVQ6NZBXBx0\nlYi7s0RRMIoIvhBDFDGLpKa8kOR2EF9NldKe8XvPOXzP+V4N+Cyl/OT1ej/wHyiXy0Xbtr9pwP3E\nxMTHzs5O6vX6P4ldLhf5fJ6tra17AfiklAghAAgEAjQ3N78rtG2bfD4PgJQSwCdeEzweD8PDw2xu\nbr5rMDk5ye7uLrZtOzMBcHV1hVIK0zTZ29tje3sbANM00TSNTCYDQFtbGy0tLViWxfn5uWPyA1CA\nWllZUYODgyoYDKrx8XGVyWTU5eWlGh0dVe3t7SoWi6m1tTX1wgdKrtcr9vb2YlkWMzMzTE1N8fDw\nwOPjI0tLSySTSXK5HF1dXQ2xnA50XUcphVKKgYEBUqkU0WgUr9dLIpHA7XYDUKvVkFI6PejAF8DT\n399Pa2srx8fHFAoF5ufn6enpQQjBwcEBuVwOgGAwSLVa5ebmBqDiRBgaGuLo6AiAw8ND4vE4p6en\nuN1uNjY2WFhYACCdThOPx50IjkFfXx9nZ2cAzM3NEQqFKJVKWJbF7OwsY2NjAFxcXBCJRN520NTU\nRKVSAZ7/w/r6OrZtI6VkenqaVCrldCCEQNM0lFLPG3R3d79kAmB5eZnV1VUMw8Dv97Ozs8Pi4qLz\nfn19TTgc/l3iyMiIp1AokM1mHdLt7S1PT0+cnJywv7/fcDpd1zFNk3Q6XRHAz1gs5isWiwQCAf6E\nYRgkk8mGmc/nwzAMgLIGfA2FQomOjg75Rv0XZLPZ+t3d3fdfZjDoe6M8nmoAAAAASUVORK5CYII=\n"
+ "total": "70272"
},
"castles": {
"id": "castles",
"name": "Castles",
- "total": "5857",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACPUlE\nQVQ4jc2RXUiTUQCGn+84dfObP9NPp0uX5gxTkmZatkqtCCyLCtQioSsvImuQmkHUbSBUIkHdFKQE\nYQR1FaFQSheZMa1ulMxsDPOnpaYzf5qeLmQfBN1417l74JyX932O0lBuuoyU9RISWc9R8LNKs5BS\n1oUe5xYdodJ9R7+zr6KOA1WNOlc3trKtuGINJJpUcBsUUCVgSbJj31xAes4uEpIzAEjPcSEUobMt\nI4+p8a/4Pnn4MT6CIlGVhsPGgAT1WtswanQ8YYZwlhbnAQiPNAHwe2kBgEijykowSDC4xNVKK0gC\negBA8Qk3BfuruXVhJwCnLz1ACMHDpjMAXLzdQ9/LR3Q/bQnNCIh1ifuXy1CDc02daLZMTOY4/KOf\nAYhLTAMFZiZ9AGgbHCwEZpibHqfZ7QJJIMyVZbgCRKwEl4kwqqjRCXQ/a8E35MGSZOdXYJqeF/fx\nDXlIzcrHO9hLf1c7o8MfAJYNoSp9Xe2YLVY0m4M3z+8BkJG7ByGEzkWHavAOvKW3o1WfoAdsdR0n\nNdOJyRxHfukpAOKtG1EUoXOU2UKqw4mzpIr+7sd/Ozh/4xVaSiZGNYapCS8AMfHJgMLs1JgeuDg/\ny9zMBDdrC/+Tb9QdmGM1dh89S6wlhS2FZfjHvpC9/SCKENg25WFSY0m0ZbH3WC3vXz/hp390zUF9\nufE7Eg3AmpaNPXsH7zrbAHDklWAIj2TQ0wGAs/QkE94Bvo18DE2YVOrKoq4LsVojkRHr6i4BRdz9\nA2DlzjjX4EtwAAAAAElFTkSuQmCC\n"
+ "total": "9444"
},
"playgrounds": {
"id": "playgrounds",
"name": "Children's playgrounds",
- "total": "176228",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB7ElE\nQVQ4jZWST0iTYRzHP8/7vpv7U8w2li0T5oZSaMtID8syGFipFGbZoVq7CAWdoklECAZeQqJDdKmw\ngg6dOtSlSxQEVpeRFB6qidaKFjOxra2Re58Ob+kGztzn9jwPv+/z+T2/R0R7rOeR8pwEN5UgSKFz\nVQ02aA+A9RUVG9ikoFkRYF8MFQoO10aEEKuTkNiV4o1j0THOXnvJ/vDwqjVKAn5l0yz8zpPLzJUt\nUDUT/q0daGYLANq/g1D/II3bQ4xEGla8sffUFdo6w0y9HefGxR7DoDl4gK7IMM4N9VS761YMePPi\nEd8+vWN6ctxoYa2zRhwfvEs+l0HqBYLdA9T6W1A187IB8YlnpL5OkfoSN1pIf0/KC31OAu2H6Ipc\nQlE19p0Yoq6xleyPWRLx1yTex0h8iPE5PkGoP0pgZy/5bJrY0/tLbxDY1cetoYPMJqcNNUWlrfMk\n3i1BXJ56tu0+jMfbhKnKhpQ6vqZ2zBbbUsC9y+ESVV0v0NJxhGr3JkZP70DXF7DaHZwZfcKrx7fx\nB/awrsarlIyxmFpfAJfHh2qqYnPrXgByP+eZS37k+cPr3Bk5SnJmUhfRbktGFv3G/2Fd4yCXmTcW\nkkxZg3IsFv9FkYJcpSFFZDVZUG4qij4gkcsPvhwSEMrYHxUDlS1OB5jhAAAAAElFTkSuQmCC\n"
+ "total": "144415"
},
"cinemas": {
"id": "cinemas",
"name": "Cinemas",
- "total": "4699",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACZUlE\nQVQ4jW2S30tTYRjHP+/Z2eaYpZLDaJWSzJqh/UD8McwLI8ikoLrssuhG8KKkYP4RQZcJav0JMkmt\na3FaWqClTgVrirrRhjtzZ2c75+3KtTWfq5fneb6f93m+7yuG+l2vkfKlBA8nhGp3othUDD1dWhDE\nsXijSClfnCQWQlDhruJq1326+p6WkyW1UjCoCnDLorxiU/E2XqPGcwEhFC7fvENjyy1mJ0fIGZnS\nSyRuMXSvQpPgrr/SgbRM3FUeHE5XocnudOE+fYacoTM7+e7/KTQV4NylVhpbekgl9jF0rbBkTV09\nZy/6qar1Uu05T3h6HDNvlDBsAZ8a1JIHjscDb1FsKvHdDTxeHw3NnaQSe0QjX4ntRNjbXsHj9bG7\n9b1Yb9gCPjUowZGMRbFMk6brvcR3t9jZ/IaePgTAzBv42+7ib+8jPDWKlAXXDOX4tPplmkD/c7J6\nGi25X2b62uJn5mc+0Nr9qCRfAFiWyeTYMLHoOja7swyQ0RI03eil5+EgQohyAMCP+Y90Pxigwd9R\nBnC4KsnpRyzMvKe5vb+QV4ubzLxBaDRIda0XxaZimXmcrkrq/Z1kMxrL4RBtt5/gdFWyEg4BUPgH\nxxC7w0VwbJX1xU9IKdGScXY2l7DMPACnauqQUhLdWGL7Z1izBXxqEHAUvDDzaMkDpGURjSySjP1G\nSqswZTaTAgGrC1MgMUpWOI6VuRDDY2vMz4zza20BgD8H28SiEbIZrQSoSkEG+W8FAP3okImRV+iZ\nFLFohPRhnFSi/GmBI1WayoiiWM8k0lFcWZ6bEDlDl3lDP0kIEhDK6F8DoQsLjGHNFAAAAABJRU5E\nrkJggg==\n"
+ "total": "7557"
},
"conveniencestores": {
"id": "conveniencestores",
"name": "Convenience stores",
- "total": "54928",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAByklE\nQVQ4jZWSv2sTYRzGP9/LkbvksG2OlNBmMtCUkoDtpEuXDt0K4iSCm4g4KNjBP8PFRQUnnSpOgi6t\nQxc3KSUokQzRnlS5NAn5dRcv9zqkaNNrGvqMz5fn8z7v931l69rWY4RNQWa5gJRSrhZqTzQReTQp\nLDGJeiLpMBY+0FBY54X1pM7Ghw00XYtCECvqnlLuRo72jzZhEJ45nwy4nqP8qjy+4cmqWmyUZ2Ut\nTNvE/ewSvxQfmfXbfVDHAMM2WH+9TqPcwEybeK6HkTII/IDe7x6rT1fxXA/TNvEbPolMgtKzEgfb\nB0OAX/fp/Oyw+3CXwt0CpeclFm4uUH1fpd/s//MWby9SeVNheXOZZqV54goKur+6FO8VSa+kKd4v\nklpKkZxLMvAHpK8MPbtgY6QMZvIztKqt0R3U9mt4NQ8VqnMbOB8drHkL1Kkl1vZq5G/lMWfN4WlL\n9pkNpi5P4e650VdofGuQnE9S/1LH2XEQEQ4/HRJ0AoxpA2fHQTd14tNxjvaPooDwT0j1XZXEXILs\nWhaFInM1M5wFIdm1LGjQ+t6i/rUeBQBU3lbGfphx0kST3oVTxxKRrs6AF2jcESQ+OfJfSikU6uVf\nhfWzorBYk/oAAAAASUVORK5CYII=\n"
+ "total": "126594"
},
"cycleparking": {
"id": "cycleparking",
"name": "Cycle parking",
- "total": "99713",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACG0lE\nQVQ4jZWSTWsTURSGn5vOZNJMSasmE11oRYPYjaFSELuQSguiP6ArwYWuRehCBVt0Y90prVShqDsX\n7lUQXGhLjdQiFiwS8QNr0iZNSJuv6SR3ZlzYpERjwHd1L+ee57zv4QomPl0GMQIixP/IJYMQtwWT\n8dRwpMM4GvLVa4WqQ7IoebFcJlGUrSApBRf9TLfOqW6dmaQJQIcqONipMj0Y5tLrNe4urjcHeISu\n1M4fMhbDz1ca6ndOhBjv38WDpQ1M6TZntIr59FuJDtWD0d72zzctAUP7/OQsh58t9lCPEA1qPDm9\nBwABRDpVjgQ1zr9MYTd33wgwpcPXjSoALjCbNJldMVlIW61MbgPi61WuzGXqheO7fRwIqKyWbRJF\nyaEuL9eP7WQ0luXL1qAGwJ8qVB3OHg6wbjn07PACoHrEX/toukSPgEinF0VAWTrkLJucZZMoSXpD\nGrq63aYAzKcturQqWptgasDAI6DP8HF1LkOf4eNd2mJwbzv5isP9kwZvVjf5UZCML+QQTMSLgA5w\nrifAckGyP6AQ9itEgxq9IY1XCZOxWIZb/UFM22U0luVitItHS4VyQ4Ta5dn3Eu/XLCq2w3xqk2ux\nDKtlG4CRmTVM6aJ6BBJcBRcT8dvB43iBqQGDfEXH8Ldx420WnyJIbzVPf8zzcCjMpnRZzFZYzldK\ngsnPN3GdCyC8NSe6IkRJui2+z5Zcce8X9v7V3yiD+xAAAAAASUVORK5CYII=\n"
+ "total": "228718"
},
"cyclesport": {
"id": "cyclesport",
"name": "Cycle sport",
- "total": "926",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACQklE\nQVQ4jZ2TvUtbYRTGf++bGK0J0TQYSxta1GAQJIik4ToIgmCpi+DQP0A6uYlCoYOTg1s3l0IhozSu\nNVmULh2iAYcmmIhEb0U0kTQf14To9d4OsVHrUPSZDpznOd9HKF+VDwIxj6CLB8A0zTNDGp/ESGTk\nFPA8RHwThVNpYtofJQYQ2OWjxdew/jVaZAvDXcMMPh2k39WPu9WNFPI6kaBQL7Bf2idZSLKV26Kq\nVxs+JaJoUz1T9pmBGTLFDKV6iXV1nV5nL4l8gvpVndHno6QKKYKeIAF3gL6OPqKHUVZ+rmhWgPmh\neWa/z5IsJAl6gqgVFQBHiwOLsHB8fsxJ9YT07zTh3TCuVhdrb9eIqlEplIiirb5ZtTusDvZKe5Qu\nSpQvypiYXF5dAmCRFiQSp82Jq9WFr9OHbuhMf5tuVBA/jRP7FaP7STf+Tj89zh7sLXbare0A1PU6\n5/o5h5VDtnPbLG0vsfh6EQOjMcRsOYvX7iWmxtg42gDAZrER8oRos7SRKWZQNbU5eV+Hj4PKwc0W\njrQjxr3jxNRYk7SsLKM8UwDQDZ2FHwvET+MABNwB8rV8oz3vO+/HXC1nm3w1SdATRDd0EDA3NNcM\nJoUkX8s3+u6bJugJEt4No11qF0KJKJpA2AH8nX5C3SEGXAOMvRi7czDZSpbNo012znZI5BKYmACa\n9TYpXUyTLqaRSCZeTmCRlqYvVUiRLWfvX6IUsmaad//BwCCqRu+R/4UQomq94uqzRL5HYPuv4hZM\n0wSTL38A+pzi2S7I3MsAAAAASUVORK5CYII=\n"
+ "total": "1049"
},
"drinkingwater": {
"id": "drinkingwater",
"name": "Drinking water",
- "total": "31316",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABkUlE\nQVQ4jY2SvUtbURjGf+cmNyZVieJHwc/FWf0HVCiCpUoXKwQd1UXEIUbSwU38XBwcXIq4qaudSnB0\nEbRLB4dqrCgxmASjV2NI7n0dCurlxphnfDjP77zPe44K9fvCiEwL1FFELreOZZq43Dr5XBYUCSxW\nNREJvhcG+Dq+wtxOjInlyH9DqBXFlKag/L0wwN7OMi7dQ2Rr8dlTQrm7+NgeegNh2tp78Nc2onu8\nDM9skIxFMfM51oI9aMUAfSOzfBoKkYr/I35+jGWZ3FxfcPH3iMuT3wAUncASi90f39n/uY5Sio6u\nQY4PIzzep19qhL54DSlxDw4JhmOC6voWBkYXyWZubX6lv57N+QBmPmfzHYCHuxRGOsHe9pLNH5xc\nc4QB5xKzGQOvr9JxUL3RouArKM1ul/kqeMwYpQMQQXO9tKuqayZ1dVo6IJ24xF/T8ArQRDIWLR2Q\nvIpS/bHVBkjFzwoC3KLIIPZ/EDv7Q28gTGf3NwB0vYyDX5uF8g8q+PnDgqZZY4J4Cl7xlgRQ2voT\ngaeJTQX262UAAAAASUVORK5CYII=\n"
+ "total": "129034"
},
"gardens": {
"id": "gardens",
"name": "Gardens",
- "total": "144666",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABMUlE\nQVQ4jZ2TMWoCURCGv5m3bqGliQewWCwUREEEYU+TOkVS5BhpBSEH8Aw29ou13R7AYCGy2+ibdAvJ\nW13MlP/87+MfZp4sl8sPEXkHnnmsvoFPFZG3fzwGeAJeI6BT1xURer0eZsbhcMDMAo+ZdaJb+Ol0\nymQyAWC325FlWa0vAERRxGw2I0mSShuNRqgqWZbhvf/l17+AJEkYDofEcVxprVaL8XhMv98PEgSA\noihuTYVzrhlwuVyCmABmRlmWzYA0TVENZESENE2bAfdGqOsFW9hsNgwGA47HI4vFAucc2+2WbrfL\nfr9vBpzP52rn8/kcMyPPc/I8r01185BUlXa7jYjgnON6vT4GAFiv1wC1Z1wBRKQ0s+A/eO85nU73\n+IhIEXnvV6r6YmbxXXd9ff0ApflqhxEgowgAAAAASUVORK5CYII=\n"
+ "total": "38010"
},
"golfcourses": {
"id": "golfcourses",
"name": "Golf courses",
- "total": "8655",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB5ElE\nQVQ4jZWRvU9aYRjFf8+F0oiVmgpoiKFKbU0IFW1ETcPsx0hMOpDgVAcHO7SDizFdJDrVjT+iYWCv\nxsSpA2jSQU1IdbjiF6lRuGp64d4OTREqTeGs5z2/nOe8MvZ5bEGQDwgumpBpmnlDMT4pIvLe3+F3\nrYXXaH/YXvNIEYXV16tMeicRpMYTEafFsLyzdL/p/tjb1muL9ceI9ccIOoOICGpRpWyW2b/Y5/j6\nmOln05zdnFHQC1UUUCqVMFFEIeQOMf9ynogvAoBW0pj1z7Kd3ybwJMDU0ykUqcSwVtfaUDdIHaRI\nn6cxTAOAYfcwE94JdEMnno7janERfR5lM7eJWlTvANnLLItfF++Nta6u47A5cNgcLLxaoO9xHz6H\nj3HvODNfZu4AuqHXXbtklOhs6ST6Ioqmayynl8leZslpud9D1039pZ38DgCtD1rZu9hDLaqVExsG\n/AkMuYZqvIYARb3IYeEQgEHnYPOAoDNIT1sPW7ktBjoGajzrPzIV2a12lkJLJL8nOb0+pcve1VyD\nucAcZaNM4lsCj93DkXbUOGDEPULEF2Els8Jt+RbPozoARZSbq59X7P7YvQcIe8KkDlJkzjMAnGgn\nlS8FEJFrGU2OxhVTeYtg+9851TJNE0ESvwBY/K34gTrSngAAAABJRU5ErkJggg==\n"
+ "total": "2892"
},
"hospitals": {
"id": "hospitals",
"name": "Hospitals",
- "total": "8086",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACRklE\nQVQ4jY2RS2gTURSGvzszSeNMQprah0FbfBURrVRKlSKCoOJC3Uk3LhULxY0VtCAUNyoitPSBunDh\nThFcKorWjaALUUMtKCmifYikjSQxk2keM3NdTDptQbT/5h7O4z//f674wshlARcFNLACauM6tI1h\nsF0qcyZupsRqyLRADIkkoymQjQAogkh3K/rRZuw5E2d+ERSBGtfRGnXyD5JY47MrSVIKSANAMQI0\n3TuMLDmkzoyTfziFCAcQukZ2OEGq5xXBnTHqbx4AVQAgUAytGtFw+xCZWx8oT/4CINrbRu35PQA4\nKYvf9z+THZtAP9ZC/bUu0v1vPNEA4VPbsZ5+94f/Bev5DG6+Qk17wzKBcWIL+UdT/x1eQnY4QbRn\nNwCaWhcSTsoi2BqjbqDTbwp1Nvlx9Nwu9CPN3tkcSfrCa9C8O2haS0TY03lCB+NETu/468ZQV5xQ\nV9wjKFTIjU0gzQqKHkCRRQdRo65ZPqrivQEFWXHQnBnTDWyLkrvyFudHYVl2bxvGyS2e58EE1osZ\nryChNJEmagSQFRfNscqIkIos2hSeTfsE+vHNflz6lF5V0zaFcRYWgeov5O5OErvUsWYXdQP7yA4l\nlgmK71LIskO4u9VvkhUXN1fCzZfBkX4+1t+B9XIW+6dnVyQZMQEDINa3FzWuk7nxHrdoo20wALBn\n86hxg/VX91N48g3z8VdvGGGKJMMLIOqXNgS2Rqnta0eJBJElx5Opa9izJpnBj773KubFFKPXXeRZ\nAcE1H6EKgbzzB0ea2rvhdk/nAAAAAElFTkSuQmCC\n"
+ "total": "11957"
},
"libraries": {
"id": "libraries",
"name": "Libraries",
- "total": "14070",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACU0lE\nQVQ4jX3RS0hUURjA8f+9M3Ob8eoYOWKZiqWOaSmJlWgYgVSSlYuoTbSIAonQwIJAFGlR4EYrKqFN\nCBEpUdBDpYIapYVMKCU4SRg6auqoozOaY/M6bcrnrW93vsePc84nXS0xXUOIKwJi0QiDYiLg960v\nSEwRpkEWQlRqDVtziqiot1Feb8OoRq8HBBYhUaGXQBUr8vHbsjh8ppqMvcXo9AoA52paeFB9nFDQ\nv/oSAlX+e4i2JHCq4j4X696yK//E0jBAStYBTl9u1HohuoI0fZUs65SyG6/J3HcUg2LUbNyclAGS\nxPfezpVpv64gTV9VWFqujA32snV7NhtMkZqAJMskZ+Sx4HUz/K17CZABjKqZ/SVldL64x+LCnCYA\noDcYOXK2lvTcQ0s5GUCSZCzxqWTll9Lx/DbBgP+fiGqO4eSlO8Ql7VgGPr5sZOhrF4nWPVhzirC/\naUIIoQmEwyHc44NERMUsA/OeSZobyphwOkjOzMds2ULPhyfrhkcHPtP6sAbHp3Z88zPLAIBrpJ+n\nd8uZdTnZmXcMgcBhbwNgxuXkXXMd3e8f43GPIiGhGFXgzxoBBWB2chjP9Bipuw+SmJaLw97G+GAf\nffZWfHOz6AwKsqwnGPTjnR7FOz3mXwUATDgdiHCYlOxCYuOttD+6jnljHKbIaBZ/egmHAvTYWnBP\nDAH49VofZXt2C9W8ibzi8xgjogiFgwT8iwx86cA10r+qVy8kfAjUtUhbUy2xCekEfvnwTP2gr+uV\n1mYWpMriiJuyHL4gEMra6n9DAJLc+Btmddh22q/GHAAAAABJRU5ErkJggg==\n"
+ "total": "26570"
},
"londoncyclehire": {
"id": "londoncyclehire",
"name": "London Cycle Hire points",
- "total": "774",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACgUlE\nQVQ4jZWQW0iTcRjGf//t2zfdptuc1kJXKaYdpCykoggKO9BJK4LoQN4EQRRFXkRSF0VEUBDkRRdR\nEHVTdIAoKDogZlR2EVFJOIUUnTt8abbZ96357d+FOCsj6bl7H9739z48ggufjoCoB1HA/0iiIcR5\nQWN7BMmkUf9gpYcF+XaaQzqX275NBIkoSJyj88lFPo5WeVEsgh3lOVQHHISHhjP7poSbHXHeRJIj\nhkU4lV+B64udVN/t5foaPwGXwvaynHFPqwMOqm50k5Yj82+AfsPk2MI8CrKsAAS//uBj/w9ehw0M\ncyRzcDCVOR4H2NcU5cpKP5ph8l5LUvckQkw3/1mD4EJ7AsZ6+JsCLoXVUx087dH5/C31y7VIWP5c\nPrMkn+ur/SwvzM54uikpcds4VOkZB1f+NM69HeBxbSFlXpUyr5rxy70qlz4MTgzYXOJiKJVG0026\n4mNx01JSU+LiUff38YBF/iwaqvJwqwLVaqH+eYytpS7udCYAWFGUzYbpTuYXZDFnSxGGKTn1pp+W\nPgOl1GOznF2az7aHYfZWuCn32JidpzIv305DlZcKn513WpKmXp0il439TVEiusntdVPY+TgqxKHm\nWDI0lFJvdyRo2zWNF30GKwMOzLQkV7Ww9l6I1ojBk02FHG7ROLXYR+39EHWzclGtIqlo+rCclmPD\nlFB+rYsKn4rNIngXS9IxmKI1YgCgGSbGsKTmfgiAGR4bL8PJtHIrGE/cqym0ZyuCyHeT3TNzOdAc\nZa7PzrOescJOtPZzddVk7nYmmJqjMNmhcPzVl7igMXjaityzsdjpdKtWHnweGtYMU/IXeVWLWFbk\nsA4YpmwJ6aaU4uJPsGrzc2pgYf0AAAAASUVORK5CYII=\n"
+ "total": "1254"
},
"museums": {
"id": "museums",
"name": "Museums",
- "total": "12506",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABrklE\nQVQ4jd2PTWsTURiFn3vnZpImwVbaSZUktirTj1BQSkVTRBgQoQouxJW4tFtBHCy4Ev9AwVA3lv4A\nhSIulC6LpSJaiFSwYruRVBBTtHEycZp0rovQhVCCcemzPec957zCvdQxhda3NVi0g6BMyLSRt9U8\nkNrPc6gvR/fhY1S2vuwnx7VgRAlI6L1QIejP5Tlx9goj+ct0WVkA/J/f+fDmBe+W5ll7u0C422j6\nNQlx71qmemZiMt43dJrswBiJA90tlwc1j82NIhurL3n9fLaqrPSgcK66IJqGncBv/bqUZOxRsvYo\npU8rUnnbZUrrKyQ7U9Trv2gENSLROCpiUvN+ANCRPPiHZkRM/MoW2+VNLb+VPoYPpy6gzCjFxScU\nXIfV5acIISm4DgXXQZkxiouPKbgO7189A2DmznlK68VQttz7F/wHAca4re4On5owM8dPIqSBlbZJ\nHRnCiERJdvZwNDdOV08aISVWeoDe7CBKRQn8Cl8/r+2I+9f7qzenl+JmLNFWc6Me8ODWuapwL8Y8\nDe1d76HxpBbU/um4ia/0rnwkZXhDo80220HIud8v4IuLh8pP6AAAAABJRU5ErkJggg==\n"
+ "total": "23413"
},
"naturereserves": {
"id": "naturereserves",
"name": "Nature reserves",
- "total": "18565",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABE0lE\nQVQ4jZ2RsW7CMBRFz3OiEBLiDGkHJMLXde7QDv2MrkiV+mGZMjAwVCAypEowz52BEIWe0br36NqW\nzWbzISLvwDOP8QN8GhF5+0cZ4Al4NUA6JR2G4c2Z9z41U8rWWsqyJI7jG9Gt9or1ek0cxzjnWK1W\nOOeo6/q+wBhDlmUcj0cAttsteZ6jqqgq3vvhBUEQkCQJQRCQZRlpmrLb7VBVDofD3YUXguVyiaoy\nm83ouo48z0fLF4K+76mqajQ8xOgvRFFEFEXTFlxTFAUA5/OZxWLBfr+fLrDWYq1lPp9zOp1QVZxz\nNE0zTdA0DW3bIiIAeO9xzj12hXuFa4yI/E5KDiAibaiqX8aYF+/9+HMP8/0H6Zdjte0hdIkAAAAA\nSUVORK5CYII=\n"
+ "total": "6607"
},
"parks": {
"id": "parks",
"name": "Parks",
- "total": "152199",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABsUlE\nQVQ4jZ2SvcoaQRSGn5ldBlkVcXXXYrHQBcGfXhALEXtvInWKpMhlpBUCuQRrSy0EOwsrsdZCIxbi\nKqI7KYL7ucQvIXmbgXPO+5wfRgwGgy9CiM+Aw7/pB/BVCiE+/YcZIA98lEAyiuTzeJ73sjqTyeB5\nHqZpRjGtddJ8Lmo0GiilWK/XMbPjOPT7faSU7Pd7hsMhWmsA5HOHcrlMsVjEtu3IXKvV6HQ6SPmr\nNJfL0ev1SKfTcUC1WsU0TaSU1Ot1ACzLot1uk81mYxOVSiWazeYbIJFIUKlUogLf90mlUtzvd67X\n68ubGIbxBnBdF6VUlFRKUSgUkFJyPp9fAoIgAMB8AB47PuS6LoVCgUwm8xJQrVZZLpfSBKKLPisM\nw3fHf3iCIMBUSuE4DqvVCtu2MQyD7XaLbdtMJhNutxtCCBzHwfd9ptMplmWx2Ww4Ho+heb1eGY1G\nAHS7XZRSjMfjqNNisQCg1WoBsNvt2G63UT6++B+USqVi70OxnzifzxFCvARMp1NmsxmXyyUOEEKc\ntdZJgMPh8O4Ep9Ppt5gQIjDDMPwmpfygtVYvfH/T95/2t38vCgWX4gAAAABJRU5ErkJggg==\n"
+ "total": "108072"
},
"policestations": {
"id": "policestations",
"name": "Police stations",
- "total": "12684",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACmklE\nQVQ4jZWSb0zMcRzHX9/rcn9y6c+6SLRzRNyVf7GSxLSpNprZ8AAz1dYDDyrG5pmFYUPW1oNMa56k\nTcYTYf60ShJibq2GWwrVVarrujp1v68HJZ22zPvh5/N9vfb57PsRJzJ0p5CyQEIY/xNBHwpXRUG6\ntgcw/q6vS9lPaLiJ8KgYutttPK+6hlYfiGdsBIFgYtyDlAoAEnrUAgLkFGy2JrM8dhsAroEeQiPM\nnC61EWxcilQUutptuIZ6+fjuKXX3Sxj3jAaoZ061JHo9EaZYivKSAEjZm8em1CNTIwue3L6IRm8g\nfuchjJErqSzKRTVTUFN1nflBYQSFRQIw4uyb7lUW5fK+ropXj8qpvnWWRSYry2NT/HwEUirYbbXE\n7zwEwEBvBwBvn1XQ9Lh8+p3dVsuY28nCqBiVzwrWxD0ss2xFozNMChydAAwPOnw+wF+jx8/PH+/E\n+J8Vdmdf4siZCoKNS7Ek7CYhPZv+LjtfWhtJSDvGyvWpCCGwJmZSWNnNwqjV2G31Xk6ka10VV7Kl\ns79Lzsz4T4+8nLtBXsiyyAFHh/w7A45OWZCmHRYXc2LdBwtu6hoelNLe8pLF5jiWRG8kIT2bb5+a\nKT65A43OwNrkfZityQgh6GhrouHBDSZ+elxqs2WrasztpOnxLaRUcHxto7mmErW/hsSMHPSGENzD\nP2h8WEbjw7JZB6nSB4aIycuSPg1n/3cAFMU7C/IRdLe3KHpDCFGrNk8XDUFG4lMP0/f9M2MjQ3MK\n1K2vq73WpEwyjhYyOjKEac0WFK+XgMAQys8dmBMGUHmld/RuST52Wz0r4rajC1jAPK2eO8XH+fDi\n3r94t8jfpT+vUilZEjlvQWikMFmS1G1vqidGXYNyTlQCQlXyCx/lGF8oeHl6AAAAAElFTkSuQmCC\n"
+ "total": "23331"
},
"postboxes": {
"id": "postboxes",
"name": "Postboxes",
- "total": "167860",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABlklE\nQVQ4jZ2S3ytkcRjGP9/vnJk5Z4xJanGzcbPlx7ZJlC3Jpd11oyQXJm1EuaWmdhUulF/ljqJcINts\nra1VtkR2iwtpI7XIJn8AJZll58w5890LmUznKOO5e5/ez9Pz1it63xkRlOpR8IxMJDgnyYTn9Qtt\nCcjLCL5VQAleSgFZT4BvSyiytLvB6zMI5RY8CoxdnhG/iQGQCigqraZraIXDne+Y8RtX0OvTKal6\nw+L4e3Z/RNMDAP7sbSCkh+hEJ+a/v+mwP0Dbh0WOd9fSfHl/ONn/yXp0lPaBJYxgTsrXAyHa+7+w\n+W2Sg+2VhwMATn9vsf55jHBkHr+Rjd8I0vbxE5vLUxz9WnWcpTkcQDeCaD6d7pFVNK+f66sL9EDI\nbdUZ8KqmkYq6Fqb7GlAqCQgAWiNzCOkonH5CcVU95bXNzA+HsRJxbCuBbZnYlsnCSJiSynrKa5vc\nG9hWgvznxdhWgo7Br651pcdLQWEZtmWmPNH7Vo+pp36jIuY8KkNJJXB/u8fpWlO2nJEy2aFQvoxQ\nBQg5+x+PFXhVE8TeawAAAABJRU5ErkJggg==\n"
+ "total": "254835"
},
"publicart": {
"id": "publicart",
"name": "Public art",
- "total": "26949",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAClElE\nQVQ4jX2TW0jTYRiHn/8253S6WpvZlIWHJiKKICkeSMSLKCW9yE50kxVdFEapHSiKLkpIsKCIkJQg\niDDpQBFGZFkRKEValpE1KtPUOc9/nfvv8HURHtM++G7e93sfHn68n1SeH3QMIcoEhAEEaIPwKC4s\n0Un0fm9nySPhxM9FlRCidHoYYNepOnTBBnaU1hCyzLw0QGAWEgfVWTbNGUA7U/d5CYu04ZKHcXR/\nZXJ8aGkJAepMm+bEXIDj1xcK91WCJLE6LhVJUiGPOvAqU4sxFM3CikoTgMkSS7DBTIBWR/rGPQjh\np/NdI09uVfDzc/P89wsB0QmZjI84GHF00fvjI8rUBJ2tjTy6fpLth6rJ2Xz4/4Chvu+EW+Pp6+rA\nGrcWrU5Pc0Mt8qiTHnsb+cXnSMwo+BegN5hYER7FUP8P3C6Z3C1HMIZZAcjdepSMvL3Ep+YhhJ/8\n3WdnADMZlFQ1YY5Ygzw2SIjBNM/KakvBakvhw+u7PK+/wLZD1ZgjYnH22P8aqNQaTJYYHtYcp/Z0\nIRNjg4slTkfLY/x+H3qDiVDjqlkDv89LR0sDHmUKxT3JsKOLxrpK4lJyiU3K5kbFTpKyCinYd57f\n9veEGMNx/v42P4M7V0rILjxASVUTfp8Xe/tL3j69ifD56LG38er+ZYJDjKxJzqG1qY7x4X5gziK5\nXTKdrc9IzNiEJSoRgzGchLQ8lq+0YrLEkFtURmBwKK0vblN/aT9+nwdAkcrzdLIA/bRJYFAo6RuK\nSV5XRFikjYDAIOQRB0gS966W8an5wWwoAlkqy9cNIFjy16St30WPvQ2tTs9A9xfkUedcgEOdEas1\nqiQRD8ILuBdej+LyqFRqz6/ONy7FPTnbE7iRVNf+ADWGAJ6DKFhCAAAAAElFTkSuQmCC\n"
+ "total": "82347"
},
"pubs": {
"id": "pubs",
"name": "Pubs",
- "total": "64292",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABoklE\nQVQ4jZWSQWsTQRiGn5lus1k2KW02bbNB0I3ttdBrD70K7aH03KsHT14ieBFB8ObBHyAI4km8i0d/\ngaCHUgrtJlTNbNPdhQYrqdkdD9KS7W4ifrf3/eZ7vneGEY+2rcdo3dawyP+U4IyUl6K9VQ6AJQAh\nJHO1BsasWThzObxgEAfXWkNgCLA1MGOUeP5eMVuypi4OTg548WD9bwiNbVw1ktElF4OYsp1ilu3M\n0HmkCLr7GKZF8nuY6RnjIg66fHz7jFsr61iVeUJ1zPejr9ex1zZ2cFtrkwFnvSPsOYfPn94Vxl9o\n3CFUfsaT4yLs+TjNlYn3ry17RNMAUdDBcb2JAMdt/SOB8qm7rcJhIQR11yNSnSkJVIfF5mohoLqw\nTJom/DwPJwMGscKqzGNa1Xz8hkd02s35GYDWmrh/gtPMv0Ot4RH1/JwvbxqR8qm7d/OApduEBQDj\nphEFHe7tPWFz92HGt6sOH948zQFEe7vcR1O/MkyrguO2kHImczBJRvS/HTIa/8qaU0Mn8pWU6X2N\nLgEMfw34cfwltylXGhDy9R+oGJDKkJ2t8AAAAABJRU5ErkJggg==\n"
+ "total": "86876"
},
"recycling": {
"id": "recycling",
"name": "Recycling",
- "total": "112589",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACfElE\nQVQ4jZWSXUjTURjGf2fOtWV+ZCqaEtkEBQstiVCijCJQy7zJDMoLKyOiD5pgUCkRVKLShWCYlB8I\nIwnxZhaJWV4YmmYUlkmhouZnpjV1/7ntdLGam9JFL5yL9znveZ7nfTgiL02Xj5QGCcH8TwmmcXBP\nGFK1E0CI+11kbBKxuw4x1NfB/mP5AAz3d9FQfgkppWtOwoRagI90e7xG58vJq3X4BYbxor4YIVSE\n6+OIiNrOwMd2vo8NMNzfhcNhR0h8VCudHcjKxy8wDADfwFAaK664VDMv3mfL1t04HHbXvAdBQHAE\nezIuAKAsmnlaU8j4YC/T374A8GNqmLbGMg9BtXszOzVC7Z0TpJ8p4k1zLT9nxgjXx9FQfhmAmfEB\n7DYrao0Wm9XizDI/3d+8OTbRRwinmdGv77AqCyAltiXFQy1cH8/h03eZmx7FWHoKJGa13aZgmZ/j\nfHEL3hod3a1GjCU5K6PBS60hp+AJ/kHhSClpN1Uw9KnTmUFY5Da8NToAdiRnsSl65yoCu81KU02B\n07YQHMktWQ4xYd9xAKSUPCzMwMcvaBVB0EY9b1uNDPV1/lknDo12rTPEzuYaouKS6Xn5mMmRzwih\nYkNoJIrFjHl2Ct26AFKyb9JUfQNT1TVCIqIZG/yA1bIAealac16aTvb3tMi6omzpXj2v6qUhVSvb\nGss8cLvdJovPJUhDivaX6q/1qluZxO89uso6QEzCQY++49kjxod6Abd/YLXM89pUic2qgBAArn1N\n1dfJOFtKX/dzlpRFmo23XWTCkKadQrI6tRWVlJZLu+mBJyiZ9ErUa9arhIwBaQOUf53RgfeKdNiX\nMYmCUFX+BoL4HBgsZzFBAAAAAElFTkSuQmCC\n"
+ "total": "225837"
},
"restaurants": {
"id": "restaurants",
"name": "Restaurants",
- "total": "209106",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACBElE\nQVQ4jZWSy2sTURSHv3vncWfyoKCJkVSttNYH1bhQUEGqiJRi0YXiTkoXrnSXFt0qglvxARYFF/4B\nKqKIuLOIou4CphvfIK2WljR9xGRmros2E6cjir/V5Zxzv/MUIwPuebQe1pDlfySYIuCKsa/bvAes\nadq37upDo1mcmwljs+s2szrXQVsmj3JSSz5NQgu2mwKSejnw0MkRjgxdYnG+wo3hg0x+HQdgT98Q\nhmkBsFCd5vmD6/xcnENokrKZJZPfRP/gBQDcZBvHz14NKzBthZASISW+18B2kqEvBOw+fAopjdDR\ntaOXbHs3UhqYlkJKAykNAt9Duek4oL2zEJtTe9dObCdJ4HsIaSCkge97KPcPFQS+FwMEgR8CpDQQ\nQqC1xnZSrfaaj/elMXr2Hm199j0+lMZQbpquwgFM0yYIfNZ29DD5pRwHvH52l/3HzrAqtxGAF49G\nmatM4TXq5DZsi8ynOjMRb6E2X+FasRetNeNvn/Lw9rkl+8IsLx/fCj+U3zxh4vO7OGApuIoQgnpt\nAa11aL8/WuRT+RWz0xPcuXgiMqcIQC3v13YSrJRlO1jKjYBjgOaBKDfFSik3jXJTCCH+Dfj90kJA\nIo2UBpaKVieGB5wfaDIAlkqQW7+FRr0WWRVAvrOAlAbfPpZaN6P5Lor9ictSBqc12o6l/Zs0IOTN\nX15/pTfzxHQBAAAAAElFTkSuQmCC\n"
+ "total": "459491"
},
"romanroads": {
"id": "romanroads",
"name": "Roman roads",
- "total": "2102",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABZklE\nQVQ4jcWRvStFcRjHP79zD+ftuofkJYO8JC9JGSi7icUqGZUd+RNsTCZKmQwig5QysWAQijJYLEK5\nl+Pec849ncfCdUmiO3jqWb5Pz+f5Ps+jpoetWUSmBGr4SygeiFlIDLTpG0AtgFIa1fVNtPcOMjm3\nQ0NLD+n7G+oaO6mub8ZyXJ4fb98Rtii61fSQ6Qk4AH2D44xMziMSY9oponxAPvQLQyWOWVuY4OJw\n+03A04pdmY6LYSUx7RQAepmB5biFtCuqGJtZwbCShR69GBD6L/jZJySOsZKVRPmAKPRJlJWj6wZh\n8MLR7ipBzvs4RfEKSilMx6Wrf5jRqWUuj3fY31rEclwMO4WfzXB2sPkxUfA+ORARcl4aL30HgJd5\n4Opk78dnaD9WfxHfAkTi0gAlOygdIPLfDv4CEEXuqyj8eoVsYqC1vEpT0gESAQEQeJn78Pp8Pzo9\nWA/87JP/rn9KIUBpS69c8oblJ95iUwAAAABJRU5ErkJggg==\n"
+ "total": "1539"
},
"ruins": {
"id": "ruins",
"name": "Ruins",
- "total": "9772",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABZElE\nQVQ4jZ2SPUgbcRiHn/8/l0vMiYNQLR0cJAqCKOhUKBT8WCKdKzoJTg4iGBAHXRUEnXRQKNkEq1I6\nKFLExaGluy4uLn6QeNHL5fzK5XU4XISouWf/Pe/vfXlVeqBmCpFJgQ9UgyJHmaXI5xZjG2ioKhyQ\nEEW7VmCFCAclBEuHDT9jAEQMk/lfeYqOjVe4wnNsik6OomPj3mTZycy8LvBLD/xZn8OMW0TNOGYs\nQXfvMFvL45ixxNsNnrn3HEoPdzj2OZ5jc3r8l4vTo1cFFW9QuL7k+8QqESMaTuBeZ6mr/0jf4HTI\nBvlL9jcW6O4ZItnxtaJApVNxV+mINTL7ExFBaY34PmZNLXfFG/zSI41NbRz/3wXg316G3NlJkBZc\nlU7F3daufivZWXnKS7yCzcHmIgiuoZTiU3MHXiH/bgEovnwb4/D3SrCChH1nwdWiuA0VDvAM8fWa\n1uVRQcwqp4PSP54Ax09+h9b49bYAAAAASUVORK5CYII=\n"
+ "total": "20441"
},
"schools": {
"id": "schools",
"name": "Schools",
- "total": "66340",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACDklE\nQVQ4jZWSvUtbYRTGf++bmw9rUi6prdBiUCgiSAW3aKXiKgouglNAEFx0KlLcXQShixKJUNxj/4Bu\nHTtIQWssCCLiFJuv672Q3M+3Q/C2ElPomV6e5zm/cw68IlvMfhCI9wie8x+llKoEMvgoJo4mysCL\nx0Kj6VE0qXFSOelCoawpVK9APOrPDc4RERFOK6coVGdA0KsBSCEZ1ocfeJlkBhS0ghazg7NcGpcP\n/Ku7K2zfRgMY7xtn5+0OpmuilCIVTaFJDR8fgAUWcAMX0zVJRBIkIgn2fuxRvCy2AUIILhoX5M/y\nWK7F4utF5gfn0do2AJvfNqnbdRp2g8JMASklwJ+EUgo9ruMGLg270XHu8e0xBzMHeIFHXMZDXd4/\n6nYd0zHpf9KPHtc5r52z+nU1DC6PLLN1vEX+LI+nvFAPN5h6OcX0q+nQKNVK3Fg3LH1ZYn1sndxI\njtxIrmMz2aH8VXpMZ2V0hf3SftdMV0C1VaVu1zn8ecjYs7GuAA2g6TWpNCsIIYjKKKZjMpAcYPfd\nLq5y6Yn0cG1ek4ql8AIPL/C4c+4AENmjrCUQvQAb4xsUSgUMx3h0WjKaZO3NGtvft+9/phWekElm\nMByjazOA5VqUm2WGng6Fmpj8PPlLKdWXiqWIyRjVVrUrACAdT+MrH8MxEELcaj7+gUSumK4ZayP/\n2U/NqQG0T1B8+g3PVtGlaPz8bAAAAABJRU5ErkJggg==\n"
+ "total": "91680"
},
"railwaystations": {
"id": "railwaystations",
"name": "Railway stations",
- "total": "2561",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB7klE\nQVQ4jZWSv2tTURzFP/e+xJe2qdXWJC0VStFBwaFFB+0gOvgDBX8UN/8HBx3EgpObDkpEBF0VB5da\nUBcFpyIIhapVqcWCkjQpaZqY5L3k5b53HdqkeXlR8MAd7rn3nO858BUkv10HcQ1EDMAQMLo9TJ8p\naYXjwlLRwVZ6g9DkEOKu4P5iFk0coCskeHNhNxNDEVJlxeSrFVytmTrUz+SeKFnL5cR0ik9rtYZJ\nVqLpaUw5NxplYihCvuqyaruMx0wOxiMApMqKRLfB1fEdW7Gk6Am1xuwzJQt5h7FnP1GebvKPPhcB\nuHc0xkivT4K/KLBUqPvErfiadwJcwCDebXQUAyQ6vIXaiSODEb5cHsFS/hRhCQcGTGaWy/82ANjf\nv+2vKdoRqPC/CBhkLJfTL1J8yFabXMnxODOT5u0vK2Dgq5CpKKrKI2O5lOpek697moylKDgeq5br\nMxAkF8uwsUwhKXhyMsGlvb0YIhh3Plfj4ssVln/XN9Wi7KugPM2Dj0U6aAGY/lHZEneqYBqCh8fj\n3J5b58Zszvfx2HAXr88P8/x7iYWWhZJo7Mbl1uEBbOVx8/1aYPq7lE1yvsDTU4OE5WZGrS2Ds1d2\ngt4XllKN7TK5M7duZyy3CtTaz2zadqJhqdMVZedrXhUtHv8Bx9nC3MYswLAAAAAASUVORK5CYII=\n"
+ "total": "2581"
},
"stonecircles": {
"id": "stonecircles",
"name": "Stone circles",
- "total": "7",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACQUlE\nQVQ4jZWTP0iqYRTGf5/ZNblBQTWYojQ1lNia0CD0B8umcGswcGlsiBt3726NcQOHVgehuSHaGloC\nQacMgga9hoGfGabf+9zh3k+LbsN94Bne8573/HnecwC+Ab8A/SfrwHeA2sjIiHZ2djQzM/PO6fT0\nVPl8XpZl9W1zc3Pa3t6Wx+MRUAVopdNpSVK9XlcsFlMsFlMul5OLXC6n2dlZJRIJPT8/S5Li8bgA\nG6A1NTWlu7s7SVK73Va325Uk9Xo9OY4jSXp9fVWn05Ek3dzcyO/3DwIACofDajQakqRKpaJUKqVg\nMKhQKKR0Oq1arSZJur+/18TEhNuSbf0N8DUUCnF7e0u1WmV5eZl2u81bTE5OcnFxgdfrJRKJ0Gw2\nAVoe12F3dxefz8fh4eGHxwCPj48cHR0xPj5ONpt9d9c6ODjoC7a4uKjp6el/cmNjo++3v78vwPYA\n2LaNMQYAv9//IbuL0dFRABzHwbZtADwAx8fHbG1tAbC+vv5pgGQyCcDS0hInJyeDFgB5vV49PDyo\n2+0qk8l8KH9vb0/GGBWLxbfDNviFZDLJ2dkZPp8PYwyFQoFSqcTQ0BALCwtsbm5iWRbNZpOVlRWu\nr6/d5LTm5+f18vIiSSoWi7q8vJQxpi+Y4zg6Pz9XpVKRJD09PSkQCAwGaXV1VcYYXV1daWxsTICi\n0ag6nY56vZ6i0agABQIBlctlOY7j2mz4s1UKh8PugvSZSCS0trb2zjY8PKxgMOieaxbwA8gCXz6V\n/3P8/A1yTJGtK6bsJgAAAABJRU5ErkJggg==\n"
+ "total": "5"
},
"supermarkets": {
"id": "supermarkets",
"name": "Supermarkets",
- "total": "57717",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACGklE\nQVQ4jZWRv08TcRjGP9/jrlfv6HmtV34GShm0lSYMkqBEg4suDPgPGAeNg4ODDk4uhji4uKEhuroQ\nZhfjHyCVBQrBJiQY0NCa9opt8e64+zqJaSQxfaY3b97n8z55X7F8efkJgscCkaYLSSl/KJHyUhVC\nPAK6MgMIIZyoJ3qoIDG7NVtZi5kXM5hps1fperMimF6YZuP1Bu1qW6oAU0+nMPoMNEvDd31UQyUK\nIqIgImbH8F0fzdQI/RDN1AhaAYc7hwCoAOGvkPXFdYauDVFaKjF8fZjWtxbuF5eJ+xOUlkqM3Byh\nUW6Qv5vn+Oj4JJEKUF2rkrudQz+nU3hQIDGawG/6eDUPZ9Kh8KCANWaRvpTGmXQoLhT/BYzNjVH9\nXP1vgtEboxx8OugEeHUPPaWTzCXJzmexL9j4rk8yn8Q+b5Odz5K6mMKZdAhaAci/Rz35Qq1UQ0qJ\nDCVezcPddqmsVmh9b1FZrdDYaRBPxmmUGx1fUf8Um282Gbw6iDlsYmUs+q/006P1EAURuTs5UMDO\n25TflU8HeHWPvQ976Em9Y0AzNYwBA2PAIJVLUV2rng5Qz6jMLs4SOxujtlnrhPRqJDIJfNenfdA+\nHRBPx6lv19FtneKzIqEfngyN3xonCiP6pvoImkEnQCjiSEppNr822f+4j//T7zAD7L7fJTOXYevt\nVkdfCNEWK9Mrz6Ui7wlEjC4kpQTBq983L9oIlGLTPQAAAABJRU5ErkJggg==\n"
+ "total": "107887"
},
"tabletennis": {
"id": "tabletennis",
"name": "Table tennis tables",
- "total": "5062",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABlElE\nQVQ4jZ2QsUtyYRTGf+ftpoiVNphDUUNd+PiGhi7UdYkaCvoXmoLaCxocImhsCBqa7AO3aMjGHJsi\nCBscigZBClquhFBYIlfv2xBdKjXxO9M573meH+d9xD6xk4JsIsToorTWT57y9iWRSTjAEEA0GMWK\nWRjKIOfkKNfKHSg4hkaHBWGkb4TUXIrB4CAAtUaNvfweZ/dn7QFCWH32y+aybwYI9gTZsrZYGlv6\n9Qgf0B/obylYn1wnZIQ6A3KlXEvBQGCA8ch4Z0D2Ictt+bal6NV97Qyoe3WSl0nyT3l/qdEcF46x\n43ZbgNgZuyJI2H9AsIYsJiITVOtVzKiJGTHZvtqmVC399FeMr1Ov6mV+eJ7Cc4Hr0vXHiaJY+bPC\n0cIRGxcbTd9UX4fVv6vsTO9wOHfoJ+9pj/RdmtPiKQezB0zFptoD3Ib7kYeu42nvmzB1kyL7kGU3\nsUs8FG+dgRLFTHyG4ksR581pDgxhcXSR88dzXM8FqDSF2GVVlBJV/U8zIvJmNGj8U6g1hEA3Zq01\naNLvtRCIgOLLzlkAAAAASUVORK5CYII=\n"
+ "total": "9125"
},
"taxiranks": {
"id": "taxiranks",
"name": "Taxi ranks",
- "total": "8988",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACMUlE\nQVQ4jYWTT0iTcRjHP7937+s73eZmzKUuraZFQqjRpcAOJRgGUUSH/lw6FBUFGh46dA869Ac9FNHJ\na4Y06BAURiuCYKUVaaaMaMzmdLPc3N73dXs7xMrtHfScHr5fns/zfQ6PYGj6KohBEPWU1Z4GO9Wy\n4FUsh1EwS02TRYS4LRieiWPiK+oC2Ntox++QGeltwG4TXAkl+LSkE4pl0fLmekhcMDSTBhxF7Ux7\nLcdanbyez5Ys3FqroOVN+l8m/olCpOXy2Jc63Bx8HCOZy5dbhE+04FEllrXCX60E0NtSg0uRON7m\ntAwDJHN5zu90cyOcsgLa3AqPDjUyNpfh3n4fogIgGMnQ3+lhZtlgbC4NgFQ0m10ys8sGA6FEScT1\nde3NIs+jWQK1SuUTuupVls4FKg4DfDy1GYCJhGYFJHMFppI67RuqeBvP8UsvTSGAnuYaouk1vq0Y\nVsDkosbN9yke9Gzk4vgC79ZtAZAErF3eRjCSYXQ2XfmEYCTDhfEFPizplvgFE04//WEFFxvVJuj0\nqsz9NGh1K3irbez2qSiSoLupGo8qMZXSqVMlZElYAVU2wZGAg1v7vGxyygzuquNhXyOyBANdHp4c\n9rPFpXD/gA+HXAGwohcYmV7hS8rgRXSVk9tdzGfyHA04mUxo7KhT+Jy0niZjkkX8+YVENs+z76v4\nnTJ3JlKEFzQ6vCo1ikRfMEZ3k53R2TR68TNNc1Uw/PU6ZuEsiCoL/n9liru/AY/YyVYqV+4tAAAA\nAElFTkSuQmCC\n"
+ "total": "16029"
},
"telephones": {
"id": "telephones",
"name": "Telephones",
- "total": "62623",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABvUlE\nQVQ4jZWSS0hUURiAv3PzccfrMx+DkogmGA6uIkzDTVCI4zppJb5pqWOIYIggugm0TWUk4ULERTRk\nljC4kwmcKFFRRrChwNFBBx81D808LqSB6zjN+K+/7+P8/Ed0mg1dSGmRkM1lRrDDCUPCUqN6gJyI\nnBCU3LyPqdxMfKIB28Qg3s3vAEjwKAK0SHJKhpHW/g8091mpqGnhKOCj/Zmduw8en8UlWlwkOTO3\niEeDM6Rn5wPw0+nAWFCKqqWx6VoOccpFspqUSmPv25B8fBTkh3Oe62VV2KdHWHV8ihwQQvCwcxRj\n/g0A5qae01OXi2pIwbvlYvrNEx0ftsKte/WYymsB+DI7jvWlBYDJ4TauxMXz9/iPjte9QNXSMDf0\nA7DvdWMdsejg83JYoNLcipaaCcDU6y6Cvv0w4b+B29VNAHi3XCzOvYsq6wJZecVcNRYA8Htvm+T0\n2D5mKPBPllKy9m2W7tEVCk2VsQeC/gMAHLYx7tS2cRjwseNejz2wsb7Akv09OddKMCRnMPG0kV+7\nnugBKQjA2Ykmh1oI+g/4/PEVzq+2qDLgFx3VSQOKctIskQmxGKGRgFBenAKto45WhHakvgAAAABJ\nRU5ErkJggg==\n"
+ "total": "70670"
},
"theatres": {
"id": "theatres",
"name": "Theatres",
- "total": "5581",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACeElE\nQVQ4jZWSW0jTcRzFP/+/f+d0a2pOU7N0pYbmJcMwDWSQgTd6kZAg6yEx6aFMhSTqJagwkAgU6UK9\nCCaVZFA9VAoZRQ+imd00vKTN6Ww63WxOt18Pkhql4Hn6PpxzvofDkSrzfM8iRIWAYNYDiQk8XPNK\nj1GagZB1iRfhJyQSZAk0a7FOXH6KseDM/0MINMpqwp1p+cSl5RBuSCQsKoFdmYdouVHJwKc3f/H+\nMdCHb2dffimm/m4itiXj46tF8Vbjpw2kqKoBt9uFEKDW6HhUX64o0clGr/TcEmbtVoQQTI4N0fag\nhmmrmZxjF1lYmGekrxNfbSBBYQacDhvagBBmJs2YB3o8imPaKvq6Wnn77PZSinBDEjt2H0DxVuH6\n5cBi+kZ0UibTP0d5cvc8heU3cTlnMQ32eOTRgW5PmCGR0Mh4TlY/JyA4Ah8/LZYffQDoNoayJ6uI\nwJBIdEGhGAvKQEBvZ+tyB1OWYVxOBw/rTrHgcjLwcbEoIQQAM1PjOB02AvQRbIlNxT5l4XnjJQBk\ngK5X90kxFjL2/TN22wQAUfHpeNwLuJyzvG6po6H6KH3v25ifn2N08APTVvNyAuvYEBqdHrXGnw2B\nm9D66zlYfJWx4a9sjU0l63AVxoIy5l1O5hw2GmuOL/Ul/znaW2rJPnKBAH0EcanZNNef5mVTNUK4\nUbzVKCpfJEmm6Xrp0ncAr4wY5RygcjpsSLIXsSn7eXHvClOWETJyS2h/XItGF4Spv5uG6iKGeztW\nzsYlVeaq7WLFnIM3x7A3pxhvlQ8drY0MfXm32lhBYJcq8tQWBPrVWWtAMK4It3xLlj3FAqFapxgk\n+c5vHcTvRx0HWgMAAAAASUVORK5CYII=\n"
+ "total": "9891"
},
"toilets": {
"id": "toilets",
"name": "Toilets",
- "total": "44491",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACF0lE\nQVQ4jb2QT0hUYRTFf9/n+A+1FBUnQQYVQ8ZKSwgm25Rt1EVFm6BoERFEUJGC1SZaRbWwtkW0aNPK\nVRFBYCsjA3GjJdMfR9N0fKaDT2fem2/ebRHzxllItOlsLufecw73XjXQVzqISL9ALf8ChYXHkBaR\n61lzMBRmZ3W9ryktr6Sl40jOoxSNbV0ECotBqBHFFa2gLCu+9miU87eHfUPNriZ6zt3x+b6uk1y+\n/5Zjp2/8CRTKAtmhm9pgauw1i7HJbbdenP1EdGKE75OjueZAb4l972K7bNprkkVs+qMMnqiUh1cP\nSezzmPT3lkh0YkS2Ij43LbdO1doaINJ7gZmp936ovbpE+GCPz4tLy6moqiP+YxqAjElj/fxGMBTW\nWilFS8dRVuOzuVVjU+yNHM97uZ1YZiOxAoCIh7XwBaU0ui7Upq2FrzTtOZz7fkUV9c3tKKW3/UcW\nei0+J++GhwiGwn5z9/5unt89i4gHgEk7zEXHAfi1NAPA8nwUO2GJTm0mpK6hNS+1OtiIlzE+zxiX\nl09vAjA+8gKA0VePseajXgDgw5tnJKx5OrvP0BiO8OBSJ05ynYaWA38/4a+K/xZgjEvGuJi0m3f/\nVph0iuTGGmk35fdUf1/JMkINQEGgiEBhEU7SBiBQWEzZjmoSKws+N2nHrwjxgkhzUZVW0gpixMs4\nGeM6gAM4nmccJ7mex/0qOCj95DcsAAFAotSLRgAAAABJRU5ErkJggg==\n"
+ "total": "95356"
},
"attractions": {
"id": "attractions",
"name": "Tourist attractions",
- "total": "24522",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB70lE\nQVQ4jZWRTWsTURiFn5lkMjOZGGmMilIwQkExGwUXNRtB0KIRqZsuulIJqBsXtqIgiKBWdKMbceEH\nIop/QetapKALF9aASAmitqYEJibpJPPxughNGjoj+O7u5Z7znvNcZbpoXkZkSmAz/zMKywTcU0Xk\nYpR4Wy7P7v1j4QZCVhQuxBWwJGLJyfP3MVNDlD+8CQ8hWGpUwuGRfZipIQKvw+jYmcgmkQYnSndw\nWjbOyh8Kx89GGsTXHrLbR9i5p0Auf4BkehPN+jIAQeBz7vZrFj6/o/Jljkp5jpWm3a0xfcxoHJ68\nao0eLdGwq4gE+G4b122D9OnE4gk03QQBK51hsTLP42vjDRXg7asZfnz7ROC5OE0bt+MMiAF8r4PT\ntPG9NvXaIs9uTvQZiAhPro9T/fkV3UxF9tUSJiLCg0uH8NzOeogv757CbTvENT3UwLA28vDKEXzf\nC4cI4LkOMUmEGnScZm/z6qz7xoSZQiToRTaS6f42TUdRlH8baJpBLJ4guSFDbalC+eMsVjqLpicJ\nAp8tw7sG3g9U0HSTzNYdLMy/5/nMJPXaLwBmX9ygePoWew9OkMsXWPpe7mmUqaJRRciuXhjJNE6r\nHsogFtMQ8QmCbkWE33Hx1UeqGpSELjmnZYeKAXx/DUABFPXpXyZfv86QJMt/AAAAAElFTkSuQmCC\n"
+ "total": "50106"
},
"touristinformation": {
"id": "touristinformation",
"name": "Tourist information",
- "total": "240689",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABJElE\nQVQ4jZ2TPyhFYRjGf993/lz3HoPBpUQG3UgGXaUMJHXvwKAkg4WBzXALGZhkZrMoWZSJ3aCsJrEI\nJQuL+Uru+V7DUcj5Tp3zrO/z/d7n/d5etTaV30BkVaBIGineMOw5IyX3FGhL9ThSQRQDWkFgcwyO\nzTK5sI3j+vEhhEAntajMbzIxt05XqWz1uEmAs/0a7d39PN9dZQM83lzy8nSbZME6Qkuxk/GZGltH\n9+TyzekTDFcXGZ1ewW8KUNr+VdbK+fEOr9/xtXbSAwCUisombGQDgEQAE2YDGGMACNMmcFyP3nKF\n1o4eAPqGqni5QiwgdgsigjEhJ7vLP0bP5/Oj/h8ginfk7z2YsMHD9YU19i/VXQn1gdZmSZD4i7FJ\nAKUPvwDgY0z1tStTgAAAAABJRU5ErkJggg==\n"
+ "total": "527741"
},
"racetracks": {
"id": "racetracks",
"name": "Tracks",
- "total": "16943",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB20lE\nQVQ4jZ2STWsTURSGn3PvJJnJmBFippJSdCNioKIoCBVcVPAvuBCEIKI/wIXuXLkUMS6Frl2Iy4og\nRRQR8Qt3JRowNmqTampwmpl0MjMugklDlbYeuIv33vN+wRUqi1dBroC47GQSviNyS7hTbZIwsSPy\nSKSpSLD/iwygxDY24iOFDLNTFpO2QUpBWiu0gN+P6UXQWY95+sXnxbJPnAw4BsDpqSzlUo6PnZB3\nKz2effX/apg3NSeLJpenHebrXe598DDstKZccqi8XyXZInE7iFhodFlodDl30GHxZ6iUH0akFLhZ\nve3quZSiaGs+/+onQqXqWYbY147ncS1NGCc8/+ZT64QogQnLYLnbRwmcP+SQNRTtIOLm21WaQewZ\nAH4/4frLHwDcmCmQz2hqhGgRlAxc4xgO78kw+6AxiiKC2hhNCRxz07xqBQCklKDlzzK8aQWcmrTG\n6owJnNmX5XWrN8SupTnqZoZ4/tMal6Z3/1vgQsnh8VJ3iJe8kEf1Ee6sx5hayJt6s0DRNogT8MJ4\n+Lg/l2KmaI45PqyvUS45mwXOHtjF/ZpHO4iHB8BJ67G7Jw2fE3tHtYTb1RWEgqmFINrqKw1mw27L\nQMldkvhiEJHeFhtGRonM/QYzTLEVVqP+kwAAAABJRU5ErkJggg==\n"
+ "total": "3545"
},
"viewpoints": {
"id": "viewpoints",
"name": "Viewpoints",
- "total": "40680",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACkUlE\nQVQ4jZWRXUiTYRiGr/f7Ptdy4jLn+lGhBLMyKlQC+8+MLMuKSCKjDhLpxwpUCjzoIKgoCKGCiOg0\nijzIDoqCkMDEKDLSLBs6NWeb+zNd0+n83g5W6jCDnsOb+7547ucRVYWzzyNlpYQk/mcEHnRqFCll\nxdTwlgOV7C+/iRYza9IrBLuOXWHfiRo0gzEiSixScEYTYJK/jSnpWRSUXECNMTDkc/Li/iUAVm8q\nZkNROaqmMeDppf7R9QhYYlKmbuXuteF12gFYtDx3Ql+Wsz0Sdn+jtfFJVJMoQGh4iNpb5XiddqzJ\n6RPrWlMy+OHto+7uOdwO28wAgM7WBh5cLwUhSF+1mfi584mbY6X2xklaXj+edkt1bbpWnbXloCF7\n6yHUGAN+Vzc+Vze2D/WYzIkMBwf52vwS24d6hBAsXrGeNduOYLYs5Lu9dVQzxSeKotKrxM2xsnHP\naTx9HXiddppfPeR9/QMA4hMWcKT6PtaUDKwpS1BUjfBYCK+jQ9HGRkfkoN+JZjAy4HHg6mmjp/0d\nX949n1jT0dFMZ0tDpLOqkpCUSjDgZ8jfL0XVTmNANRhN8QnzGPA40MfDAOTklWCMM+Ps+kRm7m7q\n7lRFAIqK2ZJMcMhHKBgIKADh0RF8ru6J8NrCMorKruHs+kR3+1tWrttH8dnbCCHQ9XH8/T2EhgN/\n/8KGPafYcfQiwYAfe1sjY6Egnr4OsvNK2Hu8ZtoXogDmxGQ2769gtsmM+9tXxsNjAPT3tqNqMeTk\nH2ZJVv7MAEVVARgN/eRj4+TPm57d4+egF6nrzDLGRgFEZaHRjcTyR7CmLsWyMI22N0+jjGmZ69H1\nMF2fmyZFSb+oKIi9rCh6qUQaphX810hAKLd/Aa2n+dfXI2hIAAAAAElFTkSuQmCC\n"
+ "total": "86434"
},
"windmills": {
"id": "windmills",
"name": "Windmills",
- "total": "2430",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACfElE\nQVQ4jX2SW0hUURiFv3MU0RmdizPjfRQ1UcpCkCxCeoiKsIQeSiiJgkoQTMHMQnrppg+J0kNpEVgm\nRBkhiBgJYkokSqVSiGRa5t2ZcW5nxrTcPYSKzOB6/Fnr23v9e0tlR0OuIsRlAaaQUB3LSwp//6zg\nT5IkodYYcDssIGFhlVpZCFEqwBSTuJMrdZ85cem+3zDAgbxyyh8Okpp5CARGIVEsS6AOUWspuNOG\nJjyK7Vk5pO/N9QnHJmeQnVuIKiyc02UNGGO2IQnUMoBXcdDWcB23fR61xsDxwhr0EfHr4WC1ljPX\nmgjTR+JxL/Kh7RGW6VEA5DVTf0cjjVX5OCxT6IxxnLv+AjkgEICzFc8xRCfiWpyjpb6UN003N/ZS\nlhPsFqBeGxijkzh/o4XwyARGPnbgdiyQsf8kitPK08pTTH77tNFL4PYBAIRqjRRVd6GPiEcIgcdl\n5UH5QSwzY5sXI3DL+JHitGFf+LXuUpxW7JYpvy/jAwhWaSiq7iQpPRunbYaFqVEizWkU13QTpovY\nGqAzmSmu7SE+dTe/vS4Gupv52tuK120nJmkXJffeE5ucsQkQsC8lsAIIMqdkUnC7FUNUIorTynB/\nO4rTihAC6+w4OpMZrSGGHXuOYZ0dZ35yBGBZBlCF6bl4qxWdMY6V5SW+D73DNvdz/RSPy0bf2ycs\neZxowqPIK6kn0py2UcHjWqTrdS0el435iWF62x/7dB3obmZipB+vYmew59XaDQhcM3S+vMvMjy9M\njw2RkJblAwhWaXhWlU9q5mEGupvX54FCwov4/w+G+9oBCNWa/AK8imNTGPBIpUdUlbK8ekEggnxS\nW0kAklz3D07/9tX20rokAAAAAElFTkSuQmCC\n"
+ "total": "3718"
},
"gritbins": {
"id": "gritbins",
"name": "Winter grit bins",
- "total": "9721",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAACN0lE\nQVQ4jXWTS0hUURjHf+fc1zxy7jjq2EMhLTUSXCW2DHITFNiiTbSJgnYtatFKIdrUKgpaCdW6IIgQ\n2uQmiMBcBOWDsOjh2JjOODlzZ+bOvfe0uGLp3L7d+R4//ud/viO4v3ADxHUQHfwTliY405Pkda5K\n3vFpCsUaQtyVCHlt9zDAgxNZnp7ax63jbc3DAIJ2lLqqo0hG1Z8tlfnpeNx+V4wGAEiR1KPyN0fa\nGB/OIAQMZizOTuX+z9idGB/OcPFoCiHC88muOG/OdXFhoCUS0KTg0mCKmXyd7j1haeLtOnFdkE3o\npC3JRj1oVtCXNuhLGwB8WHdxGmGTUjAx0saK4zOTr3Gk1UQTEVeQQnAgqZMyJadf5Lj3PjRu+ofD\nfMHl0WgnY71JYprgkG02A5bLHgAHU6GK2dU6jhdwLBtjbCrHQtFltDuB4yn2JzUyMW0noNwIKLkB\nGUuStiRD7RYJXfL8S4Ve2+Dx3G+G2i3MLf0DaQNdip0mLpc97IxJT8rgfH8LCnjyaZO4Jnj5zSFh\nFJgvuGQTGp1xjcO2wcJG4+8z/qr61H2FbUoWNxpcmV7l1XeHALAtyZ3ZAus1n6VSA0/B3oRGR1wX\n2wAFrGzt/OxqjcmPJWq+YrnsIYH+1tA811d8LjVYcXy8QO1cpFzFQwG2KbeN+rrpUfEUdV9te5Cr\neCwWXYp1X+koqojwP7i+Yq3qkzQksa1mL1DM5GtEhlKOjhSTqOAyCBNgruCiotsjAOLhHyaO1IPV\nST0qAAAAAElFTkSuQmCC\n"
+ "total": "15969"
},
"worship": {
"id": "worship",
"name": "Worship, place of",
- "total": "70805",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAABWElE\nQVQ4jZWTv2rCUBTGf4lEEUEQOrk5ZnMWFxdfwSHiYicRHAsdM/QVsvgKeYUsrg6Cq4qTtBYHaQjX\nP9zToTWYNrH1g2+4515+9zv3cAGegC0gd/odeAZ4Sztg27YEQSDj8ViKxWIW5BUgTNv0fV8u6vV6\nWYAPk281Gg201nieB8BsNgPgdDoxn8+5pRCQcrksYRiKiMhgMBDTNKXZbIrjONLv96VUKqUmSLRQ\nr9flfD6L1lp2u50cj8e4jel0KoZh3AYA0ul0RCklh8NBoiiSa9m2/Tfg2q1WKwEYDof3AQqFQiKF\n7/v3AQAJgiAG7Pf77DGmybIsqtVqvM7n89lj/OlKpSKr1Sq+PYoiabfb/0tgWRbr9ZparQbAcrmk\n2+2ilEpN8SuB4ziSpdFolEhg8PWrHq6JuVyOyWSC1prNZhPXlVK4rstisbiUtgbwAjwC6S90W94n\nv55YrIX7naYAAAAASUVORK5CYII=\n"
+ "total": "102605"
},
"youthhostels": {
"id": "youthhostels",
"name": "Youth hostels",
- "total": "3910",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB00lE\nQVQ4jZWSTU8TURiFn3vno9NpizPQMgkRTIgJEqRiJERXJAb3auLaDQsTokZZsGPlWiNbE/6FP8G4\nMDHu/AiJG6SJxQqk0887c12Ujh0QGs/yPe8599xzr2DrywaIdRAl/geafYR4JRHy+TCxFHDFt9JD\nQRGtn5hocsPET6/5SAG+0+R9pTVI5uTg8urcCFMFMyVev+5Ta0fshYpbQZbFIJM+4G8iWJv3eFz2\nEnKt7FFtRnQiDUCloViecLkxnjltcHc6T7mY4eHsCONZg2cLHmE3pn0s7mMvVNy+6HJ1zE4bPJq/\nAEApa7C9EtBQmqZKi/vYrSseXM4z49tCAqxMutyZcpOFxZLDZN78p7iPUGnCru4lWChlEAOkawvG\nssa5BtVmxG69q02ApcBJkQVLphJcKliYEn61Ig7acWpXAknLZ+H7UYcXH2rYUpzizNlRW+YswctP\nB9yfzrFzqNg57HAzcJAC4vO9MT/XOvG9txUAliccPlZbbLzbZ3NpdKgY4MyqA9dInvFHXQHgOwa2\n0btGqHpdCF5/qyIoAsz4NkftmEpD4WVk8jL1rqYba/KWxDr+OU2laUX6p4kUb9DxKgj76+9OkuBk\n2z2jEzMttv8ATEiZOLgXCtQAAAAASUVORK5CYII=\n"
+ "total": "10496"
},
"zoos": {
"id": "zoos",
"name": "Zoos",
- "total": "850",
- "icon": "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QA/wD/AP+gvaeTAAAB/0lE\nQVQ4jZ2SXUjTURiHn/Pff3O6xfpaFGul4p+kDRQtQy0ob4zVnReBF9JFQRBF4KBuu7MgikhCgrro\nA4MWBBFeaAVFRq5u/Bil2NZybM2arrXP3Oli6hCjWs/N4bzn9z7vgXOE+1D5WaTslmClFASz5Lms\na9bUh8CmkpoLVEiBUxFgWqpUOVroOHkVrb7t3y4hMalLm7p9HXS6b6FT9TS7jhOdmSQWCRCPRfgW\n8ZPLpAgHxvGNDKyQqACqwciRM33oVP3ygdWmYbVpq6ZOvHmC59op5r+GAFAAqh2tGIymVeHfsbPJ\nRXevl7VWe1HQ2NZJOhlnsL8HT+/pv0oq1qxjT/vRosBq03j1uI+B2+fxDt1FSvlHQS6TRCi6oiCX\nTTM6/AgAu9aAEGI5/On9yCqBolOZ+xIsCrLpH6j6MoQQbK9tAmA2NAXAnQtdXD/XDkDye6ywJuaI\nx8JFwfTYS7bt2I1QdNTuOkg4MM5zzxUAtlQ6+Tz1jnx+gemxF/gnhjFbNqI3GIHFZ3z24BIANXX7\nqXK0MNjfQyToA2D95krsqUaCH7z4fa+pdu5FCIXQx1EAhNtlTMjF31hWbsZssZKYj7K1pgGt/gBv\nn94jOjNZCAuF1sMnMFk2MHT/Ij+zmcQKQclIEooUpP6ruUBSlQvKDUXJH5NIQ4nTQSg3fwHHV6dc\noYgepAAAAABJRU5ErkJggg==\n"
+ "total": "994"
}
}
}
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/pois.json b/libraries/cyclestreets-core/src/test/resources/__files/pois.json
index 6478ce423..c4d7fb90b 100644
--- a/libraries/cyclestreets-core/src/test/resources/__files/pois.json
+++ b/libraries/cyclestreets-core/src/test/resources/__files/pois.json
@@ -4,9 +4,13 @@
{
"type": "Feature",
"properties": {
- "id": "101399",
+ "id": "alpha_101399",
"name": "Chris's Bikes",
"notes": "The notes section",
+ "osmTags": {
+ "opening_hours": "Mo-Fr 09:00-17:00; Sa 10:00-18:00",
+ "phone": "01234 567890"
+ },
"website": "http://www.madeup.com"
},
"geometry": {
@@ -55,6 +59,15 @@
"id": "113230",
"name": "Primo Cycles",
"notes": "",
+ "osmTags": {
+ "addr:city": "Cambridge",
+ "addr:housenumber": "5-7",
+ "addr:postcode": "CB5 8BA",
+ "addr:street": "Jesus Lane",
+ "name": "Primo Cycles",
+ "shop": "bicycle",
+ "source": "survey"
+ },
"website": null
},
"geometry": {
@@ -100,10 +113,22 @@
{
"type": "Feature",
"properties": {
- "id": "113267",
+ "id": "some-other-characters-113267",
"name": "Bicycle Ambulance",
"notes": "",
- "website": null
+ "osmTags": {
+ "addr:city": "Cambridge",
+ "addr:housename": "Level A, Park Street car park",
+ "addr:postcode": "CB5 8AS",
+ "addr:street": "Park Street",
+ "name": "Bicycle Ambulance",
+ "opening_hours": "Tu-Fr 08:30-18:00; Sa 10:00-18:00",
+ "shop": "bicycle",
+ "source": "local_knowledge",
+ "source:opening_hours": "http://bicycleambulance.com",
+ "url": "http://bicycleambulance.com"
+ },
+ "website": ""
},
"geometry": {
"type": "Point",
diff --git a/libraries/cyclestreets-core/src/test/resources/__files/upload-ok.json b/libraries/cyclestreets-core/src/test/resources/__files/upload-ok.json
index 544c95430..c4ab44028 100644
--- a/libraries/cyclestreets-core/src/test/resources/__files/upload-ok.json
+++ b/libraries/cyclestreets-core/src/test/resources/__files/upload-ok.json
@@ -1,7 +1,7 @@
{
"id": 64001,
"url": "https://www.cyclestreets.net/location/64001/",
- "shortlink": "http://cycle.st/p64001",
+ "shortlink": "https://cycle.st/p64001",
"imageUrl": "https://www.cyclestreets.net/location/64001/cyclestreets64001.jpg",
"thumbnailUrl": "https://www.cyclestreets.net/location/64001/cyclestreets64001-size425.jpg"
}
diff --git a/libraries/cyclestreets-core/src/test/resources/cyclestreets-api.key.enc b/libraries/cyclestreets-core/src/test/resources/cyclestreets-api.key.enc
new file mode 100644
index 000000000..692388637
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/resources/cyclestreets-api.key.enc
@@ -0,0 +1 @@
+Salted__-rM0гP›â¹]Ï´~Ë+²dÔ³„#OÂFì¶Ë”;;XÙ
\ No newline at end of file
diff --git a/libraries/cyclestreets-core/src/test/resources/journey-domain.json b/libraries/cyclestreets-core/src/test/resources/journey-domain.json
new file mode 100644
index 000000000..6e6278dbf
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/resources/journey-domain.json
@@ -0,0 +1,1295 @@
+{
+ "waypoints": [
+ {
+ "sequenceId": "1",
+ "longitude": "0.11783",
+ "latitude": "52.20530"
+ },
+ {
+ "sequenceId": "2",
+ "longitude": "0.13140",
+ "latitude": "52.22105"
+ },
+ {
+ "sequenceId": "3",
+ "longitude": "0.14744",
+ "latitude": "52.19962"
+ }
+ ],
+ "route": {
+ "start": "City+Centre",
+ "finish": "Thoday+Street",
+ "startBearing": "0",
+ "startSpeed": "0",
+ "start_longitude": "0.11783",
+ "start_latitude": "52.20530",
+ "finish_longitude": "0.14744",
+ "finish_latitude": "52.19962",
+ "crow_fly_distance": "4603",
+ "event": "depart",
+ "whence": "1533118269",
+ "speed": "24",
+ "itinerary": "62909947",
+ "clientRouteId": "0",
+ "plan": "quietest",
+ "note": "",
+ "length": "6257",
+ "time": "1584",
+ "busynance": "8471",
+ "quietness": "74",
+ "signalledJunctions": "3",
+ "signalledCrossings": "1",
+ "west": "0.11781",
+ "south": "52.19962",
+ "east": "0.15055",
+ "north": "52.22105",
+ "name": "City+Centre to Thoday+Street",
+ "walk": "1",
+ "leaving": "2018-08-01 11:11:09",
+ "arriving": "2018-08-01 11:37:33",
+ "coordinates": "0.11783,52.20530 0.11781,52.20545 0.11786,52.20549 0.11793,52.20550 0.11844,52.20551 0.11858,52.20553 0.11871,52.20556 0.11890,52.20561 0.11909,52.20570 0.11923,52.20575 0.11960,52.20590 0.12000,52.20603 0.12030,52.20611 0.12044,52.20612 0.12055,52.20614 0.12061,52.20616 0.12062,52.20619 0.12060,52.20628 0.12048,52.20650 0.12047,52.20651 0.12004,52.20699 0.11953,52.20752 0.11936,52.20776 0.11932,52.20776 0.11928,52.20778 0.11918,52.20785 0.11864,52.20833 0.11857,52.20838 0.11851,52.20841 0.11848,52.20844 0.11848,52.20846 0.11849,52.20848 0.11852,52.20849 0.11859,52.20852 0.11871,52.20858 0.11892,52.20869 0.11916,52.20884 0.11927,52.20889 0.11956,52.20901 0.11970,52.20917 0.11963,52.20928 0.11959,52.20940 0.11957,52.20953 0.11956,52.20964 0.11957,52.20970 0.11958,52.20974 0.11975,52.20981 0.11989,52.20984 0.12008,52.20988 0.12024,52.20991 0.12051,52.20997 0.12070,52.21004 0.12097,52.21018 0.12104,52.21021 0.12105,52.21022 0.12155,52.21057 0.12193,52.21086 0.12219,52.21098 0.12360,52.21128 0.12448,52.21152 0.12515,52.21172 0.12634,52.21207 0.12643,52.21210 0.12769,52.21224 0.12774,52.21222 0.12777,52.21224 0.12785,52.21221 0.12802,52.21215 0.12814,52.21224 0.12859,52.21259 0.12867,52.21270 0.12869,52.21275 0.12870,52.21279 0.12871,52.21290 0.12872,52.21312 0.12870,52.21431 0.12841,52.21428 0.12819,52.21427 0.12807,52.21427 0.12806,52.21436 0.12808,52.21468 0.12807,52.21476 0.12801,52.21485 0.12839,52.21494 0.12836,52.21498 0.12872,52.21506 0.13059,52.21547 0.13055,52.21556 0.13036,52.21597 0.13030,52.21618 0.13029,52.21627 0.13031,52.21638 0.13034,52.21659 0.13031,52.21675 0.13027,52.21689 0.13013,52.21705 0.12954,52.21750 0.12946,52.21751 0.12919,52.21772 0.12935,52.21780 0.12927,52.21787 0.12921,52.21792 0.12951,52.21807 0.13028,52.21846 0.13036,52.21850 0.13065,52.21864 0.13145,52.21900 0.13157,52.21909 0.13160,52.21937 0.13173,52.21942 0.13185,52.21946 0.13217,52.21944 0.13234,52.21945 0.13247,52.21941 0.13261,52.21948 0.13284,52.21958 0.13313,52.21975 0.13324,52.21981 0.13281,52.22013 0.13261,52.22024 0.13217,52.22059 0.13197,52.22071 0.13140,52.22105 0.13197,52.22071 0.13217,52.22059 0.13261,52.22024 0.13281,52.22013 0.13324,52.21981 0.13356,52.22009 0.13365,52.22006 0.13374,52.22003 0.13398,52.21991 0.13416,52.21978 0.13472,52.21939 0.13473,52.21937 0.13496,52.21921 0.13507,52.21913 0.13508,52.21910 0.13545,52.21909 0.13556,52.21908 0.13559,52.21906 0.13565,52.21901 0.13567,52.21895 0.13572,52.21890 0.13583,52.21886 0.13592,52.21884 0.13631,52.21866 0.13647,52.21878 0.13680,52.21894 0.13691,52.21887 0.13707,52.21876 0.13739,52.21859 0.13747,52.21855 0.13756,52.21850 0.13773,52.21842 0.13810,52.21827 0.13841,52.21814 0.13868,52.21800 0.13935,52.21756 0.13958,52.21761 0.13977,52.21765 0.14022,52.21729 0.14052,52.21702 0.14059,52.21692 0.14058,52.21685 0.14052,52.21663 0.14047,52.21651 0.14042,52.21641 0.14025,52.21621 0.14010,52.21606 0.13986,52.21581 0.14013,52.21565 0.14044,52.21545 0.14048,52.21543 0.14056,52.21538 0.14062,52.21532 0.14067,52.21522 0.14069,52.21513 0.14067,52.21505 0.14097,52.21504 0.14154,52.21472 0.14158,52.21467 0.14159,52.21464 0.14159,52.21458 0.14158,52.21452 0.14159,52.21445 0.14163,52.21441 0.14194,52.21419 0.14205,52.21412 0.14256,52.21388 0.14268,52.21381 0.14281,52.21377 0.14286,52.21376 0.14295,52.21372 0.14303,52.21366 0.14306,52.21361 0.14308,52.21355 0.14307,52.21349 0.14301,52.21339 0.14294,52.21326 0.14285,52.21310 0.14279,52.21298 0.14270,52.21282 0.14269,52.21274 0.14253,52.21251 0.14258,52.21247 0.14260,52.21238 0.14268,52.21238 0.14275,52.21236 0.14291,52.21231 0.14311,52.21224 0.14327,52.21221 0.14357,52.21216 0.14251,52.21125 0.14319,52.21094 0.14337,52.21085 0.14339,52.21082 0.14341,52.21079 0.14340,52.21074 0.14341,52.21067 0.14350,52.21050 0.14363,52.21044 0.14382,52.21035 0.14412,52.21024 0.14456,52.20990 0.14472,52.20997 0.14481,52.20990 0.14500,52.20976 0.14515,52.20966 0.14524,52.20962 0.14531,52.20959 0.14546,52.20955 0.14578,52.20948 0.14590,52.20945 0.14598,52.20943 0.14595,52.20934 0.14592,52.20924 0.14585,52.20913 0.14577,52.20900 0.14569,52.20886 0.14565,52.20880 0.14561,52.20873 0.14557,52.20866 0.14556,52.20863 0.14553,52.20858 0.14545,52.20845 0.14541,52.20839 0.14539,52.20833 0.14536,52.20820 0.14529,52.20808 0.14512,52.20794 0.14520,52.20791 0.14518,52.20786 0.14521,52.20782 0.14527,52.20777 0.14578,52.20760 0.14574,52.20757 0.14569,52.20753 0.14589,52.20744 0.14619,52.20733 0.14637,52.20727 0.14784,52.20686 0.14807,52.20680 0.14845,52.20674 0.14873,52.20671 0.14906,52.20668 0.14926,52.20664 0.14955,52.20654 0.14960,52.20649 0.14963,52.20637 0.14970,52.20636 0.14980,52.20636 0.14983,52.20634 0.14989,52.20636 0.14998,52.20628 0.15003,52.20620 0.15055,52.20526 0.15046,52.20525 0.15028,52.20521 0.15009,52.20516 0.14999,52.20509 0.14991,52.20502 0.14987,52.20497 0.14840,52.20293 0.14830,52.20277 0.14827,52.20272 0.14826,52.20264 0.14828,52.20255 0.14833,52.20240 0.14839,52.20219 0.14837,52.20212 0.14858,52.20203 0.14852,52.20198 0.14849,52.20193 0.14849,52.20187 0.14856,52.20163 0.14856,52.20148 0.14854,52.20140 0.14850,52.20133 0.14824,52.20091 0.14812,52.20072 0.14786,52.20029 0.14771,52.20004 0.14748,52.19967 0.14744,52.19962",
+ "elevations": "9,9,9,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,9,9,9,9,9,9,9,9,9,8,8,8,8,8,8,8,8,8,8,8,8,7,7,7,7,7,7,7,7,6,6,6,6,6,6,5,5,5,5,5,5,5,5,5,5,4,4,4,4,4,4,5,5,5,5,5,5,7,6,6,6,6,7,7,7,7,7,7,7,8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,10,10,10,9,9,10,10,10,10,9,9,9,9,9,9,10,10,10,10,10,10,10,10,10,9,9,9,9,9,9,8,8,8,8,8,8,8,8,8,8,8,8,8,7,7,8,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,6,6,6,6,6,6,6,5,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5,5,5,5,5,6,6,6,8,9,9,11,10,11,11,11,11,11,11,11,11,11,11,11,11,11,10,10,10,9,9,9,9,8,8,8,9,8,8,8,8,8,8,8,9,9,9,9,9,10,10,10,10,10,9,10,10,10,10,10,9,8,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,10,10,10,10,10,11,11,11,11,11,11,11,11,11,11,11,12,12,13,13,13,14",
+ "distances": "17,6,5,35,10,9,14,16,11,30,31,22,10,8,5,3,10,26,1,61,68,29,3,4,10,65,7,5,4,2,2,2,6,11,19,23,9,24,20,13,14,15,12,7,5,14,10,14,11,20,15,24,6,1,52,41,22,102,66,51,90,7,87,4,3,6,13,13,50,13,6,5,12,24,132,20,15,8,10,36,9,11,28,5,26,135,10,47,24,10,12,23,18,16,20,64,6,30,14,10,7,26,68,7,25,68,13,31,10,9,22,12,10,12,19,27,10,46,18,49,19,54,54,19,49,18,46,38,7,7,21,19,58,2,24,12,3,25,8,3,7,7,7,9,7,33,17,29,11,16,29,7,8,15,30,26,24,67,17,14,50,36,12,8,25,14,12,25,20,32,26,31,4,8,8,12,10,9,20,53,6,3,7,7,8,5,32,11,44,11,10,4,8,9,6,7,7,12,15,19,14,19,9,28,6,10,5,5,12,16,11,21,124,58,16,4,4,6,8,20,11,16,24,48,13,10,20,15,8,6,11,23,9,6,10,11,13,15,16,7,8,8,3,6,15,7,7,15,14,19,6,6,5,7,40,4,6,17,24,14,110,17,27,19,23,14,23,7,14,5,7,3,5,11,10,110,6,13,14,10,10,6,248,19,6,9,10,17,24,8,17,7,6,7,27,17,9,8,50,23,51,30,44,6",
+ "grammesCO2saved": "1166",
+ "calories": "116",
+ "edition": "routing180716",
+ "type": "route"
+ },
+ "segments": [
+ {
+ "name": "Senate House Hill, NCN 11",
+ "legNumber": "1",
+ "distance": "23",
+ "time": "14",
+ "busynance": "26",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "",
+ "startBearing": "357",
+ "color": "#aaaacc",
+ "points": "0.11783,52.20530 0.11781,52.20545 0.11786,52.20549",
+ "distances": "0,17,6",
+ "elevations": "9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ },
+ {
+ "name": "Senate House Hill, NCN 11",
+ "legNumber": "1",
+ "distance": "5",
+ "time": "5",
+ "busynance": "6",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "77",
+ "color": "#aaaacc",
+ "points": "0.11786,52.20549 0.11793,52.20550",
+ "distances": "0,5",
+ "elevations": "9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ },
+ {
+ "name": "St Mary's Street, NCN 11",
+ "legNumber": "1",
+ "distance": "54",
+ "time": "52",
+ "busynance": "206",
+ "flow": "against",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "88",
+ "color": "#aaaacc",
+ "points": "0.11793,52.20550 0.11844,52.20551 0.11858,52.20553 0.11871,52.20556",
+ "distances": "0,35,10,9",
+ "elevations": "9,9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ },
+ {
+ "name": "Market Hill, NCN 11",
+ "legNumber": "1",
+ "distance": "41",
+ "time": "17",
+ "busynance": "51",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "67",
+ "color": "#aaaacc",
+ "points": "0.11871,52.20556 0.11890,52.20561 0.11909,52.20570 0.11923,52.20575",
+ "distances": "0,14,16,11",
+ "elevations": "9,9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ },
+ {
+ "name": "Market Street, NCN 11",
+ "legNumber": "1",
+ "distance": "106",
+ "time": "41",
+ "busynance": "131",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "57",
+ "color": "#aaaacc",
+ "points": "0.11923,52.20575 0.11960,52.20590 0.12000,52.20603 0.12030,52.20611 0.12044,52.20612 0.12055,52.20614 0.12061,52.20616",
+ "distances": "0,30,31,22,10,8,5",
+ "elevations": "9,9,8,8,8,8,8",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ },
+ {
+ "name": "Sidney Street, NCN 11",
+ "legNumber": "1",
+ "distance": "198",
+ "time": "83",
+ "busynance": "289",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "12",
+ "color": "#aaaacc",
+ "points": "0.12061,52.20616 0.12062,52.20619 0.12060,52.20628 0.12048,52.20650 0.12047,52.20651 0.12004,52.20699 0.11953,52.20752 0.11936,52.20776",
+ "distances": "0,3,10,26,1,61,68,29",
+ "elevations": "8,8,8,9,9,9,9,9",
+ "provisionName": "Pedestrianized area",
+ "type": "segment"
+ },
+ {
+ "name": "Bridge Street, NCN 11",
+ "legNumber": "1",
+ "distance": "94",
+ "time": "15",
+ "busynance": "87",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "270",
+ "color": "#000000",
+ "points": "0.11936,52.20776 0.11932,52.20776 0.11928,52.20778 0.11918,52.20785 0.11864,52.20833 0.11857,52.20838 0.11851,52.20841",
+ "distances": "0,3,4,10,65,7,5",
+ "elevations": "9,9,9,9,9,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Bridge Street, NCN 11;51",
+ "legNumber": "1",
+ "distance": "6",
+ "time": "1",
+ "busynance": "6",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "329",
+ "color": "#000000",
+ "points": "0.11851,52.20841 0.11848,52.20844 0.11848,52.20846",
+ "distances": "0,4,2",
+ "elevations": "8,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Round Church Street",
+ "legNumber": "1",
+ "distance": "96",
+ "time": "14",
+ "busynance": "112",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "17",
+ "color": "#000000",
+ "points": "0.11848,52.20846 0.11849,52.20848 0.11852,52.20849 0.11859,52.20852 0.11871,52.20858 0.11892,52.20869 0.11916,52.20884 0.11927,52.20889 0.11956,52.20901",
+ "distances": "0,2,2,6,11,19,23,9,24",
+ "elevations": "8,8,8,8,8,8,8,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "20",
+ "time": "3",
+ "busynance": "21",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "28",
+ "color": "#ff0000",
+ "points": "0.11956,52.20901 0.11970,52.20917",
+ "distances": "0,20",
+ "elevations": "8,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Park Street",
+ "legNumber": "1",
+ "distance": "66",
+ "time": "13",
+ "busynance": "80",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "339",
+ "color": "#000000",
+ "points": "0.11970,52.20917 0.11963,52.20928 0.11959,52.20940 0.11957,52.20953 0.11956,52.20964 0.11957,52.20970 0.11958,52.20974",
+ "distances": "0,13,14,15,12,7,5",
+ "elevations": "7,7,7,7,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Lower Park Street",
+ "legNumber": "1",
+ "distance": "115",
+ "time": "18",
+ "busynance": "132",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "56",
+ "color": "#000000",
+ "points": "0.11958,52.20974 0.11975,52.20981 0.11989,52.20984 0.12008,52.20988 0.12024,52.20991 0.12051,52.20997 0.12070,52.21004 0.12097,52.21018 0.12104,52.21021 0.12105,52.21022",
+ "distances": "0,14,10,14,11,20,15,24,6,1",
+ "elevations": "7,7,6,6,6,6,6,6,5,5",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "lcn (unknown cycle network)",
+ "legNumber": "1",
+ "distance": "424",
+ "time": "69",
+ "busynance": "427",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "41",
+ "color": "#ff0000",
+ "points": "0.12105,52.21022 0.12155,52.21057 0.12193,52.21086 0.12219,52.21098 0.12360,52.21128 0.12448,52.21152 0.12515,52.21172 0.12634,52.21207",
+ "distances": "0,52,41,22,102,66,51,90",
+ "elevations": "5,5,5,5,5,5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "lcn (unknown cycle network) continuation",
+ "legNumber": "1",
+ "distance": "7",
+ "time": "3",
+ "busynance": "7",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "61",
+ "color": "#ff0000",
+ "points": "0.12634,52.21207 0.12643,52.21210",
+ "distances": "0,7",
+ "elevations": "5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "lcn (unknown cycle network)",
+ "legNumber": "1",
+ "distance": "87",
+ "time": "13",
+ "busynance": "84",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "80",
+ "color": "#ff0000",
+ "points": "0.12643,52.21210 0.12769,52.21224",
+ "distances": "0,87",
+ "elevations": "5,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "NCN 11",
+ "legNumber": "1",
+ "distance": "4",
+ "time": "1",
+ "busynance": "4",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "123",
+ "color": "#ff0000",
+ "points": "0.12769,52.21224 0.12774,52.21222",
+ "distances": "0,4",
+ "elevations": "4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "3",
+ "time": "13",
+ "busynance": "5",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "43",
+ "color": "#ff0000",
+ "points": "0.12774,52.21222 0.12777,52.21224",
+ "distances": "0,3",
+ "elevations": "4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "32",
+ "time": "6",
+ "busynance": "38",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "121",
+ "color": "#ff0000",
+ "points": "0.12777,52.21224 0.12785,52.21221 0.12802,52.21215 0.12814,52.21224",
+ "distances": "0,6,13,13",
+ "elevations": "4,4,4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Fort St George Bridge",
+ "legNumber": "1",
+ "distance": "86",
+ "time": "18",
+ "busynance": "112",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "38",
+ "color": "#ff0000",
+ "points": "0.12814,52.21224 0.12859,52.21259 0.12867,52.21270 0.12869,52.21275 0.12870,52.21279 0.12871,52.21290",
+ "distances": "0,50,13,6,5,12",
+ "elevations": "4,5,5,5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Pretoria Road",
+ "legNumber": "1",
+ "distance": "156",
+ "time": "34",
+ "busynance": "217",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "2",
+ "color": "#000000",
+ "points": "0.12871,52.21290 0.12872,52.21312 0.12870,52.21431",
+ "distances": "0,24,132",
+ "elevations": "5,5,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Hamilton Road",
+ "legNumber": "1",
+ "distance": "43",
+ "time": "6",
+ "busynance": "38",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "260",
+ "color": "#000000",
+ "points": "0.12870,52.21431 0.12841,52.21428 0.12819,52.21427 0.12807,52.21427",
+ "distances": "0,20,15,8",
+ "elevations": "7,6,6,6",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Ferry Path",
+ "legNumber": "1",
+ "distance": "66",
+ "time": "14",
+ "busynance": "90",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "356",
+ "color": "#000000",
+ "points": "0.12807,52.21427 0.12806,52.21436 0.12808,52.21468 0.12807,52.21476 0.12801,52.21485",
+ "distances": "0,10,36,9,11",
+ "elevations": "6,6,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Chesterton Road, A1303",
+ "legNumber": "1",
+ "distance": "28",
+ "time": "9",
+ "busynance": "56",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "69",
+ "color": "#3333aa",
+ "points": "0.12801,52.21485 0.12839,52.21494",
+ "distances": "0,28",
+ "elevations": "7,7",
+ "provisionName": "Major road",
+ "type": "segment"
+ },
+ {
+ "name": "Along the side of Chesterton Hall Crescent",
+ "legNumber": "1",
+ "distance": "166",
+ "time": "29",
+ "busynance": "204",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "335",
+ "color": "#ff0000",
+ "points": "0.12839,52.21494 0.12836,52.21498 0.12872,52.21506 0.13059,52.21547",
+ "distances": "0,5,26,135",
+ "elevations": "7,7,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Chesterton Hall Crescent",
+ "legNumber": "1",
+ "distance": "250",
+ "time": "51",
+ "busynance": "321",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "345",
+ "color": "#000000",
+ "points": "0.13059,52.21547 0.13055,52.21556 0.13036,52.21597 0.13030,52.21618 0.13029,52.21627 0.13031,52.21638 0.13034,52.21659 0.13031,52.21675 0.13027,52.21689 0.13013,52.21705 0.12954,52.21750 0.12946,52.21751",
+ "distances": "0,10,47,24,10,12,23,18,16,20,64,6",
+ "elevations": "7,8,8,8,8,8,8,9,9,9,9,9",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Chesterton Hall Crescent continuation",
+ "legNumber": "1",
+ "distance": "54",
+ "time": "17",
+ "busynance": "74",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "1",
+ "turn": "bear right",
+ "startBearing": "322",
+ "color": "#ff0000",
+ "points": "0.12946,52.21751 0.12919,52.21772 0.12935,52.21780 0.12927,52.21787",
+ "distances": "0,30,14,10",
+ "elevations": "9,10,10,10",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "7",
+ "time": "3",
+ "busynance": "15",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "324",
+ "color": "#ff0000",
+ "points": "0.12927,52.21787 0.12921,52.21792",
+ "distances": "0,7",
+ "elevations": "10,10",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Link joining Hurst Park Avenue, Highworth Avenue, Milton Road, A1309, Ascham Road, Milton Road, A1134",
+ "legNumber": "1",
+ "distance": "301",
+ "time": "48",
+ "busynance": "339",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "51",
+ "color": "#ff0000",
+ "points": "0.12921,52.21792 0.12951,52.21807 0.13028,52.21846 0.13036,52.21850 0.13065,52.21864 0.13145,52.21900 0.13157,52.21909 0.13160,52.21937 0.13173,52.21942 0.13185,52.21946 0.13217,52.21944 0.13234,52.21945 0.13247,52.21941",
+ "distances": "0,26,68,7,25,68,13,31,10,9,22,12,10",
+ "elevations": "10,10,10,10,10,9,9,10,10,10,10,9,9",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Milton Road, A1309",
+ "legNumber": "1",
+ "distance": "68",
+ "time": "14",
+ "busynance": "247",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "51",
+ "color": "#3333aa",
+ "points": "0.13247,52.21941 0.13261,52.21948 0.13284,52.21958 0.13313,52.21975 0.13324,52.21981",
+ "distances": "0,12,19,27,10",
+ "elevations": "9,9,9,9,9",
+ "provisionName": "Major road",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "1",
+ "distance": "46",
+ "time": "12",
+ "busynance": "103",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "321",
+ "color": "#7777cc",
+ "points": "0.13324,52.21981 0.13281,52.22013",
+ "distances": "0,46",
+ "elevations": "9,10",
+ "provisionName": "Service Road",
+ "type": "segment"
+ },
+ {
+ "name": "Pye Alley",
+ "legNumber": "1",
+ "distance": "67",
+ "time": "64",
+ "busynance": "171",
+ "flow": "against",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "312",
+ "color": "#008800",
+ "points": "0.13281,52.22013 0.13261,52.22024 0.13217,52.22059",
+ "distances": "0,18,49",
+ "elevations": "10,10,10",
+ "provisionName": "Footpath",
+ "type": "segment"
+ },
+ {
+ "name": "Mulberry Close",
+ "legNumber": "1",
+ "distance": "73",
+ "time": "12",
+ "busynance": "92",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "314",
+ "color": "#000000",
+ "points": "0.13217,52.22059 0.13197,52.22071 0.13140,52.22105",
+ "distances": "0,19,54",
+ "elevations": "10,10,10",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Mulberry Close",
+ "legNumber": "2",
+ "distance": "73",
+ "time": "12",
+ "busynance": "93",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "double-back",
+ "startBearing": "135",
+ "color": "#000000",
+ "points": "0.13140,52.22105 0.13197,52.22071 0.13217,52.22059",
+ "distances": "0,54,19",
+ "elevations": "10,10,10",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Pye Alley",
+ "legNumber": "2",
+ "distance": "67",
+ "time": "60",
+ "busynance": "124",
+ "flow": "with",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "142",
+ "color": "#008800",
+ "points": "0.13217,52.22059 0.13261,52.22024 0.13281,52.22013",
+ "distances": "0,49,18",
+ "elevations": "10,10,10",
+ "provisionName": "Footpath",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "46",
+ "time": "8",
+ "busynance": "70",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "141",
+ "color": "#7777cc",
+ "points": "0.13281,52.22013 0.13324,52.21981",
+ "distances": "0,46",
+ "elevations": "10,9",
+ "provisionName": "Service Road",
+ "type": "segment"
+ },
+ {
+ "name": "Milton Road, A1309",
+ "legNumber": "2",
+ "distance": "38",
+ "time": "6",
+ "busynance": "104",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "35",
+ "color": "#3333aa",
+ "points": "0.13324,52.21981 0.13356,52.22009",
+ "distances": "0,38",
+ "elevations": "9,9",
+ "provisionName": "Major road",
+ "type": "segment"
+ },
+ {
+ "name": "Oak Tree Avenue",
+ "legNumber": "2",
+ "distance": "153",
+ "time": "25",
+ "busynance": "144",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "119",
+ "color": "#000000",
+ "points": "0.13356,52.22009 0.13365,52.22006 0.13374,52.22003 0.13398,52.21991 0.13416,52.21978 0.13472,52.21939 0.13473,52.21937 0.13496,52.21921 0.13507,52.21913 0.13508,52.21910",
+ "distances": "0,7,7,21,19,58,2,24,12,3",
+ "elevations": "9,9,9,9,9,8,8,8,8,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "66",
+ "time": "59",
+ "busynance": "124",
+ "flow": "against",
+ "walk": "1",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "93",
+ "color": "#008800",
+ "points": "0.13508,52.21910 0.13545,52.21909 0.13556,52.21908 0.13559,52.21906 0.13565,52.21901 0.13567,52.21895 0.13572,52.21890 0.13583,52.21886",
+ "distances": "0,25,8,3,7,7,7,9",
+ "elevations": "8,8,8,8,8,8,8,8",
+ "provisionName": "Footpath",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "40",
+ "time": "6",
+ "busynance": "47",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "110",
+ "color": "#000000",
+ "points": "0.13583,52.21886 0.13592,52.21884 0.13631,52.21866",
+ "distances": "0,7,33",
+ "elevations": "8,8,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Pearl Close",
+ "legNumber": "2",
+ "distance": "46",
+ "time": "8",
+ "busynance": "65",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "39",
+ "color": "#000000",
+ "points": "0.13631,52.21866 0.13647,52.21878 0.13680,52.21894",
+ "distances": "0,17,29",
+ "elevations": "7,7,8",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Union Lane",
+ "legNumber": "2",
+ "distance": "233",
+ "time": "40",
+ "busynance": "309",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "136",
+ "color": "#33aa33",
+ "points": "0.13680,52.21894 0.13691,52.21887 0.13707,52.21876 0.13739,52.21859 0.13747,52.21855 0.13756,52.21850 0.13773,52.21842 0.13810,52.21827 0.13841,52.21814 0.13868,52.21800 0.13935,52.21756",
+ "distances": "0,11,16,29,7,8,15,30,26,24,67",
+ "elevations": "8,7,7,7,7,7,7,7,7,7,7",
+ "provisionName": "Minor road",
+ "type": "segment"
+ },
+ {
+ "name": "High Street",
+ "legNumber": "2",
+ "distance": "31",
+ "time": "8",
+ "busynance": "39",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "70",
+ "color": "#33aa33",
+ "points": "0.13935,52.21756 0.13958,52.21761 0.13977,52.21765",
+ "distances": "0,17,14",
+ "elevations": "7,7,7",
+ "provisionName": "Minor road",
+ "type": "segment"
+ },
+ {
+ "name": "Chapel Street",
+ "legNumber": "2",
+ "distance": "98",
+ "time": "18",
+ "busynance": "138",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "143",
+ "color": "#000000",
+ "points": "0.13977,52.21765 0.14022,52.21729 0.14052,52.21702 0.14059,52.21692",
+ "distances": "0,50,36,12",
+ "elevations": "7,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Church Street, NCN 11;51",
+ "legNumber": "2",
+ "distance": "136",
+ "time": "21",
+ "busynance": "139",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear right",
+ "startBearing": "185",
+ "color": "#000000",
+ "points": "0.14059,52.21692 0.14058,52.21685 0.14052,52.21663 0.14047,52.21651 0.14042,52.21641 0.14025,52.21621 0.14010,52.21606 0.13986,52.21581",
+ "distances": "0,8,25,14,12,25,20,32",
+ "elevations": "7,7,7,7,7,7,7,7",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "St Andrew's Road, NCN 11;51",
+ "legNumber": "2",
+ "distance": "108",
+ "time": "17",
+ "busynance": "97",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "134",
+ "color": "#000000",
+ "points": "0.13986,52.21581 0.14013,52.21565 0.14044,52.21545 0.14048,52.21543 0.14056,52.21538 0.14062,52.21532 0.14067,52.21522 0.14069,52.21513 0.14067,52.21505",
+ "distances": "0,26,31,4,8,8,12,10,9",
+ "elevations": "7,7,6,6,6,6,6,6,6",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "NCN 11;51",
+ "legNumber": "2",
+ "distance": "109",
+ "time": "15",
+ "busynance": "92",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "93",
+ "color": "#ff0000",
+ "points": "0.14067,52.21505 0.14097,52.21504 0.14154,52.21472 0.14158,52.21467 0.14159,52.21464 0.14159,52.21458 0.14158,52.21452 0.14159,52.21445 0.14163,52.21441",
+ "distances": "0,20,53,6,3,7,7,8,5",
+ "elevations": "6,5,4,4,4,4,4,4,4",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Riverside Bridge, NCN 11;51",
+ "legNumber": "2",
+ "distance": "209",
+ "time": "42",
+ "busynance": "260",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "139",
+ "color": "#ff0000",
+ "points": "0.14163,52.21441 0.14194,52.21419 0.14205,52.21412 0.14256,52.21388 0.14268,52.21381 0.14281,52.21377 0.14286,52.21376 0.14295,52.21372 0.14303,52.21366 0.14306,52.21361 0.14308,52.21355 0.14307,52.21349 0.14301,52.21339 0.14294,52.21326 0.14285,52.21310 0.14279,52.21298",
+ "distances": "0,32,11,44,11,10,4,8,9,6,7,7,12,15,19,14",
+ "elevations": "4,4,4,4,4,4,4,4,4,4,4,5,5,5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "NCN 11;51",
+ "legNumber": "2",
+ "distance": "28",
+ "time": "8",
+ "busynance": "37",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "199",
+ "color": "#ff0000",
+ "points": "0.14279,52.21298 0.14270,52.21282 0.14269,52.21274",
+ "distances": "0,19,9",
+ "elevations": "5,5,5",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Riverside, NCN 11",
+ "legNumber": "2",
+ "distance": "28",
+ "time": "5",
+ "busynance": "30",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "203",
+ "color": "#000000",
+ "points": "0.14269,52.21274 0.14253,52.21251",
+ "distances": "0,28",
+ "elevations": "5,5",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "16",
+ "time": "7",
+ "busynance": "46",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "143",
+ "color": "#ff0000",
+ "points": "0.14253,52.21251 0.14258,52.21247 0.14260,52.21238",
+ "distances": "0,6,10",
+ "elevations": "5,5,6",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Link between Riverside, NCN 11 and Cheddars Lane",
+ "legNumber": "2",
+ "distance": "70",
+ "time": "46",
+ "busynance": "249",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "90",
+ "color": "#ff0000",
+ "points": "0.14260,52.21238 0.14268,52.21238 0.14275,52.21236 0.14291,52.21231 0.14311,52.21224 0.14327,52.21221 0.14357,52.21216",
+ "distances": "0,5,5,12,16,11,21",
+ "elevations": "6,6,6,8,9,9,11",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Un-named link",
+ "legNumber": "2",
+ "distance": "202",
+ "time": "36",
+ "busynance": "230",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "216",
+ "color": "#ff0000",
+ "points": "0.14357,52.21216 0.14251,52.21125 0.14319,52.21094 0.14337,52.21085 0.14339,52.21082",
+ "distances": "0,124,58,16,4",
+ "elevations": "11,10,11,11,11",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Link with Newmarket Road, A1134",
+ "legNumber": "2",
+ "distance": "137",
+ "time": "22",
+ "busynance": "156",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "158",
+ "color": "#ff0000",
+ "points": "0.14339,52.21082 0.14341,52.21079 0.14340,52.21074 0.14341,52.21067 0.14350,52.21050 0.14363,52.21044 0.14382,52.21035 0.14412,52.21024 0.14456,52.20990",
+ "distances": "0,4,6,8,20,11,16,24,48",
+ "elevations": "11,11,11,11,11,11,11,11,11",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Newmarket Road, A1134",
+ "legNumber": "2",
+ "distance": "13",
+ "time": "22",
+ "busynance": "50",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "1",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "54",
+ "color": "#3333aa",
+ "points": "0.14456,52.20990 0.14472,52.20997",
+ "distances": "0,13",
+ "elevations": "11,11",
+ "provisionName": "Major road",
+ "type": "segment"
+ },
+ {
+ "name": "Newmarket Road, A1134",
+ "legNumber": "2",
+ "distance": "10",
+ "time": "22",
+ "busynance": "16",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "1",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "142",
+ "color": "#7777cc",
+ "points": "0.14472,52.20997 0.14481,52.20990",
+ "distances": "0,10",
+ "elevations": "11,11",
+ "provisionName": "Service Road",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "98",
+ "time": "16",
+ "busynance": "129",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "140",
+ "color": "#7777cc",
+ "points": "0.14481,52.20990 0.14500,52.20976 0.14515,52.20966 0.14524,52.20962 0.14531,52.20959 0.14546,52.20955 0.14578,52.20948 0.14590,52.20945 0.14598,52.20943",
+ "distances": "0,20,15,8,6,11,23,9,6",
+ "elevations": "11,10,10,10,9,9,9,9,8",
+ "provisionName": "Service Road",
+ "type": "segment"
+ },
+ {
+ "name": "Cambridge Retail Park",
+ "legNumber": "2",
+ "distance": "174",
+ "time": "65",
+ "busynance": "288",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "192",
+ "color": "#7777cc",
+ "points": "0.14598,52.20943 0.14595,52.20934 0.14592,52.20924 0.14585,52.20913 0.14577,52.20900 0.14569,52.20886 0.14565,52.20880 0.14561,52.20873 0.14557,52.20866 0.14556,52.20863 0.14553,52.20858 0.14545,52.20845 0.14541,52.20839 0.14539,52.20833 0.14536,52.20820 0.14529,52.20808 0.14512,52.20794",
+ "distances": "0,10,11,13,15,16,7,8,8,3,6,15,7,7,15,14,19",
+ "elevations": "8,8,8,9,8,8,8,8,8,8,8,9,9,9,9,9,10",
+ "provisionName": "Service Road",
+ "type": "segment"
+ },
+ {
+ "name": "Short un-named link",
+ "legNumber": "2",
+ "distance": "74",
+ "time": "18",
+ "busynance": "86",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "121",
+ "color": "#ff0000",
+ "points": "0.14512,52.20794 0.14520,52.20791 0.14518,52.20786 0.14521,52.20782 0.14527,52.20777 0.14578,52.20760 0.14574,52.20757 0.14569,52.20753",
+ "distances": "0,6,6,5,7,40,4,6",
+ "elevations": "10,10,10,10,10,9,10,10",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Coldhams Lane Cycle Bridge",
+ "legNumber": "2",
+ "distance": "228",
+ "time": "34",
+ "busynance": "223",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "126",
+ "color": "#ff0000",
+ "points": "0.14569,52.20753 0.14589,52.20744 0.14619,52.20733 0.14637,52.20727 0.14784,52.20686 0.14807,52.20680 0.14845,52.20674 0.14873,52.20671",
+ "distances": "0,17,24,14,110,17,27,19",
+ "elevations": "10,10,10,10,9,8,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Coldhams Lane (cycleway)",
+ "legNumber": "2",
+ "distance": "81",
+ "time": "12",
+ "busynance": "76",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "straight on",
+ "startBearing": "98",
+ "color": "#ff0000",
+ "points": "0.14873,52.20671 0.14906,52.20668 0.14926,52.20664 0.14955,52.20654 0.14960,52.20649 0.14963,52.20637",
+ "distances": "0,23,14,23,7,14",
+ "elevations": "7,7,7,7,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Coldhams Lane (cycleway)",
+ "legNumber": "2",
+ "distance": "20",
+ "time": "12",
+ "busynance": "19",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn left",
+ "startBearing": "103",
+ "color": "#ff0000",
+ "points": "0.14963,52.20637 0.14970,52.20636 0.14980,52.20636 0.14983,52.20634 0.14989,52.20636",
+ "distances": "0,5,7,3,5",
+ "elevations": "7,7,7,7,7",
+ "provisionName": "Cycle path",
+ "type": "segment"
+ },
+ {
+ "name": "Coldhams Lane",
+ "legNumber": "2",
+ "distance": "131",
+ "time": "43",
+ "busynance": "195",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "1",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "145",
+ "color": "#33aa33",
+ "points": "0.14989,52.20636 0.14998,52.20628 0.15003,52.20620 0.15055,52.20526",
+ "distances": "0,11,10,110",
+ "elevations": "7,7,7,7",
+ "provisionName": "Minor road",
+ "type": "segment"
+ },
+ {
+ "name": "Brampton Road",
+ "legNumber": "2",
+ "distance": "400",
+ "time": "84",
+ "busynance": "587",
+ "flow": "against",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "260",
+ "color": "#000000",
+ "points": "0.15055,52.20526 0.15046,52.20525 0.15028,52.20521 0.15009,52.20516 0.14999,52.20509 0.14991,52.20502 0.14987,52.20497 0.14840,52.20293 0.14830,52.20277 0.14827,52.20272 0.14826,52.20264 0.14828,52.20255 0.14833,52.20240 0.14839,52.20219 0.14837,52.20212",
+ "distances": "0,6,13,14,10,10,6,248,19,6,9,10,17,24,8",
+ "elevations": "7,7,7,7,7,7,7,10,10,10,10,10,11,11,11",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Fairfax Road",
+ "legNumber": "2",
+ "distance": "17",
+ "time": "4",
+ "busynance": "27",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "bear left",
+ "startBearing": "125",
+ "color": "#000000",
+ "points": "0.14837,52.20212 0.14858,52.20203",
+ "distances": "0,17",
+ "elevations": "11,11",
+ "provisionName": "Residential street",
+ "type": "segment"
+ },
+ {
+ "name": "Thoday Street",
+ "legNumber": "2",
+ "distance": "285",
+ "time": "71",
+ "busynance": "391",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "turn right",
+ "startBearing": "216",
+ "color": "#000000",
+ "points": "0.14858,52.20203 0.14852,52.20198 0.14849,52.20193 0.14849,52.20187 0.14856,52.20163 0.14856,52.20148 0.14854,52.20140 0.14850,52.20133 0.14824,52.20091 0.14812,52.20072 0.14786,52.20029 0.14771,52.20004 0.14748,52.19967 0.14744,52.19962",
+ "distances": "0,7,6,7,27,17,9,8,50,23,51,30,44,6",
+ "elevations": "11,11,11,11,11,11,11,11,12,12,13,13,13,14",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ ]
+}
diff --git a/libraries/cyclestreets-core/src/test/resources/journey-single-segment-domain.json b/libraries/cyclestreets-core/src/test/resources/journey-single-segment-domain.json
new file mode 100644
index 000000000..e8b1d2a9e
--- /dev/null
+++ b/libraries/cyclestreets-core/src/test/resources/journey-single-segment-domain.json
@@ -0,0 +1,74 @@
+{
+ "waypoints": [
+ {
+ "sequenceId": "1",
+ "longitude": "0.14771",
+ "latitude": "52.20004"
+ },
+ {
+ "sequenceId": "2",
+ "longitude": "0.14682",
+ "latitude": "52.19870"
+ }
+ ],
+ "route": {
+ "start": "Thoday Street",
+ "finish": "Thoday Street",
+ "startBearing": "0",
+ "startSpeed": "0",
+ "start_longitude": "0.14771",
+ "start_latitude": "52.20004",
+ "finish_longitude": "0.14682",
+ "finish_latitude": "52.19870",
+ "crow_fly_distance": "161",
+ "event": "depart",
+ "whence": "1535615311",
+ "speed": "20",
+ "itinerary": "63123653",
+ "clientRouteId": "0",
+ "plan": "balanced",
+ "note": "",
+ "length": "161",
+ "time": "45",
+ "busynance": "218",
+ "quietness": "74",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "west": "0.14682",
+ "south": "52.19870",
+ "east": "0.14771",
+ "north": "52.20004",
+ "name": "Thoday Street to Thoday Street",
+ "walk": "0",
+ "leaving": "2018-08-30 08:48:31",
+ "arriving": "2018-08-30 08:49:16",
+ "coordinates": "0.14771,52.20004 0.14748,52.19967 0.14714,52.19915 0.14707,52.19908 0.14704,52.19904 0.14682,52.19870",
+ "elevations": "14,15,15,15,15,16",
+ "distances": "44,62,9,5,41",
+ "grammesCO2saved": "30",
+ "calories": "4",
+ "edition": "routing180820",
+ "type": "route"
+ },
+ "segments": [
+ {
+ "name": "Thoday Street",
+ "legNumber": "1",
+ "distance": "161",
+ "time": "45",
+ "busynance": "218",
+ "flow": "with",
+ "walk": "0",
+ "signalledJunctions": "0",
+ "signalledCrossings": "0",
+ "turn": "",
+ "startBearing": "201",
+ "color": "#000000",
+ "points": "0.14771,52.20004 0.14748,52.19967 0.14714,52.19915 0.14707,52.19908 0.14704,52.19904 0.14682,52.19870",
+ "distances": "0,44,62,9,5,41",
+ "elevations": "14,15,15,15,15,16",
+ "provisionName": "Residential street",
+ "type": "segment"
+ }
+ ]
+}
diff --git a/libraries/cyclestreets-fragments/build.gradle b/libraries/cyclestreets-fragments/build.gradle
index 1a8f88341..841b306e8 100644
--- a/libraries/cyclestreets-fragments/build.gradle
+++ b/libraries/cyclestreets-fragments/build.gradle
@@ -1,6 +1,19 @@
evaluationDependsOn(':libraries:cyclestreets-view')
+android {
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+ namespace 'net.cyclestreets.fragments'
+}
+
dependencies {
- compile project(':libraries:cyclestreets-view')
- compile 'com.jjoe64:graphview:3.1.4'
+ api project(':libraries:cyclestreets-view')
+ implementation 'com.jjoe64:graphview:4.2.2'
+
+ testImplementation "junit:junit:${rootProject.ext.junitVersion}"
+ testImplementation "org.assertj:assertj-core:${rootProject.ext.assertjVersion}"
+ testImplementation "org.robolectric:robolectric:${rootProject.ext.robolectricVersion}"
}
diff --git a/libraries/cyclestreets-fragments/gradle/wrapper/gradle-wrapper.jar b/libraries/cyclestreets-fragments/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index c97a8bdb9..000000000
Binary files a/libraries/cyclestreets-fragments/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/gradle/wrapper/gradle-wrapper.properties b/libraries/cyclestreets-fragments/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 46a9213ec..000000000
--- a/libraries/cyclestreets-fragments/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Sat May 14 10:07:49 BST 2016
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip
diff --git a/libraries/cyclestreets-fragments/gradlew b/libraries/cyclestreets-fragments/gradlew
deleted file mode 100755
index 91a7e269e..000000000
--- a/libraries/cyclestreets-fragments/gradlew
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn ( ) {
- echo "$*"
-}
-
-die ( ) {
- echo
- echo "$*"
- echo
- exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
-esac
-
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=$((i+1))
- done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
-fi
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libraries/cyclestreets-fragments/gradlew.bat b/libraries/cyclestreets-fragments/gradlew.bat
deleted file mode 100644
index aec99730b..000000000
--- a/libraries/cyclestreets-fragments/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/libraries/cyclestreets-fragments/src/main/AndroidManifest.xml b/libraries/cyclestreets-fragments/src/main/AndroidManifest.xml
index 8e71645c5..1acbee3cd 100644
--- a/libraries/cyclestreets-fragments/src/main/AndroidManifest.xml
+++ b/libraries/cyclestreets-fragments/src/main/AndroidManifest.xml
@@ -1,11 +1,8 @@
-
+
-
+
-
+ android:label="CycleStreets" />
diff --git a/libraries/cyclestreets-fragments/src/main/assets/donate.png b/libraries/cyclestreets-fragments/src/main/assets/donate.png
deleted file mode 100644
index 1b982c435..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/donate.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/renderTheme.xsd b/libraries/cyclestreets-fragments/src/main/assets/renderTheme.xsd
deleted file mode 100644
index b602b77aa..000000000
--- a/libraries/cyclestreets-fragments/src/main/assets/renderTheme.xsd
+++ /dev/null
@@ -1,176 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/osmarender.xml b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/osmarender.xml
deleted file mode 100644
index 6bda6ad95..000000000
--- a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/osmarender.xml
+++ /dev/null
@@ -1,1211 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/access-destination.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/access-destination.png
deleted file mode 100644
index c0fe592e2..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/access-destination.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/access-private.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/access-private.png
deleted file mode 100644
index 40fe13ffe..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/access-private.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/cemetery.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/cemetery.png
deleted file mode 100644
index d9921b414..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/cemetery.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/marsh.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/marsh.png
deleted file mode 100644
index 2251189ea..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/marsh.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/military.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/military.png
deleted file mode 100644
index 2cecbdab9..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/military.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/nature-reserve.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/nature-reserve.png
deleted file mode 100644
index 1ab788ee5..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/nature-reserve.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-coniferous.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-coniferous.png
deleted file mode 100644
index 2c432ecfe..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-coniferous.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-deciduous.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-deciduous.png
deleted file mode 100644
index d7f1f1735..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-deciduous.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-mixed.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-mixed.png
deleted file mode 100644
index 09499d168..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/patterns/wood-mixed.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/airport.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/airport.png
deleted file mode 100644
index 0d994fd3a..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/airport.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/alpine_hut.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/alpine_hut.png
deleted file mode 100644
index e5beb11dc..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/alpine_hut.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/atm.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/atm.png
deleted file mode 100644
index f83a68347..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/atm.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bakery.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bakery.png
deleted file mode 100644
index 192d97fe9..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bakery.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bank.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bank.png
deleted file mode 100644
index 2100d4d44..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bank.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bench.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bench.png
deleted file mode 100644
index dad0204f0..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bench.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bicycle_rental.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bicycle_rental.png
deleted file mode 100644
index 00fb1fe42..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bicycle_rental.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bus.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bus.png
deleted file mode 100644
index 68317aa66..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bus.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bus_sta.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bus_sta.png
deleted file mode 100644
index a23cd0339..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/bus_sta.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cable_car.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cable_car.png
deleted file mode 100644
index d8c9a7029..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cable_car.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cafe.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cafe.png
deleted file mode 100644
index 7183876ae..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cafe.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/campSite.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/campSite.png
deleted file mode 100644
index ff134b066..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/campSite.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cave_entrance.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cave_entrance.png
deleted file mode 100644
index 3b9ffd95c..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cave_entrance.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/chair_lift_2.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/chair_lift_2.png
deleted file mode 100644
index 3ed11c163..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/chair_lift_2.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/church.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/church.png
deleted file mode 100644
index eff562630..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/church.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cinema.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cinema.png
deleted file mode 100644
index 187adf4b7..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/cinema.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/drinking_water.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/drinking_water.png
deleted file mode 100644
index f004d0bce..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/drinking_water.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/fastfood.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/fastfood.png
deleted file mode 100644
index 8d5e2f646..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/fastfood.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/firebrigade.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/firebrigade.png
deleted file mode 100644
index c3baa521c..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/firebrigade.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/florist.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/florist.png
deleted file mode 100644
index 752399805..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/florist.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/fountain.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/fountain.png
deleted file mode 100644
index edef8dd84..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/fountain.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/gondola.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/gondola.png
deleted file mode 100644
index ddd3cc6fe..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/gondola.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/helipad.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/helipad.png
deleted file mode 100644
index 0546e0c70..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/helipad.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hospital.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hospital.png
deleted file mode 100644
index e28b045df..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hospital.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hostel.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hostel.png
deleted file mode 100644
index 3ea929f7d..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hostel.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hotel.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hotel.png
deleted file mode 100644
index 123204868..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/hotel.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/information.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/information.png
deleted file mode 100644
index 4954704a3..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/information.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/kindergarten.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/kindergarten.png
deleted file mode 100644
index fb9f3a6df..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/kindergarten.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/library.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/library.png
deleted file mode 100644
index 151e6cb40..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/library.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/mosque.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/mosque.png
deleted file mode 100644
index f8538c6aa..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/mosque.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/oneway.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/oneway.png
deleted file mode 100644
index 67234f658..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/oneway.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/parking.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/parking.png
deleted file mode 100644
index 6e71e4d97..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/parking.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/peak.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/peak.png
deleted file mode 100644
index 133c73ea3..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/peak.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/petrolStation.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/petrolStation.png
deleted file mode 100644
index cc2c6b22b..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/petrolStation.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/pharmacy.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/pharmacy.png
deleted file mode 100644
index 850516f67..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/pharmacy.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/playground.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/playground.png
deleted file mode 100644
index 0df743dc8..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/playground.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/postbox.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/postbox.png
deleted file mode 100644
index 735ce80b3..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/postbox.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/postoffice.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/postoffice.png
deleted file mode 100644
index 5e30d44f3..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/postoffice.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/pub.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/pub.png
deleted file mode 100644
index 6e800c8ca..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/pub.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/railway-crossing-small.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/railway-crossing-small.png
deleted file mode 100644
index 367e26644..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/railway-crossing-small.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/railway-crossing.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/railway-crossing.png
deleted file mode 100644
index d94023980..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/railway-crossing.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/recycling.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/recycling.png
deleted file mode 100644
index f4c66d59d..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/recycling.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/restaurant.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/restaurant.png
deleted file mode 100644
index 8a735d0f6..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/restaurant.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/school.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/school.png
deleted file mode 100644
index f45b7f4af..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/school.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/shelter.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/shelter.png
deleted file mode 100644
index 4a1c04b36..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/shelter.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/soccer-borderless.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/soccer-borderless.png
deleted file mode 100644
index 7762889f6..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/soccer-borderless.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/supermarket.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/supermarket.png
deleted file mode 100644
index a24b64f19..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/supermarket.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/synagogue.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/synagogue.png
deleted file mode 100644
index 0c15506e2..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/synagogue.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/telephone.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/telephone.png
deleted file mode 100644
index 8c81f8ddb..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/telephone.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/tennis.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/tennis.png
deleted file mode 100644
index a077f2dc6..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/tennis.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/theatre.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/theatre.png
deleted file mode 100644
index 5c9b07a09..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/theatre.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/toilets.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/toilets.png
deleted file mode 100644
index b241ad128..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/toilets.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/traffic_signal.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/traffic_signal.png
deleted file mode 100644
index ddcac871e..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/traffic_signal.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/tree.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/tree.png
deleted file mode 100644
index 4c9296881..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/tree.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/university.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/university.png
deleted file mode 100644
index 032f70557..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/university.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/viewpoint.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/viewpoint.png
deleted file mode 100644
index 8f77c1abe..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/viewpoint.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/vulcan.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/vulcan.png
deleted file mode 100644
index fac7e5ce5..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/vulcan.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/windmill.png b/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/windmill.png
deleted file mode 100644
index 82f036613..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/assets/rendertheme/symbols/windmill.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/AboutFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/AboutFragment.java
deleted file mode 100644
index 88f7e0ce0..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/AboutFragment.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package net.cyclestreets;
-
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import net.cyclestreets.fragments.R;
-
-public class AboutFragment extends WebPageFragment {
- public AboutFragment() {
- super("file:///android_asset/credits.html", R.layout.about);
- } // AboutFragment
-
- @Override
- public View onCreateView(final LayoutInflater inflater,
- final ViewGroup container,
- final Bundle savedInstanceState) {
- final View about = super.onCreateView(inflater, container, savedInstanceState);
-
- final TextView versionView = (TextView)about.findViewById(R.id.version_view);
- versionView.setText(CycleStreetsAppSupport.version());
-
- return about;
- } // onCreateView
-} // AboutFragment
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/AboutFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/AboutFragment.kt
new file mode 100644
index 000000000..6d1834adc
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/AboutFragment.kt
@@ -0,0 +1,19 @@
+package net.cyclestreets
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+
+import net.cyclestreets.fragments.R
+
+class AboutFragment : WebPageFragment("file:///android_asset/credits.html", R.layout.about) {
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val about = super.onCreateView(inflater, container, savedInstanceState)
+ (about!!.findViewById(R.id.version_view) as TextView).text = CycleStreetsAppSupport.version()
+
+ return about
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/BlogFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/BlogFragment.kt
new file mode 100644
index 000000000..45b08483a
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/BlogFragment.kt
@@ -0,0 +1,17 @@
+package net.cyclestreets
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+
+private const val CYCLE_STREETS_BLOG_URL = "https://www.cyclestreets.org/news/"
+
+class BlogFragment : WebPageFragment(CYCLE_STREETS_BLOG_URL) {
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ BlogState.markBlogAsRead(requireActivity())
+ return super.onCreateView(inflater, container, savedInstanceState)
+ }
+
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleMapFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleMapFragment.java
deleted file mode 100644
index 1a8936e50..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleMapFragment.java
+++ /dev/null
@@ -1,121 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.fragments.R;
-
-import net.cyclestreets.views.CycleMapView;
-
-import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.views.overlay.Overlay;
-
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.RelativeLayout;
-import android.widget.RelativeLayout.LayoutParams;
-
-import static net.cyclestreets.util.MenuHelper.createMenuItem;
-import static net.cyclestreets.util.MenuHelper.enableMenuItem;
-
-public class CycleMapFragment extends Fragment implements Undoable
-{
- private CycleMapView map_;
- private boolean forceMenuRebuild_;
-
- @Override
- public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle saved)
- {
- super.onCreate(saved);
-
- forceMenuRebuild_ = true;
-
- map_ = new CycleMapView(getActivity(), this.getClass().getName());
-
- final RelativeLayout rl = new RelativeLayout(getActivity());
- rl.addView(map_, new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
- return rl;
- } // onCreate
-
- protected CycleMapView mapView() { return map_; }
- protected Overlay overlayPushBottom(final Overlay overlay) { return map_.overlayPushBottom(overlay); }
- protected Overlay overlayPushTop(final Overlay overlay) { return map_.overlayPushTop(overlay); }
-
- protected void findPlace() { launchFindDialog(); }
-
- @Override
- public void onPause()
- {
- map_.onPause();
- super.onPause();
- } // onPause
-
- @Override
- public void onResume()
- {
- super.onResume();
- map_.onResume();
- } // onResume
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater)
- {
- if (map_ != null)
- map_.onCreateOptionsMenu(menu);
- createMenuItem(menu, R.string.ic_menu_findplace, Menu.NONE, R.drawable.ic_menu_search);
- } // onCreateOptionsMenu
-
- @Override
- public void onPrepareOptionsMenu(final Menu menu)
- {
- if (forceMenuRebuild_) {
- forceMenuRebuild_ = false;
- menu.clear();
- onCreateOptionsMenu(menu, getActivity().getMenuInflater());
- onPrepareOptionsMenu(menu);
- } // if ...
-
- if (map_ != null)
- map_.onPrepareOptionsMenu(menu);
- enableMenuItem(menu, R.string.ic_menu_findplace, true);
- } // onPrepareOptionsMenu
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item)
- {
- if(map_.onMenuItemSelected(item.getItemId(), item))
- return true;
-
- if(item.getItemId() == R.string.ic_menu_findplace)
- {
- launchFindDialog();
- return true;
- } // if ...
-
- return false;
- } // onMenuItemSelected
-
- @Override
- public boolean onContextItemSelected(final MenuItem item)
- {
- return map_.onMenuItemSelected(item.getItemId(), item);
- } // onContextItemSelected
-
- private void launchFindDialog() {
- FindPlace.launch(getActivity(), map_.getBoundingBox(), new FindPlace.Listener() {
- @Override
- public void onPlaceFound(IGeoPoint place) {
- map_.centreOn(place);
- }
- });
- } // launchFindDialog
-
- @Override
- public boolean onBackPressed()
- {
- return map_.onBackPressed();
- } // onBackPressed
-} // CycleMapFragment
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleMapFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleMapFragment.kt
new file mode 100644
index 000000000..a4870700c
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleMapFragment.kt
@@ -0,0 +1,161 @@
+package net.cyclestreets
+
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.preference.PreferenceManager
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import net.cyclestreets.fragments.R
+import net.cyclestreets.iconics.IconicsHelper
+import net.cyclestreets.util.AsyncDelete
+import net.cyclestreets.util.Logging
+import net.cyclestreets.util.MenuHelper.createMenuItem
+import net.cyclestreets.util.MenuHelper.enableMenuItem
+import net.cyclestreets.util.Theme
+import net.cyclestreets.util.doOrRequestPermission
+import net.cyclestreets.util.requestPermissionsResultAction
+import net.cyclestreets.views.CycleMapView
+import org.osmdroid.config.Configuration
+import org.osmdroid.config.DefaultConfigurationProvider
+import org.osmdroid.views.overlay.Overlay
+import java.io.File
+import java.util.Date
+
+
+private val TAG = Logging.getTag(CycleMapFragment::class.java)
+
+
+open class CycleMapFragment : Fragment(), Undoable {
+
+ private var map: CycleMapView? = null
+ private var forceMenuRebuild: Boolean = false
+ private lateinit var searchIcon: Drawable
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, saved: Bundle?): View? {
+ super.onCreate(saved)
+
+ forceMenuRebuild = true
+
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
+ checkPermissionNoMoreThanOnceEveryFiveMinutes()
+
+ map = CycleMapView(context, this.javaClass.name, this)
+ searchIcon = IconicsHelper.materialIcon(requireContext(), GoogleMaterial.Icon.gmd_search, Theme.lowlightColorInverse(context))
+
+ return map
+ }
+
+ private fun checkPermissionNoMoreThanOnceEveryFiveMinutes() {
+ val now = Date().time
+ val fiveMinutesAgo = now - (5 * 60 * 1000)
+ if (fiveMinutesAgo > permissionLastCheckedTime) {
+ permissionLastCheckedTime = now
+ doOrRequestPermission(null, this, WRITE_EXTERNAL_STORAGE) {
+ Log.v(TAG, "Already have $WRITE_EXTERNAL_STORAGE permission")
+ }
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ Log.d(TAG, "Permission ${permissions.joinToString()} was ${if (grantResults.joinToString().equals("0")) "granted" else "denied"}")
+
+ for (i in permissions.indices) {
+ val permission = permissions[i]
+ val grantResult = grantResults[i]
+
+ // If we have permission to write to external storage, we'll use the default OSMDroid location for caching
+ // map tiles. Therefore, when permission is granted, clear state accordingly so this is possible.
+ if (permission == WRITE_EXTERNAL_STORAGE)
+ requestPermissionsResultAction(grantResult, permission) {
+ val oldCacheLocation: File = Configuration.getInstance().osmdroidTileCache
+
+ CycleStreetsPreferences.clearOsmdroidCacheLocation()
+ Configuration.setConfigurationProvider(DefaultConfigurationProvider())
+ Configuration.getInstance().load(context, PreferenceManager.getDefaultSharedPreferences(requireContext()))
+ val newCacheLocation: File = Configuration.getInstance().osmdroidTileCache
+
+ Log.i(TAG, "Permission $WRITE_EXTERNAL_STORAGE granted; update OSMDroid cache " +
+ "location from ${oldCacheLocation.absolutePath} to ${newCacheLocation.absolutePath}")
+ if (newCacheLocation.absolutePath != oldCacheLocation.absolutePath)
+ AsyncDelete().execute(oldCacheLocation)
+ }
+ }
+ }
+
+ protected fun mapView(): CycleMapView { return map!! }
+ protected fun overlayPushBottom(overlay: Overlay): Overlay { return map!!.overlayPushBottom(overlay) }
+ protected fun overlayPushTop(overlay: Overlay): Overlay { return map!!.overlayPushTop(overlay) }
+
+ override fun onPause() {
+ map!!.onPause()
+ super.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ map!!.onResume()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ if (map != null)
+ map!!.onCreateOptionsMenu(menu)
+
+ createMenuItem(menu, R.string.menu_find_place, Menu.NONE, searchIcon)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ if (forceMenuRebuild) {
+ forceMenuRebuild = false
+ menu.clear()
+ onCreateOptionsMenu(menu, requireActivity().menuInflater)
+ onPrepareOptionsMenu(menu)
+ }
+
+ if (map != null)
+ map!!.onPrepareOptionsMenu(menu)
+
+ enableMenuItem(menu, R.string.menu_find_place, true)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (map!!.onMenuItemSelected(item.itemId, item))
+ return true
+
+ if (item.itemId == R.string.menu_find_place) {
+ launchFindDialog()
+ return true
+ }
+
+ return false
+ }
+
+ override fun onContextItemSelected(item: MenuItem): Boolean {
+ return map!!.onMenuItemSelected(item.itemId, item)
+ }
+
+ private fun launchFindDialog() {
+ FindPlace.launch(requireContext(), map!!.boundingBox) { place ->
+ map!!.centreOn(place, FINDPLACE_ZOOM_LEVEL, true)
+ }
+ }
+
+ override fun onBackPressed(): Boolean {
+ return map!!.onBackPressed()
+ }
+
+ companion object {
+ var permissionLastCheckedTime: Long = 0
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleStreetsAppSupport.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleStreetsAppSupport.java
index 67cc1a726..8fbebfc84 100644
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleStreetsAppSupport.java
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/CycleStreetsAppSupport.java
@@ -2,52 +2,69 @@
import net.cyclestreets.api.ApiClient;
import net.cyclestreets.routing.Route;
+import net.cyclestreets.util.Logging;
+import net.cyclestreets.util.TurnIcons;
+
import android.content.Context;
import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
+import android.util.Log;
public final class CycleStreetsAppSupport {
- private static boolean isFirstRun_;
- private static boolean isNew_;
- private static String version_;
-
- static public void initialise(final Context context) {
- initialise(context, -1);
- } // initialise
+ private static final String TAG = Logging.getTag(CycleStreetsAppSupport.class);
+ private static boolean isFirstRun;
+ private static boolean isNew;
+ private static String version;
+ private static Integer versionCode;
+ private static String previousVersion;
+ private static Integer previousVersionCode;
public static void initialise(final Context context, final int prefsDefault) {
+ TurnIcons.initialise(context);
CycleStreetsPreferences.initialise(context, prefsDefault);
+ CycleStreetsNotifications.INSTANCE.initialise(context);
Route.initialise(context);
- ApiClient.initialise(context);
-
- version_ = version(context);
-
- isFirstRun_ = isFirstRun(context);
- isNew_ = isNew(context, version_);
-
- saveVersion(context, version_);
- } // onCreate
-
- public static String version() { return version_; }
- public static boolean isNewVersion() { return isNew_; }
- public static boolean isFirstRun() { return isFirstRun_; }
-
- private static String version(final Context context) {
- return "Version : " + AppInfo.version(context);
- } // version
+ ApiClient.INSTANCE.initialise(context);
+ BlogState.INSTANCE.initialise(context);
+
+ version = version(context);
+ versionCode = code(version);
+ previousVersion = previousVersion(context);
+ previousVersionCode = code(previousVersion);
+
+ isFirstRun = isFirstRun(context);
+ isNew = !version.equals(previousVersion);
+
+ saveVersion(context, version);
+
+ migratePreferences(previousVersionCode, versionCode);
+ }
+
+ public static String version() { return version; }
+ public static boolean isNewVersion() { return isNew; }
+ public static boolean isFirstRun() { return isFirstRun; }
+ public static void splashScreenSeen() {
+ isFirstRun = false;
+ isNew = false;
+ }
+
+ private static String version(final Context context) {
+ return "Version : " + AppInfo.INSTANCE.version(context);
+ }
+ private static Integer code(String versionString) {
+ if (UNKNOWN.equals(versionString)) {
+ return 0;
+ }
+ String[] split = versionString.split("/");
+ return Integer.valueOf(split[split.length - 1]);
+ }
private static boolean isFirstRun(final Context context) {
return UNKNOWN.equals(previousVersion(context));
- } // isFirstRun
- private static boolean isNew(final Context context, final String version) {
- String prev = previousVersion(context);
- return !version.equals(prev);
- } // isNewVersion
+ }
private static String previousVersion(final Context context) {
return prefs(context).getString(VERSION_KEY, UNKNOWN);
- } // previousVersion
+ }
private static void saveVersion(final Context context,
final String version) {
@@ -55,15 +72,23 @@ private static void saveVersion(final Context context,
edit().
putString(VERSION_KEY, version).
commit();
- } // saveVersion
+ }
private static SharedPreferences prefs(final Context context) {
return context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
- } // prefs
+ }
private static final String VERSION_KEY = "previous-version";
private static final String UNKNOWN = "unknown";
+ private static void migratePreferences(Integer previousVersionCode, Integer versionCode) {
+ Log.i(TAG, "Upgrading from " + previousVersion + " (" + previousVersionCode + ") to " + version + " (" + versionCode + ")");
+
+ if (previousVersionCode < 1667 && versionCode >= 1667) {
+ Log.i(TAG, "Clearing OSMDroid cache location after upgrade to target Android 10 (SDK 29) or higher changed accessible paths");
+ CycleStreetsPreferences.clearOsmdroidCacheLocation();
+ }
+ }
private CycleStreetsAppSupport() { }
-} // CycleStreetsAppSupport
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ElevationProfileFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ElevationProfileFragment.java
deleted file mode 100644
index 8f872bad7..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ElevationProfileFragment.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package net.cyclestreets;
-
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.jjoe64.graphview.CustomLabelFormatter;
-import com.jjoe64.graphview.GraphView;
-import com.jjoe64.graphview.GraphViewSeries;
-import com.jjoe64.graphview.GraphViewStyle;
-import com.jjoe64.graphview.LineGraphView;
-
-import net.cyclestreets.api.DistanceFormatter;
-import net.cyclestreets.fragments.R;
-import net.cyclestreets.routing.Elevation;
-import net.cyclestreets.routing.ElevationFormatter;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Route;
-import net.cyclestreets.routing.Segment;
-import net.cyclestreets.routing.Waypoints;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import static net.cyclestreets.util.StringUtils.initCap;
-
-public class ElevationProfileFragment extends Fragment
- implements Route.Listener {
- private LinearLayout graphHolder_;
-
- @Override
- public View onCreateView(final LayoutInflater inflater,
- final ViewGroup container,
- final Bundle savedInstanceState) {
- final View elevation = inflater.inflate(R.layout.elevation, container, false);
- graphHolder_ = (LinearLayout)elevation.findViewById(R.id.graphview);
- return elevation;
- } // onCreateView
-
- @Override
- public void onResume() {
- super.onResume();
- Route.onResume();
- Route.registerListener(this);
- } // onResume
-
- @Override
- public void onPause() {
- Route.unregisterListener(this);
- super.onPause();
- } // onPause
-
- @Override
- public void onNewJourney(final Journey journey, final Waypoints waypoints) {
- drawGraph(journey);
- drawText(journey);
- } // onNewJourney
-
- @Override
- public void onResetJourney() {
- } // onResetJourney
-
- private void drawGraph(final Journey journey) {
- final LineGraphView graph = new LineGraphView(getActivity(), "");
-
- List data = new ArrayList<>();
- for (Elevation elevation : journey.elevation().profile())
- data.add(new GraphView.GraphViewData(elevation.distance(), elevation.elevation()));
-
- GraphViewSeries graphSeries = new GraphViewSeries(data.toArray(new GraphView.GraphViewData[]{}));
-
- graph.addSeries(graphSeries);
- graph.setDrawBackground(true);
- graph.getGraphViewStyle().setGridStyle(GraphViewStyle.GridStyle.HORIZONTAL);
- graph.getGraphViewStyle().setNumHorizontalLabels(5);
- graph.getGraphViewStyle().setNumVerticalLabels(4);
-
- final ElevationFormatter formatter = ElevationFormatter.formatter(CycleStreetsPreferences.units());
- graph.setCustomLabelFormatter(new CustomLabelFormatter() {
- @Override
- public String formatLabel(double value, boolean isValueX) {
- if (isValueX)
- return (value != 0) ? formatter.distance((int)value) : "";
- return formatter.height((int) value);
- }
- });
-
- graphHolder_.removeAllViews();
- graphHolder_.addView(graph);
- } // drawGraph
-
- private void drawText(final Journey journey) {
- Segment.Start start = journey.segments().first();
-
- setText(R.id.title, journey.name());
- setText(R.id.journeyid, String.format("#%,d", journey.itinerary()));
- setText(R.id.routetype, initCap(journey.plan()) + " route:");
- setText(R.id.distance, distance(journey.total_distance()));
- setText(R.id.journeytime, start.totalTime());
- setText(R.id.calories, start.calories());
- setText(R.id.carbondioxide, start.co2());
- } // drawText
-
- private void setText(int id, String text) { ((TextView)getView().findViewById(id)).setText(text); }
- private String distance(final int metres) {
- return DistanceFormatter.formatter(CycleStreetsPreferences.units()).total_distance(metres);
- }
-} // ElevationProfileFragment
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FindPlace.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FindPlace.java
deleted file mode 100644
index 3c5277215..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FindPlace.java
+++ /dev/null
@@ -1,92 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.fragments.R;
-
-import net.cyclestreets.api.GeoPlace;
-
-import net.cyclestreets.util.MessageBox;
-import net.cyclestreets.views.PlaceView;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.view.View;
-import android.widget.Toast;
-
-import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.util.BoundingBoxE6;
-
-public class FindPlace {
- public interface Listener {
- void onPlaceFound(final IGeoPoint place);
- }
-
- public static void launch(final Context context,
- final BoundingBoxE6 boundingBox,
- final FindPlace.Listener listener) {
- final AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.ic_menu_findplace);
-
- final FindPlaceCallbacks fpcb = new FindPlaceCallbacks(context, builder, boundingBox, listener);
-
- final AlertDialog ad = builder.create();
- ad.show();
- ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(context, android.R.style.TextAppearance_Large);
-
- fpcb.setDialog(ad);
- } // launch
-
- private static class FindPlaceCallbacks implements View.OnClickListener, PlaceView.OnResolveListener {
- private final Context context_;
- private final PlaceView place_;
- private final Listener listener_;
- private AlertDialog ad_;
-
- public FindPlaceCallbacks(final Context context,
- final AlertDialog.Builder builder,
- final BoundingBoxE6 boundingBox,
- final Listener listener) {
- context_ = context;
-
- final View layout = View.inflate(context, R.layout.findplace, null);
- builder.setView(layout);
-
- builder.setPositiveButton(R.string.btn_find_place, MessageBox.NoAction);
-
- place_ = (PlaceView) layout.findViewById(R.id.place);
- place_.setBounds(boundingBox);
-
- listener_ = listener;
- } // onCreate
-
- public void setDialog(final AlertDialog ad) {
- ad_ = ad;
- ad_.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
- } // setDialog
-
- private void placeSelected(final GeoPlace place) {
- if (place == null || place.coord() == null)
- return;
-
- place_.addHistory(place);
-
- listener_.onPlaceFound(place.coord());
- ad_.dismiss();
- } // placeSelected
-
- @Override
- public void onClick(final View view) {
- final String from = place_.getText();
- if (from.length() == 0) {
- Toast.makeText(context_, R.string.lbl_choose_place, Toast.LENGTH_LONG).show();
- return;
- } // if ...
-
- place_.geoPlace(this);
- } // onClick
-
- @Override
- public void onResolve(final GeoPlace place) {
- placeSelected(place);
- } // onResolve
- } // class FindPlaceCallback
-} // class FindPlace
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FindPlace.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FindPlace.kt
new file mode 100644
index 000000000..a2c8e1073
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FindPlace.kt
@@ -0,0 +1,85 @@
+package net.cyclestreets
+
+import net.cyclestreets.fragments.R
+
+import net.cyclestreets.api.GeoPlace
+
+import net.cyclestreets.util.MessageBox
+import net.cyclestreets.views.place.PlaceView
+
+import android.app.AlertDialog
+import android.content.Context
+import android.view.View
+import android.view.inputmethod.EditorInfo
+import android.widget.Toast
+import net.cyclestreets.views.place.PlaceViewBase
+
+import org.osmdroid.api.IGeoPoint
+import org.osmdroid.util.BoundingBox
+
+object FindPlace {
+ fun launch(context: Context, boundingBox: BoundingBox, onPlaceFound: (IGeoPoint) -> Unit) {
+ val builder = AlertDialog.Builder(context).setTitle(R.string.menu_find_place)
+ val fpcb = FindPlaceCallbacks(context, builder, boundingBox, onPlaceFound)
+
+ val ad = builder.create()
+ ad.show()
+ ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(android.R.style.TextAppearance_Large)
+
+ fpcb.setDialog(ad)
+ }
+}
+
+private class FindPlaceCallbacks(private val context: Context,
+ builder: AlertDialog.Builder,
+ boundingBox: BoundingBox,
+ private val onPlaceFound: (IGeoPoint) -> Unit) : View.OnClickListener, PlaceViewBase.OnResolveListener {
+ private val placeView: PlaceView
+ private lateinit var ad: AlertDialog
+
+ init {
+ val layout = View.inflate(context, R.layout.findplace, null)
+ builder
+ .setView(layout)
+ .setPositiveButton(R.string.btn_find_place, MessageBox.NoAction)
+
+ placeView = layout.findViewById(R.id.place)
+ placeView.setBounds(boundingBox)
+ placeView.textView.setOnEditorActionListener { view, actionId, _ ->
+ return@setOnEditorActionListener when (actionId) {
+ EditorInfo.IME_ACTION_SEARCH -> {
+ onClick(view)
+ true
+ }
+ else -> false
+ }
+ }
+ }
+
+ fun setDialog(ad: AlertDialog) {
+ this.ad = ad
+ this.ad.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this)
+ }
+
+ private fun placeSelected(place: GeoPlace?) {
+ if (place?.coord() != null) {
+ placeView.addHistory(place)
+
+ onPlaceFound(place.coord())
+ ad.dismiss()
+ }
+ }
+
+ override fun onClick(view: View) {
+ if (placeView.getText()!!.isEmpty()) {
+ Toast.makeText(context, R.string.lbl_choose_place, Toast.LENGTH_LONG).show()
+ return
+ }
+
+ placeView.geoPlace(this)
+ }
+
+ override fun onResolve(place: GeoPlace) {
+ placeSelected(place)
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FragmentHolder.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FragmentHolder.java
deleted file mode 100644
index 729b8bf23..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FragmentHolder.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package net.cyclestreets;
-
-import android.support.v4.app.Fragment;
-import android.os.Bundle;
-import android.support.v7.app.ActionBarActivity;
-
-public abstract class FragmentHolder extends ActionBarActivity {
- @Override
- protected void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- if(savedInstanceState == null)
- getSupportFragmentManager().beginTransaction().add(android.R.id.content,
- fragment()).commit();
- } // onCreate
-
- protected abstract Fragment fragment();
-} // FragmentHolder
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FragmentHolder.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FragmentHolder.kt
new file mode 100644
index 000000000..5cf5b00d8
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/FragmentHolder.kt
@@ -0,0 +1,18 @@
+package net.cyclestreets
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import androidx.fragment.app.FragmentActivity
+
+
+abstract class FragmentHolder : FragmentActivity() {
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ if (savedInstanceState == null)
+ supportFragmentManager.beginTransaction().add(android.R.id.content, fragment()).commit()
+ }
+
+ protected abstract fun fragment(): Fragment
+}
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ItineraryAndElevationFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ItineraryAndElevationFragment.java
deleted file mode 100644
index 47e56e225..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ItineraryAndElevationFragment.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package net.cyclestreets;
-
-import android.os.Bundle;
-import android.support.v4.app.ActivityCompat;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import net.cyclestreets.fragments.R;
-
-import static net.cyclestreets.util.MenuHelper.showMenuItem;
-
-public class ItineraryAndElevationFragment extends Fragment {
- private Fragment lastFrag_;
- private Fragment itinerary_;
- private Fragment elevation_;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- setRetainInstance(true);
- itinerary_ = new ItineraryFragment();
- elevation_ = new ElevationProfileFragment();
-
- super.onCreate(savedInstanceState);
- } // onCreate
-
- @Override
- public View onCreateView(final LayoutInflater inflater,
- final ViewGroup container,
- final Bundle savedInstanceState) {
- return inflater.inflate(R.layout.itinerary_and_elevation, container, false);
- } // onCreateView
-
- @Override
- public void onPause() {
- super.onPause();
- } // onPause
-
- @Override
- public void onResume() {
- super.onResume();
- showFrag(lastFrag_ != null ? lastFrag_ : itinerary_);
- } // onResume
-
- private void showFrag(Fragment frag) {
- FragmentManager fm = getChildFragmentManager();
- FragmentTransaction ft = fm.beginTransaction();
-
- if (lastFrag_ != null)
- ft.detach(lastFrag_);
-
- if (frag != null) {
- String tag = frag.getTag();
- if (fm.findFragmentByTag(tag) == null)
- ft.add(R.id.container, frag, frag.getClass().getSimpleName());
- else
- ft.attach(frag);
- } // if ...
-
- ft.commit();
-
- lastFrag_ = frag;
-
- ActivityCompat.invalidateOptionsMenu(getActivity());
- } // showFrag
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater) {
- inflater.inflate(R.menu.itinerary_and_elevation_menu, menu);
- super.onCreateOptionsMenu(menu, inflater);
- } // onCreateOptionsMenu
-
- @Override
- public void onPrepareOptionsMenu(final Menu menu) {
- showMenuItem(menu, R.id.ic_menu_itinerary, itinerary_ != lastFrag_);
- showMenuItem(menu, R.id.ic_menu_elevation, elevation_ != lastFrag_);
- super.onPrepareOptionsMenu(menu);
- } // onPrepareOptionsMenu
-
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item) {
- if (super.onOptionsItemSelected(item))
- return true;
-
- final int menuId = item.getItemId();
-
- if (R.id.ic_menu_itinerary == menuId)
- showFrag(itinerary_);
-
- if (R.id.ic_menu_elevation == menuId)
- showFrag(elevation_);
-
- return true;
- } // onMenuItemSelected
-} // ItineraryAndElevationFragment
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ItineraryFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ItineraryFragment.java
deleted file mode 100644
index 4c80eb0ba..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/ItineraryFragment.java
+++ /dev/null
@@ -1,185 +0,0 @@
-package net.cyclestreets;
-
-import android.content.Context;
-import android.graphics.Color;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.support.v4.app.ListFragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.BaseAdapter;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import net.cyclestreets.api.DistanceFormatter;
-import net.cyclestreets.fragments.R;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Route;
-import net.cyclestreets.routing.Segment;
-import net.cyclestreets.routing.Waypoints;
-import net.cyclestreets.util.Theme;
-import net.cyclestreets.util.TurnIcons;
-
-import static net.cyclestreets.util.StringUtils.initCap;
-
-public class ItineraryFragment extends ListFragment
- implements Route.Listener {
- private Journey journey_ = Journey.NULL_JOURNEY;
-
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setListAdapter(new SegmentAdapter(getActivity(), this));
- } // onCreate
-
- @Override
- public void onResume() {
- super.onResume();
- Route.onResume();
- Route.registerListener(this);
- } // onResume
-
- @Override
- public void onPause() {
- Route.unregisterListener(this);
- super.onPause();
- } // onPause
-
- @Override
- public void onListItemClick(ListView l, View v, int position, long id) {
- if(journey_.isEmpty())
- return;
-
- journey_.setActiveSegmentIndex(position);
- try {
- ((RouteMapActivity)getActivity()).showMap();
- } catch(Exception e) {
- }
- } // onListItemClick
-
- @Override
- public void onNewJourney(final Journey journey, final Waypoints waypoints) {
- journey_ = journey;
- setSelection(journey_.activeSegmentIndex());
- } // onNewJourney
-
- @Override
- public void onResetJourney() {
- journey_ = Journey.NULL_JOURNEY;
- } // onResetJourney
-
- //////////////////////////////////
- static class SegmentAdapter extends BaseAdapter {
- private final ItineraryFragment itinerary_;
- private final TurnIcons.Mapping iconMappings_;
- private final Drawable footprints_;
- private final LayoutInflater inflater_;
- private final Drawable themeColor_;
- private final int backgroundColor_;
-
- SegmentAdapter(final Context context, final ItineraryFragment itinerary) {
- itinerary_ = itinerary;
- iconMappings_ = TurnIcons.LoadMapping(context);
- footprints_ = context.getResources().getDrawable(R.drawable.footprints);
-
- inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- themeColor_ = context.getResources().getDrawable(R.color.apptheme_color);
- backgroundColor_ = Theme.backgroundColor(context);
- } // SegmentAdaptor
-
- private Journey journey() { return itinerary_.journey_; }
-
- private boolean hasSegments() {
- return !journey().isEmpty();
- } // hasSegments
-
- @Override
- public int getCount() {
- return hasSegments() ? journey().segments().count() : 1;
- } // getCount
-
- @Override
- public Object getItem(int position) {
- if(!hasSegments())
- return null;
- return journey().segments().get(position);
- } // getItem
-
- @Override
- public long getItemId(int position) {
- return position;
- } // getItemId
-
- @Override
- public View getView(final int position, final View convertView, final ViewGroup parent) {
- if(!hasSegments())
- return inflater_.inflate(R.layout.itinerary_not_available, parent, false);
-
- final Segment seg = Route.journey().segments().get(position);
- final int layout_id = position != 0 ? R.layout.itinerary_item : R.layout.itinerary_header_item;
- final View v = inflater_.inflate(layout_id, parent, false);
-
- final boolean highlight = (position == Route.journey().activeSegmentIndex());
-
- if(position == 0) {
- Journey journey = Route.journey();
- Segment.Start start = journey.segments().first();
-
- setText(v, R.id.title, journey.name(), false);
- setText(v, R.id.journeyid, String.format("#%,d", journey.itinerary()), false);
- setText(v, R.id.routetype, initCap(journey.plan()) + " route:", false);
- setText(v, R.id.distance, distance(journey.total_distance()), false);
- setText(v, R.id.journeytime, start.totalTime(), false);
- setText(v, R.id.calories, start.calories(), false);
- setText(v, R.id.carbondioxide, start.co2(), false);
- } // if ...
- setText(v, R.id.segment_distance, seg.distance(), highlight);
- setText(v, R.id.segment_cumulative_distance, seg.runningDistance(), highlight);
- setText(v, R.id.segment_time, seg.runningTime(), highlight);
-
- setMainText(v, R.id.segment_street, seg.turn(), seg.street(), highlight);
- setTurnIcon(v, R.id.segment_type, seg.turn(), seg.walk());
-
- if (highlight && position != 0)
- v.setBackgroundDrawable(themeColor_);
-
- return v;
- } // getView
-
- private void setText(final View v, final int id, final String t, final boolean highlight) {
- final TextView n = (TextView)v.findViewById(id);
- if(n == null)
- return;
- n.setText(t);
- if(highlight)
- n.setTextColor(Color.BLACK);
- } // setText
-
- private void setMainText(final View v, final int id, final String turn, final String street, final boolean highlight) {
- String t = street;
- if(turn.length() != 0)
- t = turn + " into " + street;
- setText(v, id, t, highlight);
- } // setMainText
-
- private void setTurnIcon(final View v, final int id, final String turn, final boolean walk) {
- final ImageView iv = (ImageView)v.findViewById(id);
- if (iv == null)
- return;
-
- final Drawable icon = turnIcon(turn);
- iv.setImageDrawable(icon);
- iv.setBackgroundColor(backgroundColor_);
- if(walk)
- iv.setBackgroundDrawable(footprints_);
- } // setTurnIcon
-
- private Drawable turnIcon(final String turn) {
- return iconMappings_.icon(turn);
- } // turnIcon
-
- private String distance(final int metres) { return DistanceFormatter.formatter(CycleStreetsPreferences.units()).total_distance(metres); }
- } // class SegmentAdaptor
-} // ItineraryActivity
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationEditorActivity.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationEditorActivity.java
index d668942b1..f5e986fb2 100644
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationEditorActivity.java
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationEditorActivity.java
@@ -1,7 +1,7 @@
package net.cyclestreets;
+import android.app.Activity;
import android.os.Bundle;
-import android.support.v7.app.ActionBarActivity;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
@@ -17,20 +17,24 @@
import org.osmdroid.api.IGeoPoint;
-public class LocationEditorActivity extends ActionBarActivity
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static net.cyclestreets.util.PermissionsKt.hasPermission;
+import static net.cyclestreets.util.PermissionsKt.requestPermissionsResultAction;
+
+
+public class LocationEditorActivity extends Activity
implements ThereOverlay.LocationListener,
View.OnClickListener,
TextWatcher {
+
private CycleMapView map_;
private ThereOverlay there_;
private Button save_;
- private Button cancel_;
private EditText nameBox_;
private LocationDatabase ldb_;
private int localId_;
private boolean firstTime_;
-
@Override
public void onCreate(final Bundle saved) {
super.onCreate(saved);
@@ -44,12 +48,27 @@ public void onCreate(final Bundle saved) {
setupEditBox();
firstTime_ = true;
- } // onCreate
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ for (int i = 0; i < permissions.length; i++) {
+ // (No need to check request code here as "follow location" is the only one requested here)
+ if (permissions[i].equals(ACCESS_FINE_LOCATION)) {
+ requestPermissionsResultAction(grantResults[i], permissions[i], () -> {
+ map_.doEnableFollowLocation();
+ map_.saveLocationPrefs();
+ return null;
+ });
+ }
+ }
+ }
private void setupMap() {
- final RelativeLayout v = (RelativeLayout)(findViewById(R.id.mapholder));
+ final RelativeLayout v = (findViewById(R.id.mapholder));
- map_ = new CycleMapView(this, getClass().getName());
+ map_ = new CycleMapView(this, getClass().getName(), null);
there_ = new ThereOverlay(this);
there_.setLocationListener(this);
@@ -57,30 +76,34 @@ private void setupMap() {
map_.overlayPushTop(there_);
v.addView(map_, new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));
- map_.enableAndFollowLocation();
+ if (hasPermission(this, ACCESS_FINE_LOCATION)) {
+ map_.enableAndFollowLocation();
+ }
map_.onResume();
there_.setMapView(map_);
- } // setupMap
+ }
private void setupButtons() {
- save_ = (Button)findViewById(R.id.save);
+ save_ = findViewById(R.id.save);
save_.setCompoundDrawablesWithIntrinsicBounds(0, 0, android.R.drawable.ic_menu_save, 0);
save_.setOnClickListener(this);
save_.setEnabled(false);
- cancel_ = (Button)findViewById(R.id.cancel);
+ Button cancel_ = findViewById(R.id.cancel);
cancel_.setCompoundDrawablesWithIntrinsicBounds(android.R.drawable.ic_menu_close_clear_cancel, 0, 0, 0);
cancel_.setOnClickListener(this);
- } // setupButtons
+ }
private void setupEditBox() {
- nameBox_ = (EditText)findViewById(R.id.name);
+ nameBox_ = findViewById(R.id.name);
nameBox_.addTextChangedListener(this);
- } // setupEditBox
+ }
private void setupLocation() {
if (localId_ == -1) {
- map_.enableAndFollowLocation();
+ if (hasPermission(this, ACCESS_FINE_LOCATION)) {
+ map_.enableAndFollowLocation();
+ }
return;
}
@@ -90,7 +113,7 @@ private void setupLocation() {
map_.centreOn(location.where());
checkAllowSave();
- } // setupLocation
+ }
@Override
public void onResume() {
@@ -101,20 +124,20 @@ public void onResume() {
if (firstTime_) {
setupLocation();
firstTime_ = false;
- } // if ...
- } // onResume
+ }
+ }
@Override
public void onPause() {
super.onPause();
map_.onPause();
ldb_.close();
- } // onPause
+ }
@Override
public void onSetLocation(IGeoPoint point) {
checkAllowSave();
- } // onSetLocation
+ }
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
@@ -124,24 +147,24 @@ public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) { }
@Override
public void afterTextChanged(Editable editable) {
checkAllowSave();
- } // afterTextChanged
+ }
private void checkAllowSave() {
boolean allow = (there_.there() != null) && (nameBox_.getText().length() > 0);
save_.setEnabled(allow);
- } // checkAllowSave
+ }
@Override
public void onClick(View view) {
if (save_ == view)
saveLocation();
finish();
- } // onClick
+ }
private void saveLocation() {
if (localId_ == -1)
ldb_.addLocation(nameBox_.getText().toString(), there_.there());
else
ldb_.updateLocation(localId_, nameBox_.getText().toString(), there_.there());
- } // saveLocation
-} // LocationEditorFragment
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationsFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationsFragment.java
deleted file mode 100644
index a2e441a19..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationsFragment.java
+++ /dev/null
@@ -1,166 +0,0 @@
-package net.cyclestreets;
-
-import android.content.Context;
-import android.content.Intent;
-import android.os.Bundle;
-import android.support.v4.app.ListFragment;
-import android.view.ContextMenu;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.BaseAdapter;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import net.cyclestreets.content.LocationDatabase;
-import net.cyclestreets.content.SavedLocation;
-import net.cyclestreets.fragments.R;
-
-import java.util.List;
-
-import static net.cyclestreets.util.MenuHelper.createMenuItem;
-
-public class LocationsFragment extends ListFragment {
- private LocationDatabase locDb_;
-
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- locDb_ = new LocationDatabase(getActivity());
- setListAdapter(new LocationsAdapter(getActivity(), locDb_));
- } // onCreate
-
- @Override
- public View onCreateView(final LayoutInflater inflater,
- final ViewGroup container,
- final Bundle savedInstanceState) {
- super.onCreateView(inflater, container, savedInstanceState);
- return inflater.inflate(R.layout.locations_list, container, false);
- } // onCreateView
-
- @Override
- public void onActivityCreated(Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- setHasOptionsMenu(true);
- registerForContextMenu(getListView());
- } // onActivityCreated
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- super.onCreateOptionsMenu(menu, inflater);
- inflater.inflate(R.menu.locations, menu);
- } // onCreateOptionsMenu
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (item.getItemId() == R.id.ic_menu_addlocation) {
- editLocation(-1);
- return true;
- } // if ...
- return super.onOptionsItemSelected(item);
- } // onOptionsItemSelected
-
- @Override
- public void onCreateContextMenu(final ContextMenu menu,
- final View v,
- final ContextMenu.ContextMenuInfo menuInfo) {
- createMenuItem(menu, R.string.ic_menu_edit);
- createMenuItem(menu, R.string.ic_menu_delete);
- } // onCreateContextMenu
-
- @Override
- public boolean onContextItemSelected(final MenuItem item)
- {
- try {
- final AdapterView.AdapterContextMenuInfo info
- = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo();
- final int localId = (int)getListAdapter().getItemId(info.position);
- final int menuId = item.getItemId();
-
- if(R.string.ic_menu_edit == menuId)
- editLocation(localId);
- if(R.string.ic_menu_delete == menuId)
- deleteLocation(localId);
-
- return true;
- } // try
- catch (final ClassCastException e) {
- return false;
- } // catch
- } // onContextItemSelected
-
- @Override
- public void onListItemClick(ListView l, View v, int position, long id) {
- editLocation((int) id);
- } // onListItemClick
-
- private void editLocation(int localId) {
- Intent edit = new Intent(getActivity(), LocationEditorActivity.class);
- edit.putExtra("localId", localId);
- startActivityForResult(edit, 0);
- } // addNewLocation
-
- @Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
-
- refresh();
- } // onActivity
-
- private void deleteLocation(int localId) {
- locDb_.deleteLocation(localId);
- refresh();
- } // deleteLocation
-
- private void refresh() {
- getListAdapter().refresh();
- } // refresh
-
- @Override
- public LocationsAdapter getListAdapter() {
- return (LocationsAdapter)super.getListAdapter();
- } // getListAdapter
-
- //////////////////////////////////
- static class LocationsAdapter extends BaseAdapter {
- private final LayoutInflater inflater_;
- private LocationDatabase locDb_;
- private List locs_;
-
- LocationsAdapter(final Context context, final LocationDatabase locDb) {
- inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- locDb_ = locDb;
- locs_ = locDb_.savedLocations();
- } // SegmentAdaptor
-
- public void refresh() {
- locs_ = locDb_.savedLocations();
- notifyDataSetChanged();
- } // refresh
-
- @Override
- public int getCount() { return locs_.size(); }
-
- @Override
- public Object getItem(int position) { return locs_.get(position); }
-
- @Override
- public long getItemId(int position) { return locs_.get(position).localId(); }
-
- @Override
- public View getView(final int position, final View convertView, final ViewGroup parent) {
- final SavedLocation location = locs_.get(position);
- final View v = inflater_.inflate(R.layout.storedroutes_item, parent, false);
-
- final TextView n = (TextView)v.findViewById(R.id.route_title);
- n.setText(location.name());
-
- return v;
- } // getView
- } // class LocationsAdaptor
-
-} // LocationsFragment
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationsFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationsFragment.kt
new file mode 100644
index 000000000..d8a3b8f5a
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/LocationsFragment.kt
@@ -0,0 +1,147 @@
+package net.cyclestreets
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+import android.view.*
+import android.view.ContextMenu.ContextMenuInfo
+import android.widget.AdapterView.AdapterContextMenuInfo
+import android.widget.BaseAdapter
+import android.widget.ListView
+import android.widget.TextView
+import androidx.fragment.app.ListFragment
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import net.cyclestreets.content.LocationDatabase
+import net.cyclestreets.content.SavedLocation
+import net.cyclestreets.fragments.R
+import net.cyclestreets.iconics.IconicsHelper.materialIcon
+import net.cyclestreets.util.MenuHelper
+import net.cyclestreets.util.Theme
+
+
+class LocationsFragment : ListFragment() {
+
+ private lateinit var locDb: LocationDatabase
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ locDb = LocationDatabase(requireActivity())
+ listAdapter = LocationsAdapter(requireActivity(), locDb)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ super.onCreateView(inflater, container, savedInstanceState)
+ val layout = inflater.inflate(R.layout.locations_list, container, false)
+
+ val addLocationIcon = materialIcon(requireContext(), GoogleMaterial.Icon.gmd_add_location, Theme.lowlightColor(requireContext()))
+
+ val addLocationButton: FloatingActionButton = layout.findViewById(R.id.addlocation)
+ addLocationButton.setImageDrawable(addLocationIcon)
+ addLocationButton.setOnClickListener { _ -> editLocation(-1) }
+
+ return layout
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ setHasOptionsMenu(true)
+ registerForContextMenu(listView)
+ }
+
+ override fun onCreateContextMenu(menu: ContextMenu,
+ v: View,
+ menuInfo: ContextMenuInfo?) {
+ MenuHelper.createMenuItem(menu, R.string.ic_menu_edit)
+ MenuHelper.createMenuItem(menu, R.string.ic_menu_delete)
+ }
+
+ override fun onContextItemSelected(item: MenuItem): Boolean {
+ return try {
+ val info = item.menuInfo as AdapterContextMenuInfo
+ val localId = listAdapter!!.getItemId(info.position).toInt()
+ val menuId = item.itemId
+
+ if (R.string.ic_menu_edit == menuId)
+ editLocation(localId)
+ if (R.string.ic_menu_delete == menuId)
+ deleteLocation(localId)
+
+ true
+ } catch (e: ClassCastException) {
+ false
+ }
+ }
+
+ override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
+ editLocation(id.toInt())
+ }
+
+ private fun editLocation(localId: Int) {
+ val edit = Intent(activity, LocationEditorActivity::class.java)
+ edit.putExtra("localId", localId)
+ startActivityForResult(edit, 0)
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+
+ refresh()
+ }
+
+ private fun deleteLocation(localId: Int) {
+ locDb.deleteLocation(localId)
+ refresh()
+ }
+
+ private fun refresh() {
+ listAdapter!!.refresh()
+ }
+
+ override fun getListAdapter(): LocationsAdapter? {
+ return super.getListAdapter() as LocationsAdapter?
+ }
+
+ //////////////////////////////////
+ class LocationsAdapter(context: Context, private val locDb: LocationDatabase) : BaseAdapter() {
+
+ private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ private var locations: List
+
+ init {
+ locations = this.locDb.savedLocations()
+ }
+
+ fun refresh() {
+ locations = locDb.savedLocations()
+ notifyDataSetChanged()
+ }
+
+ override fun getCount(): Int {
+ return locations.size
+ }
+
+ override fun getItem(position: Int): Any {
+ return locations[position]
+ }
+
+ override fun getItemId(position: Int): Long {
+ return locations[position].localId().toLong()
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val location = locations[position]
+ val v = inflater.inflate(R.layout.storedroutes_item, parent, false)
+
+ val n = v.findViewById(R.id.route_title) as TextView
+ n.text = location.name()
+
+ return v
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainNavDrawerActivity.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainNavDrawerActivity.java
deleted file mode 100644
index bea7d9220..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainNavDrawerActivity.java
+++ /dev/null
@@ -1,578 +0,0 @@
-package net.cyclestreets;
-
-import android.app.Activity;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.res.Configuration;
-import android.graphics.drawable.Drawable;
-import android.support.v4.app.ActionBarDrawerToggle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.support.v4.view.GravityCompat;
-import android.support.v7.app.ActionBarActivity;
-import android.support.v7.app.ActionBar;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.support.v4.widget.DrawerLayout;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.AdapterView;
-import android.widget.BaseAdapter;
-import android.widget.ImageView;
-import android.widget.ListView;
-import android.widget.TextView;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import net.cyclestreets.fragments.R;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Route;
-import net.cyclestreets.routing.Waypoints;
-
-public abstract class MainNavDrawerActivity
- extends ActionBarActivity
- implements Route.Listener {
- private NavigationDrawerFragment navDrawer_;
- private List pages_;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.mainnavdraweractivity);
-
- navDrawer_ = (NavigationDrawerFragment)getSupportFragmentManager().findFragmentById(R.id.navigation_drawer);
- navDrawer_.setUp(R.id.navigation_drawer, (DrawerLayout)findViewById(R.id.drawer_layout));
-
- pages_ = new ArrayList<>();
-
- addDrawerItems();
-
- navDrawer_.addPages(pages_);
-
- if (CycleStreetsAppSupport.isFirstRun())
- onFirstRun();
- else if (CycleStreetsAppSupport.isNewVersion())
- onNewVersion();
- } // onCreate
-
- protected void onFirstRun() { }
- protected void onNewVersion() { }
-
- protected abstract void addDrawerItems();
-
- protected void addDrawerFragment(final int titleId,
- final int iconId,
- final Class extends Fragment> fragClass) {
- addDrawerFragment(titleId, iconId, fragClass, null, null);
- } // addDrawerFragment
-
- protected void addDrawerFragment(final PageTitle title,
- final int iconId,
- final Class extends Fragment> fragClass) {
- addDrawerFragment(title, iconId, fragClass, null, null);
- } // addDrawerFragment
-
- protected void addDrawerFragment(final int titleId,
- final int iconId,
- final Class extends Fragment> fragClass,
- final PageStatus pageStatus) {
- addDrawerFragment(titleId, iconId, fragClass, null, pageStatus);
- } // addDrawerFragment
-
- protected void addDrawerFragment(final int titleId,
- final int iconId,
- final Class extends Fragment> fragClass,
- final PageInitialiser initialiser) {
- addDrawerFragment(titleId, iconId, fragClass, initialiser, null);
- } // addDrawerFragment
-
- protected void addDrawerFragment(final int titleId,
- final int iconId,
- final Class extends Fragment> fragClass,
- final PageInitialiser initialiser,
- final PageStatus pageStatus) {
- final String title = getResources().getString(titleId);
- addDrawerFragment(new FixedTitle(title), iconId, fragClass, initialiser, pageStatus);
- } // addDrawerFragment
-
- protected void addDrawerFragment(final PageTitle title,
- final int iconId,
- final Class extends Fragment> fragClass,
- final PageInitialiser initialiser,
- final PageStatus pageStatus) {
- final Drawable icon = iconId != -1 ? getResources().getDrawable(iconId) : null;
-
- pages_.add(new FragmentItem(title, icon, fragClass, initialiser, pageStatus));
- } // addDrawerFragment
-
- protected void addDrawerActivity(int titleId,
- final int iconId,
- final Class extends Activity> fragClass) {
- addDrawerActivity(titleId, iconId, fragClass, null);
- } // addDrawerActivity
-
- protected void addDrawerActivity(final int titleId,
- final int iconId,
- final Class extends Activity> fragClass,
- final PageStatus pageStatus) {
- final String title = getResources().getString(titleId);
- final Drawable icon = iconId != -1 ? getResources().getDrawable(iconId) : null;
-
- pages_.add(new ActivityItem(new FixedTitle(title), icon, fragClass, pageStatus));
- } // addDrawerActivity
-
- @Override
- public boolean onCreateOptionsMenu(final Menu menu) {
- if (!navDrawer_.isDrawerOpen()) {
- restoreActionBar();
- return true;
- }
- return super.onCreateOptionsMenu(menu);
- } // onCreateOptionsMenu
-
- @Override
- public void onBackPressed() {
- if(navDrawer_.onBackPressed())
- return;
- super.onBackPressed();
- } // onBackPressed
-
-
- private void restoreActionBar() {
- ActionBar actionBar = getSupportActionBar();
- actionBar.setDisplayShowTitleEnabled(true);
- actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
- actionBar.setTitle(navDrawer_.title());
- } // restoreActionBar
-
- public void showPage(int page) {
- navDrawer_.selectItem(page);
- } // showPage
-
- private final String Drawer = "DRAWER";
- @Override
- public void onResume() {
- final int selectedFrag = prefs().getInt(Drawer, -1);
- if (selectedFrag != -1)
- navDrawer_.selectItem(selectedFrag);
-
- super.onResume();
- Route.registerListener(this);
- } // onResume
-
- @Override
- public void onPause() {
- Route.unregisterListener(this);
-
- final SharedPreferences.Editor edit = prefs().edit();
- edit.putInt(Drawer, navDrawer_.selectedItem());
- edit.commit();
-
- navDrawer_.fragment().onPause();
-
- super.onPause();
- } // onPause
-
- @Override
- public void onNewJourney(final Journey journey, final Waypoints waypoints) {
- supportInvalidateOptionsMenu();
- } // onNewJourney
- public void onResetJourney() {
- supportInvalidateOptionsMenu();
- } // onResetJourney
-
- private SharedPreferences prefs() {
- return getSharedPreferences("net.cyclestreets.CycleStreets", Context.MODE_PRIVATE);
- } // prefs()
-
- //////////////////////////////////////////
- //////////////////////////////////////////
- public static class NavigationDrawerFragment extends Fragment {
- private static final String STATE_SELECTED_POSITION = "selected_navigation_drawer_position";
- private ActionBarDrawerToggle drawerToggle_;
-
- private DrawerLayout drawerLayout_;
- private ListView drawerListView_;
- private PageInfoAdapter drawerContents_;
- private View fragmentContainerView_;
-
- private int currentSelectedPosition_ = 0;
- private int nextSelectedPosition_ = 0;
- private boolean firstRun_;
-
- public NavigationDrawerFragment() { }
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- firstRun_ = CycleStreetsAppSupport.isFirstRun();
-
- if (savedInstanceState != null)
- currentSelectedPosition_ = savedInstanceState.getInt(STATE_SELECTED_POSITION);
- } // onCreate
-
- void addPages(final List pages) {
- drawerContents_ = new PageInfoAdapter(this, pages, getActionBar().getThemedContext());
- drawerListView_.setAdapter(drawerContents_);
-
- selectItem(currentSelectedPosition_);
- drawerListView_.setItemChecked(currentSelectedPosition_, true);
- } // addDrawerItems
-
- public String title() { return drawerContents_.getItem(currentSelectedPosition_).title(); }
- public Fragment fragment() { return ((FragmentItem)drawerContents_.getItem(currentSelectedPosition_)).fragment(); }
-
- @Override
- public void onActivityCreated (Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- // Indicate that this fragment would like to influence the set of actions in the action bar.
- setHasOptionsMenu(true);
- } // onActivityCreated
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- drawerListView_ = (ListView)inflater.inflate(R.layout.navigation_drawer, container, false);
- drawerListView_.setOnItemClickListener(new AdapterView.OnItemClickListener() {
- @Override
- public void onItemClick(AdapterView> parent, View view, int position, long id) {
- nextSelectedPosition_ = position;
- drawerLayout_.closeDrawer(fragmentContainerView_);
- } // onItemClick
- });
-
- return drawerListView_;
- } // onCreateView
-
- public boolean isDrawerOpen() {
- return drawerLayout_ != null && drawerLayout_.isDrawerOpen(fragmentContainerView_);
- } // isDrawerOpen
-
- public void setUp(int fragmentId, DrawerLayout drawerLayout) {
- fragmentContainerView_ = getActivity().findViewById(fragmentId);
- drawerLayout_ = drawerLayout;
-
- // set a custom shadow that overlays the maintabbedactivity content when the drawer opens
- drawerLayout_.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START);
- // set up the drawer's list view with items and click listener
-
- ActionBar actionBar = getActionBar();
- actionBar.setDisplayHomeAsUpEnabled(true);
- actionBar.setHomeButtonEnabled(true);
-
- // ActionBarDrawerToggle ties together the the proper interactions
- // between the navigation drawer and the action bar app icon.
- drawerToggle_ = new ActionBarDrawerToggle(
- getActivity(), /* host Activity */
- drawerLayout_, /* DrawerLayout object */
- R.drawable.apptheme_ic_navigation_drawer, /* nav drawer image to replace 'Up' caret */
- R.string.navigation_drawer_open, /* "open drawer" description for accessibility */
- R.string.navigation_drawer_close /* "close drawer" description for accessibility */
- ) {
- @Override
- public void onDrawerClosed(View drawerView) {
- super.onDrawerClosed(drawerView);
- if (!isAdded()) { return; }
- getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu()
- if (nextSelectedPosition_ != currentSelectedPosition_)
- selectItem(nextSelectedPosition_);
- }
-
- @Override
- public void onDrawerOpened(View drawerView) {
- super.onDrawerOpened(drawerView);
- if (!isAdded()) { return; }
- getActivity().supportInvalidateOptionsMenu(); // calls onPrepareOptionsMenu()
- }
- };
-
- if (firstRun_) {
- drawerLayout_.openDrawer(fragmentContainerView_);
- firstRun_ = false;
- } // if ...
-
- // Defer code dependent on restoration of previous instance state.
- drawerLayout_.post(new Runnable() {
- @Override
- public void run() {
- drawerToggle_.syncState();
- }
- });
- drawerLayout_.setDrawerListener(drawerToggle_);
- } // setUp
-
- public void selectItem(int position) {
- if (position >= drawerContents_.getCount())
- return;
-
- final DrawerItem di = drawerContents_.getItem(position);
- if (drawerLayout_ != null)
- drawerLayout_.closeDrawer(fragmentContainerView_);
- getActivity().supportInvalidateOptionsMenu();
-
- if (di instanceof FragmentItem) {
- currentSelectedPosition_ = position;
- drawerListView_.setItemChecked(position, true);
-
- final FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
- final Fragment newFrag = ((FragmentItem)di).create();
-
- final FragmentTransaction ft = fragmentManager.beginTransaction();
- ft.replace(R.id.container, newFrag);
- ft.commit();
- } //
- if (di instanceof ActivityItem) {
- final Intent intent = new Intent(getActivity(), ((ActivityItem)di).activityClass());
- startActivity(intent);
- } //
- } // selectItem
-
- public int selectedItem() { return currentSelectedPosition_; }
-
- @Override
- public void onSaveInstanceState(Bundle outState) {
- super.onSaveInstanceState(outState);
- outState.putInt(STATE_SELECTED_POSITION, currentSelectedPosition_);
- } // onSaveInstanceState
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- drawerToggle_.onConfigurationChanged(newConfig);
- } // onConfigurationChanged
-
- @Override
- public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
- // If the drawer is open, show the global app actions in the action bar. See also
- // showGlobalContextActionBar, which controls the top-left area of the action bar.
- if (drawerLayout_ != null && isDrawerOpen())
- showGlobalContextActionBar();
- else
- fragment().onCreateOptionsMenu(menu, inflater);
-
- super.onCreateOptionsMenu(menu, inflater);
- } // onCreateOptionsMenu
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- if (drawerToggle_.onOptionsItemSelected(item))
- return true;
-
- if (fragment().onOptionsItemSelected(item))
- return true;
-
- return super.onOptionsItemSelected(item);
- } // onOptionsItemSelected
-
- @Override
- public void onPrepareOptionsMenu(Menu menu) {
- if (drawerLayout_ != null && isDrawerOpen())
- ;
- else
- fragment().onPrepareOptionsMenu(menu);
-
- super.onPrepareOptionsMenu(menu);
- } // onPrepareOptionsMenu
-
- public boolean onBackPressed() {
- if (isDrawerOpen()) {
- drawerLayout_.closeDrawers();
- return true;
- } // if ...
- if(!(fragment() instanceof Undoable))
- return false;
- return ((Undoable)fragment()).onBackPressed();
- } // onBackPressed
-
- private void showGlobalContextActionBar() {
- ActionBar actionBar = getActionBar();
- actionBar.setDisplayShowTitleEnabled(true);
- actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
- actionBar.setTitle(R.string.app_name);
- } // showGlobalContextActionBar
-
- private ActionBar getActionBar() {
- return ((ActionBarActivity) getActivity()).getSupportActionBar();
- } // getActionBar
- } // NavigationDrawerFragment
-
-
- //////////////////////////////////////////
- //////////////////////////////////////////
- static class PageInfoAdapter extends BaseAdapter {
- private final List pageInfo_;
- private final List activePages_;
- private final LayoutInflater inflater_;
- private final NavigationDrawerFragment parentFrag_;
- private final Drawable themeColor_;
- private final Context context_;
-
- PageInfoAdapter(final NavigationDrawerFragment parentFrag,
- final List pageInfo,
- final Context context) {
- context_ = context;
- parentFrag_ = parentFrag;
- pageInfo_ = pageInfo;
- for (DrawerItem di : pageInfo_)
- di.setAdapter(this);
- activePages_ = new ArrayList<>();
- inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- themeColor_ = context.getResources().getDrawable(R.color.apptheme_color);
-
- buildActiveList();
- } // PageInfoAdaptor
-
- private void buildActiveList() {
- activePages_.clear();
- for (DrawerItem di : pageInfo_)
- if (di.enabled())
- activePages_.add(di);
- } // buildActiveList
-
- @Override
- public void notifyDataSetChanged() {
- buildActiveList();
- super.notifyDataSetChanged();
- } // notifyDataSetChanged
-
- @Override
- public int getCount() { return activePages_.size(); }
-
- @Override
- public DrawerItem getItem(int position) { return activePages_.get(position); }
-
- @Override
- public long getItemId(int position) { return position; }
-
- @Override
- public View getView(final int position, final View convertView, final ViewGroup parent) {
- final View v = inflater_.inflate(R.layout.navigation_item, parent, false);
-
- final boolean highlight = (position == parentFrag_.currentSelectedPosition_);
-
- setText(v, getItem(position).title(), highlight);
- setIcon(v, getItem(position).icon());
-
- return v;
- } // getView
-
- private void setText(final View v, final String t, boolean highlight) {
- final TextView n = (TextView)v.findViewById(R.id.menu_name);
- n.setText(t);
-
- if (highlight) {
- v.setBackgroundDrawable(themeColor_);
- n.setTextAppearance(context_, android.R.style.TextAppearance);
- } // if ...
- } // setText
-
- private void setIcon(final View v, final Drawable icon) {
- if (icon == null)
- return;
- final ImageView iv = (ImageView)v.findViewById(R.id.menu_icon);
- iv.setImageDrawable(icon);
- } // setIcon
- } // class PageInfoAdaptor
-
- //////////////////////////////////////////
- //////////////////////////////////////////
- private static abstract class DrawerItem {
- private PageTitle title_;
- private Drawable icon_;
- private PageStatus pageStatus_;
-
- public DrawerItem(final PageTitle title,
- final Drawable icon,
- final PageStatus pageStatus) {
- title_ = title;
- icon_ = icon;
- pageStatus_ = pageStatus;
- } // DrawerItem
-
- public String title() { return title_.title(); }
- public Drawable icon() { return icon_; }
- public boolean enabled() { return (pageStatus_ != null) ? pageStatus_.enabled() : true; }
-
- @Override
- public String toString() { return title_.title(); }
-
- public void setAdapter(final BaseAdapter adapter) {
- if (pageStatus_ != null)
- pageStatus_.setAdapter(adapter);
- } // setAdapter
- } // DrawerItem
-
- private static class FragmentItem extends DrawerItem {
- private Class extends Fragment> fragClass_;
- private Fragment fragment_;
- private PageInitialiser initialiser_;
-
- public FragmentItem(final PageTitle title,
- final Drawable icon,
- final Class extends Fragment> fragClass,
- final PageInitialiser initialiser,
- final PageStatus pageStatus) {
- super(title, icon, pageStatus);
- fragClass_ = fragClass;
- initialiser_ = initialiser;
- } // FragmentItem
-
- public Fragment create() {
- try {
- fragment_ = fragClass_.newInstance();
- if (initialiser_ != null)
- initialiser_.initialise(fragment_);
- } catch (Exception e) {
- throw new RuntimeException(e);
- } // try
-
- return fragment_;
- } // attach
-
- public Fragment fragment() { return fragment_; }
- } // FragmentInfo
-
- private static class ActivityItem extends DrawerItem {
- private Class extends Activity> activityClass_;
-
- public ActivityItem(final PageTitle title,
- final Drawable icon,
- final Class extends Activity> activityClass,
- final PageStatus pageStatus) {
-
- super(title, icon, pageStatus);
- activityClass_ = activityClass;
- } // ActivityItem
-
- public Class extends Activity> activityClass() {
- return activityClass_;
- } // activityClass
- } // ActivityItem
-
- public interface PageTitle {
- String title();
- } // PageTitle
-
- public class FixedTitle implements PageTitle {
- private final String title_;
- public FixedTitle(String t) { title_ = t; }
- public String title() { return title_; }
- } // FixedTitle
-
- public interface PageInitialiser {
- void initialise(final Fragment page);
- } // PageInitialiser
-
- public interface PageStatus {
- void setAdapter(BaseAdapter adapter);
- boolean enabled();
- } // PageStatus
-} // class Main
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainNavDrawerActivity.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainNavDrawerActivity.kt
new file mode 100644
index 000000000..9227c8b46
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainNavDrawerActivity.kt
@@ -0,0 +1,232 @@
+package net.cyclestreets
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.os.Bundle
+import android.util.Log
+import android.util.SparseArray
+import android.view.MenuItem
+import android.view.View
+import androidx.appcompat.app.AppCompatActivity
+import androidx.appcompat.widget.Toolbar
+import androidx.core.view.GravityCompat
+import androidx.drawerlayout.widget.DrawerLayout
+import androidx.fragment.app.Fragment
+import androidx.transition.Fade
+import com.google.android.material.navigation.NavigationView
+import com.google.android.material.navigation.NavigationView.OnNavigationItemSelectedListener
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import net.cyclestreets.addphoto.AddPhotoFragment
+import net.cyclestreets.fragments.R
+import net.cyclestreets.iconics.IconicsHelper.materialIcons
+import net.cyclestreets.itinerary.ItineraryAndElevationFragment
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Route
+import net.cyclestreets.routing.Waypoints
+import net.cyclestreets.util.Logging
+
+private val TAG = Logging.getTag(MainNavDrawerActivity::class.java)
+private const val DRAWER_ITEMID_SELECTED_KEY = "DRAWER_ITEM_SELECTED"
+
+
+abstract class MainNavDrawerActivity : AppCompatActivity(), OnNavigationItemSelectedListener, Route.Listener {
+
+ private lateinit var drawerLayout: DrawerLayout
+ private lateinit var navigationView: NavigationView
+ private lateinit var toolbar: Toolbar
+ private var selectedItem: Int = 0
+
+ private val menuItemIdToFragment = object : SparseArray>() {
+ init {
+ put(R.id.nav_journey_planner, RouteMapFragment::class.java)
+ put(R.id.nav_itinerary, ItineraryAndElevationFragment::class.java)
+ put(R.id.nav_photomap, PhotoMapFragment::class.java)
+ put(R.id.nav_addphoto, AddPhotoFragment::class.java)
+ put(R.id.nav_blog, BlogFragment::class.java)
+ put(R.id.nav_settings, SettingsFragment::class.java)
+ }
+ }
+
+ private val fragmentToMenuItemId = mapOf(
+ RouteMapFragment::class.java to R.id.nav_journey_planner,
+ ItineraryAndElevationFragment::class.java to R.id.nav_itinerary,
+ PhotoMapFragment::class.java to R.id.nav_photomap,
+ AddPhotoFragment::class.java to R.id.nav_addphoto,
+ BlogFragment::class.java to R.id.nav_blog,
+ SettingsFragment::class.java to R.id.nav_settings
+ )
+
+ // If you're in one of these fragments at pause, then you'll be returned to it on resume.
+ private val resumableFragments = setOf(R.id.nav_journey_planner, R.id.nav_photomap, R.id.nav_addphoto, R.id.nav_settings)
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.main_navdrawer_activity)
+
+ val (burgerIcon, addPhotoIcon, blogIcon, settingsIcon) =
+ materialIcons(context = this, iconIds = listOf(GoogleMaterial.Icon.gmd_menu, GoogleMaterial.Icon.gmd_add_a_photo, GoogleMaterial.Icon.gmd_chat, GoogleMaterial.Icon.gmd_settings))
+ burgerIcon.setTint(resources.getColor(R.color.cs_primary_material_light, null))
+
+ drawerLayout = findViewById(R.id.drawer_layout)
+ navigationView = (findViewById(R.id.nav_view)).apply {
+ setNavigationItemSelectedListener(this@MainNavDrawerActivity)
+ menu.findItem(R.id.nav_itinerary).isVisible = Route.routeAvailable()
+ menu.findItem(R.id.nav_addphoto).icon = addPhotoIcon
+ menu.findItem(R.id.nav_blog).icon = blogIcon
+ menu.findItem(R.id.nav_settings).icon = settingsIcon
+ }
+
+ toolbar = findViewById(R.id.toolbar)
+ toolbar.visibility = View.VISIBLE
+ setSupportActionBar(toolbar)
+ supportActionBar!!.apply {
+ setDisplayHomeAsUpEnabled(true)
+ setHomeAsUpIndicator(burgerIcon)
+ }
+
+ if (CycleStreetsAppSupport.isFirstRun())
+ onFirstRun()
+ else if (CycleStreetsAppSupport.isNewVersion())
+ onNewVersion()
+ CycleStreetsAppSupport.splashScreenSeen()
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ when (item.itemId) {
+ android.R.id.home -> {
+ setBlogStateTitle()
+ drawerLayout.openDrawer(GravityCompat.START)
+ return true
+ }
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
+ //////////// OnNavigationItemSelectedListener method implementation
+ override fun onNavigationItemSelected(menuItem: MenuItem): Boolean {
+ // Swap UI fragments based on the selection
+ this.supportFragmentManager.beginTransaction().let { ft ->
+ ft.replace(R.id.content_frame, instantiateFragmentFor(menuItem))
+ ft.commit()
+ }
+
+ updateMenuDisplayFor(menuItem)
+
+ return true
+ }
+
+ private fun currentMenuItemId(): Int? {
+ return this.supportFragmentManager.findFragmentById(R.id.content_frame)?.javaClass?.let {
+ fragmentToMenuItemId[it]
+ }
+ }
+
+ private fun updateMenuDisplayFor(menuItem: MenuItem) {
+ // set item as selected to persist highlight
+ menuItem.isChecked = true
+ // close drawer when item is tapped
+ drawerLayout.closeDrawers()
+ // Save which item is selected
+ selectedItem = menuItem.itemId
+ // Update the ActionBar title to be the title of the chosen fragment
+ toolbar.title = menuItem.title
+ }
+
+ private fun instantiateFragmentFor(menuItem: MenuItem): Fragment {
+ val fragmentClass = menuItemIdToFragment.get(menuItem.itemId)
+ try {
+ return fragmentClass.newInstance().apply {
+ enterTransition = Fade()
+ exitTransition = Fade()
+ }
+ }
+ catch (e: InstantiationException) { throw RuntimeException(e) }
+ catch (e: IllegalAccessException) { throw RuntimeException(e) }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onBackPressed() {
+ if (drawerLayout.isDrawerOpen(navigationView)) {
+ drawerLayout.closeDrawers()
+ return
+ }
+
+ visibleFragment()?.let {
+ if (it is Undoable && it.onBackPressed())
+ return
+ if (it !is RouteMapFragment) {
+ showPage(R.id.nav_journey_planner)
+ return
+ }
+ }
+
+ super.onBackPressed()
+ }
+
+ private fun visibleFragment(): Fragment? {
+ for (fragment in supportFragmentManager.fragments)
+ if (fragment?.isVisible() == true)
+ return fragment
+ return null
+ }
+
+ protected open fun onFirstRun() {}
+ protected open fun onNewVersion() {}
+
+ fun showPage(menuItemId: Int): Boolean {
+ val menuItem = navigationView.menu.findItem(menuItemId)
+ if (menuItem != null) {
+ Log.d(TAG, "Loading page with menuItemId=$menuItemId (${menuItem.title})")
+ onNavigationItemSelected(menuItem)
+ return true
+ }
+ Log.d(TAG, "Page with menuItemId=$menuItemId could not be found")
+ return false
+ }
+
+ public override fun onResume() {
+ val selectedItem = prefs().getInt(DRAWER_ITEMID_SELECTED_KEY, R.id.nav_journey_planner)
+ if (!showPage(selectedItem))
+ showPage(R.id.nav_journey_planner)
+ super.onResume()
+ Route.registerListener(this)
+ setBlogStateTitle()
+ }
+
+ public override fun onPause() {
+ Route.unregisterListener(this)
+ currentMenuItemId()?.let {
+ saveCurrentMenuSelection(it)
+ }
+ super.onPause()
+ }
+
+ private fun saveCurrentMenuSelection(menuItemId: Int) {
+ if (resumableFragments.contains(menuItemId))
+ prefs().edit().let {
+ it.putInt(DRAWER_ITEMID_SELECTED_KEY, selectedItem)
+ it.apply()
+ }
+ }
+
+ private fun prefs(): SharedPreferences {
+ return getSharedPreferences("net.cyclestreets.CycleStreets", Context.MODE_PRIVATE)
+ }
+
+ private fun setBlogStateTitle() {
+ val titleId = if (BlogState.isBlogUpdateAvailable(this)) R.string.blog_updated else R.string.blog
+ navigationView.menu.findItem(R.id.nav_blog).title = getString(titleId)
+ }
+
+ ////////// Route.Listener method implementations
+ override fun onNewJourney(journey: Journey, waypoints: Waypoints) {
+ navigationView.menu.findItem(R.id.nav_itinerary).isVisible = Route.routeAvailable()
+ invalidateOptionsMenu()
+ }
+
+ override fun onResetJourney() {
+ navigationView.menu.findItem(R.id.nav_itinerary).isVisible = Route.routeAvailable()
+ invalidateOptionsMenu()
+ }
+
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.java
deleted file mode 100644
index d27f6c733..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package net.cyclestreets;
-
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-
-import net.cyclestreets.routing.Route;
-import net.cyclestreets.util.MapPack;
-
-public class MainSupport {
- public static boolean switchMapFile(final Intent launchIntent) {
- final String mappackage = launchIntent.getStringExtra("mapfile");
- if(mappackage == null)
- return false;
- final MapPack pack = MapPack.findByPackage(mappackage);
- if(pack == null)
- return false;
- CycleStreetsPreferences.enableMapFile(pack.path());
- return true;
- } // switchMapFile
-
- public static boolean loadRoute(final Intent launchIntent,
- final Context context) {
- final Uri launchUri = launchIntent.getData();
- if (launchUri == null)
- return false;
-
- final int itinerary = findItinerary(launchUri);
- if (itinerary == -1)
- return false;
-
- Route.FetchRoute(CycleStreetsPreferences.routeType(),
- itinerary,
- CycleStreetsPreferences.speed(),
- context);
- return true;
- } // loadRoute
-
- private static int findItinerary(final Uri launchUri) {
- try {
- final String itinerary = extractItinerary(launchUri);
- return Integer.parseInt(itinerary);
- } catch(Exception whatever) {
- return -1;
- } // catch
- } // findItinerary
-
- private static String extractItinerary(final Uri launchUri) {
- final String host = launchUri.getHost();
-
- if ("cycle.st".equals(host))
- return launchUri.getPath().substring(2);
-
- if ("m.cyclestreets.net".equals(host)) {
- final String frag = launchUri.getFragment();
- return frag.substring(0, frag.indexOf('/'));
- }
-
- final String path = launchUri.getPath().substring(8);
- return path.replace("/", "");
- } // extractItinerary
-
- private MainSupport() { }
-} // MainSupport
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.kt
new file mode 100644
index 000000000..650f9c7b5
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainSupport.kt
@@ -0,0 +1,107 @@
+package net.cyclestreets
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.util.Log
+
+import net.cyclestreets.routing.Route
+import net.cyclestreets.util.Logging
+import net.cyclestreets.util.MapPack
+import net.cyclestreets.LaunchIntent.Type.*
+import net.cyclestreets.photos.IndividualPhoto
+
+private val TAG = Logging.getTag(MainSupport::class.java)
+
+object MainSupport {
+
+ fun switchMapFile(intent: Intent, context: Context): Boolean {
+ val mapPackage = intent.getStringExtra("mapfile") ?: return false
+ val pack = MapPack.findByPackage(context, mapPackage) ?: return false
+ CycleStreetsPreferences.enableMapFile(pack.path())
+ return true
+ }
+
+ fun handleLaunchIntent(intent: Intent, activity: Activity): Boolean {
+ val launchUri = intent.data ?: return false
+ Log.d(TAG, "Handling launch intent with URI: $launchUri")
+
+ val launchIntent = determineLaunchIntent(launchUri) ?: return false
+
+ when(launchIntent.type) {
+ JOURNEY -> {
+ Log.d(TAG, "Loading journey #${launchIntent.id}")
+ (activity as RouteMapActivity).showRouteMap()
+ Route.FetchRoute(CycleStreetsPreferences.routeType(),
+ launchIntent.id,
+ CycleStreetsPreferences.speed(),
+ activity as Context)
+ }
+ LOCATION -> {
+ Log.d(TAG, "Loading location #${launchIntent.id}")
+ (activity as PhotoMapActivity).showPhotoMap()
+ IndividualPhoto.fetchPhoto(launchIntent.id)
+ }
+ }
+ return true
+ }
+}
+
+internal fun determineLaunchIntent(launchUri: Uri): LaunchIntent? {
+ return try {
+ return extractLaunchIntent(launchUri)
+ } catch (whatever: Exception) {
+ Log.w(TAG, "Failed to extract itinerary number from $launchUri")
+ null
+ }
+}
+
+private fun extractLaunchIntent(launchUri: Uri): LaunchIntent {
+ val host = launchUri.host!!
+ val path = launchUri.path!!.substring(1) // Drop the leading '/'
+ val intentType: LaunchIntent.Type
+ val id: Long
+
+ when(host) {
+ "cycle.st" -> {
+ // e.g. https://cycle.st/j61207326 or https://cycle.st/p93348
+ intentType = if (path.startsWith("j")) JOURNEY else LOCATION
+ id = path.drop(1).toLong()
+ }
+ "m.cyclestreets.net" -> {
+ // e.g. https://m.cyclestreets.net/journey/#57201887/balanced or https://m.cyclestreets.net/location/#4444
+ val frag = launchUri.fragment!! // everything after the #
+ if (path.startsWith("journey")) {
+ intentType = JOURNEY
+ id = frag.substring(0, frag.indexOf('/')).toLong()
+ } else {
+ intentType = LOCATION
+ id = frag.toLong()
+ }
+ }
+ "cyclestreets.net", "www.cyclestreets.net" -> {
+ // e.g. http(s)://(www.)cyclestreets.net/journey/61207326(/#balanced) or .../location/93348
+ if (path.startsWith("journey")) {
+ intentType = JOURNEY
+ id = path.drop(7).replace("/", "").toLong()
+ } else {
+ intentType = LOCATION
+ id = path.drop(8).replace("/", "").toLong()
+ }
+ }
+ else -> throw IllegalStateException("Unrecognised host pattern '$host' in '$launchUri'")
+ }
+ return intentType.withId(id)
+}
+
+// Helper classes
+internal class LaunchIntent private constructor(val type: Type, val id: Long) {
+ internal enum class Type {
+ JOURNEY, LOCATION;
+
+ fun withId(id: Long): LaunchIntent {
+ return LaunchIntent(this, id)
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainTabbedActivity.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainTabbedActivity.java
deleted file mode 100644
index 7499d6a1d..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/MainTabbedActivity.java
+++ /dev/null
@@ -1,291 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.fragments.R;
-
-import java.util.HashMap;
-import java.util.Map;
-
-import android.app.AlertDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentActivity;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.app.FragmentTransaction;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.webkit.WebView;
-import android.widget.TabHost;
-import android.widget.TabHost.OnTabChangeListener;
-import android.widget.TabHost.TabSpec;
-
-public abstract class MainTabbedActivity extends FragmentActivity implements OnTabChangeListener, TabHost.TabContentFactory
-{
- private TabHost tabHost_;
- private final Map tabs_ = new HashMap<>();
- private TabInfo lastTab_;
-
- public void onCreate(final Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.maintabbedactivity);
- tabHost_ = (TabHost)findViewById(android.R.id.tabhost);
- tabHost_.setup();
- tabHost_.setOnTabChangedListener(this);
-
- addTabs(tabHost_);
-
- for(int i = 0; i != tabs_.size(); ++i)
- {
- final ViewGroup.LayoutParams layout = tabHost_.getTabWidget().getChildAt(i).getLayoutParams();
- layout.height = (int)(layout.height*0.66);
- tabHost_.getTabWidget().getChildAt(i).setLayoutParams(layout);
- } // for ...
-
- // start with route tab
- showMap();
-
- showWhatsNew();
- } // onCreate
-
- protected abstract void addTabs(final TabHost tabHost);
-
- protected void setCurrentTab(final int tab) {
- tabHost_.setCurrentTab(tab);
- } // setCurrentTab
-
- public void showMap()
- {
- tabHost_.setCurrentTab(0);
- } // showMap
-
- public void showWhatsNew()
- {
- if(!CycleStreetsAppSupport.isNewVersion())
- return;
-
- final LayoutInflater layoutInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- final View whatsnewView = layoutInflater.inflate(R.layout.whatsnew, null);
- final WebView htmlView = (WebView)whatsnewView.findViewById(R.id.html_view);
- htmlView.loadUrl("file:///android_asset/whatsnew.html");
-
- final AlertDialog.Builder builder = new AlertDialog.Builder(this);
- builder.setTitle("What's New")
- .setPositiveButton("OK", new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface dialog, int which) { }
- })
- .setView(whatsnewView)
- .show();
- } // showWhatsNew
-
- protected SharedPreferences prefs() {
- return getSharedPreferences("net.cyclestreets.CycleStreets", Context.MODE_PRIVATE);
- } // prefs()
-
- //////////////////////////////////////////////////////////////////////////////////////////////////
- // Tab handling
- final class TabInfo
- {
- private final String tag_;
- private final Class extends Fragment> clss_;
- private final Bundle args_;
- private Fragment fragment_;
- private boolean menuCreated_;
-
- TabInfo(final FragmentManager fm,
- final String t,
- final Class extends Fragment> fc,
- final Bundle a)
- {
- tag_ = t;
- clss_ = fc;
- args_ = a;
- menuCreated_ = false;
-
- // Check to see if we already have a fragment for this tab, probably
- // from a previously saved state. If so, deactivate it, because our
- // initial state is that a tab isn't shown.
-
- fragment_ = fm.findFragmentByTag(tag_);
- if (fragment_ != null && !fragment_.isDetached())
- {
- final FragmentTransaction ft = fm.beginTransaction();
- ft.detach(fragment_);
- ft.commit();
- } // if
- } // TabInfo
-
- void attach(final FragmentTransaction ft)
- {
- if(fragment_ == null)
- {
- fragment_ = Fragment.instantiate(MainTabbedActivity.this,
- clss_.getName(),
- args_);
- ft.add(R.id.realtabcontent, fragment_, tag_);
- }
- else
- ft.attach(fragment_);
- } // attach
-
- void detach(final FragmentTransaction ft)
- {
- if(fragment_ == null)
- return;
- ft.detach(fragment_);
- } // detach
-
- void onPrepareOptionsMenu(final Menu menu, final MenuInflater inflater)
- {
- if(!menuCreated_)
- {
- fragment_.onCreateOptionsMenu(menu, inflater);
- menuCreated_ = true;
- } // if ...
- fragment_.onPrepareOptionsMenu(menu);
- } // onPrepareOptionsMenu
-
- boolean onOptionsItemSelected(final MenuItem item)
- {
- return fragment_.onOptionsItemSelected(item);
- } // onOptionsItemSelected
-
- boolean onContextItemSelected(final MenuItem item)
- {
- return fragment_.onContextItemSelected(item);
- } // onContextItemSelected
-
- boolean onBackPressed()
- {
- if(!(fragment_ instanceof Undoable))
- return false;
- return ((Undoable)fragment_).onBackPressed();
- } // onBackPressed
- } // class TabInfo
-
- @Override
- public View createTabContent(String tag)
- {
- final View v = new View(this);
- v.setMinimumWidth(0);
- v.setMinimumHeight(0);
- return v;
- } // createTabContent
-
- protected void addTab(final String tabId,
- final int iconId,
- final Class extends Fragment> fragClass)
- {
- final TabSpec tabSpec = tabHost_.newTabSpec(tabId);
- tabSpec.setIndicator("", getResources().getDrawable(iconId));
- tabSpec.setContent(this);
-
- final TabInfo info = new TabInfo(getSupportFragmentManager(),
- tabId, fragClass, null);
-
- tabs_.put(tabId, info);
- tabHost_.addTab(tabSpec);
- } // addTab
-
- @Override
- public void onTabChanged(String tabId)
- {
- final TabInfo newTab = tabs_.get(tabId);
- if(lastTab_ == newTab)
- return;
-
- final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
- if(lastTab_ != null)
- lastTab_.detach(ft);
-
- if(newTab != null)
- newTab.attach(ft);
-
- lastTab_ = newTab;
- ft.commit();
- getSupportFragmentManager().executePendingTransactions();
-
- setTitle(applicationName() + " : " + tabId);
- } // onTabChanged
-
- public String applicationName() {
- int stringId = getApplicationInfo().labelRes;
- return getString(stringId);
- } // applicationName
-
- // pause/resume
- @Override
- protected void onPause()
- {
- super.onPause();
-
- final SharedPreferences.Editor edit = prefs().edit();
- edit.putString("TAB", tabHost_.getCurrentTabTag());
- edit.commit();
- } // onPause
-
- @Override
- protected void onResume()
- {
- final String tab = prefs().getString("TAB", "");
- tabHost_.setCurrentTabByTag(tab);
-
- super.onResume();
- } // onResume
-
- // menus
- @Override
- public boolean onCreateOptionsMenu(final Menu menu)
- {
- // fragments all share the same menu
- super.onCreateOptionsMenu(menu);
- return true;
- } // onCreateOptionsMenu
-
- @Override
- public boolean onPrepareOptionsMenu(final Menu menu)
- {
- // turn them all off
- for(int i = 0; i != menu.size(); ++i)
- {
- final MenuItem mi = menu.getItem(i);
- mi.setVisible(false);
- } // for ...
-
- // then let each fragment reenable
- lastTab_.onPrepareOptionsMenu(menu, getMenuInflater());
- return super.onPrepareOptionsMenu(menu);
- } // onPrepareOptionsMenu
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item)
- {
- if(lastTab_.onOptionsItemSelected(item))
- return true;
-
- return super.onOptionsItemSelected(item);
- } // onOptionsItemSelected
-
- @Override
- public boolean onContextItemSelected(final MenuItem item)
- {
- if(lastTab_.onContextItemSelected(item))
- return true;
- return super.onContextItemSelected(item);
- } // onContextItemSelected
-
- // touch and buttons
- @Override
- public void onBackPressed()
- {
- if(lastTab_.onBackPressed())
- return;
- super.onBackPressed();
- }
-} // class CycleStreets
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoMapActivity.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoMapActivity.java
new file mode 100644
index 000000000..6cc723299
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoMapActivity.java
@@ -0,0 +1,5 @@
+package net.cyclestreets;
+
+public interface PhotoMapActivity {
+ void showPhotoMap();
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoMapFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoMapFragment.java
index ef2693ee3..4b66504ed 100644
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoMapFragment.java
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoMapFragment.java
@@ -9,12 +9,18 @@
public class PhotoMapFragment extends CycleMapFragment
{
- public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle saved)
- {
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ setHasOptionsMenu(true);
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle saved) {
final View v = super.onCreateView(inflater, container, saved);
- overlayPushBottom(new PhotosOverlay(getActivity(), mapView()));
+ overlayPushBottom(new PhotosOverlay(mapView()));
return v;
- } // onCreate
-} // PhotomapActivity
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoUploadFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoUploadFragment.java
deleted file mode 100644
index efe44a48b..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/PhotoUploadFragment.java
+++ /dev/null
@@ -1,883 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.fragments.R;
-import net.cyclestreets.api.PhotomapCategory;
-import net.cyclestreets.api.PhotomapCategories;
-import net.cyclestreets.api.Upload;
-import net.cyclestreets.util.Bitmaps;
-import net.cyclestreets.util.Dialog;
-import net.cyclestreets.util.MessageBox;
-import net.cyclestreets.util.Share;
-import net.cyclestreets.views.CycleMapView;
-import net.cyclestreets.views.overlay.ThereOverlay;
-import net.cyclestreets.views.overlay.ThereOverlay.LocationListener;
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.media.ExifInterface;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.MediaStore;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.BaseAdapter;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.Toast;
-import android.widget.RelativeLayout.LayoutParams;
-
-import java.io.File;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.util.GeoPoint;
-
-import static net.cyclestreets.util.MenuHelper.createMenuItem;
-import static net.cyclestreets.util.MenuHelper.enableMenuItem;
-
-public class PhotoUploadFragment extends Fragment
- implements View.OnClickListener, LocationListener, Undoable
-{
- public enum AddStep
- {
- PHOTO(null),
- CAPTION(PHOTO),
- CATEGORY(CAPTION),
- LOCATION(CATEGORY),
- VIEW(LOCATION),
- DONE(VIEW);
-
- AddStep(AddStep p)
- {
- prev_ = p;
- if(prev_ != null)
- prev_.next_ = this;
- save(this);
- } // AddStep
-
- public AddStep prev() { return prev_; }
- public AddStep next() { return next_; }
-
- public int value() { return Value_.get(this); }
-
- private AddStep prev_;
- private AddStep next_;
-
- public static AddStep fromInt(int a)
- {
- for(AddStep s : Value_.keySet())
- if(s.value() == a)
- return s;
- return null;
- } // AddStep
-
- private static void save(AddStep a)
- {
- if(Value_ == null)
- Value_ = new HashMap<>();
- Value_.put(a, Value_.size());
- } // save
-
- private static Map Value_;
- } // AddStep
-
- private static final int TakePhoto = 2;
- private static final int ChoosePhoto = 3;
- private static final int AccountDetails = 4;
-
- private LinearLayout photoRoot_;
- private View photoView_;
- private View photoCaption_;
- private View photoCategory_;
- private View photoLocation_;
- private View photoWebView_;
-
- private CycleMapView map_;
- private ThereOverlay there_;
- private boolean geolocated_;
- private static PhotomapCategories photomapCategories;
-
- private AddStep step_;
-
- private String photoFile_ = null;
- private Bitmap photo_ = null;
- private String caption_;
- private String dateTime_;
- private int metaCatId_;
- private int catId_;
- private String uploadedUrl_;
-
- private boolean allowUploadByKey_;
- private boolean allowTextOnly_;
- private boolean noShare_;
-
- private LayoutInflater inflater_;
- private InputMethodManager imm_;
-
- @Override
- public View onCreateView(final LayoutInflater inflater,
- final ViewGroup container,
- final Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
-
- final String metaData = photoUploadMetaData();
- allowUploadByKey_ = metaData.contains("ByKey");
- allowTextOnly_ = metaData.contains("AllowTextOnly");
- noShare_ = metaData.contains("NoShare");
-
- inflater_ = LayoutInflater.from(getActivity());
- imm_ = (InputMethodManager)getActivity().getSystemService(Context.INPUT_METHOD_SERVICE);
-
- photoRoot_ = (LinearLayout)inflater_.inflate(R.layout.addphoto, null);
-
- step_ = AddStep.PHOTO;
- caption_ = "";
- dateTime_ = "";
- metaCatId_ = -1;
- catId_ = -1;
-
- photoView_ = inflater_.inflate(R.layout.addphotostart, null);
- {
- final Button takePhoto = (Button)photoView_.findViewById(R.id.takephoto_button);
- if(getActivity().getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
- takePhoto.setOnClickListener(this);
- else
- takePhoto.setEnabled(false);
- }
- photoView_.findViewById(R.id.chooseexisting_button).setOnClickListener(this);
- {
- final Button textOnly = (Button)photoView_.findViewById(R.id.textonly_button);
- if (allowTextOnly_)
- textOnly.setOnClickListener(this);
- else
- textOnly.setVisibility(View.GONE);
- }
-
- photoCategory_ = inflater_.inflate(R.layout.addphotocategory, null);
- backNextButtons(photoCategory_, "Back", android.R.drawable.ic_media_rew, "Next", android.R.drawable.ic_media_ff);
-
- photoLocation_ = inflater_.inflate(R.layout.addphotolocation, null);
- backNextButtons(photoLocation_, "Back", android.R.drawable.ic_media_rew, "Upload!", android.R.drawable.ic_menu_upload);
-
- photoWebView_ = inflater_.inflate(R.layout.addphotoview, null);
- backNextButtons(photoWebView_, "Upload another", android.R.drawable.ic_menu_revert, "Close", android.R.drawable.ic_menu_close_clear_cancel);
- final Button closeButton = (Button)photoWebView_.findViewById(R.id.next);
- closeButton.setEnabled(false);
- closeButton.setVisibility(View.GONE);
-
-
- // start reading categories
- if(photomapCategories == null)
- new GetPhotomapCategoriesTask().execute();
- else
- setupSpinners();
-
- there_ = new ThereOverlay(getActivity());
- there_.setLocationListener(this);
-
- setupView();
-
- return photoRoot_;
- } // PhotoUploadFragment
-
- private void backNextButtons(final View parentView,
- final String backText, final int backDrawable,
- final String nextText, final int nextDrawable) {
- final Button back = (Button)parentView.findViewById(R.id.back);
- back.setText(backText);
- back.setCompoundDrawablesWithIntrinsicBounds(backDrawable, 0, 0, 0);
- final Button next = (Button)parentView.findViewById(R.id.next);
- next.setText(nextText);
- next.setCompoundDrawablesWithIntrinsicBounds(0, 0, nextDrawable, 0);
- } // backNextButtons
-
- private String photoUploadMetaData() {
- try {
- final ApplicationInfo ai = getActivity().getPackageManager().getApplicationInfo(getActivity().getPackageName(), PackageManager.GET_META_DATA);
- final Bundle bundle = ai.metaData;
- final String upload = bundle.getString("CycleStreetsPhotoUpload");
- return upload != null ? upload : "";
- } catch(final Exception e) {
- return "";
- } // catch
- } // photoUploadMetaData
-
- private void setContentView(final View child)
- {
- photoRoot_.removeAllViewsInLayout();
- photoRoot_.addView(child);
- } // setContentView
-
- private void store()
- {
- final SharedPreferences.Editor edit = prefs().edit();
- edit.putInt("STEP", step_.value());
- edit.putString("PHOTOFILE", photoFile_);
- edit.putString("DATETIME", dateTime_);
- edit.putString("CAPTION", caption_);
- edit.putBoolean("GEOLOC", geolocated_);
- edit.commit();
- } // store
-
- @Override
- public void onPause()
- {
- final SharedPreferences.Editor edit = prefs().edit();
- edit.putString("CAPTION", captionText());
- edit.putInt("METACAT", metaCategoryId());
- edit.putInt("CATEGORY", categoryId());
- final IGeoPoint p = there_.there();
- if(p != null)
- {
- edit.putInt("THERE-LAT", p.getLatitudeE6());
- edit.putInt("THERE-LON", p.getLongitudeE6());
- }
- else
- edit.putInt("THERE-LAT", -1);
- edit.putLong("WHEN", new Date().getTime());
- edit.putBoolean("GEOLOC", geolocated_);
- edit.commit();
-
- if(map_ != null)
- map_.onPause();
- super.onPause();
- } // onPause
-
- private final long fiveMinutes = 5 * 60 * 1000;
-
- @Override
- public void onResume()
- {
- try {
- doOnResume();
- } // try
- catch(RuntimeException e) {
- step_ = AddStep.fromInt(0);
- } // catch
-
- super.onResume();
- setupView();
- } // onResume
-
- private void doOnResume()
- {
- final SharedPreferences prefs = prefs();
-
- step_ = AddStep.fromInt(prefs.getInt("STEP", 0));
- photoFile_ = prefs.getString("PHOTOFILE", photoFile_);
- if(photo_ == null && photoFile_ != null)
- photo_ = Bitmaps.loadFile(photoFile_);
- dateTime_ = prefs.getString("DATETIME", "");
- caption_ = prefs.getString("CAPTION", "");
-
- metaCatId_ = prefs.getInt("METACAT", -1);
- catId_ = prefs.getInt("CATEGORY", -1);
- setSpinnerSelections();
-
- final int tlat = prefs.getInt("THERE-LAT", -1);
- final int tlon = prefs.getInt("THERE-LON", -1);
- if((tlat != -1) && (tlon != -1))
- there_.noOverThere(new GeoPoint(tlat, tlon));
- geolocated_ = prefs.getBoolean("GEOLOC", false);
-
- if(map_ != null)
- map_.onResume();
-
- final long now = new Date().getTime();
- final long when = prefs.getLong("WHEN", now);
- if((now - when) > fiveMinutes)
- step_ = AddStep.fromInt(0);
- } // doOnResume
-
- private SharedPreferences prefs()
- {
- return getActivity().getSharedPreferences("net.cyclestreets.AddPhotoActivity", Context.MODE_PRIVATE);
- } // prefs()
-
- ///////////////////////////////////////////////////////////////////
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater)
- {
- createMenuItem(menu, R.string.ic_menu_restart, Menu.NONE, R.drawable.ic_menu_rotate);
- createMenuItem(menu, R.string.ic_menu_back, Menu.NONE, R.drawable.ic_menu_revert);
- } // onCreateOptionsMenu
-
- @Override
- public void onPrepareOptionsMenu(final Menu menu)
- {
- enableMenuItem(menu, R.string.ic_menu_restart, step_ != AddStep.PHOTO);
- enableMenuItem(menu, R.string.ic_menu_back, step_ != AddStep.PHOTO && step_ != AddStep.VIEW);
- } // onPrepareOptionsMenu
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item)
- {
- final int menuItem = item.getItemId();
-
- if(R.string.ic_menu_restart == menuItem) {
- step_ = AddStep.PHOTO;
- setupView();
- return true;
- }
-
- if(R.string.ic_menu_back == menuItem) {
- onBackPressed();
- return true;
- }
-
- return false;
- } // onMenuItemSelected
-
- ///////////////////////////////////////////////////////////////////
- @Override
- public boolean onBackPressed()
- {
- if(step_ == AddStep.PHOTO || step_ == AddStep.VIEW)
- {
- step_ = AddStep.PHOTO;
- store();
-
- return false;
- } // if ...
-
- step_ = step_.prev();
- store();
- setupView();
-
- return true;
- } // onBackPressed
-
- private void nextStep()
- {
- if((step_ == AddStep.LOCATION) && (there_.there() == null))
- {
- Toast.makeText(getActivity(), "Please set photo location", Toast.LENGTH_LONG).show();
- return;
- } // if ...
-
- step_ = step_.next();
-
- store();
- setupView();
- } // nextStep
-
- private void setupView()
- {
- switch(step_)
- {
- case PHOTO:
- metaCategorySpinner().setSelection(0);
- categorySpinner().setSelection(0);
- caption_ = "";
- geolocated_ = false;
- there_.noOverThere(null);
- setContentView(photoView_);
- break;
- case CAPTION:
- // why recreate this view each time - well *sigh* because we have to force the
- // keyboard to hide, if we don't recreate the view afresh, Android won't redisplay
- // the keyboard if we come back to this view
- photoCaption_ = inflater_.inflate(R.layout.addphotocaption, null);
- backNextButtons(photoCaption_, "Back", android.R.drawable.ic_media_rew, "Next", android.R.drawable.ic_media_ff);
- setContentView(photoCaption_);
- captionEditor().setText(caption_);
- if (photo_ == null && allowTextOnly_) {
- ((TextView)photoRoot_.findViewById(R.id.label)).setText("Your Report");
- ((EditText)photoRoot_.findViewById(R.id.caption)).setLines(10);
- } // if ...
- break;
- case CATEGORY:
- caption_ = captionText();
- store();
- setContentView(photoCategory_);
- break;
- case LOCATION:
- metaCatId_ = metaCategoryId();
- catId_ = categoryId();
- setupMap();
- setContentView(photoLocation_);
- there_.recentre();
- if (photo_ == null && allowTextOnly_) {
- ((TextView) photoRoot_.findViewById(R.id.label)).setText("Where is the location your report describes?");
- photoRoot_.findViewById(R.id.nogeo).setVisibility(View.GONE);
- }
- else {
- ((TextView) photoRoot_.findViewById(R.id.label)).setText("Where was this photo taken?");
- photoRoot_.findViewById(R.id.nogeo).setVisibility(geolocated_ ? View.GONE : View.VISIBLE);
- }
- break;
- case VIEW:
- setContentView(photoWebView_);
- {
- final TextView text = (TextView)photoWebView_.findViewById(R.id.photo_text);
- text.setText(caption_);
- final TextView url = (TextView)photoWebView_.findViewById(R.id.photo_url);
- final Button share = (Button)photoWebView_.findViewById(R.id.photo_share);
- if (noShare_) {
- url.setVisibility(View.GONE);
- share.setVisibility(View.GONE);
- } else {
- url.setText(uploadedUrl_);
- share.setOnClickListener(this);
- }
- }
- break;
- case DONE:
- step_ = AddStep.PHOTO;
- setupView();
- break;
- } // switch ...
-
- previewPhoto();
- hookUpNext();
- } // setupView
-
- private void previewPhoto()
- {
- final ImageView iv = (ImageView)photoRoot_.findViewById(R.id.photo);
- if(iv == null)
- return;
-
- if (photo_ == null && allowTextOnly_) {
- iv.setVisibility(View.GONE);
- return;
- }
-
- iv.setImageBitmap(photo_);
- int newHeight = getActivity().getWindowManager().getDefaultDisplay().getHeight() / 10 * 4;
- int newWidth = getActivity().getWindowManager().getDefaultDisplay().getWidth();
-
- iv.setLayoutParams(new LinearLayout.LayoutParams(newWidth, newHeight));
- iv.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
- } // previewPhoto
-
- private void hookUpNext()
- {
- final Button b = (Button)photoRoot_.findViewById(R.id.back);
- if(b != null)
- b.setOnClickListener(this);
-
- final Button n = (Button)photoRoot_.findViewById(R.id.next);
- if(n != null)
- n.setOnClickListener(this);
-
- if(step_ == AddStep.LOCATION)
- n.setEnabled(there_.there() != null);
- } // hookUpNext
-
- private EditText captionEditor() { return (EditText)photoCaption_.findViewById(R.id.caption); }
- private String captionText()
- {
- if(photoCaption_ == null)
- return caption_;
- imm_.hideSoftInputFromWindow(captionEditor().getWindowToken(), 0);
- return captionEditor().getText().toString();
- } // captionText
- private int metaCategoryId() { return (int)metaCategorySpinner().getSelectedItemId(); }
- private int categoryId() { return (int)categorySpinner().getSelectedItemId(); }
- private Spinner metaCategorySpinner() { return (Spinner)photoCategory_.findViewById(R.id.metacat); }
- private Spinner categorySpinner() { return (Spinner)photoCategory_.findViewById(R.id.category); }
-
- private void setupSpinners()
- {
- metaCategorySpinner().setAdapter(new CategoryAdapter(getActivity(), photomapCategories.metaCategories()));
- categorySpinner().setAdapter(new CategoryAdapter(getActivity(), photomapCategories.categories()));
-
- setSpinnerSelections();
- } // setupSpinners
-
- private void setSpinnerSelections()
- {
- // ids == position
- if(metaCatId_ != -1)
- metaCategorySpinner().setSelection(metaCatId_);
- if(catId_ != -1)
- categorySpinner().setSelection(catId_);
- } // setSpinnerSelections
-
- private void setupMap()
- {
- final RelativeLayout v = (RelativeLayout)(photoLocation_.findViewById(R.id.mapholder));
-
- if(map_ != null) {
- map_.onPause();
- ((RelativeLayout)map_.getParent()).removeView(map_);
- }
- else
- {
- map_ = new CycleMapView(getActivity(), this.getClass().getName());
- map_.overlayPushTop(there_);
- }
-
- v.addView(map_, new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
- map_.enableAndFollowLocation();
- map_.onResume();
- there_.setMapView(map_);
- } // setupMap
-
- @Override
- public void onClick(final View v)
- {
- int clicked = v.getId();
-
- if (R.id.takephoto_button == clicked)
- startActivityForResult(new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE),
- TakePhoto);
-
- if (R.id.chooseexisting_button == clicked)
- startActivityForResult(new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI),
- ChoosePhoto);
-
- if (R.id.textonly_button == clicked) {
- photo_ = null;
- photoFile_ = null;
- dateTime_ = null;
- nextStep();
- }
-
- if (R.id.photo_share == clicked)
- Share.Url(getActivity(), uploadedUrl_, caption_, "Photo on CycleStreets.net");
-
- if (R.id.back == clicked) {
- if (step_ == AddStep.VIEW) {
- step_ = AddStep.PHOTO;
- store();
- setupView();
- } else
- onBackPressed();
- }
-
- if (R.id.next == clicked) {
- if(step_ == AddStep.LOCATION) {
- final boolean needAccountDetails = !allowUploadByKey_ && !CycleStreetsPreferences.accountOK();
- if(needAccountDetails)
- startActivityForResult(new Intent(getActivity(), AccountDetailsActivity.class), AccountDetails);
- else
- upload();
- } else if (step_ == AddStep.VIEW) {
-
- }
- else
- nextStep();
- } // switch
- } // onClick
-
- @Override
- public void onSetLocation(IGeoPoint point)
- {
- final Button u = (Button)photoLocation_.findViewById(R.id.next);
- u.setEnabled(point != null);
- } // onSetLocation
-
- @Override
- public void onActivityResult(final int requestCode,
- final int resultCode,
- final Intent data)
- {
- if (resultCode != Activity.RESULT_OK)
- return;
-
- try
- {
- /*
- String url = intent.getData().toString();
-Bitmap bitmap = null;
-InputStream is = null;
-if (url.startsWith("content://com.google.android.apps.photos.content")){
- is = getContentResolver().openInputStream(Uri.parse(url));
- bitmap = getBitmapFromInputStream(is);
-}
- */
-
- photoFile_ = getImageFilePath(data);
- if(photo_ != null)
- photo_.recycle();
- photo_ = Bitmaps.loadFile(photoFile_);
-
- final ExifInterface exif = new ExifInterface(photoFile_);
-
- dateTime_ = photoTimestamp(exif);
- final GeoPoint photoLoc = photoLocation(exif);
- geolocated_ = (photoLoc != null);
- there_.noOverThere(photoLocation(exif));
-
- nextStep();
- }
- catch(Exception e)
- {
- Toast.makeText(getActivity(), "There was a problem grabbing the photo : " + e.getMessage(), Toast.LENGTH_LONG).show();
- if(requestCode == TakePhoto)
- startActivityForResult(new Intent(Intent.ACTION_PICK,
- android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI),
- ChoosePhoto);
- }
- } // onActivityResult
-
- private String getImageFilePath(final Intent data)
- {
- final Uri selectedImage = data.getData();
- final String[] filePathColumn = { MediaStore.Images.Media.DATA };
-
- final Cursor cursor = getActivity().getContentResolver().query(selectedImage, filePathColumn, null, null, null);
- try
- {
- cursor.moveToFirst();
- return cursor.getString(cursor.getColumnIndex(filePathColumn[0]));
- } // try
- finally
- {
- cursor.close();
- } // finally
- } // getImageFilePath
-
- private void upload()
- {
- try
- {
- doUpload();
- }
- catch(Exception e)
- {
- Toast.makeText(getActivity(), "Could not upload photo. Please check your network connection.", Toast.LENGTH_LONG).show();
- step_ = AddStep.LOCATION;
- }
- } // upload
-
- private void doUpload() throws Exception
- {
- final String filename = photoFile_;
- final String username = CycleStreetsPreferences.username();
- final String password = CycleStreetsPreferences.password();
- final IGeoPoint location = there_.there();
- final String metaCat = photomapCategories.metaCategories().get(metaCatId_).getTag();
- final String category = photomapCategories.categories().get(catId_).getTag();
- final String dateTime = dateTime_ != null ? dateTime_ : Long.toString(new Date().getTime() / 1000);
- final String caption = caption_;
-
- final UploadPhotoTask uploader = new UploadPhotoTask(getActivity(),
- filename,
- username,
- password,
- location,
- metaCat,
- category,
- dateTime,
- caption);
- uploader.execute();
- } // upload
-
- private void uploadComplete(final String photo_url)
- {
- uploadedUrl_ = photo_url;
- nextStep();
- } // uploadComplete
-
- private void uploadFailed(final String msg)
- {
- MessageBox.OK(photoLocation_,
- msg,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- step_ = AddStep.LOCATION;
- setupView();
- }
- });
- } // uploadFailed
-
- private GeoPoint photoLocation(final ExifInterface photoExif)
- {
- final float[] coords = new float[2];
- if(!photoExif.getLatLong(coords))
- return null;
- int lat = (int)(((double)coords[0]) * 1E6);
- int lon = (int)(((double)coords[1]) * 1E6);
- return new GeoPoint(lat, lon);
- } // photoLocation
-
- private String photoTimestamp(final ExifInterface photoExif)
- {
- Date date = new Date();
-
- try
- {
- final DateFormat df = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
- final String dateString = photoExif.getAttribute(ExifInterface.TAG_DATETIME);
- if(dateString != null && dateString.length() > 0)
- date = df.parse(dateString);
- } // try
- catch(Exception e)
- {
- // ah well
- } // catch
-
- return Long.toString(date.getTime() / 1000);
- } // photoTimestamp
-
- ///////////////////////////////////////////////////////////////////////////
- private class GetPhotomapCategoriesTask extends AsyncTask
- {
- protected PhotomapCategories doInBackground(Object... params)
- {
- PhotomapCategories photomapCategories = null;
- try {
- photomapCategories = PhotomapCategories.get();
- }
- catch (Exception ex) {
- }
- return photomapCategories;
- } // PhotomapCategories
-
- @Override
- protected void onPostExecute(PhotomapCategories photomapCategories)
- {
- if(photomapCategories == null)
- {
- Toast.makeText(getActivity(), "Could not load photomap categories. Please check network connection.", Toast.LENGTH_LONG).show();
- return;
- } // if ...
- PhotoUploadFragment.photomapCategories = photomapCategories;
- setupSpinners();
- } // onPostExecute
- } // class GetPhotomapCategoriesTask
-
- //////////////////////////////////////////////////////////////////////////
- private class UploadPhotoTask extends AsyncTask
- {
- private final String filename_;
- private final String username_;
- private final String password_;
- private final IGeoPoint location_;
- private final String metaCat_;
- private final String category_;
- private final String dateTime_;
- private final String caption_;
- private final ProgressDialog progress_;
- private final boolean smallImage_;
-
- UploadPhotoTask(final Context context,
- final String filename,
- final String username,
- final String password,
- final IGeoPoint location,
- final String metaCat,
- final String category,
- final String dateTime,
- final String caption)
- {
- smallImage_ = CycleStreetsPreferences.uploadSmallImages();
- filename_ = smallImage_ ? Bitmaps.resizePhoto(filename) : filename;
- username_ = username;
- password_ = password;
- location_ = location;
- metaCat_ = metaCat;
- category_ = category;
- dateTime_ = dateTime;
- caption_ = caption;
-
- progress_ = Dialog.createProgressDialog(context, R.string.uploading_photo);
- } // UploadPhotoTask
-
- @Override
- protected void onPreExecute()
- {
- super.onPreExecute();
- progress_.show();
- } // onPreExecute
-
- protected Upload.Result doInBackground(Object... params)
- {
- try {
- return Upload.photo(filename_,
- username_,
- password_,
- location_,
- metaCat_,
- category_,
- dateTime_,
- caption_);
- } catch (Exception e) {
- return Upload.Result.forError("There was a problem uploading your photo: \n" + e.getMessage());
- }
- } // doInBackground
-
- @Override
- protected void onPostExecute(final Upload.Result result)
- {
- if(smallImage_)
- new File(filename_).delete();
- progress_.dismiss();
- if(result.ok())
- uploadComplete(result.url());
- else
- uploadFailed(result.error());
- } // onPostExecute
- } // class UploadPhotoTask
-
- //////////////////////////////////////////////////////////
- static private class CategoryAdapter extends BaseAdapter
- {
- private final LayoutInflater inflater_;
- private final List list_;
-
- public CategoryAdapter(final Context context,
- final List list)
- {
- inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- list_ = list;
- } // CategoryAdapter
-
- @Override
- public int getCount()
- {
- return list_.size();
- } // getCount
-
- @Override
- public String getItem(final int position)
- {
- final PhotomapCategory c = list_.get(position);
- return c.getName();
- } // getItem
-
- @Override
- public long getItemId(final int position)
- {
- return position;
- } // getItemId
-
- @Override
- public View getView(final int position, final View convertView, final ViewGroup parent)
- {
- final int id = (parent instanceof Spinner) ? android.R.layout.simple_spinner_item : android.R.layout.simple_spinner_dropdown_item;
- final TextView tv = (TextView)inflater_.inflate(id, parent, false);
- tv.setText(getItem(position));
- return tv;
- } // getView
- } // CategoryAdapter
-} // class AddPhotoActivity
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteAvailablePageStatus.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteAvailablePageStatus.java
deleted file mode 100644
index 46c7721e0..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteAvailablePageStatus.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package net.cyclestreets;
-
-import android.widget.BaseAdapter;
-
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Route;
-import net.cyclestreets.routing.Waypoints;
-
-public class RouteAvailablePageStatus
- implements MainNavDrawerActivity.PageStatus,
- Route.Listener {
- private BaseAdapter adapter_;
-
- public RouteAvailablePageStatus() {
- Route.registerListener(this);
- } // RouteAvailablePageStatus
-
- @Override
- public void setAdapter(final BaseAdapter adapter) {
- adapter_ = adapter;
- } // setAdapter
-
- @Override
- public boolean enabled() {
- return Route.available();
- } // enabled
-
- @Override
- public void onNewJourney(final Journey journey, final Waypoints waypoints) {
- ping();
- } // onNewJourney
-
- public void onResetJourney() {
- ping();
- } // onResetJourney
-
- private void ping() {
- if (adapter_ != null)
- adapter_.notifyDataSetChanged();
- } // ping
-
-} // RouteAvailablePageStatus
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByAddress.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByAddress.java
index 1ae06f1d3..9864eb3f8 100644
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByAddress.java
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByAddress.java
@@ -8,25 +8,26 @@
import net.cyclestreets.routing.Route;
import net.cyclestreets.routing.Waypoints;
import net.cyclestreets.util.MessageBox;
-import net.cyclestreets.views.PlaceViewWithCancel;
+import net.cyclestreets.views.place.PlaceViewWithCancel;
import net.cyclestreets.api.GeoPlace;
import net.cyclestreets.views.RouteType;
import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.util.BoundingBoxE6;
+import org.osmdroid.util.BoundingBox;
import org.osmdroid.util.GeoPoint;
import android.app.AlertDialog;
import android.content.Context;
import android.location.Location;
+import androidx.annotation.NonNull;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.Button;
import android.widget.LinearLayout;
public class RouteByAddress {
- public static void launch(final Context context,
- final BoundingBoxE6 boundingBox,
+ public static void launch(@NonNull final Context context,
+ final BoundingBox boundingBox,
final Location lastFix,
final Waypoints waypoints) {
final AlertDialog.Builder builder = new AlertDialog.Builder(context);
@@ -36,10 +37,10 @@ public static void launch(final Context context,
final AlertDialog ad = builder.create();
ad.show();
- ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(context, android.R.style.TextAppearance_Large);
+ ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(android.R.style.TextAppearance_Large);
rbac.setDialog(ad);
- } // launch
+ }
private static class RouteByAddressCallbacks implements View.OnClickListener {
private final Context context_;
@@ -47,7 +48,7 @@ private static class RouteByAddressCallbacks implements View.OnClickListener {
private final RouteType routeType_;
private final Button addWaypoint_;
- private final BoundingBoxE6 bounds_;
+ private final BoundingBox bounds_;
private final IGeoPoint currentLoc_;
private final Waypoints waypoints_;
@@ -60,7 +61,7 @@ private static class RouteByAddressCallbacks implements View.OnClickListener {
public RouteByAddressCallbacks(final Context context,
final AlertDialog.Builder builder,
- final BoundingBoxE6 boundingBox,
+ final BoundingBox boundingBox,
final Location lastFix,
final Waypoints waypoints) {
context_ = context;
@@ -72,46 +73,46 @@ public RouteByAddressCallbacks(final Context context,
final View layout = View.inflate(context, R.layout.routebyaddress, null);
builder.setView(layout);
- builder.setPositiveButton(R.string.go, MessageBox.NoAction);
+ builder.setPositiveButton(R.string.find_route, MessageBox.NoAction);
bounds_ = boundingBox;
currentLoc_ = lastFix != null ? new GeoPoint(lastFix.getLatitude(), lastFix.getLongitude()) : null;
- placeHolder_ = (LinearLayout) layout.findViewById(R.id.places);
+ placeHolder_ = layout.findViewById(R.id.places);
waypoints_ = waypoints;
- addWaypoint_ = (Button) layout.findViewById(R.id.addVia);
+ addWaypoint_ = layout.findViewById(R.id.addVia);
addWaypoint_.setOnClickListener(this);
- routeType_ = (RouteType)layout.findViewById(R.id.routeType);
+ routeType_ = layout.findViewById(R.id.routeType);
final View from = addWaypointBox();
addWaypointBox();
if (currentLoc_ == null)
from.requestFocus();
- } // RouteActivity
+ }
public void setDialog(final AlertDialog ad) {
ad_ = ad;
View button = ad_.getButton(AlertDialog.BUTTON_POSITIVE);
button.setOnClickListener(this);
findId_ = button.getId();
- } // setDialog
+ }
private void findRoute(final List places) {
for (final GeoPlace wp : places)
for (int i = 0; i != placeHolder_.getChildCount(); ++i) {
final PlaceViewWithCancel p = (PlaceViewWithCancel) placeHolder_.getChildAt(i);
p.addHistory(wp);
- } // for ...
+ }
final String routeType = routeType_.selectedType();
final int speed = CycleStreetsPreferences.speed();
Route.PlotRoute(routeType, speed, context_, asWaypoints(places));
ad_.dismiss();
- } // findRoute
+ }
private View addWaypointBox() {
final PlaceViewWithCancel pv = new PlaceViewWithCancel(context_);
@@ -129,7 +130,7 @@ else if (w + 1 == waypoints_.count())
label = FINISH_MARKER_LABEL;
pv.allowLocation(waypoints_.get(w), label);
- } // for ...
+ }
pv.setCancelOnClick(new OnRemove(pv));
@@ -139,12 +140,12 @@ else if (w + 1 == waypoints_.count())
enableRemoveButtons();
return pv;
- } // addWaypointBox
+ }
private void removeWaypointBox(final PlaceViewWithCancel pv) {
placeHolder_.removeView(pv);
enableRemoveButtons();
- } // removeWaypointBox
+ }
private void enableRemoveButtons() {
final boolean enable = placeHolder_.getChildCount() > 2;
@@ -152,17 +153,17 @@ private void enableRemoveButtons() {
for (int i = 0; i != placeHolder_.getChildCount(); ++i) {
final PlaceViewWithCancel p = (PlaceViewWithCancel) placeHolder_.getChildAt(i);
p.enableCancel(enable);
- } // for ...
+ }
addWaypoint_.setEnabled(placeHolder_.getChildCount() < 12);
- } // enableRemoveButtons
+ }
private Waypoints asWaypoints(final List places) {
- final Waypoints points = new Waypoints();
+ final List geoPoints = new ArrayList<>();
for (GeoPlace place : places)
- points.add(place.coord());
- return points;
- } // asWaypoints
+ geoPoints.add(place.coord());
+ return new Waypoints(geoPoints);
+ }
@Override
public void onClick(final View view) {
@@ -172,37 +173,34 @@ public void onClick(final View view) {
resolvePlaces();
if (R.id.addVia == viewId)
addWaypointBox();
- } // onClick
+ }
private void resolvePlaces() {
- resolveNextPlace(new ArrayList(), 0);
- } // resolvePlaces
+ resolveNextPlace(new ArrayList<>(), 0);
+ }
private void resolveNextPlace(final List resolvedPlaces, final int index) {
if (index != placeHolder_.getChildCount()) {
final PlaceViewWithCancel pv = (PlaceViewWithCancel) placeHolder_.getChildAt(index);
- pv.geoPlace(new PlaceViewWithCancel.OnResolveListener() {
- @Override
- public void onResolve(GeoPlace place) {
- resolvedPlaces.add(place);
- resolveNextPlace(resolvedPlaces, index + 1);
- }
+ pv.geoPlace(place -> {
+ resolvedPlaces.add(place);
+ resolveNextPlace(resolvedPlaces, index + 1);
});
} else
findRoute(resolvedPlaces);
- } // resolveNextPlace
+ }
private class OnRemove implements OnClickListener {
private final PlaceViewWithCancel pv_;
public OnRemove(final PlaceViewWithCancel pv) {
pv_ = pv;
- } // OnRemove
+ }
@Override
public void onClick(final View view) {
removeWaypointBox(pv_);
- } // onClick
- } // class OnRemove
- } // RouteByAddressCallbacks
-} // RouteByAddress
+ }
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByNumber.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByNumber.java
index 68cf47932..d51d28773 100644
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByNumber.java
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteByNumber.java
@@ -9,73 +9,74 @@
import android.app.AlertDialog;
import android.content.Context;
+import androidx.annotation.NonNull;
import android.view.View;
import android.widget.AutoCompleteTextView;
public class RouteByNumber {
- public static void launch(final Context context) {
- final AlertDialog.Builder builder = new AlertDialog.Builder(context);
- builder.setTitle(R.string.ic_menu_route_number);
+ public static void launch(@NonNull final Context context) {
+ final AlertDialog.Builder builder = new AlertDialog.Builder(context)
+ .setTitle(R.string.menu_route_by_number)
+ .setMessage(R.string.routenumber_desc);
final RouteByNumberCallbacks rbnc = new RouteByNumberCallbacks(context, builder);
final AlertDialog ad = builder.create();
ad.show();
- ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(context, android.R.style.TextAppearance_Large);
+ ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(android.R.style.TextAppearance_Large);
rbnc.setDialog(ad);
- } // launch
+ }
- private static class RouteByNumberCallbacks
- implements View.OnClickListener {
- private final Context context_;
- private final AutoCompleteTextView numberText_;
- private final RouteType routeType_;
- private final EditTextHistory history_;
- private AlertDialog ad_;
+ private static class RouteByNumberCallbacks implements View.OnClickListener {
+ private final Context context;
+ private final AutoCompleteTextView numberText;
+ private final RouteType routeType;
+ private final EditTextHistory history;
+ private AlertDialog ad;
- public RouteByNumberCallbacks(final Context context,
- final AlertDialog.Builder builder) {
- context_ = context;
+ private RouteByNumberCallbacks(final Context context,
+ final AlertDialog.Builder builder) {
+ this.context = context;
final View layout = View.inflate(context, R.layout.routenumber, null);
- builder.setView(layout);
+ builder
+ .setView(layout)
+ .setPositiveButton(R.string.load_route, MessageBox.NoAction);
- builder.setPositiveButton(R.string.go, MessageBox.NoAction);
+ numberText = layout.findViewById(R.id.routeNumber);
+ history = new EditTextHistory(context, "RouteNumber");
+ numberText.setAdapter(history);
- numberText_ = (AutoCompleteTextView)layout.findViewById(R.id.routeNumber);
- history_ = new EditTextHistory(context, "RouteNumber");
- numberText_.setAdapter(history_);
-
- routeType_ = (RouteType)layout.findViewById(R.id.routeType);
- } // RouteByNumberCallbacks
+ routeType = layout.findViewById(R.id.routeType);
+ }
private void findRoute(long routeNumber) {
- final String routeType = routeType_.selectedType();
+ final String routeType = this.routeType.selectedType();
final int speed = CycleStreetsPreferences.speed();
- Route.FetchRoute(routeType, routeNumber, speed, context_);
- } // findRoute
+ Route.FetchRoute(routeType, routeNumber, speed, context);
+ }
public void setDialog(final AlertDialog ad) {
- ad_ = ad;
- ad_.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
- } // setDialog
+ this.ad = ad;
+ this.ad.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener(this);
+ }
@Override
public void onClick(final View view) {
- final String entered = numberText_.getText().toString();
+ final String entered = numberText.getText().toString();
if (entered.length() == 0)
return;
try {
- history_.addHistory(entered);
+ history.addHistory(entered);
long number = Long.parseLong(entered);
findRoute(number);
- ad_.dismiss();
- } //try
+ ad.dismiss();
+ }
catch (final NumberFormatException e) {
// let's just swallow this, because hopefully it won't happen
- } // catch
- } // onClick
- } // class RouteByNumberCallbacks
-} // RouteByNumber
+ }
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapActivity.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapActivity.java
index ea00ae5a4..31c2161d8 100644
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapActivity.java
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapActivity.java
@@ -1,5 +1,5 @@
package net.cyclestreets;
public interface RouteMapActivity {
- void showMap();
-} // RouteMapActivity
+ void showRouteMap();
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapFragment.java
deleted file mode 100644
index 758a88991..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapFragment.java
+++ /dev/null
@@ -1,183 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.fragments.R;
-import net.cyclestreets.util.GPS;
-import net.cyclestreets.util.MessageBox;
-import net.cyclestreets.views.overlay.POIOverlay;
-import net.cyclestreets.views.overlay.RouteOverlay;
-import net.cyclestreets.views.overlay.RouteHighlightOverlay;
-import net.cyclestreets.views.overlay.TapToRouteOverlay;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Route;
-import net.cyclestreets.routing.Waypoints;
-
-import android.content.DialogInterface;
-import android.os.Bundle;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import static net.cyclestreets.util.MenuHelper.enableMenuItem;
-import static net.cyclestreets.util.MenuHelper.showMenuItem;
-
-public class RouteMapFragment extends CycleMapFragment
- implements Route.Listener
-{
- private TapToRouteOverlay routeSetter_;
- private POIOverlay poiOverlay_;
- private boolean hasGps_;
-
- @Override
- public View onCreateView(final LayoutInflater inflater, final ViewGroup container, final Bundle saved)
- {
- final View v = super.onCreateView(inflater, container, saved);
-
- overlayPushBottom(new RouteHighlightOverlay(getActivity(), mapView()));
-
- poiOverlay_ = new POIOverlay(getActivity(), mapView());
- overlayPushBottom(poiOverlay_);
-
- overlayPushBottom(new RouteOverlay(getActivity()));
-
- routeSetter_ = new TapToRouteOverlay(getActivity(), mapView());
- overlayPushTop(routeSetter_);
-
- hasGps_ = GPS.deviceHasGPS(getActivity());
-
- return v;
- } // onCreate
-
- @Override
- public void onResume()
- {
- super.onResume();
- Route.registerListener(this);
- Route.onResume();
- } // onResume
-
- @Override
- public void onPause()
- {
- Route.setWaypoints(routeSetter_.waypoints());
- Route.unregisterListener(this);
- super.onPause();
- } // onPause
-
- public void onRouteNow(int itinerary)
- {
- Route.FetchRoute(CycleStreetsPreferences.routeType(),
- itinerary,
- CycleStreetsPreferences.speed(),
- getActivity());
- } // onRouteNow
-
- @Override
- public void onCreateOptionsMenu(final Menu menu, final MenuInflater inflater)
- {
- inflater.inflate(R.menu.route_map, menu);
- super.onCreateOptionsMenu(menu, inflater);
- } // onCreateOptionsMenu
-
- @Override
- public void onPrepareOptionsMenu(final Menu menu)
- {
- showMenuItem(menu, R.id.ic_menu_liveride, Route.available() && hasGps_);
- enableMenuItem(menu, R.id.ic_menu_directions, true);
- showMenuItem(menu, R.id.ic_menu_saved_routes, Route.storedCount() != 0);
- enableMenuItem(menu, R.id.ic_menu_route_number, true);
- super.onPrepareOptionsMenu(menu);
- } // onPrepareOptionsMenu
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item)
- {
- if(super.onOptionsItemSelected(item))
- return true;
-
- final int menuId = item.getItemId();
- if(R.id.ic_menu_liveride == menuId) {
- startLiveRide();
- return true;
- }
- if(R.id.ic_menu_directions == menuId) {
- launchRouteDialog();
- return true;
- }
- if(R.id.ic_menu_saved_routes == menuId) {
- launchStoredRoutes();
- return true;
- }
- if(R.id.ic_menu_route_number == menuId) {
- launchFetchRouteDialog();
- return true;
- }
-
- return false;
- } // onMenuItemSelected
-
- private void startLiveRide()
- {
- LiveRideActivity.launch(getActivity());
- } // startLiveRide
-
- private void launchRouteDialog()
- {
- startNewRoute(new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface arg0, int arg1) {
- doLaunchRouteDialog();
- }
- });
- } // launchRouteDialog
-
- private void doLaunchRouteDialog() {
- RouteByAddress.launch(getActivity(),
- mapView().getBoundingBox(),
- mapView().getLastFix(),
- routeSetter_.waypoints());
- } // doLaunchRouteDialog
-
- private void launchFetchRouteDialog()
- {
- startNewRoute(new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface arg0, int arg1) {
- doLaunchFetchRouteDialog();
- }
- });
- } // launchFetchRouteDialog
-
- private void doLaunchFetchRouteDialog()
- {
- RouteByNumber.launch(getActivity());
- } // doLaunchFetchRouteDialog
-
- private void launchStoredRoutes() {
- StoredRoutes.launch(getActivity());
- } // launchStoredRoutes
-
- private void startNewRoute(final DialogInterface.OnClickListener listener)
- {
- if(Route.available() && CycleStreetsPreferences.confirmNewRoute())
- MessageBox.YesNo(mapView(),
- R.string.confirm_new_route,
- listener);
- else
- listener.onClick(null, 0);
- } // startNewRoute
-
- @Override
- public void onNewJourney(final Journey journey, final Waypoints waypoints)
- {
- if(!waypoints.isEmpty())
- mapView().getController().setCenter(waypoints.first());
- mapView().postInvalidate();
- } // onNewJourney
-
- @Override
- public void onResetJourney()
- {
- mapView().invalidate();
- } // onReset
-} // class MapActivity
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapFragment.kt
new file mode 100644
index 000000000..ec1e37f18
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/RouteMapFragment.kt
@@ -0,0 +1,200 @@
+package net.cyclestreets
+
+import net.cyclestreets.fragments.R
+import net.cyclestreets.iconics.IconicsHelper
+import net.cyclestreets.util.*
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Route
+import net.cyclestreets.routing.Waypoints
+
+import android.Manifest
+import android.app.Activity
+import android.content.DialogInterface
+import android.content.Intent
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+
+import net.cyclestreets.util.MenuHelper.enableMenuItem
+import net.cyclestreets.util.MenuHelper.showMenuItem
+import net.cyclestreets.views.overlay.*
+
+private val TAG = Logging.getTag(RouteMapFragment::class.java)
+
+class RouteMapFragment : CycleMapFragment(), Route.Listener {
+ private lateinit var routeSetter: TapToRouteOverlay
+ private var hasGps: Boolean = false
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setHasOptionsMenu(true)
+ super.onCreate(savedInstanceState)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, saved: Bundle?): View? {
+ val v = super.onCreateView(inflater, container, saved)
+
+ overlayPushBottom(RouteHighlightOverlay(requireContext(), mapView()))
+ overlayPushBottom(POIOverlay(mapView()))
+ overlayPushBottom(CircularRoutePOIOverlay(mapView()))
+ overlayPushBottom(RouteOverlay(mapView(),false))
+ // Alternative route overlay:
+ overlayPushBottom(RouteOverlay(mapView(),true))
+
+ routeSetter = TapToRouteOverlay(mapView(), this)
+ overlayPushTop(routeSetter)
+
+ hasGps = GPS.deviceHasGPS(requireContext())
+
+ return v
+ }
+
+ override fun onPause() {
+ Route.onPause(routeSetter.waypoints())
+ Route.unregisterListener(this)
+ super.onPause()
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Route.registerListener(this)
+ Route.onResume()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ IconicsHelper.inflate(inflater, R.menu.route_map, menu)
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ showMenuItem(menu, R.id.ic_menu_liveride, Route.routeAvailable() && hasGps)
+ enableMenuItem(menu, R.id.ic_menu_directions, true)
+ showMenuItem(menu, R.id.ic_menu_saved_routes, Route.storedCount() != 0)
+ enableMenuItem(menu, R.id.ic_menu_route_number, true)
+ super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (super.onOptionsItemSelected(item))
+ return true
+
+ when (item.itemId) {
+ R.id.ic_menu_liveride -> {
+ startLiveRide()
+ return true
+ }
+ R.id.ic_menu_directions -> {
+ launchRouteDialog()
+ return true
+ }
+ R.id.ic_menu_saved_routes -> {
+ launchStoredRoutes()
+ return true
+ }
+ R.id.ic_menu_route_number -> {
+ launchFetchRouteDialog()
+ return true
+ }
+ else -> return false
+ }
+
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ super.onActivityResult(requestCode, resultCode, data)
+ if ((requestCode == CIRCULAR_ROUTE_ACTIVITY_REQUEST_CODE) && (resultCode == Activity.RESULT_OK)) {
+ if (data != null) {
+ val distance = data.getIntExtra(EXTRA_CIRCULAR_ROUTE_DISTANCE, 0)
+ val duration = data.getIntExtra(EXTRA_CIRCULAR_ROUTE_DURATION, 0)
+ Route.plotCircularRoute(RoutePlans.PLAN_LEISURE,
+ if (distance != 0) distance else null,
+ if (duration != 0) duration else null,
+ data.getStringExtra(EXTRA_CIRCULAR_ROUTE_POI_CATEGORIES),
+ requireContext())
+ }
+ }
+ }
+
+ private fun startLiveRide() {
+ doOrRequestPermission(null, this, Manifest.permission.ACCESS_FINE_LOCATION, LIVERIDE_LOCATION_PERMISSION_REQUEST) {
+ LiveRideActivity.launch(requireContext())
+ }
+ }
+
+ private fun launchRouteDialog() {
+ startNewRoute(DialogInterface.OnClickListener { _, _ -> doLaunchRouteDialog() })
+ }
+
+ private fun doLaunchRouteDialog() {
+ RouteByAddress.launch(requireContext(),
+ mapView().boundingBox,
+ mapView().lastFix,
+ routeSetter.waypoints())
+ }
+
+ private fun launchFetchRouteDialog() {
+ startNewRoute(DialogInterface.OnClickListener { _, _ -> doLaunchFetchRouteDialog() })
+ }
+
+ private fun doLaunchFetchRouteDialog() {
+ RouteByNumber.launch(requireContext())
+ }
+
+ private fun launchStoredRoutes() {
+ StoredRoutes.launch(requireContext())
+ }
+
+ private fun startNewRoute(listener: DialogInterface.OnClickListener) {
+ if (Route.routeAvailable() && CycleStreetsPreferences.confirmNewRoute())
+ MessageBox.YesNo(mapView(), R.string.confirm_new_route, listener)
+ else
+ listener.onClick(null, 0)
+ }
+
+ override fun onNewJourney(journey: Journey, waypoints: Waypoints) {
+ if (!waypoints.isEmpty()) {
+ Log.d(TAG, "Setting map centre to " + waypoints.first()!!)
+ mapView().controller.setCenter(waypoints.first())
+ }
+ mapView().postInvalidate()
+ }
+
+ override fun onResetJourney() {
+ mapView().invalidate()
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ Log.d(TAG, "Permission ${permissions.joinToString()} was ${if (grantResults.joinToString().equals("0")) "granted" else "denied"}")
+
+ for (i in permissions.indices) {
+ val permission = permissions[i]
+ val grantResult = grantResults[i]
+
+ if (permission == Manifest.permission.ACCESS_FINE_LOCATION) {
+ if (requestCode == LIVERIDE_LOCATION_PERMISSION_REQUEST) {
+ requestPermissionsResultAction(grantResult, permission) {
+ LiveRideActivity.launch(requireContext())
+ }
+ }
+ else if (requestCode == FOLLOW_LOCATION_PERMISSION_REQUEST) {
+ // Sequence of events is: onPause / (Android) permissions box / onRequestPermissionsResult / mainNavDrawerActivity.onResume
+ // After enabling location, need to save values, as mainNavDrawerActivity.onResume will subsequently be called
+ // and Fragments/Overlays will be re-initialised, so these values will be lost otherwise
+ requestPermissionsResultAction(grantResult, permission) {
+ mapView().doEnableFollowLocation()
+ mapView().saveLocationPrefs()
+ }
+ }
+ return
+ }
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsActivity.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsActivity.java
deleted file mode 100644
index fdd2aa605..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsActivity.java
+++ /dev/null
@@ -1,147 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.fragments.R;
-import android.content.DialogInterface;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.preference.EditTextPreference;
-import android.preference.ListPreference;
-import android.preference.Preference;
-import android.preference.PreferenceActivity;
-import android.preference.PreferenceScreen;
-
-import net.cyclestreets.tiles.TileSource;
-import net.cyclestreets.util.MapPack;
-import net.cyclestreets.util.MessageBox;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class SettingsActivity extends PreferenceActivity
- implements SharedPreferences.OnSharedPreferenceChangeListener
-
-{
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- addPreferencesFromResource(R.xml.prefs);
-
- setupMapStyles();
- setupMapFileList();
-
- setSummary(CycleStreetsPreferences.PREF_ROUTE_TYPE_KEY);
- setSummary(CycleStreetsPreferences.PREF_UNITS_KEY);
- setSummary(CycleStreetsPreferences.PREF_SPEED_KEY);
- setSummary(CycleStreetsPreferences.PREF_MAPSTYLE_KEY);
- setSummary(CycleStreetsPreferences.PREF_MAPFILE_KEY);
- setSummary(CycleStreetsPreferences.PREF_UPLOAD_SIZE);
- setSummary(CycleStreetsPreferences.PREF_NEARING_TURN);
- setSummary(CycleStreetsPreferences.PREF_OFFTRACK_DISTANCE);
- setSummary(CycleStreetsPreferences.PREF_REPLAN_DISTANCE);
- } // onCreate
-
- private void setupMapStyles() {
- final ListPreference mapstylePref= (ListPreference)findPreference(CycleStreetsPreferences.PREF_MAPSTYLE_KEY);
- if (mapstylePref == null)
- return;
- TileSource.configurePreference(mapstylePref);
- } // setupMapStyles
-
- private void setupMapFileList() {
- final ListPreference mapfilePref= (ListPreference)findPreference(CycleStreetsPreferences.PREF_MAPFILE_KEY);
- if (mapfilePref == null)
- return;
- populateMapFileList(mapfilePref);
- } // setupMapFileList
-
- private void populateMapFileList(final ListPreference mapfilePref) {
- final List names = new ArrayList<>();
- final List files = new ArrayList<>();
-
- for(final MapPack pack : MapPack.availableMapPacks()) {
- names.add(pack.name());
- files.add(pack.path());
- } // for
-
- mapfilePref.setEntries(names.toArray(new String[] { }));
- mapfilePref.setEntryValues(files.toArray(new String[] { }));
- } // populateMapFileList
-
- @Override
- protected void onResume() {
- super.onResume();
-
- getPreferenceScreen().getSharedPreferences().registerOnSharedPreferenceChangeListener(this);
-
- setAccountSummary();
- } // onResume
-
- @Override
- protected void onPause() {
- super.onPause();
-
- // stop listening while paused
- getPreferenceScreen().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this);
- } // onPause
-
- public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
- setSummary(key);
- } // onSharedPreferencesChanged
-
- private void setSummary(final String key) {
- final Preference prefUI = findPreference(key);
- if (prefUI instanceof ListPreference)
- prefUI.setSummary(((ListPreference)prefUI).getEntry());
- if (prefUI instanceof EditTextPreference)
- prefUI.setSummary(((EditTextPreference)prefUI).getText());
-
- if(CycleStreetsPreferences.PREF_MAPSTYLE_KEY.equals(key))
- setMapFileSummary(((ListPreference)prefUI).getValue());
- } // setSummary
-
- private void setMapFileSummary(final String style) {
- final ListPreference mapfilePref= (ListPreference)findPreference(CycleStreetsPreferences.PREF_MAPFILE_KEY);
- if (mapfilePref == null)
- return;
-
- final boolean enabled = style.equals(CycleStreetsPreferences.MAPSTYLE_MAPSFORGE);
- mapfilePref.setEnabled(enabled);
-
- if(!enabled)
- return;
-
- if(mapfilePref.getEntryValues().length == 0) {
- mapfilePref.setEnabled(false);
- MessageBox.YesNo(getListView(),
- R.string.no_map_packs,
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface arg0, int arg1) {
- MapPack.searchGooglePlay(SettingsActivity.this);
- } // onClick
- });
- return;
- } // if ...
-
- final String mapfile = CycleStreetsPreferences.mapfile();
- int index = mapfilePref.findIndexOfValue(mapfile);
- if(index == -1)
- index = 0; // default to something
-
- mapfilePref.setValueIndex(index);
- mapfilePref.setSummary(mapfilePref.getEntries()[index]);
- } // setMapFileSummary
-
- private void setAccountSummary() {
- final PreferenceScreen account = (PreferenceScreen)findPreference(CycleStreetsPreferences.PREF_ACCOUNT_KEY);
- if (account == null)
- return;
-
- if(CycleStreetsPreferences.accountOK())
- account.setSummary(R.string.settings_signed_in);
- else if(CycleStreetsPreferences.accountPending())
- account.setSummary(R.string.settings_awaiting);
- else
- account.setSummary("");
- } // setAccountSummary
-} // class SettingsActivity
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsFragment.kt
new file mode 100644
index 000000000..24413c0e4
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/SettingsFragment.kt
@@ -0,0 +1,158 @@
+package net.cyclestreets
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import androidx.transition.Fade
+import androidx.transition.Slide
+import androidx.preference.*
+
+import android.util.Log
+import android.view.Gravity
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import net.cyclestreets.fragments.R
+import net.cyclestreets.iconics.IconicsHelper.materialIcon
+import net.cyclestreets.tiles.TileSource
+import net.cyclestreets.util.Logging
+import net.cyclestreets.util.MapPack
+import net.cyclestreets.util.MessageBox
+
+
+private val TAG = Logging.getTag(SettingsFragment::class.java)
+private const val PREFERENCE_SCREEN_ARG: String = "preferenceScreenArg"
+private val SETTINGS_ICONS = mapOf(
+ "screen-maps-display" to GoogleMaterial.Icon.gmd_map,
+ "mapstyle" to null,
+ "confirm-new-route" to null,
+ "screen-routing-preferences" to GoogleMaterial.Icon.gmd_directions,
+ "routetype" to null,
+ "speed" to null,
+ "units" to null,
+ "screen-liveride" to GoogleMaterial.Icon.gmd_navigation,
+ "nearing-turn-distance" to null,
+ "offtrack-distance" to null,
+ "replan-distance" to null,
+ "screen-locations" to GoogleMaterial.Icon.gmd_edit_location,
+ "screen-account" to GoogleMaterial.Icon.gmd_account_circle,
+ "cyclestreets-account" to null,
+ "username" to null,
+ "password" to null,
+ "uploadsize" to null,
+ "screen-about" to GoogleMaterial.Icon.gmd_info_outline
+)
+
+
+class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener, Undoable {
+
+ private var undoable = false
+
+ override fun onCreate(savedInstance: Bundle?) {
+ super.onCreate(savedInstance)
+
+ setupMapStyles()
+
+ setSummary(CycleStreetsPreferences.PREF_ROUTE_TYPE_KEY)
+ setSummary(CycleStreetsPreferences.PREF_UNITS_KEY)
+ setSummary(CycleStreetsPreferences.PREF_SPEED_KEY)
+ setSummary(CycleStreetsPreferences.PREF_MAPSTYLE_KEY)
+ setSummary(CycleStreetsPreferences.PREF_UPLOAD_SIZE)
+ setSummary(CycleStreetsPreferences.PREF_NEARING_TURN)
+ setSummary(CycleStreetsPreferences.PREF_OFFTRACK_DISTANCE)
+ setSummary(CycleStreetsPreferences.PREF_REPLAN_DISTANCE)
+ }
+
+ override fun onCreatePreferences(savedInstance: Bundle?, rootKey: String?) {
+ if (arguments != null) {
+ val key = requireArguments().getString(PREFERENCE_SCREEN_ARG)
+ Log.d(TAG, "Creating preferences subscreen with key $key")
+ setPreferencesFromResource(R.xml.prefs, key)
+ undoable = true
+ this.enterTransition = Slide(Gravity.END)
+ this.exitTransition = Slide(Gravity.END)
+ } else {
+ Log.d(TAG, "Creating root preferences page")
+ setPreferencesFromResource(R.xml.prefs, rootKey)
+ this.enterTransition = Fade()
+ this.exitTransition = Fade()
+ }
+ populateSettingsIcons();
+ }
+
+ private fun populateSettingsIcons() {
+ for (prefIndex in 0 until preferenceScreen.preferenceCount) {
+ val pref = preferenceScreen.getPreference(prefIndex)
+ SETTINGS_ICONS[pref.key]?.let {
+ pref.icon = materialIcon(requireContext(), it)
+ }
+ }
+ }
+
+ override fun onNavigateToScreen(preferenceScreen: PreferenceScreen) {
+ showScreen(preferenceScreen.key)
+ }
+
+ override fun onBackPressed(): Boolean {
+ if (undoable)
+ showScreen()
+ return undoable
+ }
+
+ private fun showScreen(key: String? = null) {
+ val screen = SettingsFragment()
+
+ if (key != null) {
+ val args = Bundle()
+ args.putString(PREFERENCE_SCREEN_ARG, key)
+ screen.arguments = args
+ }
+
+ parentFragmentManager
+ .beginTransaction()
+ .replace(id, screen)
+ .commit()
+
+ }
+
+ private fun setupMapStyles() {
+ findPreference(CycleStreetsPreferences.PREF_MAPSTYLE_KEY)?.apply {
+ TileSource.configurePreference(this)
+ }
+ }
+
+ override fun onResume() {
+ super.onResume()
+
+ preferenceScreen.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
+
+ setAccountSummary()
+ }
+
+ override fun onPause() {
+ super.onPause()
+
+ // stop listening while paused
+ preferenceScreen.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
+ }
+
+ override fun onSharedPreferenceChanged(prefs: SharedPreferences, key: String?) {
+ if (key != null)
+ setSummary(key)
+ }
+
+ private fun setSummary(key: String) {
+ val prefUI = findPreference(key) ?: return
+ if (prefUI is ListPreference)
+ prefUI.summary = prefUI.entry
+ if (prefUI is EditTextPreference)
+ prefUI.summary = prefUI.text
+ }
+
+ private fun setAccountSummary() {
+ val pref = findPreference(CycleStreetsPreferences.PREF_ACCOUNT_KEY) ?: return
+ val account = pref as PreferenceScreen
+
+ when {CycleStreetsPreferences.accountOK() -> account.setSummary(R.string.settings_signed_in)
+ CycleStreetsPreferences.accountPending() -> account.setSummary(R.string.settings_awaiting)
+ else -> account.summary = ""
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/StoredRoutes.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/StoredRoutes.java
index cf03bcd70..6a25981a0 100644
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/StoredRoutes.java
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/StoredRoutes.java
@@ -2,6 +2,7 @@
import android.app.AlertDialog;
import android.content.Context;
+import androidx.annotation.NonNull;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.MenuItem;
@@ -24,16 +25,16 @@
import static net.cyclestreets.util.StringUtils.initCap;
public class StoredRoutes {
- public static void launch(final Context context) {
+ public static void launch(@NonNull final Context context) {
RouteSummaryAdapter rsa = new RouteSummaryAdapter(context);
AlertDialog ad = Dialog.listViewDialog(context,
- R.string.ic_menu_saved_routes,
+ R.string.menu_saved_routes,
rsa,
null,
null);
- ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(context, android.R.style.TextAppearance_Large);
+ ad.getButton(AlertDialog.BUTTON_POSITIVE).setTextAppearance(android.R.style.TextAppearance_Large);
rsa.setDialog(ad);
- } // launch
+ }
//////////////////////////////////
private static class RouteSummaryAdapter extends BaseAdapter
@@ -51,7 +52,7 @@ private static class RouteSummaryAdapter extends BaseAdapter
inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
routes_ = Route.storedRoutes();
viewRoute_ = new HashMap<>();
- } // SegmentAdaptor
+ }
public void setDialog(final AlertDialog ad) { ad_ = ad; }
@@ -60,19 +61,19 @@ private void refresh() {
notifyDataSetChanged();
if (routes_.size() == 0)
closeDialog();
- } // refresh
+ }
private void closeDialog() {
if (ad_ != null)
ad_.cancel();
- } // closeDialog
+ }
public RouteSummary getRouteSummary(int localId) {
- for(final RouteSummary r : routes_)
- if(r.localId() == localId)
+ for (final RouteSummary r : routes_)
+ if (r.localId() == localId)
return r;
return null;
- } // getRouteSummary
+ }
@Override
public int getCount() { return routes_.size(); }
@@ -97,27 +98,27 @@ public View getView(final int position, final View convertView, final ViewGroup
final String plan = initCap(summary.plan());
titleView.setText(summary.title());
- detailView.setText(plan + " route, " +
- Segment.formatter.total_distance(summary.distance()));
+ detailView.setText(context_.getString(R.string.storedroutes_detail_format, plan,
+ Segment.formatter.totalDistance(summary.distance())));
view.setOnClickListener(this);
view.setOnLongClickListener(this);
view.setOnCreateContextMenuListener(this);
return view;
- } // getView
+ }
@Override
public void onClick(final View view) {
final int localId = viewRoute_.get(view);
openRoute(localId);
- } // onClick
+ }
@Override
public boolean onLongClick(final View view) {
view.showContextMenu();
return true;
- } // onClick
+ }
@Override
public void onCreateContextMenu(final ContextMenu menu,
@@ -127,34 +128,33 @@ public void onCreateContextMenu(final ContextMenu menu,
public boolean onMenuItemClick(final MenuItem item) {
RouteSummaryAdapter.this.onViewMenuClick(view, item);
return true;
- } // onMenuItemClick
+ }
};
createMenuItem(menu, R.string.ic_menu_open).setOnMenuItemClickListener(listener);
createMenuItem(menu, R.string.ic_menu_rename).setOnMenuItemClickListener(listener);
createMenuItem(menu, R.string.ic_menu_delete).setOnMenuItemClickListener(listener);
- } // onCreateContextMenu
+ }
private void onViewMenuClick(final View view, final MenuItem item) {
final int localId = viewRoute_.get(view);
final int menuId = item.getItemId();
- if(R.string.ic_menu_open == menuId)
+ if (R.string.ic_menu_open == menuId)
openRoute(localId);
- if(R.string.ic_menu_rename == menuId)
+ if (R.string.ic_menu_rename == menuId)
renameRoute(localId);
- if(R.string.ic_menu_delete == menuId)
+ if (R.string.ic_menu_delete == menuId)
deleteRoute(localId);
- } // onMenuItemClick
+ }
//////////////////////////////////////////////
/////////////////////////////////////////////
private void openRoute(final int localId) {
Route.PlotStoredRoute(localId, context_);
closeDialog();
- } // routeSelected
+ }
- private void renameRoute(final int localId)
- {
+ private void renameRoute(final int localId) {
final RouteSummary route = getRouteSummary(localId);
Dialog.editTextDialog(context_, route.title(), "Rename",
new Dialog.UpdatedTextListener() {
@@ -162,15 +162,15 @@ private void renameRoute(final int localId)
public void updatedText(final String updated) {
Route.RenameRoute(localId, updated);
refresh();
- } // updatedText
+ }
});
- } // renameRoute
+ }
private void deleteRoute(final int localId) {
Route.DeleteRoute(localId);
refresh();
- } // deleteRoute
- } // class RouteSummaryAdaptor
+ }
+ }
private StoredRoutes() { }
-} // StoredRoutes
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/WebPageFragment.java b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/WebPageFragment.java
deleted file mode 100644
index d74cba363..000000000
--- a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/WebPageFragment.java
+++ /dev/null
@@ -1,112 +0,0 @@
-package net.cyclestreets;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.content.Intent;
-import android.net.Uri;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.webkit.WebView;
-import android.webkit.WebViewClient;
-
-import net.cyclestreets.fragments.R;
-
-@SuppressLint("ValidFragment")
-public class WebPageFragment extends Fragment {
- public static MainNavDrawerActivity.PageInitialiser initialiser(final String url) {
- return initialiser(url, null);
- } // initialiser
- public static MainNavDrawerActivity.PageInitialiser initialiser(final String url, final String postLoadJs) {
- return new WebPageInitialiser(url, postLoadJs);
- } // initialiser
-
- private static class WebPageInitialiser implements MainNavDrawerActivity.PageInitialiser {
- private final String url_;
- private final String customiser_;
- public WebPageInitialiser(final String url) { this(url, null); }
- public WebPageInitialiser(final String url, final String customiser) {
- url_ = url;
- customiser_ = customiser;
- } // WebPageInitialiser
-
- public void initialise(Fragment page) {
- ((WebPageFragment)page).loadUrl(url_, customiser_);
- } // initialise
- } // WebPageInitialiser
-
- //////////////////////////////////////////////////
- private String homePage_;
- private String postLoadJs_;
- private int layout_ = R.layout.webpage;
-
- public WebPageFragment() {
- homePage_ = null;
- } // WebPageFragment
-
- protected WebPageFragment(final String url) {
- homePage_ = url;
- } // WebPageFragment
-
- protected WebPageFragment(final String url,
- final int layout) {
- this(url);
- layout_ = layout;
- } // WebPageFragment
-
- public View onCreateView(final LayoutInflater inflater,
- final ViewGroup container,
- final Bundle saved) {
- final View webPage = inflater.inflate(layout_, null);
-
- final WebView htmlView = (WebView)webPage.findViewById(R.id.html_view);
- if (homePage_ != null) {
- htmlView.setWebViewClient(new FragmentViewClient(getActivity(), homePage_, postLoadJs_));
- htmlView.getSettings().setJavaScriptEnabled(true);
- htmlView.loadUrl(homePage_);
- } // if ...
-
- return webPage;
- } // onCreateView
-
- public void loadUrl(final String url,
- final String customiser) {
- homePage_ = url;
- postLoadJs_ = customiser;
- } // loadUrl
-
- private static class FragmentViewClient extends WebViewClient {
- private final Context context_;
- private String homePage_;
- private String postLoadJs_;
-
- public FragmentViewClient(final Context context,
- final String homePage,
- final String postLoadJs) {
- context_ = context;
- homePage_ = homePage;
- postLoadJs_ = postLoadJs;
- } // FragmentViewClient
-
- @Override
- public boolean shouldOverrideUrlLoading(WebView view, String url) {
- if (url.equals(homePage_))
- return false;
-
- // Otherwise, give the default behavior (open in browser)
- Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
- context_.startActivity(intent);
- return true;
- }
-
- @Override
- public void onPageFinished(WebView view, String url) {
- if (url.equals(homePage_) && postLoadJs_ != null)
- view.loadUrl("javascript:(function() { " +
- postLoadJs_ +
- "})()");
- } // onPageFinished
- }
-} // WebPageFragment
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/WebPageFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/WebPageFragment.kt
new file mode 100644
index 000000000..a5d3ebbeb
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/WebPageFragment.kt
@@ -0,0 +1,64 @@
+package net.cyclestreets
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.webkit.WebResourceRequest
+import android.webkit.WebView
+import android.webkit.WebViewClient
+
+import net.cyclestreets.fragments.R
+
+@SuppressLint("ValidFragment")
+open class WebPageFragment : Fragment {
+ private val homePage: String
+ private val layout: Int
+
+ protected constructor(url: String) {
+ homePage = url
+ this.layout = R.layout.webpage
+ }
+
+ protected constructor(url: String, layout: Int) {
+ homePage = url
+ this.layout = layout
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ val webPage = inflater.inflate(layout, null)
+ val htmlView = webPage.findViewById(R.id.html_view)
+
+ htmlView.webViewClient = FragmentViewClient(requireContext(), homePage)
+ htmlView.settings.javaScriptEnabled = true
+ htmlView.loadUrl(homePage)
+
+ return webPage
+ }
+
+ private class FragmentViewClient(private val context: Context, private val homePage: String) : WebViewClient() {
+ override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
+ return shouldOverrideUrlLoading(request.url.toString())
+ }
+
+ @Deprecated("")
+ override fun shouldOverrideUrlLoading(view: WebView, url: String): Boolean {
+ return shouldOverrideUrlLoading(url)
+ }
+
+ private fun shouldOverrideUrlLoading(url: String): Boolean {
+ if (url == homePage)
+ return false
+
+ // Otherwise, give the default behavior (open in browser)
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ context.startActivity(intent)
+ return true
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/AddPhotoFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/AddPhotoFragment.kt
new file mode 100644
index 000000000..d25b7df87
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/AddPhotoFragment.kt
@@ -0,0 +1,741 @@
+package net.cyclestreets.addphoto
+
+import android.Manifest.permission.ACCESS_FINE_LOCATION
+import android.Manifest.permission.READ_EXTERNAL_STORAGE
+import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.SharedPreferences
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.Point
+import android.net.Uri
+import android.os.AsyncTask
+import android.os.Build
+import android.os.Bundle
+import android.provider.MediaStore
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.view.inputmethod.InputMethodManager
+import android.widget.Button
+import android.widget.EditText
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.RelativeLayout
+import android.widget.Spinner
+import android.widget.TextView
+import android.widget.Toast
+import androidx.activity.result.ActivityResultLauncher
+import androidx.activity.result.PickVisualMediaRequest
+import androidx.activity.result.contract.ActivityResultContracts.PickVisualMedia
+import androidx.core.content.FileProvider
+import androidx.exifinterface.media.ExifInterface
+import androidx.fragment.app.Fragment
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import net.cyclestreets.AccountDetailsActivity
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.Undoable
+import net.cyclestreets.api.PhotomapCategories
+import net.cyclestreets.api.Upload
+import net.cyclestreets.fragments.R
+import net.cyclestreets.iconics.IconicsHelper.materialIcons
+import net.cyclestreets.util.AsyncDelete
+import net.cyclestreets.util.Bitmaps
+import net.cyclestreets.util.Dialog
+import net.cyclestreets.util.Logging
+import net.cyclestreets.util.MenuHelper.createMenuItem
+import net.cyclestreets.util.MenuHelper.enableMenuItem
+import net.cyclestreets.util.MessageBox
+import net.cyclestreets.util.ProgressDialog
+import net.cyclestreets.util.Share
+import net.cyclestreets.util.doOrRequestPermission
+import net.cyclestreets.util.hasPermission
+import net.cyclestreets.util.requestPermissionsResultAction
+import net.cyclestreets.views.CycleMapView
+import net.cyclestreets.views.overlay.ThereOverlay
+import org.osmdroid.api.IGeoPoint
+import org.osmdroid.util.GeoPoint
+import java.io.File
+import java.util.Date
+import androidx.core.net.toUri
+
+
+internal val TAG = Logging.getTag(AddPhotoFragment::class.java)
+
+
+class AddPhotoFragment : Fragment(), View.OnClickListener, Undoable, ThereOverlay.LocationListener {
+ // Android classes
+ private lateinit var inflater: LayoutInflater
+ private lateinit var inputMethodManager: InputMethodManager
+
+ // Package configuration
+ private var allowUploadByKey: Boolean = false
+ private var allowTextOnly: Boolean = false
+ private var noShare: Boolean = false
+
+ // Views for each step in the Add Photo process
+ private lateinit var photoRoot: LinearLayout
+ private lateinit var photo1Start: View
+ private var photo2Caption: View? = null
+ private lateinit var photo3Category: View
+ private lateinit var photo4Location: View
+ private lateinit var photo5View: View
+
+ // Location view/overlay
+ private var map: CycleMapView? = null
+ private lateinit var there: ThereOverlay
+
+ // State
+ private var step: AddStep = AddStep.START
+ private var photoUri: Uri? = null
+ private var photo: Bitmap? = null
+ private var dateTime: String? = ""
+ private lateinit var caption: String
+ private var metaCatId: Int = -1
+ private var catId: Int = -1
+ private var geolocated: Boolean = false
+ private var uploadedUrl: String? = null
+
+ private lateinit var pickMedia: ActivityResultLauncher
+
+ companion object {
+ private var photomapCategories: PhotomapCategories? = null
+ }
+
+ ///////////// Fragment methods - views
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setHasOptionsMenu(true)
+
+ pickMedia = registerForActivityResult(PickVisualMedia()) {
+ uri -> photoPicked(uri)
+ }
+
+
+ super.onCreate(savedInstanceState)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ this.inflater = LayoutInflater.from(activity)
+ inputMethodManager = requireActivity().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
+
+ initialiseFromMetadata()
+ initialiseViews(this.inflater)
+ caption = ""
+
+ there = ThereOverlay(activity)
+ there.setLocationListener(this)
+
+ setupView()
+
+ return photoRoot
+ }
+
+ private fun initialiseFromMetadata() {
+ val metaData = photoUploadMetaData(activity)
+ allowUploadByKey = metaData.contains("ByKey")
+ allowTextOnly = metaData.contains("AllowTextOnly")
+ noShare = metaData.contains("NoShare")
+ }
+
+ private fun initialiseViews(inflater: LayoutInflater) {
+
+ val (restartIcon, prevIcon, nextIcon, uploadIcon, shareIcon) = materialIcons(inflater.context!!,
+ iconIds = listOf(GoogleMaterial.Icon.gmd_replay, GoogleMaterial.Icon.gmd_fast_rewind,
+ GoogleMaterial.Icon.gmd_fast_forward, GoogleMaterial.Icon.gmd_file_upload,
+ GoogleMaterial.Icon.gmd_share))
+
+ photoRoot = inflater.inflate(R.layout.addphoto_root, null) as LinearLayout
+
+ photo1Start = inflater.inflate(R.layout.addphoto_1_start, null)
+ photo1Start.findViewById(R.id.takephoto_button).apply {
+ setOnClickListener(this@AddPhotoFragment)
+ isEnabled = requireActivity().packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)
+ }
+ photo1Start.findViewById(R.id.chooseexisting_button).setOnClickListener(this)
+ photo1Start.findViewById(R.id.textonly_button).apply {
+ setOnClickListener(this@AddPhotoFragment)
+ if (!allowTextOnly) visibility = View.GONE
+ }
+
+ // photo2CaptionView is recreated each time
+
+ photo3Category = inflater.inflate(R.layout.addphoto_3_category, null)
+ backNextButtons(photo3Category,
+ getString(R.string.all_button_back), prevIcon,
+ getString(R.string.all_button_next), nextIcon)
+ if (photomapCategories == null)
+ GetPhotomapCategoriesTask().execute()
+ else
+ setupSpinners()
+
+ photo4Location = inflater.inflate(R.layout.addphoto_4_location, null)
+ backNextButtons(photo4Location,
+ getString(R.string.all_button_back), prevIcon,
+ "Upload!", uploadIcon)
+
+ photo5View = inflater.inflate(R.layout.addphoto_5_view, null)
+ backNextButtons(photo5View,
+ "Upload another", restartIcon,
+ "", restartIcon) // icon irrelevant, we disable this anyway
+ photo5View.findViewById(R.id.next).apply {
+ isEnabled = false
+ visibility = View.GONE
+ }
+ photo5View.findViewById(R.id.photo_share).apply {
+ setCompoundDrawables(null, null, shareIcon, null)
+ }
+ }
+
+ private fun setupMap() {
+ val v = photo4Location.findViewById(R.id.mapholder) as RelativeLayout
+
+ if (map != null) {
+ map!!.onPause()
+ (map!!.parent as RelativeLayout).removeView(map)
+ } else {
+ map = CycleMapView(activity, this.javaClass.name, this)
+ map!!.overlayPushTop(there)
+ }
+
+ map!!.apply {
+ v.addView(this, RelativeLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT))
+ if (hasPermission(context, ACCESS_FINE_LOCATION)) {
+ enableAndFollowLocation()
+ }
+ onResume()
+ there.setMapView(this)
+ }
+ }
+
+ private fun setupView() {
+ when (step) {
+ AddStep.START -> {
+ metaCategorySpinner().setSelection(0)
+ categorySpinner().setSelection(0)
+ caption = ""
+ geolocated = false
+ there.noOverThere(null)
+ setContentView(photo1Start)
+ }
+ AddStep.CAPTION -> {
+ // why recreate this view each time - well *sigh* because we have to force the
+ // keyboard to hide, if we don't recreate the view afresh, Android won't redisplay
+ // the keyboard if we come back to this view
+ val (prevIcon, nextIcon) = materialIcons(inflater.context!!, listOf(GoogleMaterial.Icon.gmd_fast_rewind, GoogleMaterial.Icon.gmd_fast_forward))
+ photo2Caption = inflater.inflate(R.layout.addphoto_2_caption, null).apply {
+ backNextButtons(this,
+ getString(R.string.all_button_back), prevIcon,
+ getString(R.string.all_button_next), nextIcon)
+ setContentView(this)
+ }
+ captionEditor().setText(caption)
+ if (photo == null && allowTextOnly) {
+ (photoRoot.findViewById(R.id.label) as TextView).setText(R.string.report_title)
+ (photoRoot.findViewById(R.id.caption) as EditText).setLines(10)
+ }
+ }
+ AddStep.CATEGORY -> {
+ caption = captionText()
+ store()
+ setContentView(photo3Category)
+ }
+ AddStep.LOCATION -> {
+ metaCatId = metaCategoryId()
+ catId = categoryId()
+ setupMap()
+ setContentView(photo4Location)
+ there.recentre()
+ if (photo == null && allowTextOnly) {
+ (photoRoot.findViewById(R.id.label) as TextView).setText(R.string.report_location_hint)
+ (photoRoot.findViewById(R.id.nogeo) as View).visibility = View.GONE
+ } else {
+ (photoRoot.findViewById(R.id.label) as TextView).setText(R.string.photo_location_hint)
+ (photoRoot.findViewById(R.id.nogeo) as View).visibility = if (geolocated) View.GONE else View.VISIBLE
+ }
+ }
+ AddStep.VIEW -> {
+ setContentView(photo5View)
+ (photo5View.findViewById(R.id.photo_text) as TextView).text = caption
+ val url = photo5View.findViewById(R.id.photo_url) as TextView
+ val share = photo5View.findViewById(R.id.photo_share) as Button
+ if (noShare) {
+ url.visibility = View.GONE
+ share.visibility = View.GONE
+ } else {
+ url.text = uploadedUrl
+ share.setOnClickListener(this)
+ }
+ }
+ AddStep.DONE -> {
+ step = AddStep.START
+ setupView()
+ }
+ }
+
+ previewPhoto()
+ hookUpNext()
+ }
+
+ private fun setContentView(child: View) {
+ photoRoot.removeAllViewsInLayout()
+ photoRoot.addView(child)
+ }
+
+ private fun hookUpNext() {
+ (photoRoot.findViewById(R.id.back) as Button?)?.setOnClickListener(this)
+ (photoRoot.findViewById(R.id.next) as Button?)?.apply {
+ setOnClickListener(this@AddPhotoFragment)
+ if (step === AddStep.LOCATION)
+ isEnabled = there.there() != null
+ }
+ }
+
+ private fun previewPhoto() {
+ val iv = photoRoot.findViewById(R.id.photo) as ImageView? ?: return
+ if (photo == null && allowTextOnly) {
+ iv.visibility = View.GONE
+ return
+ }
+
+ // TODO: scaling?
+ iv.setImageBitmap(photo)
+ val size = Point()
+ requireActivity().windowManager.defaultDisplay.getSize(size)
+ val newHeight = size.y / 10 * 4
+ val newWidth = size.x
+
+ iv.layoutParams = LinearLayout.LayoutParams(newWidth, newHeight)
+ iv.scaleType = ImageView.ScaleType.CENTER_INSIDE
+ }
+
+ ///////////// Fragment methods - options menus
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ // No icons for these are ever shown, so don't bother setting them
+ createMenuItem(menu, R.string.all_menu_restart, Menu.NONE, null)
+ createMenuItem(menu, R.string.all_menu_back, Menu.NONE, null)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ enableMenuItem(menu, R.string.all_menu_restart, step !== AddStep.START)
+ enableMenuItem(menu, R.string.all_menu_back, step !== AddStep.START && step !== AddStep.VIEW)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+
+ return when (item.itemId) {
+ R.string.all_menu_restart -> {
+ step = AddStep.START
+ setupView()
+ true
+ }
+ R.string.all_menu_back -> {
+ onBackPressed()
+ true
+ }
+ else -> false
+ }
+ }
+
+ ///////////// Fragment methods - Activity result processing
+ @Deprecated("Deprecated in Java")
+ override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
+ if (resultCode != Activity.RESULT_OK)
+ return
+ if (requestCode == TAKE_PHOTO)
+ photoPicked(photoUri)
+ }
+
+ private fun launchPhotoPicker() {
+ try {
+ // Launching the photo picker (photos & video included)
+ pickMedia.launch(
+ PickVisualMediaRequest(PickVisualMedia.ImageOnly)
+ )
+ } catch (e: Exception) {
+ Toast.makeText(activity, "There was a problem launching the photo picker : " + e.message, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ private fun photoPicked(uri: Uri?) {
+ if (uri == null)
+ return
+
+ try {
+ photoUri = uri
+
+ photo?.recycle()
+ photo = Bitmaps.LoadUri(context, photoUri)
+
+ val photoStream = context?.contentResolver?.openInputStream(photoUri!!)
+ val exif = ExifInterface(photoStream!!)
+ dateTime = photoTimestamp(exif)
+ val photoLoc = photoLocation(exif)
+ geolocated = photoLoc != null
+ there.noOverThere(photoLoc)
+ nextStep()
+ } catch (e: Exception) {
+ Toast.makeText(activity, "There was a problem grabbing the photo : " + e.message, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ ///////////// Fragment methods - State store / retrieval
+ override fun onPause() {
+ prefs().edit().apply {
+ putLong("WHEN", Date().time)
+ putString("CAPTION", captionText())
+ putInt("METACAT", metaCategoryId())
+ putInt("CATEGORY", categoryId())
+ val p = there.there()
+ if (p != null) {
+ putInt("THERE-LAT", (p.latitude * 1e6).toInt())
+ putInt("THERE-LON", (p.longitude * 1e6).toInt())
+ } else
+ putInt("THERE-LAT", -1)
+ putBoolean("GEOLOC", geolocated)
+ putString("UPLOADED-URL", uploadedUrl)
+ apply()
+ }
+ store()
+
+ map?.onPause()
+
+ super.onPause()
+ }
+
+ private fun store() {
+ prefs().edit().apply {
+ putInt("STEP", step.id)
+ putString("PHOTOFILE", photoUri?.toString())
+ putString("DATETIME", dateTime)
+ putString("CAPTION", caption)
+ putBoolean("GEOLOC", geolocated)
+ apply()
+ }
+ }
+
+ override fun onResume() {
+ try {
+ doOnResume()
+ } catch (e: RuntimeException) {
+ step = AddStep.START
+ }
+ super.onResume()
+ setupView()
+ }
+
+ private fun doOnResume() {
+ prefs().apply {
+ step = AddStep.fromId(getInt("STEP", 1))!!
+
+ val photoFilename = getString("PHOTOFILE", null)
+ if (photo == null && photoFilename != null) {
+ // TODO scaling?
+ photoUri = photoFilename.toUri()
+ photo = Bitmaps.LoadUri(context, photoUri)
+ }
+ dateTime = getString("DATETIME", "")
+
+ caption = getString("CAPTION", "")!!
+
+ metaCatId = getInt("METACAT", -1)
+ catId = getInt("CATEGORY", -1)
+ setSpinnerSelections()
+
+ val lat = getInt("THERE-LAT", -1)
+ val lon = getInt("THERE-LON", -1)
+ if (lat != -1 && lon != -1)
+ there.noOverThere(GeoPoint(lat / 1e6, lon / 1e6))
+ geolocated = getBoolean("GEOLOC", false)
+
+ uploadedUrl = getString("UPLOADED-URL", uploadedUrl)
+
+ map?.onResume()
+
+ // If we've not viewed the fragment for more than 5 minutes, reset to the starting step.
+ val now = Date().time
+ val fragmentPauseTime = getLong("WHEN", now)
+ if (Date().time - fragmentPauseTime > fiveMinutes)
+ step = AddStep.START
+ }
+ }
+
+ private val fiveMinutes = (5 * 60 * 1000).toLong()
+
+ private fun prefs(): SharedPreferences {
+ return requireActivity().getSharedPreferences("net.cyclestreets.AddPhotoActivity", Context.MODE_PRIVATE)
+ }
+
+ ///////////// Caption text
+ private fun captionEditor(): EditText {
+ return photo2Caption!!.findViewById(R.id.caption)
+ }
+ private fun captionText(): String {
+ if (photo2Caption == null)
+ return caption
+ inputMethodManager.hideSoftInputFromWindow(captionEditor().windowToken, 0)
+ return captionEditor().text.toString()
+ }
+
+ ///////////// Category spinners
+ private fun metaCategorySpinner(): Spinner { return photo3Category.findViewById(R.id.metacat) }
+ private fun categorySpinner(): Spinner { return photo3Category.findViewById(R.id.category) }
+ private fun metaCategoryId(): Int { return metaCategorySpinner().selectedItemId.toInt() }
+ private fun categoryId(): Int { return categorySpinner().selectedItemId.toInt() }
+
+ private fun setupSpinners() {
+ if (activity == null) {
+ Log.d(TAG, "Activity was null when setting up spinners - break out")
+ return
+ }
+ metaCategorySpinner().adapter = CategoryAdapter(requireActivity(), photomapCategories!!.metaCategories())
+ categorySpinner().adapter = CategoryAdapter(requireActivity(), photomapCategories!!.categories())
+ setSpinnerSelections()
+ }
+ private fun setSpinnerSelections() {
+ // ids == position
+ if (metaCatId != -1)
+ metaCategorySpinner().setSelection(metaCatId)
+ if (catId != -1)
+ categorySpinner().setSelection(catId)
+ }
+
+ ///////////// View.OnClickListener methods
+ override fun onClick(v: View) {
+ when (v.id) {
+ R.id.takephoto_button -> doOrLogin {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
+ doOrRequestPermission(null, this, WRITE_EXTERNAL_STORAGE) {
+ dispatchTakePhotoIntent()
+ }
+ else
+ dispatchTakePhotoIntent()
+ }
+ R.id.chooseexisting_button -> doOrLogin {
+ launchPhotoPicker()
+ }
+ R.id.textonly_button -> doOrLogin {
+ photo = null
+ photoUri = null
+ dateTime = null
+ nextStep()
+ }
+ R.id.photo_share ->
+ Share.Url(activity, uploadedUrl, caption, "Photo on CycleStreets.net")
+ R.id.back -> {
+ if (step === AddStep.VIEW) {
+ step = AddStep.START
+ store()
+ setupView()
+ } else
+ onBackPressed()
+ }
+ R.id.next -> {
+ if (step === AddStep.LOCATION) {
+ if (needAccountDetails()) {
+ throw IllegalStateException("Shouldn't have reached this point without account details available")
+ }
+ upload()
+ } else if (step != AddStep.VIEW) {
+ nextStep()
+ }
+ }
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults)
+
+ Log.d(TAG, "Permission ${permissions.joinToString()} was ${if (grantResults.joinToString().equals("0")) "granted" else "denied"}")
+
+ for (i in permissions.indices) {
+ val permission = permissions[i]
+ val grantResult = grantResults[i]
+
+ when (permission) {
+ READ_EXTERNAL_STORAGE -> requestPermissionsResultAction(grantResult, permission) {
+ // Putting startActivityForResult here doesn't work as onActivityResult callback can't find fragment,
+ // because it gets re-initialised in mainNavDrawerActivity.onResume, so I'm removing it for now.
+ /* startActivityForResult(Intent(Intent.ACTION_PICK, MediaStore.Images.Media.INTERNAL_CONTENT_URI),
+ CHOOSE_PHOTO) */
+ }
+ WRITE_EXTERNAL_STORAGE -> requestPermissionsResultAction(grantResult, permission) {
+ // As above
+ //dispatchTakePhotoIntent()
+ }
+ ACCESS_FINE_LOCATION -> requestPermissionsResultAction(grantResult, permission) {
+ if (map != null) {
+ map!!.doEnableFollowLocation()
+ map!!.saveLocationPrefs()
+ }
+ }
+ }
+ }
+ }
+
+ private fun doOrLogin(function: () -> Unit) {
+ if (needAccountDetails())
+ startActivityForResult(Intent(activity, AccountDetailsActivity::class.java), ACCOUNT_DETAILS)
+ else
+ function()
+ }
+
+ private fun needAccountDetails(): Boolean {
+ return !allowUploadByKey && !CycleStreetsPreferences.accountOK()
+ }
+
+ private fun dispatchTakePhotoIntent() {
+ val takePictureIntent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
+
+ // Ensure that there's a camera activity to handle the intent
+ if (takePictureIntent.resolveActivity(requireActivity().packageManager) == null) {
+ Log.i(TAG, "Unable to identify a camera activity")
+ Toast.makeText(activity, "Unable to identify a camera activity", Toast.LENGTH_LONG).show()
+ return
+ }
+
+ try {
+ // Create the File where the photo should go
+ val photoFile: File = createImageFile(activity)
+ photoUri = FileProvider.getUriForFile(requireActivity(), "net.cyclestreets.fileprovider", photoFile)
+ takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri)
+ startActivityForResult(takePictureIntent, TAKE_PHOTO)
+ } catch (e: Exception) {
+ // Error occurred while creating the File
+ Log.w(TAG, "Error occured while creating image file", e)
+ Toast.makeText(activity, "There was a problem creating an image file : " + e.message, Toast.LENGTH_LONG).show()
+ }
+ }
+
+ private fun nextStep() {
+ if (step === AddStep.LOCATION && there.there() == null) {
+ Toast.makeText(activity, "Please set photo location", Toast.LENGTH_LONG).show()
+ return
+ }
+ step = step.next!!
+ store()
+ setupView()
+ }
+
+ private fun upload() {
+ try {
+ UploadPhotoTask(requireActivity(),
+ photoUri!!,
+ CycleStreetsPreferences.username(),
+ CycleStreetsPreferences.password(),
+ there.there(),
+ photomapCategories!!.metaCategories()[metaCatId].tag,
+ photomapCategories!!.categories()[catId].tag,
+ dateTime!!,
+ caption).execute()
+ } catch (e: RuntimeException) {
+ Toast.makeText(activity, R.string.photo_could_not_upload, Toast.LENGTH_LONG).show()
+ step = AddStep.LOCATION
+ }
+ }
+
+ ///////////// Undoable methods
+ override fun onBackPressed(): Boolean {
+ if (step === AddStep.START || step === AddStep.VIEW) {
+ step = AddStep.START
+ store()
+ return false
+ }
+ step = step.previous!!
+ store()
+ setupView()
+ return true
+ }
+
+ ///////////// LocationListener methods
+ override fun onSetLocation(point: IGeoPoint?) {
+ (photo4Location.findViewById(R.id.next) as Button).isEnabled = point != null
+ }
+
+ ///////////// Tasks
+ private inner class GetPhotomapCategoriesTask : AsyncTask() {
+ @Deprecated("Deprecated in Java")
+ override fun doInBackground(vararg params: Any): PhotomapCategories? {
+ return try {
+ PhotomapCategories.get()
+ } catch (ex: Exception) {
+ null
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onPostExecute(categories: PhotomapCategories?) {
+ if (categories == null) {
+ if (activity != null) {
+ Toast.makeText(activity, R.string.photo_could_not_load_categories, Toast.LENGTH_LONG).show()
+ }
+ return
+ }
+ photomapCategories = categories
+ setupSpinners()
+ }
+ }
+
+ private inner class UploadPhotoTask(context: Context,
+ uri: Uri,
+ private val username: String,
+ private val password: String,
+ private val location: IGeoPoint,
+ private val metaCat: String,
+ private val category: String,
+ private val dateTime: String,
+ private val caption: String) : AsyncTask() {
+ private val photoUri: Uri
+ private val progress: ProgressDialog
+
+ init {
+ photoUri = uri
+ progress = Dialog.createProgressDialog(context, R.string.photo_uploading)
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onPreExecute() {
+ super.onPreExecute()
+ progress.show()
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun doInBackground(vararg params: Any): Upload.Result {
+ return try {
+ Upload.photo(photoUri, username, password, location,
+ metaCat, category, dateTime, caption)
+ } catch (e: Exception) {
+ Upload.Result.error(e.message)
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onPostExecute(result: Upload.Result) {
+ progress.dismiss()
+
+ if (result.ok())
+ uploadComplete(result.url())
+ else
+ uploadFailed(result.message())
+ }
+ }
+
+ private fun uploadComplete(photoUrl: String) {
+ uploadedUrl = photoUrl
+ nextStep()
+ }
+
+ private fun uploadFailed(msg: String) {
+ MessageBox.OK(photo4Location, msg) { _, _ ->
+ step = AddStep.LOCATION
+ setupView()
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/AddStep.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/AddStep.kt
new file mode 100644
index 000000000..23e0fe64c
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/AddStep.kt
@@ -0,0 +1,22 @@
+package net.cyclestreets.addphoto
+
+internal enum class AddStep(val id: Int, val previous: AddStep?) {
+ START(1, null),
+ CAPTION(2, START),
+ CATEGORY(3, CAPTION),
+ LOCATION(4, CATEGORY),
+ VIEW(5, LOCATION),
+ DONE(6, VIEW);
+
+ var next: AddStep? = null
+
+ companion object {
+ private val map: Map = AddStep.values().associateBy(AddStep::id);
+ fun fromId(type: Int) = map[type]
+ }
+
+ init {
+ if (previous != null)
+ previous.next = this
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/CategoryAdapter.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/CategoryAdapter.kt
new file mode 100644
index 000000000..9a321793b
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/CategoryAdapter.kt
@@ -0,0 +1,36 @@
+package net.cyclestreets.addphoto
+
+import android.content.Context
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.Spinner
+import android.widget.TextView
+
+import net.cyclestreets.api.PhotomapCategory
+
+internal class CategoryAdapter(context: Context,
+ private val categories: List) : BaseAdapter() {
+ private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+
+ override fun getCount(): Int {
+ return categories.size
+ }
+
+ override fun getItem(position: Int): String {
+ val c = categories[position]
+ return c.name
+ }
+
+ override fun getItemId(position: Int): Long {
+ return position.toLong()
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val id = if (parent is Spinner) android.R.layout.simple_spinner_item else android.R.layout.simple_spinner_dropdown_item
+ val tv = inflater.inflate(id, parent, false) as TextView
+ tv.text = getItem(position)
+ return tv
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/Utils.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/Utils.kt
new file mode 100644
index 000000000..ef73d448b
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/addphoto/Utils.kt
@@ -0,0 +1,87 @@
+package net.cyclestreets.addphoto
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.os.Environment
+import android.provider.MediaStore
+import androidx.exifinterface.media.ExifInterface
+import android.text.TextUtils
+import android.util.Log
+import android.view.View
+import android.widget.Button
+import net.cyclestreets.fragments.R
+import net.cyclestreets.util.AsyncDelete
+import org.osmdroid.util.GeoPoint
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.io.File
+
+internal const val TAKE_PHOTO = 2
+internal const val CHOOSE_PHOTO = 3
+internal const val ACCOUNT_DETAILS = 4
+
+private const val PHOTO_FILE_PREFIX = "CS_PHOTO_"
+
+internal fun photoUploadMetaData(activity: Activity?): String {
+ try {
+ val ai = activity!!.packageManager.getApplicationInfo(activity.packageName, PackageManager.GET_META_DATA)
+ return ai.metaData.getString("CycleStreetsPhotoUpload") ?: ""
+ } catch (e: Exception) {
+ return ""
+ }
+}
+
+internal fun backNextButtons(parentView: View,
+ backText: String, backDrawable: Drawable,
+ nextText: String, nextDrawable: Drawable) {
+ parentView.findViewById(R.id.back).apply {
+ text = backText
+ setCompoundDrawables(backDrawable, null, null, null)
+ }
+ parentView.findViewById(R.id.next).apply {
+ text = nextText
+ setCompoundDrawables(null, null, nextDrawable, null)
+ }
+}
+
+@SuppressLint("SimpleDateFormat")
+internal fun createImageFile(context: Context?): File {
+ deletePreviouslyCapturedImages(context)
+ val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
+ val imageFileName = "$PHOTO_FILE_PREFIX$timeStamp"
+ val file = File.createTempFile(imageFileName, ".jpg", storageDir(context))
+ Log.i(TAG, "Created temporary image file ${file.absolutePath}")
+ return file
+}
+
+private fun deletePreviouslyCapturedImages(context: Context?) {
+ val files = storageDir(context).listFiles { _, filename -> filename.matches(Regex("$PHOTO_FILE_PREFIX.*")) }
+ AsyncDelete().execute(*files)
+}
+
+private fun storageDir(context: Context?): File {
+ return context?.getExternalFilesDir(Environment.DIRECTORY_PICTURES)!!
+}
+
+internal fun photoLocation(photoExif: ExifInterface): GeoPoint? {
+ val coords: DoubleArray? = photoExif.latLong
+ return if (coords != null) GeoPoint(coords[0], coords[1]) else null
+}
+
+@SuppressLint("SimpleDateFormat")
+internal fun photoTimestamp(photoExif: ExifInterface): String {
+ var date = Date()
+ try {
+ val df = SimpleDateFormat("yyyy:MM:dd HH:mm:ss")
+ val dateString = photoExif.getAttribute(ExifInterface.TAG_DATETIME)!!
+ if (!TextUtils.isEmpty(dateString))
+ date = df.parse(dateString)!!
+ } catch (e: Exception) {
+ // ah well
+ }
+ return (date.time / 1000).toString()
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ElevationProfileFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ElevationProfileFragment.kt
new file mode 100644
index 000000000..cc45bf651
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ElevationProfileFragment.kt
@@ -0,0 +1,111 @@
+package net.cyclestreets.itinerary
+
+import android.graphics.Color
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.LinearLayout
+
+import com.jjoe64.graphview.GraphView
+import com.jjoe64.graphview.GridLabelRenderer
+import com.jjoe64.graphview.LabelFormatter
+import com.jjoe64.graphview.Viewport
+import com.jjoe64.graphview.series.DataPoint
+import com.jjoe64.graphview.series.LineGraphSeries
+
+import net.cyclestreets.fragments.R
+import net.cyclestreets.routing.ElevationFormatter
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Route
+import net.cyclestreets.routing.Waypoints
+import net.cyclestreets.util.Theme
+
+import java.util.ArrayList
+
+class ElevationProfileFragment : Fragment(), Route.Listener {
+ private lateinit var graphHolder: LinearLayout
+
+ override fun onCreateView(inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?): View {
+ val elevation = inflater.inflate(R.layout.elevation, container, false)
+ graphHolder = elevation.findViewById(R.id.graphview)
+ return elevation
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Route.onResume()
+ Route.registerListener(this)
+ }
+
+ override fun onPause() {
+ Route.unregisterListener(this)
+ super.onPause()
+ }
+
+ override fun onNewJourney(journey: Journey, waypoints: Waypoints) {
+ drawGraph(journey)
+ fillInOverview(journey, requireView(), requireContext().getString(R.string.elevation_route))
+ }
+
+ override fun onResetJourney() {}
+
+ private fun drawGraph(journey: Journey) {
+ val graphView = GraphView(context)
+ val formatter = elevationFormatter()
+
+ // The elevation data series for the whole route
+ val elevationData = ArrayList()
+ for (elevation in journey.elevation.profile())
+ elevationData.add(DataPoint(elevation.distance().toDouble(), elevation.elevation().toDouble()))
+ val elevationSeries = LineGraphSeries(elevationData.toTypedArray())
+ elevationSeries.isDrawBackground = true
+ graphView.addSeries(elevationSeries)
+
+ // The elevation data series for the current segment - highlight
+ journey.activeSegment()?.let { segment ->
+ val segmentEndDistance = segment.cumulativeDistance
+ val segmentStartDistance = segmentEndDistance - segment.distance
+ val segmentElevationData = elevationData.slice(IntRange(
+ elevationData.indexOfFirst { dp -> dp.x >= segmentStartDistance.toDouble() },
+ elevationData.indexOfLast { dp -> dp.x <= segmentEndDistance.toDouble() }
+ ))
+ val segmentElevationSeries = LineGraphSeries(segmentElevationData.toTypedArray())
+ segmentElevationSeries.color = Theme.highlightColor(context)
+ segmentElevationSeries.backgroundColor = Color.argb(153, 0, 152, 0) //0x99009800
+ segmentElevationSeries.isDrawBackground = true
+ graphView.addSeries(segmentElevationSeries)
+ }
+
+ // Allow zooming & scrolling on the x-axis (y-axis remains fixed)
+ val viewport = graphView.viewport
+ viewport.isScalable = true
+ viewport.isYAxisBoundsManual = true
+ viewport.setMinY(formatter.roundHeightBelow(journey.elevation.minimum()))
+ viewport.setMaxY(formatter.roundHeightAbove(journey.elevation.maximum()))
+
+ val gridLabelRenderer = graphView.gridLabelRenderer
+ gridLabelRenderer.gridStyle = GridLabelRenderer.GridStyle.BOTH
+ gridLabelRenderer.numHorizontalLabels = 5
+ gridLabelRenderer.numVerticalLabels = 5
+ gridLabelRenderer.labelFormatter = ElevationLabelFormatter(formatter)
+ // we handle y-rounding ourselves, and x-rounding makes labels overlap - see https://github.com/jjoe64/GraphView/issues/413
+ gridLabelRenderer.setHumanRounding(false, false)
+
+ graphHolder.removeAllViews()
+ graphHolder.addView(graphView)
+ }
+
+ private class ElevationLabelFormatter constructor(private val formatter: ElevationFormatter) : LabelFormatter {
+ override fun setViewport(viewport: Viewport) {}
+
+ override fun formatLabel(value: Double, isValueX: Boolean): String {
+ return if (isValueX)
+ if (value != 0.0) formatter.distance(value.toInt()) else ""
+ else formatter.roundedHeight(value.toInt())
+ }
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ItineraryAndElevationFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ItineraryAndElevationFragment.kt
new file mode 100644
index 000000000..1087baed6
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ItineraryAndElevationFragment.kt
@@ -0,0 +1,84 @@
+package net.cyclestreets.itinerary
+
+import android.os.Bundle
+import androidx.fragment.app.Fragment
+import android.view.LayoutInflater
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.ViewGroup
+
+import net.cyclestreets.fragments.R
+
+import net.cyclestreets.util.MenuHelper.showMenuItem
+
+class ItineraryAndElevationFragment : Fragment() {
+ private var lastFrag: Fragment? = null
+ private lateinit var itinerary: Fragment
+ private lateinit var elevation: Fragment
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ setHasOptionsMenu(true)
+ retainInstance = true
+ itinerary = ItineraryFragment()
+ elevation = ElevationProfileFragment()
+
+ super.onCreate(savedInstanceState)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.itinerary_and_elevation, container, false)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ showFrag(lastFrag ?: itinerary)
+ }
+
+ private fun showFrag(frag: Fragment) {
+ val fm = childFragmentManager
+ val ft = fm.beginTransaction()
+
+ if (lastFrag != null)
+ ft.detach(lastFrag!!)
+
+ if (fm.findFragmentByTag(frag.tag) == null)
+ ft.add(R.id.container, frag, frag.javaClass.simpleName)
+ else
+ ft.attach(frag)
+ ft.commit()
+
+ lastFrag = frag
+
+ requireActivity().invalidateOptionsMenu()
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
+ inflater.inflate(R.menu.itinerary_and_elevation_menu, menu)
+ super.onCreateOptionsMenu(menu, inflater)
+ }
+
+ override fun onPrepareOptionsMenu(menu: Menu) {
+ showMenuItem(menu, R.id.ic_menu_itinerary, itinerary !== lastFrag)
+ showMenuItem(menu, R.id.ic_menu_elevation, elevation !== lastFrag)
+ super.onPrepareOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (super.onOptionsItemSelected(item))
+ return true
+
+ val menuId = item.itemId
+
+ if (R.id.ic_menu_itinerary == menuId)
+ showFrag(itinerary)
+
+ if (R.id.ic_menu_elevation == menuId)
+ showFrag(elevation)
+
+ return true
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ItineraryFragment.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ItineraryFragment.kt
new file mode 100644
index 000000000..1e804d1d9
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/ItineraryFragment.kt
@@ -0,0 +1,49 @@
+package net.cyclestreets.itinerary
+
+import android.os.Bundle
+import androidx.fragment.app.ListFragment
+import android.view.View
+import android.widget.ListView
+import net.cyclestreets.RouteMapActivity
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Route
+import net.cyclestreets.routing.Waypoints
+
+class ItineraryFragment : ListFragment(), Route.Listener {
+ internal var journey = Journey.NULL_JOURNEY
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ listAdapter = SegmentAdapter(requireActivity())
+ }
+
+ override fun onResume() {
+ super.onResume()
+ Route.onResume()
+ Route.registerListener(this)
+ }
+
+ override fun onPause() {
+ Route.unregisterListener(this)
+ super.onPause()
+ }
+
+ override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
+ if (journey.isEmpty())
+ return
+
+ journey.setActiveSegmentIndex(position)
+ try {
+ (activity as RouteMapActivity).showRouteMap()
+ } catch (e: Exception) {}
+ }
+
+ override fun onNewJourney(journey: Journey, waypoints: Waypoints) {
+ this.journey = journey
+ setSelection(this.journey.activeSegmentIndex())
+ }
+
+ override fun onResetJourney() {
+ journey = Journey.NULL_JOURNEY
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/Overview.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/Overview.kt
new file mode 100644
index 000000000..8ab3628e7
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/Overview.kt
@@ -0,0 +1,36 @@
+package net.cyclestreets.itinerary
+
+import android.view.View
+import android.widget.TextView
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.api.DistanceFormatter
+import net.cyclestreets.fragments.R
+import net.cyclestreets.routing.ElevationFormatter
+import net.cyclestreets.routing.Journey
+import java.util.*
+
+internal fun fillInOverview(journey: Journey, parent: View, routeString: String) {
+ val start = journey.segments.first()
+
+ setText(parent, R.id.title, journey.name())
+ setText(parent, R.id.journeyid, String.format(Locale.getDefault(), "#%,d", journey.itinerary()))
+ setText(parent, R.id.routetype, "${journey.plan().capitalize()} $routeString:")
+ setText(parent, R.id.distance, distanceFormatter().total_distance(journey.totalDistance()))
+ setText(parent, R.id.journeytime, start.totalTime())
+ setText(parent, R.id.calories, start.calories())
+ setText(parent, R.id.carbondioxide, start.co2())
+ setText(parent, R.id.elevation_gain, elevationFormatter().height(journey.elevation.totalElevationGain()))
+ setText(parent, R.id.elevation_loss, elevationFormatter().height(journey.elevation.totalElevationLoss()))
+}
+
+internal fun elevationFormatter(): ElevationFormatter {
+ return ElevationFormatter.formatter(CycleStreetsPreferences.units())
+}
+
+private fun distanceFormatter(): DistanceFormatter {
+ return DistanceFormatter.formatter(CycleStreetsPreferences.units())
+}
+
+private fun setText(parent: View, id: Int, text: String) {
+ parent.findViewById(id)!!.text = text
+}
diff --git a/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/SegmentAdapter.kt b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/SegmentAdapter.kt
new file mode 100644
index 000000000..f12ac1da5
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/java/net/cyclestreets/itinerary/SegmentAdapter.kt
@@ -0,0 +1,96 @@
+package net.cyclestreets.itinerary
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import androidx.core.content.res.ResourcesCompat
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.BaseAdapter
+import android.widget.ImageView
+import android.widget.TextView
+import net.cyclestreets.fragments.R
+import net.cyclestreets.routing.Route
+import net.cyclestreets.routing.Segment
+import net.cyclestreets.util.Theme
+import net.cyclestreets.util.Turn
+import net.cyclestreets.util.TurnIcons
+
+internal class SegmentAdapter(context: Context) : BaseAdapter() {
+ private val footprints: Drawable = ResourcesCompat.getDrawable(context.resources, R.drawable.footprints2, null)!!
+ private val inflater: LayoutInflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
+ private val themeColor: Drawable = ResourcesCompat.getDrawable(context.resources, R.color.apptheme_color, null)!!
+ private val backgroundColor: Int = Theme.backgroundColor(context)
+ private val routeString: String = context.getString(R.string.elevation_route)
+ private var v: View? = null
+
+ private fun hasSegments(): Boolean {
+ return !Route.journey().isEmpty()
+ }
+
+ override fun getCount(): Int {
+ return if (hasSegments()) Route.journey().segments.count() else 1
+ }
+
+ override fun getItem(position: Int): Segment? {
+ return if (!hasSegments()) null else Route.journey().segments.get(position)
+ }
+
+ override fun getItemId(position: Int): Long {
+ return position.toLong()
+ }
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ if (!hasSegments())
+ return inflater.inflate(R.layout.itinerary_not_available, parent, false)
+
+ val seg = Route.journey().segments.get(position)
+ val layoutId = if (position != 0) R.layout.itinerary_item else R.layout.itinerary_header_item
+ val view: View = inflater.inflate(layoutId, parent, false)
+ v = view;
+
+ val highlight = position == Route.journey().activeSegmentIndex()
+
+ if (position == 0) {
+ fillInOverview(Route.journey(), view, routeString)
+ }
+ setText(R.id.segment_distance, seg.formattedDistance(), highlight)
+ setText(R.id.segment_cumulative_distance, seg.runningDistance(), highlight)
+ setText(R.id.segment_time, seg.runningTime(), highlight)
+
+ setMainText(R.id.segment_street, seg.turnInstruction(), seg.street(), highlight)
+ setTurnIcon(R.id.segment_type, seg.turn(), seg.walk())
+
+ if (highlight && position != 0)
+ view.background = themeColor
+
+ return view
+ }
+
+ private fun setText(id: Int, t: String, highlight: Boolean) {
+ val n = getTextView(id) ?: return
+ n.text = t
+ if (highlight)
+ n.setTextColor(Color.BLACK)
+ }
+
+ private fun setMainText(id: Int, turn: String, street: String, highlight: Boolean) {
+ val text = if (turn.isNotEmpty()) "$turn into $street" else street
+ setText(id, text, highlight)
+ }
+
+ private fun setTurnIcon(id: Int, turn: Turn, walk: Boolean) {
+ val iv = v!!.findViewById(id) ?: return
+
+ val icon = TurnIcons.icon(turn)
+ iv.setImageDrawable(icon)
+ iv.setBackgroundColor(backgroundColor)
+ if (walk)
+ iv.background = footprints
+ }
+
+ private fun getTextView(id: Int): TextView? {
+ return v!!.findViewById(id)
+ }
+}
diff --git a/libraries/cyclestreets-fragments/src/main/res/color/nav_state_icon.xml b/libraries/cyclestreets-fragments/src/main/res/color/nav_state_icon.xml
new file mode 100644
index 000000000..71614736c
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/color/nav_state_icon.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/color/nav_state_text.xml b/libraries/cyclestreets-fragments/src/main/res/color/nav_state_text.xml
new file mode 100644
index 000000000..85c345c67
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/color/nav_state_text.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/apptheme_ic_navigation_drawer.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/apptheme_ic_navigation_drawer.png
deleted file mode 100644
index c59f601ca..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/apptheme_ic_navigation_drawer.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/drawer_shadow.9.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/drawer_shadow.9.png
deleted file mode 100644
index 236bff558..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/drawer_shadow.9.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/footprints.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/footprints.png
deleted file mode 100644
index 509f0e704..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/footprints.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera.png
deleted file mode 100644
index 3caa1aeae..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera_black.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera_black.png
deleted file mode 100644
index 8b859b782..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera_black.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera_white.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera_white.png
deleted file mode 100644
index 52ec8e656..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_camera_white.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_directions.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_directions.png
deleted file mode 100644
index 23f6eb3a1..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_directions.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_gallery_black.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_gallery_black.png
deleted file mode 100644
index 3f1c21e70..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_gallery_black.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_gallery_white.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_gallery_white.png
deleted file mode 100644
index cfc3a40c6..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_gallery_white.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details.png
deleted file mode 100644
index 013e988be..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details_black.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details_black.png
deleted file mode 100644
index 34a64932e..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details_black.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details_white.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details_white.png
deleted file mode 100644
index 7ad89639a..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_info_details_white.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_mapmode_black.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_mapmode_black.png
deleted file mode 100644
index 2351b892a..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_mapmode_black.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_mapmode_white.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_mapmode_white.png
deleted file mode 100644
index 8b128ad40..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_mapmode_white.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_places.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_places.png
deleted file mode 100644
index 6cb7c8b00..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_places.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_route_number.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_route_number.png
deleted file mode 100644
index 06a1cf0e1..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_route_number.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_search.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_search.png
deleted file mode 100644
index 9154d6e14..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_search.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_settings.png b/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_settings.png
deleted file mode 100644
index 968017eac..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-hdpi/ic_menu_settings.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_directions.png b/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_directions.png
deleted file mode 100644
index e51f392d1..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_directions.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_places.png b/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_places.png
deleted file mode 100644
index 9d2e8dc17..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_places.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_route_number.png b/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_route_number.png
deleted file mode 100644
index cf69b77cc..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_route_number.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_search.png b/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_search.png
deleted file mode 100644
index 1d95408c6..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_search.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_settings.png b/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_settings.png
deleted file mode 100644
index 1b58381d0..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-ldpi/ic_menu_settings.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/apptheme_ic_navigation_drawer.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/apptheme_ic_navigation_drawer.png
deleted file mode 100644
index 1ed2c56ee..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/apptheme_ic_navigation_drawer.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/drawer_shadow.9.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/drawer_shadow.9.png
deleted file mode 100644
index ffe3a28d7..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/drawer_shadow.9.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_launcher.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_launcher.png
deleted file mode 100644
index 80ea5a971..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_launcher.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_directions.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_directions.png
deleted file mode 100644
index 00a288f04..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_directions.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_places.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_places.png
deleted file mode 100644
index 040f9b181..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_places.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_route_number.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_route_number.png
deleted file mode 100644
index 42d511bf4..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_route_number.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_search.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_search.png
deleted file mode 100644
index ef949d500..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_search.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_settings.png b/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_settings.png
deleted file mode 100644
index 739f2dbc8..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-mdpi/ic_menu_settings.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-xhdpi/apptheme_ic_navigation_drawer.png b/libraries/cyclestreets-fragments/src/main/res/drawable-xhdpi/apptheme_ic_navigation_drawer.png
deleted file mode 100644
index a5fa74def..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-xhdpi/apptheme_ic_navigation_drawer.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-xhdpi/drawer_shadow.9.png b/libraries/cyclestreets-fragments/src/main/res/drawable-xhdpi/drawer_shadow.9.png
deleted file mode 100644
index fabe9d965..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-xhdpi/drawer_shadow.9.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-xxhdpi/apptheme_ic_navigation_drawer.png b/libraries/cyclestreets-fragments/src/main/res/drawable-xxhdpi/apptheme_ic_navigation_drawer.png
deleted file mode 100644
index 9c4685d6e..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-xxhdpi/apptheme_ic_navigation_drawer.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable-xxhdpi/drawer_shadow.9.png b/libraries/cyclestreets-fragments/src/main/res/drawable-xxhdpi/drawer_shadow.9.png
deleted file mode 100644
index b91e9d7f2..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable-xxhdpi/drawer_shadow.9.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/cyclestreets.jpg b/libraries/cyclestreets-fragments/src/main/res/drawable/cyclestreets.jpg
deleted file mode 100644
index 196697f47..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable/cyclestreets.jpg and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/footprints.png b/libraries/cyclestreets-fragments/src/main/res/drawable/footprints.png
deleted file mode 100644
index 6760973fe..000000000
Binary files a/libraries/cyclestreets-fragments/src/main/res/drawable/footprints.png and /dev/null differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/footprints2.png b/libraries/cyclestreets-fragments/src/main/res/drawable/footprints2.png
new file mode 100644
index 000000000..4a6503ea7
Binary files /dev/null and b/libraries/cyclestreets-fragments/src/main/res/drawable/footprints2.png differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_itinerary.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_itinerary.xml
deleted file mode 100644
index 62c0e71e6..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_itinerary.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_more.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_more.xml
deleted file mode 100644
index 0d398e6af..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_more.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_navigate.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_navigate.xml
deleted file mode 100644
index 2aa05b9ae..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_navigate.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_photomap.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_photomap.xml
deleted file mode 100644
index aa61e7ee4..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_photomap.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_photoupload.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_photoupload.xml
deleted file mode 100644
index 74767da87..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_photoupload.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_planroute.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_planroute.xml
deleted file mode 100644
index 0c904f3ce..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/drawable/ic_tab_planroute.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/nav_header.png b/libraries/cyclestreets-fragments/src/main/res/drawable/nav_header.png
new file mode 100644
index 000000000..9e6dd1574
Binary files /dev/null and b/libraries/cyclestreets-fragments/src/main/res/drawable/nav_header.png differ
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/nav_state_background.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/nav_state_background.xml
new file mode 100644
index 000000000..1f81240da
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/drawable/nav_state_background.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/drawable/roundrect.xml b/libraries/cyclestreets-fragments/src/main/res/drawable/roundrect.xml
index 7a9577ccd..7e9b9cd6f 100644
--- a/libraries/cyclestreets-fragments/src/main/res/drawable/roundrect.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/drawable/roundrect.xml
@@ -1,6 +1,10 @@
-
-
-
-
\ No newline at end of file
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/about.xml b/libraries/cyclestreets-fragments/src/main/res/layout/about.xml
index d5dd1c103..f831c0d90 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/about.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/about.xml
@@ -5,20 +5,20 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent">
+ android:scrollbars="vertical">
+ android:gravity="bottom">
+ android:gravity="center" />
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/blog.xml b/libraries/cyclestreets-fragments/src/main/res/layout/blog.xml
index 67ccc27f3..847c97f16 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/blog.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/blog.xml
@@ -5,10 +5,10 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent">
+ android:scrollbars="vertical">
-
+ android:text="@string/blog_notifications" />
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/elevation.xml b/libraries/cyclestreets-fragments/src/main/res/layout/elevation.xml
index dc721032e..a7c211d77 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/elevation.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/elevation.xml
@@ -7,9 +7,9 @@
layout="@layout/itinerary_header_item"
android:layout_height="wrap_content"
android:layout_width="match_parent"
- android:layout_marginBottom="20px"/>
+ android:layout_marginBottom="20dp" />
+ layout="@layout/elevation_graph" />
-
\ No newline at end of file
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/elevation_graph.xml b/libraries/cyclestreets-fragments/src/main/res/layout/elevation_graph.xml
index d08e0b117..601e26c0e 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/elevation_graph.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/elevation_graph.xml
@@ -8,5 +8,4 @@
android:paddingTop="50dp"
android:paddingBottom="50dp"
android:paddingLeft="@dimen/abc_dropdownitem_text_padding_left"
- android:paddingRight="@dimen/abc_dropdownitem_text_padding_right">
-
\ No newline at end of file
+ android:paddingRight="@dimen/abc_dropdownitem_text_padding_right" />
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/elevation_text.xml b/libraries/cyclestreets-fragments/src/main/res/layout/elevation_text.xml
index 2e6a1bfc2..209085224 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/elevation_text.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/elevation_text.xml
@@ -7,27 +7,26 @@
android:id="@+id/title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:textSize="24dip"
- android:text="Fruit"
- android:textStyle="bold"/>
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
+ android:text="@string/elevation_dummy_string" />
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ android:id="@+id/routetype"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_route" />
+ android:id="@+id/distance"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1.4"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_dummy_string" />
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_estimated_time" />
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_dummy_string" />
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_calories_burned" />
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_dummy_string" />
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_co2_saved" />
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_dummy_string" />
+
+
+
+
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+
+
+
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_loss" />
+ android:id="@+id/elevation_loss"
+ android:layout_height="wrap_content"
+ android:layout_width="0dp"
+ android:layout_weight="1.4"
+ android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1"
+ android:text="@string/elevation_dummy_string" />
-
\ No newline at end of file
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/findplace.xml b/libraries/cyclestreets-fragments/src/main/res/layout/findplace.xml
index 163b43cac..a9e8d3a48 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/findplace.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/findplace.xml
@@ -1,18 +1,18 @@
-
-
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_and_elevation.xml b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_and_elevation.xml
index 339de4574..172031aa6 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_and_elevation.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_and_elevation.xml
@@ -3,7 +3,4 @@
android:id="@+id/container"
android:orientation="vertical"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:background="?android:attr/colorBackground">
-
-
\ No newline at end of file
+ android:layout_height="match_parent" />
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_header_item.xml b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_header_item.xml
index f60355300..8efa7dbd9 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_header_item.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_header_item.xml
@@ -5,9 +5,9 @@
android:orientation="horizontal"
android:paddingLeft="@dimen/abc_dropdownitem_text_padding_left"
android:paddingRight="@dimen/abc_dropdownitem_text_padding_right"
- android:paddingBottom="@dimen/abc_action_bar_subtitle_bottom_margin">
+ android:paddingBottom="@dimen/abc_action_bar_subtitle_bottom_margin_material">
-
\ No newline at end of file
+ android:gravity="center_vertical" />
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_item.xml b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_item.xml
index 9021934ab..068cb3edc 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_item.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_item.xml
@@ -3,66 +3,65 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
- android:paddingTop="6dip"
- android:paddingBottom="6dip"
+ android:paddingTop="6dp"
+ android:paddingBottom="6dp"
android:paddingLeft="@dimen/abc_dropdownitem_text_padding_left"
android:paddingRight="@dimen/abc_dropdownitem_text_padding_right">
+ android:contentDescription="@string/itinerary_direction" />
+ android:text="@string/itinerary_street" />
-
-
+
-
+
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+ android:textAppearance="?android:attr/textAppearanceSmall" />
-
\ No newline at end of file
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_not_available.xml b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_not_available.xml
index fd8cf0f88..7258c286a 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_not_available.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/itinerary_not_available.xml
@@ -2,11 +2,10 @@
+ android:padding="6dp">
+ android:text="@string/itinerary_not_available" />
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/location_editor.xml b/libraries/cyclestreets-fragments/src/main/res/layout/location_editor.xml
index 24ffe36c9..cafcf3bd5 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/location_editor.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/location_editor.xml
@@ -7,7 +7,7 @@
@@ -20,7 +20,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
- android:text="Place the mark on the map and then name the location."/>
+ android:text="@string/location_editor_hint" />
+ android:text="@string/location_editor_name" />
+ android:layout_height="wrap_content"
+ android:inputType="textCapSentences"/>
-
+
-
+
+ android:layout_width="30dp" />
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/locations_list.xml b/libraries/cyclestreets-fragments/src/main/res/layout/locations_list.xml
index 100d4248a..15087b005 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/locations_list.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/locations_list.xml
@@ -1,28 +1,29 @@
-
+
-
-
-
+
+
+
-
-
\ No newline at end of file
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/main_navdrawer_activity.xml b/libraries/cyclestreets-fragments/src/main/res/layout/main_navdrawer_activity.xml
new file mode 100644
index 000000000..8e30985fa
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/main_navdrawer_activity.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/mainnavdraweractivity.xml b/libraries/cyclestreets-fragments/src/main/res/layout/mainnavdraweractivity.xml
deleted file mode 100644
index dcb884185..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/layout/mainnavdraweractivity.xml
+++ /dev/null
@@ -1,31 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/maintabbedactivity.xml b/libraries/cyclestreets-fragments/src/main/res/layout/maintabbedactivity.xml
deleted file mode 100644
index 4bcdf6110..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/layout/maintabbedactivity.xml
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/nav_header.xml b/libraries/cyclestreets-fragments/src/main/res/layout/nav_header.xml
new file mode 100644
index 000000000..6034f9b2d
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/nav_header.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/navigation_drawer.xml b/libraries/cyclestreets-fragments/src/main/res/layout/navigation_drawer.xml
deleted file mode 100644
index c0e7acde2..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/layout/navigation_drawer.xml
+++ /dev/null
@@ -1,7 +0,0 @@
-
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/navigation_item.xml b/libraries/cyclestreets-fragments/src/main/res/layout/navigation_item.xml
deleted file mode 100644
index 6b78124ad..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/layout/navigation_item.xml
+++ /dev/null
@@ -1,24 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/routebyaddress.xml b/libraries/cyclestreets-fragments/src/main/res/layout/routebyaddress.xml
index acd9d29eb..8e6fda887 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/routebyaddress.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/routebyaddress.xml
@@ -1,34 +1,33 @@
-
+
-
+ android:layout_marginLeft="2.0dp"
+ android:layout_marginTop="1.0dp"
+ android:layout_marginRight="2.0dp"
+ android:layout_marginBottom="2.0dp">
-
+
+ android:layout_height="wrap_content" />
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/routenumber.xml b/libraries/cyclestreets-fragments/src/main/res/layout/routenumber.xml
index 9e01a101f..84785ebbc 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/routenumber.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/routenumber.xml
@@ -1,23 +1,22 @@
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:paddingLeft="9dp"
+ android:paddingRight="9dp"
+ android:paddingTop="12dp"
+ android:paddingBottom="6dp">
+ android:hint="@string/routenumber_hint" />
+ android:layout_height="wrap_content" />
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes.xml b/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes.xml
index a4d3fe88d..0842736af 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes.xml
@@ -1,20 +1,19 @@
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:clipChildren="false"
+ android:padding="20dp">
-
-
+ android:orientation="vertical"
+ android:layout_width="wrap_content"
+ android:layout_height="fill_parent"
+ android:clipChildren="false"
+ android:padding="5dp"
+ android:background="@drawable/roundrect">
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes_item.xml b/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes_item.xml
index 442d1faff..060dc9a52 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes_item.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/storedroutes_item.xml
@@ -1,33 +1,25 @@
-
-
+
+ android:id="@+id/route_title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="left"
+ android:ellipsize="marquee"
+ android:text="@string/storedroutes_title_default"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
-
-
\ No newline at end of file
+ android:text="@string/storedroutes_detail_default"
+ android:textAppearance="?android:attr/textAppearanceSmall" />
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/webpage.xml b/libraries/cyclestreets-fragments/src/main/res/layout/webpage.xml
index 75438ed9b..3e78927d4 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/webpage.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/webpage.xml
@@ -6,7 +6,7 @@
android:layout_height="fill_parent">
diff --git a/libraries/cyclestreets-fragments/src/main/res/layout/whatsnew.xml b/libraries/cyclestreets-fragments/src/main/res/layout/whatsnew.xml
index c3db7efe4..7c8580bb9 100644
--- a/libraries/cyclestreets-fragments/src/main/res/layout/whatsnew.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/layout/whatsnew.xml
@@ -5,9 +5,9 @@
android:layout_width="fill_parent"
android:layout_height="fill_parent">
+ android:scrollbars="vertical">
diff --git a/libraries/cyclestreets-fragments/src/main/res/menu/itinerary_and_elevation_menu.xml b/libraries/cyclestreets-fragments/src/main/res/menu/itinerary_and_elevation_menu.xml
index b08989188..b90803380 100644
--- a/libraries/cyclestreets-fragments/src/main/res/menu/itinerary_and_elevation_menu.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/menu/itinerary_and_elevation_menu.xml
@@ -1,10 +1,10 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+ app:showAsAction="ifRoom|withText" />
-
\ No newline at end of file
+ app:showAsAction="ifRoom|withText" />
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/menu/locations.xml b/libraries/cyclestreets-fragments/src/main/res/menu/locations.xml
deleted file mode 100644
index d89690ae3..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/menu/locations.xml
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-fragments/src/main/res/menu/navigation_drawer.xml b/libraries/cyclestreets-fragments/src/main/res/menu/navigation_drawer.xml
new file mode 100644
index 000000000..b3e9dee57
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/menu/navigation_drawer.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/menu/route_map.xml b/libraries/cyclestreets-fragments/src/main/res/menu/route_map.xml
index 87c22d28a..e836182c5 100644
--- a/libraries/cyclestreets-fragments/src/main/res/menu/route_map.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/menu/route_map.xml
@@ -1,20 +1,29 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto">
+
+ app:showAsAction="ifRoom|withText" />
+ app:showAsAction="ifRoom|withText" />
+ app:ico_icon="gmd_history"
+ app:ico_color="?android:textColorSecondary"
+ app:ico_size="24dp"
+ android:title="@string/menu_saved_routes"
+ app:showAsAction="never" />
-
\ No newline at end of file
+ app:ico_icon="gmd_filter_1"
+ app:ico_color="?android:textColorSecondary"
+ app:ico_size="24dp"
+ android:title="@string/menu_route_by_number"
+ app:showAsAction="never" />
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values-de/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..2dbd96bc0
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/values-de/strings.xml
@@ -0,0 +1,65 @@
+
+
+
+ Finde Route
+ Load route
+ Füge Wegpunkt hinzu
+ CycleStreets-Blog-Update
+ Blog-Update
+ Starte Markierung
+ Beende Markierung
+ Wegpunkt %d
+ Neue Tour starten?
+
+
+ Tour planen
+ Gespeicherte Touren
+ Öffne Tour nach Zahl
+ Ort suchen
+ Einstellungen
+ Öffnen
+ Umbenennen
+ Bearbeiten
+ Entfernen
+
+ Reiseplan
+ Geländeprofil
+
+
+ Finden
+ Ort auswählen
+
+
+ Starte LiveRide
+ Beende LiveRide
+ Neuer Ort
+
+ Füge einen neuen Ort mit dem Knopf oben hinzu.\nTippe und halte auf einen Ort in der Liste, um ihn zu bearbeiten oder zu entfernen.
+
+ COâ‚‚-Ersparnis
+ Kalorien:
+ Zeit:
+ Reisenummer:
+ Total elevation gain:
+ Total elevation loss:
+ Tour
+
+ Füge einen Namen oder eine Adresse ein
+
+ Richtung
+ Straße
+ 00m00
+ 0m
+ (0km)
+ Informationen zur Routenführung werden hier angezeigt\nwenn eine Tour geplant ist.
+
+ Platziere eine Markierung auf die Karte und benenne dann den Ort.
+ Ortsname :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ Tour
+ Details
+ %1$s Tour, %2$s
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values-es/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..90f8c864d
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/values-es/strings.xml
@@ -0,0 +1,65 @@
+
+
+
+ Encontrar ruta
+ Load route
+ Añadir punto en el camino
+ Actualización del blog CycleStreets
+ Actualización del Blog
+ Comienzo de marcador
+ Finalización del marcador
+ Punto en el camino %d
+ ¿Comenzar una nueva ruta?
+
+
+ Planear una ruta
+ Rutas guardadas
+ Abrir ruta por su número
+ Encontrar lugar
+ Configuración
+ Abrir
+ Renombrar
+ Editar
+ Eliminar
+
+ Itinerario
+ Elevación
+
+
+ Encontrar
+ Elige lugar
+
+
+ Comenzar Vuelta en Directo
+ Cerrar Vuelta en Directo
+ Nueva Localización
+
+ Añade nuevas localizaciones usando el botón de arriba.\nTocar y mantener en una localización en la lista para editar o eliminar.
+
+ COâ‚‚ ahorrado:
+ Calorias:
+ Tiempo:
+ Número de viajes:
+ Total elevation gain:
+ Total elevation loss:
+ Ruta
+
+ Indique nombre o dirección
+
+ dirección
+ Calle
+ 00m00
+ 0m
+ (0km)
+ La información Turn-by-turn de la ruta se mostrará \naquà una vez se planee una ruta.
+
+ Coloca la marca en el mapa y después nombra la localización.
+ Nombre de la localización :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ Ruta
+ detalles
+ %1$s ruta, %2$s
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values-fr/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values-fr/strings.xml
new file mode 100644
index 000000000..f8e70b0f4
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/values-fr/strings.xml
@@ -0,0 +1,65 @@
+
+
+
+ Trouver la route
+ Load route
+ Ajouter un point sur chemin
+ CycleStreets blog à jour
+ Blog à jour
+ Lancer le marqueur
+ Terminer le marqueur
+ point sur chemin %d
+ Démarrer une nouvelle route?
+
+
+ Planifier un itinéraire
+ Les routes enregistrées
+ Ouvrir la route par numéro
+ Chercher un lieu
+ Paramètres
+ Ouvrir
+ Renommer
+ Editer
+ Effacer
+
+ Itinéraire
+ Élévation
+
+
+ Trouver
+ Choisissez Lieu
+
+
+ Démarrer en direct Tour
+ Terminer en direct Tour
+ Nouvel emplacement
+
+ Ajouter de nouveaux emplacements en utilisant le bouton ci-dessus.\nTouchez et maintenez un emplacement dans la liste pour modifier ou supprimer.
+
+ COâ‚‚ sauvegarde:
+ Calories:
+ Temps:
+ numéro Journey:
+ Total elevation gain:
+ Total elevation loss:
+ route
+
+ Placez le nom ou l\'adresse
+
+ direction
+ Rue
+ 00m00
+ 0m
+ (0km)
+ virage par virage les informations sur l\'itinéraire seront affichées\nici une fois qu\'un itinéraire est programmé.
+
+ Placez la marque sur la carte et puis nommez l\'emplacement.
+ Nom de la localisation :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ Route
+ Détails
+ %1$s route, %2$s
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values-it/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..3326a70ca
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/values-it/strings.xml
@@ -0,0 +1,65 @@
+
+
+
+ Cerca percorso
+ Load route
+ Aggiungi destinazione
+ News dal blog di CycleStreets
+ Blog news
+ Punto di partenza
+ Tappa %d
+ Creare un nuovo tragitto?
+
+
+ Calcola percorso
+ Apri percorso per numero
+ Cerca luogo
+ Impostazioni
+ Apri
+ Rinomina
+ Modifica
+ Elimina
+
+ Itinerario
+
+
+ Cerca
+ Scegli Luogo
+
+
+ Inizia LiveRide
+ Termina LiveRide
+ Nuova Destinazione
+
+ Aggiungi una nuova località usando il pulsante qui sopra.\nTocca e tieni premuto su una località nella lista per modificarla o cancellarla.
+
+ COâ‚‚ evitata:
+ Calorie:
+ Tempo:
+ Itinerario numero:
+ Total elevation gain:
+ Total elevation loss:
+ percorso
+
+ Nome del luogo o indirizzo
+
+ Strada
+ 00m00
+ 0m
+ (0km)
+ Le indicazioni svolta per svolta saranno mostrate\nqui dopo che il percorso sarà calcolato.
+
+ Piazza il puntatore sulla mappa e dai un nome al luogo.
+ Nome del luogo :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ "Percorso "
+ Dettagli
+ %1$s percorso, %2$s
+ Dislivello
+ direzione
+ Percorsi salvati
+ Punto di arrivo
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values-pt/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values-pt/strings.xml
new file mode 100644
index 000000000..678c4814f
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/values-pt/strings.xml
@@ -0,0 +1,65 @@
+
+
+
+ Encontrar Rota
+ Load route
+ Adicionar destino
+ Atualizações do blog CycleStreets
+ Atualizar Blog
+ Iniciar marcador
+ Terminar marcador
+ Destino %d
+ Iniciar nova rota?
+
+
+ Planejar uma rota
+ Rotas salvas
+ Abrir rota por número
+ Encontrar lugar
+ Configurações
+ Abrir
+ Renomear
+ Editar
+ Deletar
+
+ Intierário
+ Elevação
+
+
+ Encontrar
+ Escolher lugar
+
+
+ Iniciar LiveRide
+ Terminar LiveRide
+ Nova localização
+
+ Adicione novas localizações usando o botão acima.\nToque e segure em uma localização na listapara editar ou deletar.
+
+ Economia de COâ‚‚:
+ Calorias:
+ Hora:
+ Número da jornada:
+ Total elevation gain:
+ Total elevation loss:
+ Rota
+
+ Nome ou endereço do local
+
+ caminho
+ Rua
+ 00m00
+ 0m
+ (0km)
+ Navegação curva-a-curva vai ser mostrada\naqui quando a rota for planejada.
+
+ Coloque a marca no mapa e nomeie a nova localização.
+ Nome do local :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ Rota
+ Detalhes
+ %1$s rota, %2$s
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values-ru/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values-ru/strings.xml
new file mode 100644
index 000000000..4aedc5e39
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/values-ru/strings.xml
@@ -0,0 +1,65 @@
+
+
+
+ Ðайти маршрут
+ Load route
+ Добавить точку
+ Обновление блога CycleStreets
+ Обновлении блога
+ ЗапуÑтить маркер
+ Закончить маркер
+ Точка %d
+ Ðачать новый маршрут?
+
+
+ Планирование маршрута
+ Сохраненные маршруты
+ Открыть маршрут по номеру
+ Ðайти меÑто
+ ÐаÑтройки
+ Открыть
+ Переименовывать
+ Редактировать
+ Удалить
+
+ Маршрут
+ Ð’Ñ‹Ñота
+
+
+ Ðайти
+ Выберите меÑто
+
+
+ Ðачатъ LiveRide
+ Закончить LiveRide
+ Ðовое меÑто
+
+ Добавить новые меÑта Ñ Ð¿Ð¾Ð¼Ð¾Ñ‰ÑŒÑŽ кнопки выше.\nÐажмите и удерживайте меÑтоположение в ÑпиÑке, чтобы изменить или удалить.
+
+ COâ‚‚ ÑкономиÑ:
+ Калории:
+ ВремÑ:
+ КоличеÑтво ПутешеÑтвие:
+ Total elevation gain:
+ Total elevation loss:
+ маршрут
+
+ ПомеÑтите Ð¸Ð¼Ñ Ð¸Ð»Ð¸ адреÑ
+
+ Ðаправление
+ Улица
+ 00м00
+ 0м
+ (0км)
+ Turn-by-turn route information will be shown\nhere once a route is planned.
+
+ ПомеÑтите метку на карте, а затем назовите меÑтоположение.
+ Ðазвание меÑÑ‚Ð¾Ð¿Ð¾Ð»Ð¾Ð¶ÐµÐ½Ð¸Ñ :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ Маршрут
+ Детали
+ %1$s маршрут, %2$s
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values-tr/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values-tr/strings.xml
new file mode 100644
index 000000000..b93f97eae
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/values-tr/strings.xml
@@ -0,0 +1,65 @@
+
+
+
+ Güzergâh bul
+ Load route
+ Ara nokta ekle
+ CycleStreets blogu güncelle
+ Blog update
+ İşaretçi başlat
+ İşaretçi bitir
+ Ara nokta %d
+ Yeni güzergaha başla?
+
+
+ Güzergahı planla
+ Güzergahları kaydet
+ Güzergâh numarasına göre aç
+ Yer bul
+ Ayarlar
+ Aç
+ İsim değiştir
+ Düzenle
+ Sil
+
+ İzlenecek Yol
+ Yükselti
+
+
+ Bul
+ Yer Seç
+
+
+ LiveRide Başlangıç
+ LiveRide Sonu
+ Yeni Konum
+
+ Yukarıdaki düğmeyi kullanarak yeni konumlar ekle. \n Düzenlemek veya silmek için listedeki bir konuma dokunun ve basılı tutun.
+
+ COâ‚‚ birikimi:
+ Kalori:
+ Zaman:
+ Seyahat numarası:
+ Total elevation gain:
+ Total elevation loss:
+ güzergâh
+
+ Yer adı ya da adresi
+
+ yön
+ Cadde
+ 00m00
+ 0m
+ (0km)
+ Turn-by-turn güzergâh bilgileri planlanırken bir kez gösterilecek.
+
+ Harita üzerinde ve daha sonra işareti koyun ve konumu adlandırın.
+ Konum adı :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ Güzergâh
+ Detaylar
+ %1$s güzergâh, %2$s
+
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/values/colors.xml b/libraries/cyclestreets-fragments/src/main/res/values/colors.xml
deleted file mode 100755
index 2624fa272..000000000
--- a/libraries/cyclestreets-fragments/src/main/res/values/colors.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- #0000ff
-
diff --git a/libraries/cyclestreets-fragments/src/main/res/values/dimens.xml b/libraries/cyclestreets-fragments/src/main/res/values/dimens.xml
index 3d6e299df..aaaa29de6 100644
--- a/libraries/cyclestreets-fragments/src/main/res/values/dimens.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/values/dimens.xml
@@ -1,9 +1,9 @@
- 632.0dip
- 598.0dip
- 632.0dip
- 598.0dip
+ 632.0dp
+ 598.0dp
+ 632.0dp
+ 598.0dp
diff --git a/libraries/cyclestreets-fragments/src/main/res/values/strings.xml b/libraries/cyclestreets-fragments/src/main/res/values/strings.xml
index d61730904..1374ffa82 100644
--- a/libraries/cyclestreets-fragments/src/main/res/values/strings.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/values/strings.xml
@@ -1,49 +1,68 @@
- Open navigation drawer
- Close navigation drawer
- CycleStreets
- Settings
- About
- Blog
- Find route
- Add waypoint
- CycleStreets blog update
- Blog update
- Start marker
- Finish marker
- Waypoint %d
- Start a new route?
-
-
- Plan a route
- Saved routes
- Open route by number
- Find place
- Settings
- Open
- Rename
- Edit
- Delete
-
- Itinerary
- Elevation
-
-
- Find
- Choose Place
-
-
- Start LiveRide
- End LiveRide
- New Location
- Hello blank fragment
-
- Add new locations using the button above.\nTouch and hold a location in the list to edit or delete.
- COâ‚‚ saving:
- Calories:
- Time:
- Journey number:
+ Find route
+ Load route
+ Add waypoint
+ CycleStreets blog update
+ Blog update
+ Start marker
+ Finish marker
+ Waypoint %d
+ Start a new route?
+
+
+ Plan a route
+ Saved routes
+ Open route by number
+ Find a place
+ Settings
+ Open
+ Rename
+ Edit
+ Delete
+
+ Itinerary
+ Elevation
+
+
+ Find
+ Choose Place
+
+
+ Start LiveRide
+ End LiveRide
+ New Location
+
+ Add new locations using the button above.\nTouch and hold a location in the list to edit or delete.
+
+ COâ‚‚ saving:
+ Calories:
+ Time:
+ Journey number:
+ Total elevation gain:
+ Total elevation loss:
+ Fruit
+ route
+
+ net.cyclestreets
+
+ Place name or address
+
+ direction
+ Street
+ 00m00
+ 0m
+ (0km)
+ Turn-by-turn route information will be shown\nhere once a route is planned.
+
+ Place the mark on the map and then name the location.
+ Location name :
+ Routes previously planned on CycleStreets can be loaded here, by entering the journey number shown on the webpage or itinerary
+ CycleStreets journey number
+
+ Route
+ Details
+ %1$s route, %2$s
diff --git a/libraries/cyclestreets-fragments/src/main/res/values/styles.xml b/libraries/cyclestreets-fragments/src/main/res/values/styles.xml
index 9b634e6f7..9e8da0e4e 100644
--- a/libraries/cyclestreets-fragments/src/main/res/values/styles.xml
+++ b/libraries/cyclestreets-fragments/src/main/res/values/styles.xml
@@ -18,4 +18,4 @@
- @android:color/transparent
- true
-
\ No newline at end of file
+
diff --git a/libraries/cyclestreets-fragments/src/main/res/xml/file_paths.xml b/libraries/cyclestreets-fragments/src/main/res/xml/file_paths.xml
new file mode 100644
index 000000000..89f038c24
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/main/res/xml/file_paths.xml
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/libraries/cyclestreets-fragments/src/test/java/net/cyclestreets/MainSupportTest.kt b/libraries/cyclestreets-fragments/src/test/java/net/cyclestreets/MainSupportTest.kt
new file mode 100644
index 000000000..dd859fa99
--- /dev/null
+++ b/libraries/cyclestreets-fragments/src/test/java/net/cyclestreets/MainSupportTest.kt
@@ -0,0 +1,59 @@
+package net.cyclestreets
+
+import android.net.Uri
+import net.cyclestreets.LaunchIntent.Type.JOURNEY
+import net.cyclestreets.LaunchIntent.Type.LOCATION
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.annotation.Config
+
+
+@Config(manifest = Config.NONE, sdk = [33])
+@RunWith(RobolectricTestRunner::class)
+class MainSupportTest {
+
+ @Test
+ fun cycleStJourney() {
+ val launchIntent = determineLaunchIntent(Uri.parse("http://cycle.st/j61207326"))!!
+ assertThat(launchIntent.type).isEqualTo(JOURNEY)
+ assertThat(launchIntent.id).isEqualTo(61207326)
+ }
+
+ @Test
+ fun cycleStLocation() {
+ val launchIntent = determineLaunchIntent(Uri.parse("https://cycle.st/p93348"))!!
+ assertThat(launchIntent.type).isEqualTo(LOCATION)
+ assertThat(launchIntent.id).isEqualTo(93348)
+ }
+
+ @Test
+ fun mobileJourney() {
+ val launchIntent = determineLaunchIntent(Uri.parse("http://m.cyclestreets.net/journey/#57201887/balanced"))!!
+ assertThat(launchIntent.type).isEqualTo(JOURNEY)
+ assertThat(launchIntent.id).isEqualTo(57201887)
+ }
+
+ @Test
+ fun mobileLocation() {
+ val launchIntent = determineLaunchIntent(Uri.parse("https://m.cyclestreets.net/location/#5678"))!!
+ assertThat(launchIntent.type).isEqualTo(LOCATION)
+ assertThat(launchIntent.id).isEqualTo(5678)
+ }
+
+ @Test
+ fun cycleStreetsNetJourney() {
+ val launchIntent = determineLaunchIntent(Uri.parse("http://cyclestreets.net/journey/61207326/#balanced"))!!
+ assertThat(launchIntent.type).isEqualTo(JOURNEY)
+ assertThat(launchIntent.id).isEqualTo(61207326)
+ }
+
+ @Test
+ fun cycleStreetsNetLocation() {
+ val launchIntent = determineLaunchIntent(Uri.parse("https://www.cyclestreets.net/location/1234"))!!
+ assertThat(launchIntent.type).isEqualTo(LOCATION)
+ assertThat(launchIntent.id).isEqualTo(1234)
+ }
+
+}
diff --git a/libraries/cyclestreets-track/build.gradle b/libraries/cyclestreets-track/build.gradle
index bb2596c58..35d26e58f 100644
--- a/libraries/cyclestreets-track/build.gradle
+++ b/libraries/cyclestreets-track/build.gradle
@@ -1,5 +1,5 @@
evaluationDependsOn(':libraries:cyclestreets-view')
dependencies {
- compile project(':libraries:cyclestreets-view')
+ implementation project(':libraries:cyclestreets-view')
}
diff --git a/libraries/cyclestreets-track/gradle/wrapper/gradle-wrapper.jar b/libraries/cyclestreets-track/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index c97a8bdb9..000000000
Binary files a/libraries/cyclestreets-track/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/libraries/cyclestreets-track/gradle/wrapper/gradle-wrapper.properties b/libraries/cyclestreets-track/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 46a9213ec..000000000
--- a/libraries/cyclestreets-track/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Sat May 14 10:07:49 BST 2016
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip
diff --git a/libraries/cyclestreets-track/gradlew b/libraries/cyclestreets-track/gradlew
deleted file mode 100755
index 91a7e269e..000000000
--- a/libraries/cyclestreets-track/gradlew
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn ( ) {
- echo "$*"
-}
-
-die ( ) {
- echo
- echo "$*"
- echo
- exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
-esac
-
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=$((i+1))
- done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
-fi
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libraries/cyclestreets-track/gradlew.bat b/libraries/cyclestreets-track/gradlew.bat
deleted file mode 100644
index aec99730b..000000000
--- a/libraries/cyclestreets-track/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/libraries/cyclestreets-track/src/main/AndroidManifest.xml b/libraries/cyclestreets-track/src/main/AndroidManifest.xml
index ea1672bf8..e72914fa0 100644
--- a/libraries/cyclestreets-track/src/main/AndroidManifest.xml
+++ b/libraries/cyclestreets-track/src/main/AndroidManifest.xml
@@ -1,7 +1,10 @@
-
+
+
+
+
-
+
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Controller.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Controller.java
index 2b752664f..7aef9d6ee 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Controller.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Controller.java
@@ -7,9 +7,7 @@
import android.content.ServiceConnection;
import android.os.IBinder;
-class Controller
- implements TrackerControl,
- ServiceConnection {
+class Controller implements TrackerControl, ServiceConnection {
public static TrackerControl create(final Activity context, final TrackListener listener) {
Controller control = new Controller(context, listener);
@@ -17,7 +15,7 @@ public static TrackerControl create(final Activity context, final TrackListener
context.bindService(rService, control, Context.BIND_AUTO_CREATE);
return control;
- } // create
+ }
private final Activity context_;
private final TrackListener listener_;
@@ -30,7 +28,7 @@ private Controller(
final TrackListener listener) {
context_ = context;
listener_ = listener;
- } // Controller
+ }
@Override
public void onServiceConnected(
@@ -42,7 +40,7 @@ public void onServiceConnected(
if (shouldStart_)
rs_.startRecording();
- } // onServiceConnected
+ }
@Override
public void onServiceDisconnected(ComponentName name) {}
@@ -53,7 +51,7 @@ public void start() {
shouldStart_ = true;
else
rs_.startRecording();
- } // start
+ }
@Override
public void stop() {
@@ -71,5 +69,5 @@ public void stop() {
else
listener_.abandoned(trip);
}
- } // stop
-} // Controller
+ }
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/CyclePoint.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/CyclePoint.java
index e73879c8e..1a46e3427 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/CyclePoint.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/CyclePoint.java
@@ -1,57 +1,52 @@
-package net.cyclestreets.track;
-
-import android.os.Parcel;
-import android.os.Parcelable;
-
-import org.osmdroid.util.GeoPoint;
-
-public class CyclePoint extends GeoPoint {
- public float accuracy;
- public double altitude;
- public float speed;
- public long time;
-
- public CyclePoint(int lat, int lgt, long currentTime) {
- super(lat, lgt);
- time = currentTime;
- }
-
- public CyclePoint(int lat, int lgt, long currentTime, float accuracy, double altitude, float speed) {
- super(lat, lgt);
- time = currentTime;
- this.accuracy = accuracy;
- this.altitude = altitude;
- this.speed = speed;
- }
-
- public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
- @Override
- public CyclePoint createFromParcel(final Parcel in) {
- return new CyclePoint(in);
- }
-
- @Override
- public CyclePoint[] newArray(final int size) {
- return new CyclePoint[size];
- }
- };
-
- private CyclePoint(final Parcel in) {
- super(in.readInt(), in.readInt());
- this.time = in.readLong();
- this.accuracy = in.readFloat();
- this.altitude = in.readDouble();
- this.speed = in.readFloat();
- }
-
- @Override
- public void writeToParcel(final Parcel out, final int flags) {
- out.writeInt(getLatitudeE6());
- out.writeInt(getLongitudeE6());
- out.writeLong(this.time);
- out.writeFloat(this.accuracy);
- out.writeDouble(this.altitude);
- out.writeFloat(this.speed);
- }
-
-} // class CyclePoint
+package net.cyclestreets.track;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import org.osmdroid.util.GeoPoint;
+
+public class CyclePoint extends GeoPoint {
+ public float accuracy;
+ public double altitude;
+ public float speed;
+ public long time;
+
+ public CyclePoint(double lat, double lgt, long currentTime, float accuracy, double altitude, float speed) {
+ super(lat, lgt);
+ time = currentTime;
+ this.accuracy = accuracy;
+ this.altitude = altitude;
+ this.speed = speed;
+ }
+
+ public static final Parcelable.Creator CREATOR = new Parcelable.Creator() {
+ @Override
+ public CyclePoint createFromParcel(final Parcel in) {
+ return new CyclePoint(in);
+ }
+
+ @Override
+ public CyclePoint[] newArray(final int size) {
+ return new CyclePoint[size];
+ }
+ };
+
+ private CyclePoint(final Parcel in) {
+ super(in.readDouble(), in.readDouble());
+ this.time = in.readLong();
+ this.accuracy = in.readFloat();
+ this.altitude = in.readDouble();
+ this.speed = in.readFloat();
+ }
+
+ @Override
+ public void writeToParcel(final Parcel out, final int flags) {
+ out.writeDouble(getLatitude());
+ out.writeDouble(getLongitude());
+ out.writeLong(this.time);
+ out.writeFloat(this.accuracy);
+ out.writeDouble(this.altitude);
+ out.writeFloat(this.speed);
+ }
+
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/DbAdapter.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/DbAdapter.java
index 0f3a86f21..d09fbf5ff 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/DbAdapter.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/DbAdapter.java
@@ -1,353 +1,354 @@
-package net.cyclestreets.track;
-
-import android.content.ContentValues;
-import android.content.Context;
-import android.database.Cursor;
-import android.database.SQLException;
-import android.database.sqlite.SQLiteDatabase;
-import android.database.sqlite.SQLiteOpenHelper;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class DbAdapter {
- private static final int DATABASE_VERSION = 22;
-
- public static final String K_TRIP_ROWID = "_id";
- public static final String K_TRIP_PURP = "purp";
- public static final String K_TRIP_START = "start";
- public static final String K_TRIP_END = "endtime";
- public static final String K_TRIP_FANCYSTART = "fancystart";
- public static final String K_TRIP_FANCYINFO = "fancyinfo";
- public static final String K_TRIP_NOTE = "note";
- public static final String K_TRIP_AGE = "age";
- public static final String K_TRIP_GENDER = "gender";
- public static final String K_TRIP_DISTANCE = "distance";
- public static final String K_TRIP_STATUS = "status";
- public static final String K_TRIP_EXPERIENCE = "experience";
-
- public static final String K_POINT_ROWID = "_id";
- public static final String K_POINT_TRIP = "trip";
- public static final String K_POINT_TIME = "time";
- public static final String K_POINT_LAT = "lat";
- public static final String K_POINT_LGT = "lgt";
- public static final String K_POINT_ACC = "acc";
- public static final String K_POINT_ALT = "alt";
- public static final String K_POINT_SPEED = "speed";
-
- private static final String TAG = "DbAdapter";
- private static final String TABLE_CREATE_TRIPS = "create table trips "
- + "(_id integer primary key autoincrement, purp text, start integer, endtime integer, "
- + "fancystart text, fancyinfo text, distance float, note text, age text, gender text, experience text, "
- + "status integer);";
-
- private static final String TABLE_CREATE_COORDS = "create table coords "
- + "(_id integer primary key autoincrement, "
- + "trip integer, lat int, lgt int, "
- + "time double, acc float, alt double, speed float);";
-
- private static final String DATABASE_NAME = "data";
- private static final String DATA_TABLE_TRIPS = "trips";
- private static final String DATA_TABLE_COORDS = "coords";
-
- private final Context context_;
- private DatabaseHelper dbHelper_;
- private SQLiteDatabase db_;
-
- private static class DatabaseHelper extends SQLiteOpenHelper {
- DatabaseHelper(Context context) {
- super(context, DATABASE_NAME, null, DATABASE_VERSION);
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- db.execSQL(TABLE_CREATE_TRIPS);
- db.execSQL(TABLE_CREATE_COORDS);
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
- if (oldVersion < 22)
- db.execSQL("alter table " + DATA_TABLE_TRIPS + " add column experience text");
- } // onUpgrade
- } // DatabaseHelper
-
- public static int unfinishedTrip(final Context context) {
- final DbAdapter db = new DbAdapter(context.getApplicationContext());
- db.openReadOnly();
-
- Cursor c = null;
- try {
- c = db.db_.query(DATA_TABLE_TRIPS,
- new String[]{ K_TRIP_ROWID },
- K_TRIP_STATUS + "=" + TripData.STATUS_RECORDING_COMPLETE,
- null, null, null, null);
- if (c.getCount() != 0) {
- c.moveToFirst();
- return c.getInt(c.getColumnIndex(K_TRIP_ROWID));
- } // if ...
- } finally {
- c.close();
- db.close();
- }
-
- return -1;
- } // availableForUpload
-
- public static List unUploadedTrips(final Context context) {
- final List tripIds = unUploadedTripIds(context);
-
- final List tripData = new ArrayList<>();
- for (int id : tripIds)
- tripData.add(TripData.fetchTrip(context, id));
-
- return tripData;
- } // unUploadedTrips
-
- public static List unUploadedTripIds(final Context context) {
- final DbAdapter db = new DbAdapter(context.getApplicationContext());
- db.openReadOnly();
-
- final List result = new ArrayList<>();
-
- Cursor c = null;
- try {
- c = db.db_.query(DATA_TABLE_TRIPS,
- new String[]{ K_TRIP_ROWID },
- K_TRIP_STATUS + "=" + TripData.STATUS_COMPLETE_FAILED,
- null, null, null, null);
- c.moveToFirst();
- while(!c.isAfterLast()) {
- int id = c.getInt(c.getColumnIndex(K_TRIP_ROWID));
- result.add(id);
- c.moveToNext();
- } // while
- } finally {
- c.close();
- db.close();
- }
-
- return result;
- } // unUploadedTrips
-
- public DbAdapter(final Context ctx) {
- context_ = ctx;
- }
-
- public DbAdapter open() throws SQLException {
- dbHelper_ = new DatabaseHelper(context_);
- db_ = dbHelper_.getWritableDatabase();
- return this;
- }
-
- public DbAdapter openReadOnly() throws SQLException {
- dbHelper_ = new DatabaseHelper(context_);
- db_ = dbHelper_.getReadableDatabase();
- return this;
- }
-
- public void close() {
- dbHelper_.close();
- }
-
- // #### Coordinate table methods ####
- public boolean addCoordToTrip(long tripid, CyclePoint pt) {
- boolean success = true;
-
- // Add the latest point
- ContentValues rowValues = new ContentValues();
- rowValues.put(K_POINT_TRIP, tripid);
- rowValues.put(K_POINT_LAT, pt.getLatitudeE6());
- rowValues.put(K_POINT_LGT, pt.getLongitudeE6());
- rowValues.put(K_POINT_TIME, pt.time);
- rowValues.put(K_POINT_ACC, pt.accuracy);
- rowValues.put(K_POINT_ALT, pt.altitude);
- rowValues.put(K_POINT_SPEED, pt.speed);
-
- success = success && (db_.insert(DATA_TABLE_COORDS, null, rowValues) > 0);
-
- // And update the trip stats
- rowValues = new ContentValues();
- rowValues.put(K_TRIP_END, pt.time);
-
- success = success && (db_.update(DATA_TABLE_TRIPS, rowValues, K_TRIP_ROWID + "=" + tripid, null) > 0);
-
- return success;
- }
-
- public boolean deleteAllCoordsForTrip(long tripid) {
- return db_.delete(DATA_TABLE_COORDS, K_POINT_TRIP + "=" + tripid, null) > 0;
- }
-
- public Cursor fetchAllCoordsForTrip(long tripid) {
- try {
- Cursor mCursor = db_.query(true, DATA_TABLE_COORDS, new String[] {
- K_POINT_LAT, K_POINT_LGT, K_POINT_TIME,
- K_POINT_ACC, K_POINT_ALT, K_POINT_SPEED },
- K_POINT_TRIP + "=" + tripid,
- null, null, null, K_POINT_TIME, null);
-
- if (mCursor != null) {
- mCursor.moveToFirst();
- }
- return mCursor;
- } catch (Exception e) {
- //Log.v("GOT!",e.toString());
- return null;
- }
- }
-
- // #### Trip table methods ####
-
- /**
- * Create a new trip using the data provided. If the trip is successfully
- * created return the new rowId for that trip, otherwise return a -1 to
- * indicate failure.
- */
- private long createTrip(String purp,
- long starttime,
- String fancystart,
- String note) {
- ContentValues initialValues = new ContentValues();
- initialValues.put(K_TRIP_PURP, purp);
- initialValues.put(K_TRIP_START, starttime);
- initialValues.put(K_TRIP_FANCYSTART, fancystart);
- initialValues.put(K_TRIP_NOTE, note);
- initialValues.put(K_TRIP_STATUS, TripData.STATUS_RECORDING);
-
- return db_.insert(DATA_TABLE_TRIPS, null, initialValues);
- }
-
- public long createTrip() {
- return createTrip("", System.currentTimeMillis()/1000, "", "");
- }
-
- /**
- * Delete the trip with the given rowId
- *
- * @param rowId
- * id of note to delete
- * @return true if deleted, false otherwise
- */
- public boolean deleteTrip(long rowId) {
- return db_.delete(DATA_TABLE_TRIPS, K_TRIP_ROWID + "=" + rowId, null) > 0;
- }
-
- public float totalDistance() {
- try {
- float distance = 0;
-
- Cursor c = db_.query(DATA_TABLE_TRIPS, new String[] { K_TRIP_DISTANCE }, null, null, null, null, null);
- c.moveToFirst();
-
- while (!c.isAfterLast()) {
- distance += c.getFloat(c.getColumnIndex("distance"));
- c.moveToNext();
- }
-
- c.close();
-
- return distance;
- }
- catch(RuntimeException e) {
- String s = e.getMessage();
- throw new RuntimeException(e);
- }
- } // totalDistance
-
- /**
- * Return a Cursor over the list of all notes in the database
- *
- * @return Cursor over all trips
- */
- public Cursor fetchAllTrips() {
- Cursor c = db_.query(DATA_TABLE_TRIPS, new String[] { K_TRIP_ROWID,
- K_TRIP_PURP, K_TRIP_START, K_TRIP_FANCYSTART, K_TRIP_NOTE, K_TRIP_FANCYINFO },
- null, null, null, null, "-" + K_TRIP_START);
- if (c != null && c.getCount()>0) {
- c.moveToFirst();
- }
- return c;
- }
-
- public Cursor fetchUnsentTrips() {
- Cursor c = db_.query(DATA_TABLE_TRIPS, new String[] { K_TRIP_ROWID },
- K_TRIP_STATUS + "=" + TripData.STATUS_COMPLETE_UNSENT,
- null, null, null, null);
- if (c != null && c.getCount()>0) {
- c.moveToFirst();
- }
- return c;
- }
-
- /**
- * Return a Cursor positioned at the trip that matches the given rowId
- *
- * @param rowId id of trip to retrieve
- * @return Cursor positioned to matching trip, if found
- * @throws SQLException if trip could not be found/retrieved
- */
- public Cursor fetchTrip(long rowId) throws SQLException {
- Cursor mCursor = db_.query(true, DATA_TABLE_TRIPS, new String[] {
- K_TRIP_ROWID, K_TRIP_PURP, K_TRIP_START, K_TRIP_FANCYSTART,
- K_TRIP_NOTE, K_TRIP_AGE, K_TRIP_GENDER, K_TRIP_EXPERIENCE, K_TRIP_STATUS, K_TRIP_END,
- K_TRIP_FANCYINFO, K_TRIP_DISTANCE },
- K_TRIP_ROWID + "=" + rowId,
-
- null, null, null, null, null);
- if (mCursor != null) {
- mCursor.moveToFirst();
- }
- return mCursor;
- }
-
- public boolean updateNotes(long tripid,
- String purp,
- String fancystart,
- String fancyinfo,
- String note,
- String age,
- String gender,
- String experience) {
- ContentValues initialValues = new ContentValues();
- initialValues.put(K_TRIP_PURP, purp);
- initialValues.put(K_TRIP_FANCYSTART, fancystart);
- initialValues.put(K_TRIP_NOTE, note);
- initialValues.put(K_TRIP_AGE, age);
- initialValues.put(K_TRIP_GENDER, gender);
- initialValues.put(K_TRIP_EXPERIENCE, experience);
- initialValues.put(K_TRIP_FANCYINFO, fancyinfo);
-
- return db_.update(DATA_TABLE_TRIPS,
- initialValues,
- K_TRIP_ROWID + "=" + tripid, null) > 0;
- }
-
- public boolean setDistance(long tripid, float distance) {
- ContentValues initialValues = new ContentValues();
- initialValues.put(K_TRIP_DISTANCE, distance);
-
- return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
- }
-
- public boolean setStartTime(long tripid, long starttime) {
- ContentValues initialValues = new ContentValues();
- initialValues.put(K_TRIP_START, starttime);
-
- return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
- }
-
- public boolean setEndTime(long tripid, long endTime) {
- ContentValues initialValues = new ContentValues();
- initialValues.put(K_TRIP_END, endTime);
-
- return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
- }
-
- public boolean updateTripStatus(long tripid, int tripStatus) {
- ContentValues initialValues = new ContentValues();
- initialValues.put(K_TRIP_STATUS, tripStatus);
-
- return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
- }
-}
+package net.cyclestreets.track;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteOpenHelper;
+import net.cyclestreets.util.Logging;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DbAdapter {
+ private static final int DATABASE_VERSION = 22;
+
+ public static final String K_TRIP_ROWID = "_id";
+ public static final String K_TRIP_PURP = "purp";
+ public static final String K_TRIP_START = "start";
+ public static final String K_TRIP_END = "endtime";
+ public static final String K_TRIP_FANCYSTART = "fancystart";
+ public static final String K_TRIP_FANCYINFO = "fancyinfo";
+ public static final String K_TRIP_NOTE = "note";
+ public static final String K_TRIP_AGE = "age";
+ public static final String K_TRIP_GENDER = "gender";
+ public static final String K_TRIP_DISTANCE = "distance";
+ public static final String K_TRIP_STATUS = "status";
+ public static final String K_TRIP_EXPERIENCE = "experience";
+
+ public static final String K_POINT_ROWID = "_id";
+ public static final String K_POINT_TRIP = "trip";
+ public static final String K_POINT_TIME = "time";
+ public static final String K_POINT_LAT = "lat";
+ public static final String K_POINT_LGT = "lgt";
+ public static final String K_POINT_ACC = "acc";
+ public static final String K_POINT_ALT = "alt";
+ public static final String K_POINT_SPEED = "speed";
+
+ private static final String TAG = Logging.getTag(DbAdapter.class);
+ private static final String TABLE_CREATE_TRIPS = "create table trips "
+ + "(_id integer primary key autoincrement, purp text, start integer, endtime integer, "
+ + "fancystart text, fancyinfo text, distance float, note text, age text, gender text, experience text, "
+ + "status integer);";
+
+ private static final String TABLE_CREATE_COORDS = "create table coords "
+ + "(_id integer primary key autoincrement, "
+ + "trip integer, lat int, lgt int, "
+ + "time double, acc float, alt double, speed float);";
+
+ private static final String DATABASE_NAME = "data";
+ private static final String DATA_TABLE_TRIPS = "trips";
+ private static final String DATA_TABLE_COORDS = "coords";
+
+ private final Context context_;
+ private DatabaseHelper dbHelper_;
+ private SQLiteDatabase db_;
+
+ private static class DatabaseHelper extends SQLiteOpenHelper {
+ DatabaseHelper(Context context) {
+ super(context, DATABASE_NAME, null, DATABASE_VERSION);
+ }
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL(TABLE_CREATE_TRIPS);
+ db.execSQL(TABLE_CREATE_COORDS);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ if (oldVersion < 22)
+ db.execSQL("alter table " + DATA_TABLE_TRIPS + " add column experience text");
+ }
+ }
+
+ public static int unfinishedTrip(final Context context) {
+ final DbAdapter db = new DbAdapter(context.getApplicationContext());
+ db.openReadOnly();
+
+ Cursor c = null;
+ try {
+ c = db.db_.query(DATA_TABLE_TRIPS,
+ new String[]{ K_TRIP_ROWID },
+ K_TRIP_STATUS + "=" + TripData.STATUS_RECORDING_COMPLETE,
+ null, null, null, null);
+ if (c.getCount() != 0) {
+ c.moveToFirst();
+ return c.getInt(c.getColumnIndex(K_TRIP_ROWID));
+ }
+ } finally {
+ c.close();
+ db.close();
+ }
+
+ return -1;
+ }
+
+ public static List unUploadedTrips(final Context context) {
+ final List tripIds = unUploadedTripIds(context);
+
+ final List tripData = new ArrayList<>();
+ for (int id : tripIds)
+ tripData.add(TripData.fetchTrip(context, id));
+
+ return tripData;
+ }
+
+ public static List unUploadedTripIds(final Context context) {
+ final DbAdapter db = new DbAdapter(context.getApplicationContext());
+ db.openReadOnly();
+
+ final List result = new ArrayList<>();
+
+ Cursor c = null;
+ try {
+ c = db.db_.query(DATA_TABLE_TRIPS,
+ new String[]{ K_TRIP_ROWID },
+ K_TRIP_STATUS + "=" + TripData.STATUS_COMPLETE_FAILED,
+ null, null, null, null);
+ c.moveToFirst();
+ while (!c.isAfterLast()) {
+ int id = c.getInt(c.getColumnIndex(K_TRIP_ROWID));
+ result.add(id);
+ c.moveToNext();
+ }
+ } finally {
+ c.close();
+ db.close();
+ }
+
+ return result;
+ }
+
+ public DbAdapter(final Context ctx) {
+ context_ = ctx;
+ }
+
+ public DbAdapter open() throws SQLException {
+ dbHelper_ = new DatabaseHelper(context_);
+ db_ = dbHelper_.getWritableDatabase();
+ return this;
+ }
+
+ public DbAdapter openReadOnly() throws SQLException {
+ dbHelper_ = new DatabaseHelper(context_);
+ db_ = dbHelper_.getReadableDatabase();
+ return this;
+ }
+
+ public void close() {
+ dbHelper_.close();
+ }
+
+ // #### Coordinate table methods ####
+ public boolean addCoordToTrip(long tripid, CyclePoint pt) {
+ boolean success = true;
+
+ // Add the latest point
+ ContentValues rowValues = new ContentValues();
+ rowValues.put(K_POINT_TRIP, tripid);
+ rowValues.put(K_POINT_LAT, pt.getLatitudeE6());
+ rowValues.put(K_POINT_LGT, pt.getLongitudeE6());
+ rowValues.put(K_POINT_TIME, pt.time);
+ rowValues.put(K_POINT_ACC, pt.accuracy);
+ rowValues.put(K_POINT_ALT, pt.altitude);
+ rowValues.put(K_POINT_SPEED, pt.speed);
+
+ success = success && (db_.insert(DATA_TABLE_COORDS, null, rowValues) > 0);
+
+ // And update the trip stats
+ rowValues = new ContentValues();
+ rowValues.put(K_TRIP_END, pt.time);
+
+ success = success && (db_.update(DATA_TABLE_TRIPS, rowValues, K_TRIP_ROWID + "=" + tripid, null) > 0);
+
+ return success;
+ }
+
+ public boolean deleteAllCoordsForTrip(long tripid) {
+ return db_.delete(DATA_TABLE_COORDS, K_POINT_TRIP + "=" + tripid, null) > 0;
+ }
+
+ public Cursor fetchAllCoordsForTrip(long tripid) {
+ try {
+ Cursor mCursor = db_.query(true, DATA_TABLE_COORDS, new String[] {
+ K_POINT_LAT, K_POINT_LGT, K_POINT_TIME,
+ K_POINT_ACC, K_POINT_ALT, K_POINT_SPEED },
+ K_POINT_TRIP + "=" + tripid,
+ null, null, null, K_POINT_TIME, null);
+
+ if (mCursor != null) {
+ mCursor.moveToFirst();
+ }
+ return mCursor;
+ } catch (Exception e) {
+ //Log.v("GOT!",e.toString());
+ return null;
+ }
+ }
+
+ // #### Trip table methods ####
+
+ /**
+ * Create a new trip using the data provided. If the trip is successfully
+ * created return the new rowId for that trip, otherwise return a -1 to
+ * indicate failure.
+ */
+ private long createTrip(String purp,
+ long starttime,
+ String fancystart,
+ String note) {
+ ContentValues initialValues = new ContentValues();
+ initialValues.put(K_TRIP_PURP, purp);
+ initialValues.put(K_TRIP_START, starttime);
+ initialValues.put(K_TRIP_FANCYSTART, fancystart);
+ initialValues.put(K_TRIP_NOTE, note);
+ initialValues.put(K_TRIP_STATUS, TripData.STATUS_RECORDING);
+
+ return db_.insert(DATA_TABLE_TRIPS, null, initialValues);
+ }
+
+ public long createTrip() {
+ return createTrip("", System.currentTimeMillis()/1000, "", "");
+ }
+
+ /**
+ * Delete the trip with the given rowId
+ *
+ * @param rowId
+ * id of note to delete
+ * @return true if deleted, false otherwise
+ */
+ public boolean deleteTrip(long rowId) {
+ return db_.delete(DATA_TABLE_TRIPS, K_TRIP_ROWID + "=" + rowId, null) > 0;
+ }
+
+ public float totalDistance() {
+ try {
+ float distance = 0;
+
+ Cursor c = db_.query(DATA_TABLE_TRIPS, new String[] { K_TRIP_DISTANCE }, null, null, null, null, null);
+ c.moveToFirst();
+
+ while (!c.isAfterLast()) {
+ distance += c.getFloat(c.getColumnIndex("distance"));
+ c.moveToNext();
+ }
+
+ c.close();
+
+ return distance;
+ }
+ catch (RuntimeException e) {
+ String s = e.getMessage();
+ throw new RuntimeException(e);
+ }
+ }
+
+ /**
+ * Return a Cursor over the list of all notes in the database
+ *
+ * @return Cursor over all trips
+ */
+ public Cursor fetchAllTrips() {
+ Cursor c = db_.query(DATA_TABLE_TRIPS, new String[] { K_TRIP_ROWID,
+ K_TRIP_PURP, K_TRIP_START, K_TRIP_FANCYSTART, K_TRIP_NOTE, K_TRIP_FANCYINFO },
+ null, null, null, null, "-" + K_TRIP_START);
+ if (c != null && c.getCount()>0) {
+ c.moveToFirst();
+ }
+ return c;
+ }
+
+ public Cursor fetchUnsentTrips() {
+ Cursor c = db_.query(DATA_TABLE_TRIPS, new String[] { K_TRIP_ROWID },
+ K_TRIP_STATUS + "=" + TripData.STATUS_COMPLETE_UNSENT,
+ null, null, null, null);
+ if (c != null && c.getCount()>0) {
+ c.moveToFirst();
+ }
+ return c;
+ }
+
+ /**
+ * Return a Cursor positioned at the trip that matches the given rowId
+ *
+ * @param rowId id of trip to retrieve
+ * @return Cursor positioned to matching trip, if found
+ * @throws SQLException if trip could not be found/retrieved
+ */
+ public Cursor fetchTrip(long rowId) throws SQLException {
+ Cursor mCursor = db_.query(true, DATA_TABLE_TRIPS, new String[] {
+ K_TRIP_ROWID, K_TRIP_PURP, K_TRIP_START, K_TRIP_FANCYSTART,
+ K_TRIP_NOTE, K_TRIP_AGE, K_TRIP_GENDER, K_TRIP_EXPERIENCE, K_TRIP_STATUS, K_TRIP_END,
+ K_TRIP_FANCYINFO, K_TRIP_DISTANCE },
+ K_TRIP_ROWID + "=" + rowId,
+
+ null, null, null, null, null);
+ if (mCursor != null) {
+ mCursor.moveToFirst();
+ }
+ return mCursor;
+ }
+
+ public boolean updateNotes(long tripid,
+ String purp,
+ String fancystart,
+ String fancyinfo,
+ String note,
+ String age,
+ String gender,
+ String experience) {
+ ContentValues initialValues = new ContentValues();
+ initialValues.put(K_TRIP_PURP, purp);
+ initialValues.put(K_TRIP_FANCYSTART, fancystart);
+ initialValues.put(K_TRIP_NOTE, note);
+ initialValues.put(K_TRIP_AGE, age);
+ initialValues.put(K_TRIP_GENDER, gender);
+ initialValues.put(K_TRIP_EXPERIENCE, experience);
+ initialValues.put(K_TRIP_FANCYINFO, fancyinfo);
+
+ return db_.update(DATA_TABLE_TRIPS,
+ initialValues,
+ K_TRIP_ROWID + "=" + tripid, null) > 0;
+ }
+
+ public boolean setDistance(long tripid, float distance) {
+ ContentValues initialValues = new ContentValues();
+ initialValues.put(K_TRIP_DISTANCE, distance);
+
+ return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
+ }
+
+ public boolean setStartTime(long tripid, long starttime) {
+ ContentValues initialValues = new ContentValues();
+ initialValues.put(K_TRIP_START, starttime);
+
+ return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
+ }
+
+ public boolean setEndTime(long tripid, long endTime) {
+ ContentValues initialValues = new ContentValues();
+ initialValues.put(K_TRIP_END, endTime);
+
+ return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
+ }
+
+ public boolean updateTripStatus(long tripid, int tripStatus) {
+ ContentValues initialValues = new ContentValues();
+ initialValues.put(K_TRIP_STATUS, tripStatus);
+
+ return db_.update(DATA_TABLE_TRIPS, initialValues, K_TRIP_ROWID + "=" + tripid, null) > 0;
+ }
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/IRecordService.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/IRecordService.java
index 265a036bd..8d7a1cd55 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/IRecordService.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/IRecordService.java
@@ -1,13 +1,13 @@
-package net.cyclestreets.track;
-
-import android.app.Activity;
-
-interface IRecordService {
- int getState();
-
- TripData startRecording();
- TripData stopRecording();
-
- void setListener(TrackListener ra);
- void setNotificationActivity(Class activityClass);
-}
+package net.cyclestreets.track;
+
+import android.app.Activity;
+
+interface IRecordService {
+ int getState();
+
+ TripData startRecording();
+ TripData stopRecording();
+
+ void setListener(TrackListener ra);
+ void setNotificationActivity(Class activityClass);
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/JourneyOverlay.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/JourneyOverlay.java
index dd27c9647..3070ee224 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/JourneyOverlay.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/JourneyOverlay.java
@@ -8,6 +8,7 @@
import android.graphics.Path;
import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
+import androidx.core.content.res.ResourcesCompat;
import net.cyclestreets.views.CycleMapView;
@@ -22,17 +23,9 @@ public static JourneyOverlay CompletedJourneyOverlay(final Context context,
final CycleMapView mapView,
final TripData tripData) {
return new JourneyOverlay(context, mapView, tripData);
- } // CompletedJourneyOverlay
+ }
- public static JourneyOverlay InProgressJourneyOverlay(final Context context,
- final CycleMapView mapView,
- final TripData tripData) {
- JourneyOverlay jo = new JourneyOverlay(context, mapView, tripData);
- jo.inProgress();
- return jo;
- } // InProgressJourneyOverlay
-
- static private int ROUTE_COLOUR = 0x80ff00ff;
+ private static int ROUTE_COLOUR = 0x80ff00ff;
private final CycleMapView mapView_;
private boolean initial_ = true;
@@ -42,19 +35,15 @@ public static JourneyOverlay InProgressJourneyOverlay(final Context context,
private Path ridePath_;
private int zoomLevel_ = -1;
private IGeoPoint mapCentre_;
- private final BitmapDrawable greenWisp_;
- private final BitmapDrawable redWisp_;
- private final Matrix canvasTransform_ = new Matrix();
- private final float[] transformValues_ = new float[9];
+ private final BitmapDrawable wispWpStart;
+ private final BitmapDrawable wispWpFinish;
private final Matrix bitmapTransform_ = new Matrix();
private final Paint bitmapPaint_ = new Paint();
- private boolean inProgress_ = false;
-
private JourneyOverlay(final Context context,
final CycleMapView mapView,
final TripData tripData) {
- super(context);
+ super();
mapView_ = mapView;
trip_ = tripData;
@@ -62,49 +51,39 @@ private JourneyOverlay(final Context context,
rideBrush_ = createBrush(ROUTE_COLOUR);
final Resources res = context.getResources();
- greenWisp_ = (BitmapDrawable)res.getDrawable(R.drawable.greep_wisp);
- redWisp_ = (BitmapDrawable)res.getDrawable(R.drawable.red_wisp);
- } // JourneyOverlay
-
- private void inProgress() {
- inProgress_ = true;
- } // inProgress
-
- public void update(final TripData trip) {
- trip_ = trip;
- mapView_.invalidate();
- } // update
+ wispWpStart = (BitmapDrawable)ResourcesCompat.getDrawable(res, R.drawable.green_wisp, null);
+ wispWpFinish = (BitmapDrawable)ResourcesCompat.getDrawable(res, R.drawable.red_wisp, null);
+ }
@Override
public void draw(final Canvas canvas, final MapView mapView, final boolean shadow) {
if (shadow)
return;
- if(!trip_.dataAvailable())
+ if (!trip_.dataAvailable())
return;
final IGeoPoint centre = mapView.getMapCenter();
- if(zoomLevel_ != mapView.getZoomLevel() ||
+ if (zoomLevel_ != (int)mapView.getZoomLevelDouble() ||
!centre.equals(mapCentre_)) {
ridePath_ = null;
- zoomLevel_ = mapView.getProjection().getZoomLevel();
+ zoomLevel_ = (int)mapView.getProjection().getZoomLevel();
mapCentre_ = centre;
- } // if ...
+ }
- if(ridePath_ == null || inProgress_)
+ if (ridePath_ == null)
ridePath_ = journeyPath(mapView.getProjection());
canvas.drawPath(ridePath_, rideBrush_);
- drawMarker(canvas, mapView.getProjection(), trip_.startLocation(), greenWisp_);
- if(!inProgress_)
- drawMarker(canvas, mapView.getProjection(), trip_.endLocation(), redWisp_);
+ drawMarker(canvas, mapView.getProjection(), trip_.startLocation(), wispWpStart);
+ drawMarker(canvas, mapView.getProjection(), trip_.endLocation(), wispWpFinish);
- if (initial_ && !inProgress_) {
+ if (initial_) {
mapView_.zoomToBoundingBox(trip_.boundingBox());
initial_ = false;
- } // if ...
- } // draw
+ }
+ }
private Path journeyPath(final IProjection projection) {
Path ridePath = newPath();
@@ -112,18 +91,18 @@ private Path journeyPath(final IProjection projection) {
Point screenPoint = new Point();
boolean first = true;
- for(final GeoPoint gp : trip_.journey()) {
+ for (final GeoPoint gp : trip_.journey()) {
screenPoint = projection.toPixels(gp, screenPoint);
- if(first) {
+ if (first) {
ridePath.moveTo(screenPoint.x, screenPoint.y);
first = false;
} else
ridePath.lineTo(screenPoint.x, screenPoint.y);
- } // for ...
+ }
return ridePath;
- } // drawJourney
+ }
private void drawMarker(final Canvas canvas,
final IProjection projection,
@@ -132,8 +111,9 @@ private void drawMarker(final Canvas canvas,
Point screenPoint = new Point();
projection.toPixels(location, screenPoint);
- canvas.getMatrix(canvasTransform_);
- canvasTransform_.getValues(transformValues_);
+ Matrix transform = mapView_.getMatrix();
+ float[] transformValues_ = new float[9];
+ transform.getValues(transformValues_);
final int halfWidth = marker.getIntrinsicWidth()/2;
final int halfHeight = marker.getIntrinsicHeight()/2;
@@ -141,7 +121,7 @@ private void drawMarker(final Canvas canvas,
bitmapTransform_.postScale(1/transformValues_[Matrix.MSCALE_X], 1/transformValues_[Matrix.MSCALE_Y]);
bitmapTransform_.postTranslate(screenPoint.x, screenPoint.y);
canvas.drawBitmap(marker.getBitmap(), bitmapTransform_, bitmapPaint_);
- } // drawMarker
+ }
private Paint createBrush(int colour) {
final Paint brush = new Paint();
@@ -152,13 +132,12 @@ private Paint createBrush(int colour) {
brush.setStrokeWidth(10.0f);
return brush;
- } // createBrush
+ }
private Path newPath() {
final Path path = new Path();
path.rewind();
return path;
- } // newPath
-
-} // JourneyOverlay
+ }
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/RecordingService.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/RecordingService.java
index 693106677..4d7f17da0 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/RecordingService.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/RecordingService.java
@@ -1,313 +1,318 @@
-package net.cyclestreets.track;
-
-import java.util.Timer;
-import java.util.TimerTask;
-import java.util.List;
-
-import android.app.Activity;
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.media.AudioManager;
-import android.media.SoundPool;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.Handler;
-import android.os.IBinder;
-
-public class RecordingService
- extends Service
- implements LocationListener {
- private static int updateDistance = 5; // metres
- private static int updateTime = 5000; // milliseconds
- private static final int NOTIFICATION_ID = 1;
-
- private TrackListener trackListener_;
- private Class activityClass_;
- private LocationManager locationManager_ = null;
-
- // Bike bell variables
- private static int BELL_FIRST_INTERVAL = 20;
- private static int BELL_NEXT_INTERVAL = 5;
- private static long BAIL_TIME = 300;
- private Timer tickTimer_;
- private Timer bellTimer_;
- private SoundPool soundpool_;
- private int bikebell_;
- private final Handler handler_ = new Handler();
- private final Runnable ringBell_ = new Runnable() {
- public void run() { remindUser(); }
- };
- private final Runnable tick_ = new Runnable() {
- public void run() {
- notifyUpdate();
- }
- };
-
- private float curSpeedMph_;
- private TripData trip_;
-
- public final static int STATE_IDLE = 0;
- public final static int STATE_RECORDING = 1;
- public final static int STATE_FULL = 3;
-
- private int state_ = STATE_IDLE;
-
- @Override
- public void onCreate() {
- super.onCreate();
- soundpool_ = new SoundPool(1,AudioManager.STREAM_NOTIFICATION,0);
- bikebell_ = soundpool_.load(this.getBaseContext(), R.raw.bikebell,1);
- locationManager_ = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
- } // onCreate
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- stopTimers();
- } // onDestroy
-
- @Override
- public IBinder onBind(
- final Intent intent) {
- return new ServiceBinder(this);
- }
- @Override
- public int onStartCommand(
- final Intent intent,
- final int flags,
- final int startId) {
- return Service.START_STICKY;
- } // onStartCommand
-
- private static class ServiceBinder extends Binder implements IRecordService {
- private final RecordingService rs_;
-
- public ServiceBinder(final RecordingService rs) {
- rs_ = rs;
- } // MyServiceBinder
-
- public int getState() {
- return rs_.state_;
- }
- public TripData startRecording() {
- return rs_.startRecording();
- }
- public TripData stopRecording() {
- return rs_.stopRecording();
- } // stopRecording
-
- public void setListener(
- final TrackListener ra) {
- rs_.trackListener_ = ra;
- }
- public void setNotificationActivity(
- final Class activityClass) {
- rs_.activityClass_ = activityClass;
- }
- } // class MyServiceBinder
-
- // ---end SERVICE methods -------------------------
-
- private TripData startRecording() {
- if (state_ == STATE_RECORDING)
- return trip_;
-
- startForeground(NOTIFICATION_ID, createNotification(
- "Recording ...",
- Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT));
-
- state_ = STATE_RECORDING;
- trip_ = TripData.createTrip(this);
-
- curSpeedMph_ = 0.0f;
-
- // Start listening for GPS updates!
- locationManager_.requestLocationUpdates(
- LocationManager.GPS_PROVIDER,
- updateTime,
- updateDistance,
- this);
-
- startTimers();
-
- if (trackListener_ != null)
- trackListener_.started(trip_);
-
- return trip_;
- }
-
- private TripData stopRecording() {
- if (trip_.dataAvailable())
- finishRecording();
- else
- cancelRecording();
- return trip_;
- } // stopRecording
-
- private void finishRecording() {
- state_ = STATE_FULL;
-
- clearUp();
-
- trip_.recordingStopped();
- }
-
- private void cancelRecording() {
- if (trip_ != null)
- trip_.dropTrip();
-
- clearUp();
-
- state_ = STATE_IDLE;
- } // cancelRecording
-
- private void clearUp() {
- locationManager_.removeUpdates(this);
-
- clearNotifications();
-
- stopTimers();
-
- stopForeground(true);
- } // clearUp
-
- private void startTimers() {
- bellTimer_ = new Timer();
- bellTimer_.schedule(new TimerTask() {
- @Override
- public void run() {
- handler_.post(ringBell_);
- }
- }, BELL_FIRST_INTERVAL * 60000, BELL_NEXT_INTERVAL * 60000);
-
- tickTimer_ = new Timer();
- tickTimer_.scheduleAtFixedRate(new TimerTask() {
- @Override
- public void run() {
- handler_.post(tick_);
- }
- }, 0, 1000); // every second
- } // startTimers
-
- private void stopTimers() {
- if (bellTimer_ != null) {
- bellTimer_.cancel();
- bellTimer_.purge();
- bellTimer_ = null;
- }
- if (tickTimer_ != null) {
- tickTimer_.cancel();
- tickTimer_.purge();
- tickTimer_ = null;
- }
- } // stopTimers
-
- // LocationListener implementation:
- @Override
- public void onLocationChanged(
- final Location loc) {
- updateTripStats(loc);
- trip_.addPointNow(loc);
- notifyUpdate();
- } // onLocationChanged
-
- private void updateTripStats(
- final Location newLocation) {
- final float spdConvert = 2.2369f;
-
- // Stats should only be updated if accuracy is decent
- if (newLocation.getAccuracy() > 20)
- return;
-
- // Speed data is sometimes awful, too:
- curSpeedMph_ = newLocation.getSpeed() * spdConvert;
- } // updateTripStats
-
- @Override
- public void onProviderDisabled(String arg0) {
- }
-
- @Override
- public void onProviderEnabled(String arg0) {
- }
-
- @Override
- public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
- }
- // END LocationListener implementation:
-
- private NotificationManager nm() {
- return (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
- } // nm
-
- private Notification createNotification(
- final String tickerText,
- final int flags) {
- final Notification notification = new Notification(R.drawable.icon25, tickerText, System.currentTimeMillis());
- notification.flags = flags;
- final Intent notificationIntent = new Intent(this, activityClass_);
- final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
- notification.setLatestEventInfo(this, "Cycle Hackney - Recording", "Tap to see your ongoing trip", contentIntent);
- return notification;
- } // createNotification
-
- private void showNotification(
- final String tickerText,
- final int flags) {
- final Notification notification = createNotification(tickerText, flags);
- nm().notify(NOTIFICATION_ID, notification);
- } // showNotification
-
- private void remindUser() {
- soundpool_.play(bikebell_, 1.0f, 1.0f, 1, 0, 1.0f);
-
- int minutes = (int) (trip_.secondsElapsed() / 60);
- String tickerText = String.format("Still recording (%d min)", minutes);
-
- showNotification(tickerText, Notification.FLAG_ONGOING_EVENT);
- } // remindUser
-
- private void clearNotifications() {
- nm().cancel(NOTIFICATION_ID);
- } // clearNotifications
-
- private boolean hasRiderStopped() {
- if (trip_.secondsElapsed() < BAIL_TIME)
- return false;
- if (trip_.lastPointElapsed() > BAIL_TIME)
- return true;
- if (!trip_.dataAvailable())
- return false;
-
- final List points = trip_.journey();
- final CyclePoint end = points.get(points.size()-1);
- for(int i = points.size()-1; i != 0; --i) {
- final CyclePoint cur = points.get(i);
-
- if (end.distanceTo(cur) > 100)
- return false;
-
- if ((end.time - cur.time) > BAIL_TIME)
- break;
- } // for ...
-
- return true;
- } // checkForAutoStop
-
- private void notifyUpdate() {
- if (trackListener_ == null)
- return;
-
- trackListener_.updateStatus(curSpeedMph_, trip_);
-
- if (hasRiderStopped())
- trackListener_.riderHasStopped(trip_);
- } // notifyStatusUpdate
-} // RecordingService
+package net.cyclestreets.track;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.List;
+
+import android.app.Activity;
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.media.AudioAttributes;
+import android.media.SoundPool;
+import android.os.Binder;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+
+import net.cyclestreets.CycleStreetsNotifications;
+
+import static net.cyclestreets.CycleStreetsNotifications.CHANNEL_TRACK_ID;
+
+public class RecordingService extends Service implements LocationListener {
+ private static final int updateDistance = 5; // metres
+ private static final int updateTime = 5000; // milliseconds
+ private static final int NOTIFICATION_ID = 1;
+
+ private TrackListener trackListener_;
+ private Class activityClass_;
+ private LocationManager locationManager_ = null;
+
+ // Bike bell variables
+ private static final int BELL_FIRST_INTERVAL = 20;
+ private static final int BELL_NEXT_INTERVAL = 5;
+ private static final long BAIL_TIME = 300;
+ private Timer tickTimer_;
+ private Timer bellTimer_;
+ private SoundPool soundpool_;
+ private int bikebell_;
+ private final Handler handler_ = new Handler();
+ private final Runnable ringBell_ = this::remindUser;
+ private final Runnable tick_ = this::notifyUpdate;
+
+ private float curSpeedMph_;
+ private TripData trip_;
+
+ public final static int STATE_IDLE = 0;
+ public final static int STATE_RECORDING = 1;
+ public final static int STATE_FULL = 3;
+
+ private int state_ = STATE_IDLE;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ AudioAttributes attributes = new AudioAttributes.Builder()
+ .setUsage(AudioAttributes.USAGE_NOTIFICATION)
+ .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+ .build();
+ soundpool_ = new SoundPool.Builder().setAudioAttributes(attributes).build();
+ bikebell_ = soundpool_.load(this.getBaseContext(), R.raw.bikebell,1);
+ locationManager_ = (LocationManager) getSystemService(Context.LOCATION_SERVICE);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ stopTimers();
+ }
+
+ @Override
+ public IBinder onBind(
+ final Intent intent) {
+ return new ServiceBinder(this);
+ }
+ @Override
+ public int onStartCommand(
+ final Intent intent,
+ final int flags,
+ final int startId) {
+ return Service.START_STICKY;
+ }
+
+ private static class ServiceBinder extends Binder implements IRecordService {
+ private final RecordingService rs_;
+
+ public ServiceBinder(final RecordingService rs) {
+ rs_ = rs;
+ }
+
+ public int getState() {
+ return rs_.state_;
+ }
+ public TripData startRecording() {
+ return rs_.startRecording();
+ }
+ public TripData stopRecording() {
+ return rs_.stopRecording();
+ }
+
+ public void setListener(
+ final TrackListener ra) {
+ rs_.trackListener_ = ra;
+ }
+ public void setNotificationActivity(
+ final Class activityClass) {
+ rs_.activityClass_ = activityClass;
+ }
+ }
+
+ // ---end SERVICE methods -------------------------
+
+ private TripData startRecording() {
+ if (state_ == STATE_RECORDING)
+ return trip_;
+
+ startForeground(NOTIFICATION_ID, createNotification(
+ "Recording ...",
+ Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT));
+
+ state_ = STATE_RECORDING;
+ trip_ = TripData.createTrip(this);
+
+ curSpeedMph_ = 0.0f;
+
+ // Start listening for GPS updates!
+ locationManager_.requestLocationUpdates(
+ LocationManager.GPS_PROVIDER,
+ updateTime,
+ updateDistance,
+ this);
+
+ startTimers();
+
+ if (trackListener_ != null)
+ trackListener_.started(trip_);
+
+ return trip_;
+ }
+
+ private TripData stopRecording() {
+ if (trip_.dataAvailable())
+ finishRecording();
+ else
+ cancelRecording();
+ return trip_;
+ }
+
+ private void finishRecording() {
+ state_ = STATE_FULL;
+
+ clearUp();
+
+ trip_.recordingStopped();
+ }
+
+ private void cancelRecording() {
+ if (trip_ != null)
+ trip_.dropTrip();
+
+ clearUp();
+
+ state_ = STATE_IDLE;
+ }
+
+ private void clearUp() {
+ locationManager_.removeUpdates(this);
+
+ clearNotifications();
+
+ stopTimers();
+
+ stopForeground(true);
+ }
+
+ private void startTimers() {
+ bellTimer_ = new Timer();
+ bellTimer_.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ handler_.post(ringBell_);
+ }
+ }, BELL_FIRST_INTERVAL * 60000, BELL_NEXT_INTERVAL * 60000);
+
+ tickTimer_ = new Timer();
+ tickTimer_.scheduleAtFixedRate(new TimerTask() {
+ @Override
+ public void run() {
+ handler_.post(tick_);
+ }
+ }, 0, 1000); // every second
+ }
+
+ private void stopTimers() {
+ if (bellTimer_ != null) {
+ bellTimer_.cancel();
+ bellTimer_.purge();
+ bellTimer_ = null;
+ }
+ if (tickTimer_ != null) {
+ tickTimer_.cancel();
+ tickTimer_.purge();
+ tickTimer_ = null;
+ }
+ }
+
+ // LocationListener implementation:
+ @Override
+ public void onLocationChanged(
+ final Location loc) {
+ updateTripStats(loc);
+ trip_.addPointNow(loc);
+ notifyUpdate();
+ }
+
+ private void updateTripStats(
+ final Location newLocation) {
+ final float spdConvert = 2.2369f;
+
+ // Stats should only be updated if accuracy is decent
+ if (newLocation.getAccuracy() > 20)
+ return;
+
+ // Speed data is sometimes awful, too:
+ curSpeedMph_ = newLocation.getSpeed() * spdConvert;
+ }
+
+ @Override
+ public void onProviderDisabled(String arg0) {
+ }
+
+ @Override
+ public void onProviderEnabled(String arg0) {
+ }
+
+ @Override
+ public void onStatusChanged(String arg0, int arg1, Bundle arg2) {
+ }
+ // END LocationListener implementation:
+
+ private NotificationManager nm() {
+ return (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
+ }
+
+ private Notification createNotification(final String tickerText, final int flags) {
+ final Intent notificationIntent = new Intent(this, activityClass_);
+ final PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);
+
+ Notification notification = CycleStreetsNotifications.INSTANCE.getBuilder(this, CHANNEL_TRACK_ID)
+ .setSmallIcon(R.drawable.icon25)
+ .setTicker(tickerText)
+ .setWhen(java.lang.System.currentTimeMillis())
+ .setContentTitle("Cycle Hackney - Recording")
+ .setContentText("Tap to see your ongoing trip")
+ .setContentIntent(contentIntent)
+ .build();
+ notification.flags = flags;
+ return notification;
+ }
+
+ private void showNotification(
+ final String tickerText,
+ final int flags) {
+ final Notification notification = createNotification(tickerText, flags);
+ nm().notify(NOTIFICATION_ID, notification);
+ }
+
+ private void remindUser() {
+ soundpool_.play(bikebell_, 1.0f, 1.0f, 1, 0, 1.0f);
+
+ int minutes = (int) (trip_.secondsElapsed() / 60);
+ String tickerText = String.format("Still recording (%d min)", minutes);
+
+ showNotification(tickerText, Notification.FLAG_ONGOING_EVENT);
+ }
+
+ private void clearNotifications() {
+ nm().cancel(NOTIFICATION_ID);
+ }
+
+ private boolean hasRiderStopped() {
+ if (trip_.secondsElapsed() < BAIL_TIME)
+ return false;
+ if (trip_.lastPointElapsed() > BAIL_TIME)
+ return true;
+ if (!trip_.dataAvailable())
+ return false;
+
+ final List points = trip_.journey();
+ final CyclePoint end = points.get(points.size()-1);
+ for (int i = points.size()-1; i != 0; --i) {
+ final CyclePoint cur = points.get(i);
+
+ if (end.distanceToAsDouble(cur) > 100)
+ return false;
+
+ if ((end.time - cur.time) > BAIL_TIME)
+ break;
+ }
+
+ return true;
+ }
+
+ private void notifyUpdate() {
+ if (trackListener_ == null)
+ return;
+
+ trackListener_.updateStatus(curSpeedMph_, trip_);
+
+ if (hasRiderStopped())
+ trackListener_.riderHasStopped(trip_);
+ }
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/SaveTrip.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/SaveTrip.java
index ed3f72521..a3c207649 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/SaveTrip.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/SaveTrip.java
@@ -1,286 +1,280 @@
-package net.cyclestreets.track;
-
-import java.text.DateFormat;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-import android.app.Activity;
-import android.app.Application;
-import android.content.Context;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.text.Html;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.WindowManager;
-import android.widget.AdapterView;
-import android.widget.BaseAdapter;
-import android.widget.Button;
-import android.widget.CompoundButton;
-import android.widget.EditText;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.Toast;
-import android.widget.ToggleButton;
-
-import net.cyclestreets.util.ListFactory;
-
-public class SaveTrip extends Activity
- implements View.OnClickListener, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener {
- public static void start(final Context context, final long tripid) {
- final Intent fi = new Intent(context, SaveTrip.class);
- fi.putExtra("showtrip", tripid);
- context.startActivity(fi);
- } // start
-
- public static void startWithUnsaved(final Context context) {
- final int unfinishedTrip = DbAdapter.unfinishedTrip(context);
- start(context, unfinishedTrip);
- } // startWithUnsaved
-
- private final Map purpButtons = new HashMap<>();
- private final Map purpDescriptions = new HashMap<>();
- private TripData trip_;
- private String purpose_;
- private Spinner age_;
- private Spinner gender_;
- private Spinner experience_;
- private SharedPreferences prefs_;
-
- @Override
- public void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.save);
-
- prefs_ = getSharedPreferences("PersonalInfo", Application.MODE_PRIVATE);
-
- final Bundle cmds = getIntent().getExtras();
- final long journeyId = cmds.getLong("showtrip");
- trip_ = TripData.fetchTrip(this, journeyId);
-
- // Set up trip purpose buttons
- purpose_ = "";
- setupPurposeButtons();
-
- // Discard btn
- final Button btnDiscard = viewById(R.id.ButtonDiscard);
- btnDiscard.setOnClickListener(this);
-
- // Submit btn
- final Button btnSubmit = viewById(R.id.ButtonSubmit);
- btnSubmit.setOnClickListener(this);
- btnSubmit.setEnabled(false);
-
- age_ = viewById(R.id.age);
- setupAge(age_);
-
- gender_ = viewById(R.id.gender);
- setupGender(gender_);
-
- experience_ = viewById(R.id.experience);
- setupExperience(experience_);
-
- // Don't pop up the soft keyboard until user clicks!
- getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
- } // onCreate
-
- private T viewById(final int id) { return (T)findViewById(id); }
-
- public void onClick(final View v) {
- if(v.getId() == R.id.ButtonDiscard)
- discardTrip();
-
- if(v.getId() == R.id.ButtonSubmit)
- uploadTrip();
- } // onClick
-
- private void discardTrip() {
- Toast.makeText(getBaseContext(), "Trip discarded.", Toast.LENGTH_SHORT).show();
-
- trip_.dropTrip();
-
- //CycleHackney.start(this);
- finish();
- } // discardTrip
-
- private void uploadTrip() {
- if (purpose_.equals("")) {
- // Oh no! No trip purpose!
- Toast.makeText(getBaseContext(), "You must select a trip purpose before submitting! Choose from the purposes above.", Toast.LENGTH_SHORT).show();
- return;
- }
-
- EditText notes = (EditText)findViewById(R.id.NotesField);
-
- String fancyStartTime = DateFormat.getInstance().format(trip_.startTime()*1000);
-
- // "3.5 miles in 26 minutes"
- final long minutes = trip_.secondsElapsed() / 60;
- String fancyEndInfo = String.format("%1.1f miles, %d minutes. %s",
- trip_.distanceTravelled(),
- minutes,
- notes.getEditableText().toString());
-
- // Save the trip details to the phone database. W00t!
- trip_.updateTrip(purpose_,
- fancyStartTime,
- fancyEndInfo,
- notes.getEditableText().toString(),
- age_.getSelectedItem().toString(),
- gender_.getSelectedItem().toString(),
- experience_.getSelectedItem().toString());
- trip_.metaDataComplete();
-
- SharedPreferences.Editor e = prefs_.edit();
- e.putInt("age", age_.getSelectedItemPosition());
- e.putInt("gender", gender_.getSelectedItemPosition());
- e.putInt("experience", experience_.getSelectedItemPosition());
- e.commit();
-
- TripDataUploader.upload(this, trip_);
-
- //CycleHackney.start(this);
- finish();
- } // uploadTrip
-
- private void setupAge(final Spinner age) {
- final List ages = ListFactory.list("Please select",
- "0-10",
- "11-16",
- "17-24",
- "25-44",
- "45-64",
- "65-74",
- "75-84",
- "85+");
- age.setAdapter(new SpinnerList(this, ages));
- int index = prefs_.getInt("age", 0);
- age.setSelection(index);
- age.setOnItemSelectedListener(this);
- } // setupAge
-
- private void setupGender(final Spinner gender) {
- final List genders = ListFactory.list("Please select",
- "male",
- "female",
- "prefer not to say");
- gender.setPrompt("Please select gender");
- gender.setAdapter(new SpinnerList(this, genders));
- int index = prefs_.getInt("gender", 0);
- gender.setSelection(index);
- gender.setOnItemSelectedListener(this);
- } // setupGender
-
- private void setupExperience(final Spinner experience) {
- final List experienceLevels = ListFactory.list("Please select",
- "experienced",
- "infrequent",
- "beginner");
- experience.setPrompt("Please select experience level");
- experience.setAdapter(new SpinnerList(this, experienceLevels));
- int index = prefs_.getInt("experience", 0);
- experience.setSelection(index);
- experience.setOnItemSelectedListener(this);
- } // setupExperience
-
- private void setupPurposeButtons() {
- purpButtons.put(R.id.ToggleCommute, (ToggleButton)findViewById(R.id.ToggleCommute));
- purpButtons.put(R.id.ToggleSchool, (ToggleButton)findViewById(R.id.ToggleSchool));
- purpButtons.put(R.id.ToggleWorkRel, (ToggleButton)findViewById(R.id.ToggleWorkRel));
- purpButtons.put(R.id.ToggleExercise,(ToggleButton)findViewById(R.id.ToggleExercise));
- purpButtons.put(R.id.ToggleSocial, (ToggleButton)findViewById(R.id.ToggleSocial));
- purpButtons.put(R.id.ToggleShopping,(ToggleButton)findViewById(R.id.ToggleShopping));
- purpButtons.put(R.id.ToggleErrand, (ToggleButton)findViewById(R.id.ToggleErrand));
- purpButtons.put(R.id.ToggleOther, (ToggleButton)findViewById(R.id.ToggleOther));
-
- purpDescriptions.put(R.id.ToggleCommute,
- "Commute: this bike trip was primarily to get between home and your main workplace.");
- purpDescriptions.put(R.id.ToggleSchool,
- "School: this bike trip was primarily to go to or from school or college.");
- purpDescriptions.put(R.id.ToggleWorkRel,
- "Work-Related: this bike trip was primarily to go to or from a business related meeting, function, or work-related errand for your job.");
- purpDescriptions.put(R.id.ToggleExercise,
- "Exercise: this bike trip was primarily for exercise, or biking for the sake of biking.");
- purpDescriptions.put(R.id.ToggleSocial,
- "Social: this bike trip was primarily for going to or from a social activity, e.g. at a friend's house, the park, a restaurant, the movies.");
- purpDescriptions.put(R.id.ToggleShopping,
- "Shopping: this bike trip was primarily to purchase or bring home goods or groceries.");
- purpDescriptions.put(R.id.ToggleErrand,
- "Errand: this bike trip was primarily to attend to personal business such as banking, a doctor visit, going to the gym, etc.");
- purpDescriptions.put(R.id.ToggleOther,
- "Other: if none of the other reasons applied to this trip, you can enter comments below to tell us more.");
-
- for (Entry e: purpButtons.entrySet())
- e.getValue().setOnCheckedChangeListener(this);
- } // preparePurposeButtons
-
- @Override
- public void onCheckedChanged(CompoundButton v, boolean isChecked) {
- if (!isChecked)
- return;
-
- for (Entry e: purpButtons.entrySet())
- e.getValue().setChecked(false);
-
- v.setChecked(true);
- purpose_ = v.getText().toString();
- ((TextView)findViewById(R.id.TextPurpDescription)).setText(
- Html.fromHtml(purpDescriptions.get(v.getId())));
-
- enableSubmit();
- } // onCheckedChanged
-
- @Override
- public void onItemSelected(AdapterView> adapterView, View view, int i, long l) {
- enableSubmit();
- } // onItemClick
- @Override
- public void onNothingSelected(AdapterView> adapterView) {
- enableSubmit();
- } // onItemClick
-
- private void enableSubmit() {
- boolean enabled = false;
- for (Entry e: purpButtons.entrySet())
- enabled |= e.getValue().isChecked();
-
- if (!enabled)
- return;
-
- final Button btnSubmit = (Button)findViewById(R.id.ButtonSubmit);
- btnSubmit.setEnabled((age_.getSelectedItemPosition() != 0 &&
- gender_.getSelectedItemPosition() != 0 &&
- experience_.getSelectedItemPosition() != 0));
- } // enabledSubmit
-
- ///////////////////////
- static private class SpinnerList extends BaseAdapter {
- private final LayoutInflater inflater_;
- private final List list_;
-
- public SpinnerList(final Context context, final List list) {
- inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- list_ = list;
- } // CategoryAdapter
-
- @Override
- public int getCount() { return list_.size(); }
- @Override
- public String getItem(final int position) { return list_.get(position); } // getItem
- @Override
- public long getItemId(final int position) { return position; } // getItemId
-
- @Override
- public View getView(final int position, final View convertView, final ViewGroup parent) {
- final int id = (parent instanceof Spinner) ? android.R.layout.simple_spinner_item : android.R.layout.simple_spinner_dropdown_item;
- final TextView tv = (TextView)inflater_.inflate(id, parent, false);
- tv.setText(getItem(position));
- return tv;
- } // getView
- } // SpinnerList
-
-} // SaveTrip
+package net.cyclestreets.track;
+
+import java.text.DateFormat;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import android.app.Activity;
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.AdapterView;
+import android.widget.BaseAdapter;
+import android.widget.Button;
+import android.widget.CompoundButton;
+import android.widget.EditText;
+import android.widget.Spinner;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ToggleButton;
+import net.cyclestreets.util.HtmlKt;
+
+public class SaveTrip extends Activity
+ implements View.OnClickListener, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener {
+ public static void start(final Context context, final long tripid) {
+ final Intent fi = new Intent(context, SaveTrip.class);
+ fi.putExtra("showtrip", tripid);
+ context.startActivity(fi);
+ }
+
+ public static void startWithUnsaved(final Context context) {
+ final int unfinishedTrip = DbAdapter.unfinishedTrip(context);
+ start(context, unfinishedTrip);
+ }
+
+ private final Map purpButtons = new HashMap<>();
+ private final Map purpDescriptions = new HashMap<>();
+ private TripData trip_;
+ private String purpose_;
+ private Spinner age_;
+ private Spinner gender_;
+ private Spinner experience_;
+ private SharedPreferences prefs_;
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.save);
+
+ prefs_ = getSharedPreferences("PersonalInfo", Application.MODE_PRIVATE);
+
+ final Bundle cmds = getIntent().getExtras();
+ final long journeyId = cmds.getLong("showtrip");
+ trip_ = TripData.fetchTrip(this, journeyId);
+
+ // Set up trip purpose buttons
+ purpose_ = "";
+ setupPurposeButtons();
+
+ // Discard btn
+ final Button btnDiscard = viewById(R.id.ButtonDiscard);
+ btnDiscard.setOnClickListener(this);
+
+ // Submit btn
+ final Button btnSubmit = viewById(R.id.ButtonSubmit);
+ btnSubmit.setOnClickListener(this);
+ btnSubmit.setEnabled(false);
+
+ age_ = viewById(R.id.age);
+ setupAge(age_);
+
+ gender_ = viewById(R.id.gender);
+ setupGender(gender_);
+
+ experience_ = viewById(R.id.experience);
+ setupExperience(experience_);
+
+ // Don't pop up the soft keyboard until user clicks!
+ getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN);
+ }
+
+ private T viewById(final int id) { return (T)findViewById(id); }
+
+ public void onClick(final View v) {
+ if (v.getId() == R.id.ButtonDiscard)
+ discardTrip();
+
+ if (v.getId() == R.id.ButtonSubmit)
+ uploadTrip();
+ }
+
+ private void discardTrip() {
+ Toast.makeText(getBaseContext(), R.string.savetrip_discarded, Toast.LENGTH_SHORT).show();
+
+ trip_.dropTrip();
+
+ //CycleHackney.start(this);
+ finish();
+ }
+
+ private void uploadTrip() {
+ if (TextUtils.isEmpty(purpose_)) {
+ // Oh no! No trip purpose!
+ Toast.makeText(getBaseContext(), R.string.savetrip_no_purpose, Toast.LENGTH_SHORT).show();
+ return;
+ }
+
+ EditText notes = (EditText)findViewById(R.id.NotesField);
+
+ String fancyStartTime = DateFormat.getInstance().format(trip_.startTime()*1000);
+
+ // "3.5 miles in 26 minutes"
+ final long minutes = trip_.secondsElapsed() / 60;
+ String fancyEndInfo = getString(R.string.savetrip_end_info_format,
+ trip_.distanceTravelled(),
+ minutes,
+ notes.getEditableText().toString());
+
+ // Save the trip details to the phone database. W00t!
+ trip_.updateTrip(purpose_,
+ fancyStartTime,
+ fancyEndInfo,
+ notes.getEditableText().toString(),
+ age_.getSelectedItem().toString(),
+ gender_.getSelectedItem().toString(),
+ experience_.getSelectedItem().toString());
+ trip_.metaDataComplete();
+
+ SharedPreferences.Editor e = prefs_.edit();
+ e.putInt("age", age_.getSelectedItemPosition());
+ e.putInt("gender", gender_.getSelectedItemPosition());
+ e.putInt("experience", experience_.getSelectedItemPosition());
+ e.apply();
+
+ TripDataUploader.upload(this, trip_);
+
+ finish();
+ }
+
+ private void setupAge(final Spinner age) {
+ final List ages = Arrays.asList(getString(R.string.savetrip_please_select),
+ "0-10",
+ "11-16",
+ "17-24",
+ "25-44",
+ "45-64",
+ "65-74",
+ "75-84",
+ "85+");
+ age.setAdapter(new SpinnerList(this, ages));
+ int index = prefs_.getInt("age", 0);
+ age.setSelection(index);
+ age.setOnItemSelectedListener(this);
+ }
+
+ private void setupGender(final Spinner gender) {
+ final List genders = Arrays.asList(getString(R.string.savetrip_please_select),
+ getString(R.string.savetrip_gender_male),
+ getString(R.string.savetrip_gender_female),
+ getString(R.string.savetrip_gender_private));
+ gender.setPrompt(getString(R.string.savetrip_gender_prompt));
+ gender.setAdapter(new SpinnerList(this, genders));
+ int index = prefs_.getInt("gender", 0);
+ gender.setSelection(index);
+ gender.setOnItemSelectedListener(this);
+ }
+
+ private void setupExperience(final Spinner experience) {
+ final List experienceLevels = Arrays.asList(getString(R.string.savetrip_please_select),
+ getString(R.string.savetrip_experience_experienced),
+ getString(R.string.savetrip_experience_infrequent),
+ getString(R.string.savetrip_experience_beginner));
+ experience.setPrompt(getString(R.string.savetrip_experience_prompt));
+ experience.setAdapter(new SpinnerList(this, experienceLevels));
+ int index = prefs_.getInt("experience", 0);
+ experience.setSelection(index);
+ experience.setOnItemSelectedListener(this);
+ }
+
+ private void setupPurposeButtons() {
+ purpButtons.put(R.id.ToggleCommute, (ToggleButton)findViewById(R.id.ToggleCommute));
+ purpButtons.put(R.id.ToggleSchool, (ToggleButton)findViewById(R.id.ToggleSchool));
+ purpButtons.put(R.id.ToggleWorkRel, (ToggleButton)findViewById(R.id.ToggleWorkRel));
+ purpButtons.put(R.id.ToggleExercise,(ToggleButton)findViewById(R.id.ToggleExercise));
+ purpButtons.put(R.id.ToggleSocial, (ToggleButton)findViewById(R.id.ToggleSocial));
+ purpButtons.put(R.id.ToggleShopping,(ToggleButton)findViewById(R.id.ToggleShopping));
+ purpButtons.put(R.id.ToggleErrand, (ToggleButton)findViewById(R.id.ToggleErrand));
+ purpButtons.put(R.id.ToggleOther, (ToggleButton)findViewById(R.id.ToggleOther));
+
+ purpDescriptions.put(R.id.ToggleCommute, getString(R.string.savetrip_purpose_commute));
+ purpDescriptions.put(R.id.ToggleSchool, getString(R.string.savetrip_purpose_school));
+ purpDescriptions.put(R.id.ToggleWorkRel, getString(R.string.savetrip_purpose_work_related));
+ purpDescriptions.put(R.id.ToggleExercise, getString(R.string.savetrip_purpose_exercise));
+ purpDescriptions.put(R.id.ToggleSocial, getString(R.string.savetrip_purpose_social));
+ purpDescriptions.put(R.id.ToggleShopping, getString(R.string.savetrip_purpose_shopping));
+ purpDescriptions.put(R.id.ToggleErrand, getString(R.string.savetrip_purpose_errand));
+ purpDescriptions.put(R.id.ToggleOther, getString(R.string.savetrip_purpose_other));
+
+ for (Entry e: purpButtons.entrySet())
+ e.getValue().setOnCheckedChangeListener(this);
+ }
+
+ @Override
+ public void onCheckedChanged(CompoundButton v, boolean isChecked) {
+ if (!isChecked)
+ return;
+
+ for (Entry e: purpButtons.entrySet())
+ e.getValue().setChecked(false);
+
+ v.setChecked(true);
+ purpose_ = v.getText().toString();
+
+ Spanned styledText = HtmlKt.fromHtml(purpDescriptions.get(v.getId()));
+
+ ((TextView)findViewById(R.id.TextPurpDescription)).setText(styledText);
+
+ enableSubmit();
+ }
+
+ @Override
+ public void onItemSelected(AdapterView> adapterView, View view, int i, long l) {
+ enableSubmit();
+ }
+ @Override
+ public void onNothingSelected(AdapterView> adapterView) {
+ enableSubmit();
+ }
+
+ private void enableSubmit() {
+ boolean enabled = false;
+ for (Entry e: purpButtons.entrySet())
+ enabled |= e.getValue().isChecked();
+
+ if (!enabled)
+ return;
+
+ final Button btnSubmit = (Button)findViewById(R.id.ButtonSubmit);
+ btnSubmit.setEnabled((age_.getSelectedItemPosition() != 0 &&
+ gender_.getSelectedItemPosition() != 0 &&
+ experience_.getSelectedItemPosition() != 0));
+ }
+
+ ///////////////////////
+ private static class SpinnerList extends BaseAdapter {
+ private final LayoutInflater inflater_;
+ private final List list_;
+
+ public SpinnerList(final Context context, final List list) {
+ inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+ list_ = list;
+ }
+
+ @Override
+ public int getCount() { return list_.size(); }
+ @Override
+ public String getItem(final int position) { return list_.get(position); }
+ @Override
+ public long getItemId(final int position) { return position; }
+
+ @Override
+ public View getView(final int position, final View convertView, final ViewGroup parent) {
+ final int id = (parent instanceof Spinner) ? android.R.layout.simple_spinner_item : android.R.layout.simple_spinner_dropdown_item;
+ final TextView tv = (TextView)inflater_.inflate(id, parent, false);
+ tv.setText(getItem(position));
+ return tv;
+ }
+ }
+
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/ShowJourney.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/ShowJourney.java
index 181d15e04..66c5e1d53 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/ShowJourney.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/ShowJourney.java
@@ -1,43 +1,45 @@
-package net.cyclestreets.track;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.widget.RelativeLayout;
-import android.widget.TextView;
-
-import net.cyclestreets.views.CycleMapView;
-
-public class ShowJourney extends Activity {
- private CycleMapView mapView_;
-
- @Override
- public void onCreate(final Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- setContentView(R.layout.completed_journey);
-
- mapView_ = new CycleMapView(this, getClass().getName());
- mapView_.hideLocationButton();
- final RelativeLayout v = (RelativeLayout)findViewById(R.id.mapholder);
- v.addView(mapView_,
- new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.FILL_PARENT,
- RelativeLayout.LayoutParams.FILL_PARENT));
-
- final Bundle cmds = getIntent().getExtras();
- final long journeyId = cmds.getLong("showtrip");
- final TripData trip = TripData.fetchTrip(this, journeyId);
-
- setText(R.id.journey_info, trip.info());
- setText(R.id.journey_purpose, trip.purpose());
- setText(R.id.journey_start, trip.fancyStart());
-
- // zoomToBoundingBox works better if setZoom first
- mapView_.getController().setZoom(14);
- mapView_.overlayPushTop(JourneyOverlay.CompletedJourneyOverlay(this, mapView_, trip));
- } // onCreate
-
- private void setText(final int id, final String text) {
- final TextView tv = (TextView)findViewById(id);
- tv.setText(text);
- } // setText
-} // ShowJourney
+package net.cyclestreets.track;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import net.cyclestreets.views.CycleMapView;
+
+import static net.cyclestreets.CycleStreetsConstantsKt.DEFAULT_ZOOM_LEVEL;
+
+public class ShowJourney extends Activity {
+ private CycleMapView mapView_;
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ setContentView(R.layout.completed_journey);
+
+ mapView_ = new CycleMapView(this, getClass().getName());
+ mapView_.hideLocationButton();
+ final RelativeLayout v = findViewById(R.id.mapholder);
+ v.addView(mapView_,
+ new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.MATCH_PARENT));
+
+ final Bundle cmds = getIntent().getExtras();
+ final long journeyId = cmds.getLong("showtrip");
+ final TripData trip = TripData.fetchTrip(this, journeyId);
+
+ setText(R.id.journey_info, trip.info());
+ setText(R.id.journey_purpose, trip.purpose());
+ setText(R.id.journey_start, trip.fancyStart());
+
+ // zoomToBoundingBox works better if setZoom first
+ mapView_.getController().setZoom(DEFAULT_ZOOM_LEVEL);
+ mapView_.overlayPushTop(JourneyOverlay.CompletedJourneyOverlay(this, mapView_, trip));
+ }
+
+ private void setText(final int id, final String text) {
+ final TextView tv = findViewById(id);
+ tv.setText(text);
+ }
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Tracker.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Tracker.java
index 2e7d1c637..d5c10acf6 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Tracker.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/Tracker.java
@@ -12,7 +12,7 @@
public class Tracker {
public static TrackerControl create(final Activity context, final TrackListener listener) {
return Controller.create(context, listener);
- } // create
+ }
public static void checkStatus(final Context context, final StatusCallback callback) {
// check to see if already recording here
@@ -28,7 +28,7 @@ public void onServiceConnected(ComponentName name, IBinder service) {
if (unfinishedTrip != -1) {
callback.unsavedTrip();
}
- } // if ...
+ }
context.unbindService(this); // race? this says we no longer care
}
@@ -37,7 +37,7 @@ public void onServiceDisconnected(ComponentName name) {}
// This needs to block until the onServiceConnected (above) completes.
// Thus, we can check the recording status before continuing on.
context.bindService(rService, sc, Context.BIND_AUTO_CREATE);
- } // checkStatus
+ }
public static int uploadLeftOverTrips(final Context context) {
final List trips = DbAdapter.unUploadedTrips(context);
@@ -47,8 +47,7 @@ public static int uploadLeftOverTrips(final Context context) {
TripDataUploader.upload(context, trips);
return trips.size();
- } // uploadLeftOverTrips
-
+ }
private Tracker() { }
-} // class Tracker
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TrackerControl.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TrackerControl.java
index 7397be9ce..10711f4c0 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TrackerControl.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TrackerControl.java
@@ -3,4 +3,4 @@
public interface TrackerControl {
void start();
void stop();
-} // TrackerControl
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripData.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripData.java
index 771ef130d..9e5b60ed1 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripData.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripData.java
@@ -1,249 +1,246 @@
-package net.cyclestreets.track;
-
-import android.content.Context;
-import android.database.Cursor;
-import android.location.Location;
-
-import org.osmdroid.util.BoundingBoxE6;
-import org.osmdroid.util.GeoPoint;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class TripData {
- private long tripid;
- private long startTime_ = 0;
- private long endTime_ = 0;
- private int status;
- private float distance;
- private String purp_;
- private String info_;
- private String fancystart_;
- private List gpspoints;
- private String note_;
- private String age_;
- private String gender_;
- private String experience_;
-
- private DbAdapter mDb;
-
- public static int STATUS_RECORDING = 0;
- public static int STATUS_RECORDING_COMPLETE = 5;
- public static int STATUS_COMPLETE_UNSENT = 1;
- public static int STATUS_COMPLETE = 2;
- public static int STATUS_COMPLETE_FAILED = 3;
-
- public static TripData createTrip(Context c) {
- TripData t = new TripData(c.getApplicationContext(), 0);
- t.createTripInDatabase(c);
- t.initializeData();
- return t;
- }
-
- public static TripData fetchTrip(Context c, long tripid) {
- TripData t = new TripData(c.getApplicationContext(), tripid);
- t.populateDetails();
- return t;
- }
-
- public TripData(Context ctx, long tripid) {
- Context context = ctx.getApplicationContext();
- this.tripid = tripid;
- mDb = new DbAdapter(context);
- }
-
- private void initializeData() {
- startTime_ = now();
- endTime_ = now();
- distance = 0;
-
- purp_ = fancystart_ = info_ = "";
-
- gpspoints = new ArrayList<>();
-
- mDb.open();
- mDb.setStartTime(tripid, startTime_);
- mDb.close();
- }
-
- // Get lat/long extremes, etc, from trip record
- private void populateDetails() {
- mDb.openReadOnly();
-
- Cursor tripdetails = mDb.fetchTrip(tripid);
- startTime_ = tripdetails.getInt(tripdetails.getColumnIndex("start"));
- status = tripdetails.getInt(tripdetails.getColumnIndex("status"));
- endTime_ = tripdetails.getInt(tripdetails.getColumnIndex("endtime"));
- distance = tripdetails.getFloat(tripdetails.getColumnIndex("distance"));
-
- purp_ = tripdetails.getString(tripdetails.getColumnIndex("purp"));
- fancystart_ = tripdetails.getString(tripdetails.getColumnIndex("fancystart"));
- info_ = tripdetails.getString(tripdetails.getColumnIndex("fancyinfo"));
- note_ = tripdetails.getString(tripdetails.getColumnIndex("note"));
- age_ = tripdetails.getString(tripdetails.getColumnIndex("age"));
- gender_ = tripdetails.getString(tripdetails.getColumnIndex("gender"));
- experience_ = tripdetails.getString(tripdetails.getColumnIndex("experience"));
-
- tripdetails.close();
- mDb.close();
-
- loadJourney();
- }
-
- private void loadJourney() {
- // Otherwise, we need to query DB and build points from scratch.
- gpspoints = new ArrayList<>();
-
- mDb.openReadOnly();
-
- Cursor points = mDb.fetchAllCoordsForTrip(tripid);
- int COL_LAT = points.getColumnIndex("lat");
- int COL_LGT = points.getColumnIndex("lgt");
- int COL_TIME = points.getColumnIndex("time");
- int COL_ACC = points.getColumnIndex(DbAdapter.K_POINT_ACC);
- int COL_SPEED = points.getColumnIndex(DbAdapter.K_POINT_SPEED);
- int COL_ALT = points.getColumnIndex(DbAdapter.K_POINT_ALT);
-
- while (!points.isAfterLast()) {
- int lat = points.getInt(COL_LAT);
- int lgt = points.getInt(COL_LGT);
- long time = points.getInt(COL_TIME);
- double altitude = points.getDouble(COL_ALT);
- float speed = (float)points.getDouble(COL_SPEED);
- float acc = (float)points.getDouble(COL_ACC);
-
- gpspoints.add(new CyclePoint(lat, lgt, time, acc, altitude, speed));
-
- points.moveToNext();
- } // while
- points.close();
- mDb.close();
- } // loadJourney
-
- private void createTripInDatabase(Context c) {
- mDb.open();
- tripid = mDb.createTrip();
- mDb.close();
- }
-
- void dropTrip() {
- mDb.open();
- mDb.deleteAllCoordsForTrip(tripid);
- mDb.deleteTrip(tripid);
- mDb.close();
- }
-
- public long id() { return tripid; }
- public boolean dataAvailable() { return gpspoints.size() != 0; }
- public GeoPoint startLocation() { return gpspoints.get(0); }
- public GeoPoint endLocation() { return gpspoints.get(gpspoints.size()-1); }
- public BoundingBoxE6 boundingBox() {
- int lathigh = Integer.MIN_VALUE;
- int lgthigh = Integer.MIN_VALUE;
- int latlow = Integer.MAX_VALUE;
- int lgtlow = Integer.MAX_VALUE;
-
- for(GeoPoint gp : gpspoints) {
- lathigh = Math.max(gp.getLatitudeE6(), lathigh);
- latlow = Math.min(gp.getLatitudeE6(), latlow);
- lgthigh = Math.max(gp.getLongitudeE6(), lgthigh);
- lgtlow = Math.min(gp.getLongitudeE6(), lgtlow);
- }
-
- return new BoundingBoxE6(lathigh, lgtlow, latlow, lgthigh);
- }
- public List journey() { return gpspoints; }
- public long startTime() { return startTime_; }
- public long endTime() { return endTime_; }
- public long secondsElapsed() {
- if(status == STATUS_RECORDING)
- return now() - startTime_;
- return endTime_ - startTime_;
- } // secondsElapsed
- public long lastPointElapsed() {
- if (!dataAvailable())
- return secondsElapsed();
- return now() - endTime_;
- } // lastPointElapsed
- public float distanceTravelled() {
- return (0.0006212f * distance);
- } // distanceTravelled
- public String notes() { return note_; }
- public String purpose() { return purp_; }
- public String info() { return info_; }
- public String fancyStart() { return fancystart_; }
- public String age() { return age_; }
- public String gender() { return gender_; }
- public String experience() { return experience_; }
-
- private long now() { return System.currentTimeMillis()/1000; }
-
- public void addPointNow(Location loc) {
- int lat = (int)(loc.getLatitude() * 1E6);
- int lgt = (int)(loc.getLongitude() * 1E6);
-
- float accuracy = loc.getAccuracy();
- double altitude = loc.getAltitude();
- float speed = loc.getSpeed();
-
- endTime_ = (loc.getTime()/1000);
- CyclePoint pt = new CyclePoint(lat, lgt, endTime_, accuracy, altitude, speed);
-
- if (gpspoints.size() > 1) {
- CyclePoint gp = gpspoints.get(gpspoints.size()-1);
-
- float segmentDistance = gp.distanceTo(pt);
- if (segmentDistance == 0)
- return; // we haven't gone anywhere
-
- distance += segmentDistance;
- } // if ...
-
- gpspoints.add(pt);
-
-
- mDb.open();
- mDb.addCoordToTrip(tripid, pt);
- mDb.setDistance(tripid, distance);
- mDb.close();
-
- return;
- } // addPointNow
-
- public void recordingStopped() {
- endTime_ = now();
- mDb.open();
- mDb.updateTripStatus(tripid, STATUS_RECORDING_COMPLETE);
- mDb.setEndTime(tripid, endTime_);
- mDb.close();
- }
- public void metaDataComplete() { updateTripStatus(STATUS_COMPLETE_UNSENT);}
- public void successfullyUploaded() { updateTripStatus(STATUS_COMPLETE); }
- public void uploadFailed() { updateTripStatus(STATUS_COMPLETE_FAILED); }
-
- private void updateTripStatus(int tripStatus) {
- mDb.open();
- mDb.updateTripStatus(tripid, tripStatus);
- mDb.close();
- }
-
- public void updateTrip(String purpose,
- String fancyStart,
- String fancyInfo,
- String notes,
- String age,
- String gender,
- String experience) {
- // Save the trip details to the phone database. W00t!
- mDb.open();
- mDb.updateNotes(tripid, purpose, fancyStart, fancyInfo, notes, age, gender, experience);
- mDb.close();
-
- purp_ = purpose;
- note_ = notes;
- age_ = age;
- gender_ = gender;
- experience_ = experience;
- } // updateTrip
-
-} // TripData
+package net.cyclestreets.track;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.location.Location;
+
+import org.osmdroid.util.BoundingBox;
+import org.osmdroid.util.GeoPoint;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TripData {
+ private long tripid;
+ private long startTime_ = 0;
+ private long endTime_ = 0;
+ private int status;
+ private float distance;
+ private String purp_;
+ private String info_;
+ private String fancystart_;
+ private List gpspoints;
+ private String note_;
+ private String age_;
+ private String gender_;
+ private String experience_;
+
+ private DbAdapter mDb;
+
+ public static int STATUS_RECORDING = 0;
+ public static int STATUS_RECORDING_COMPLETE = 5;
+ public static int STATUS_COMPLETE_UNSENT = 1;
+ public static int STATUS_COMPLETE = 2;
+ public static int STATUS_COMPLETE_FAILED = 3;
+
+ public static TripData createTrip(Context c) {
+ TripData t = new TripData(c.getApplicationContext(), 0);
+ t.createTripInDatabase(c);
+ t.initializeData();
+ return t;
+ }
+
+ public static TripData fetchTrip(Context c, long tripid) {
+ TripData t = new TripData(c.getApplicationContext(), tripid);
+ t.populateDetails();
+ return t;
+ }
+
+ public TripData(Context ctx, long tripid) {
+ Context context = ctx.getApplicationContext();
+ this.tripid = tripid;
+ mDb = new DbAdapter(context);
+ }
+
+ private void initializeData() {
+ startTime_ = now();
+ endTime_ = now();
+ distance = 0;
+
+ purp_ = fancystart_ = info_ = "";
+
+ gpspoints = new ArrayList<>();
+
+ mDb.open();
+ mDb.setStartTime(tripid, startTime_);
+ mDb.close();
+ }
+
+ // Get lat/long extremes, etc, from trip record
+ private void populateDetails() {
+ mDb.openReadOnly();
+
+ Cursor tripdetails = mDb.fetchTrip(tripid);
+ startTime_ = tripdetails.getInt(tripdetails.getColumnIndex("start"));
+ status = tripdetails.getInt(tripdetails.getColumnIndex("status"));
+ endTime_ = tripdetails.getInt(tripdetails.getColumnIndex("endtime"));
+ distance = tripdetails.getFloat(tripdetails.getColumnIndex("distance"));
+
+ purp_ = tripdetails.getString(tripdetails.getColumnIndex("purp"));
+ fancystart_ = tripdetails.getString(tripdetails.getColumnIndex("fancystart"));
+ info_ = tripdetails.getString(tripdetails.getColumnIndex("fancyinfo"));
+ note_ = tripdetails.getString(tripdetails.getColumnIndex("note"));
+ age_ = tripdetails.getString(tripdetails.getColumnIndex("age"));
+ gender_ = tripdetails.getString(tripdetails.getColumnIndex("gender"));
+ experience_ = tripdetails.getString(tripdetails.getColumnIndex("experience"));
+
+ tripdetails.close();
+ mDb.close();
+
+ loadJourney();
+ }
+
+ private void loadJourney() {
+ // Otherwise, we need to query DB and build points from scratch.
+ gpspoints = new ArrayList<>();
+
+ mDb.openReadOnly();
+
+ Cursor points = mDb.fetchAllCoordsForTrip(tripid);
+ int COL_LAT = points.getColumnIndex("lat");
+ int COL_LGT = points.getColumnIndex("lgt");
+ int COL_TIME = points.getColumnIndex("time");
+ int COL_ACC = points.getColumnIndex(DbAdapter.K_POINT_ACC);
+ int COL_SPEED = points.getColumnIndex(DbAdapter.K_POINT_SPEED);
+ int COL_ALT = points.getColumnIndex(DbAdapter.K_POINT_ALT);
+
+ while (!points.isAfterLast()) {
+ double lat = points.getInt(COL_LAT) / 1e6;
+ double lgt = points.getInt(COL_LGT) / 1e6;
+ long time = points.getInt(COL_TIME);
+ double altitude = points.getDouble(COL_ALT);
+ float speed = (float)points.getDouble(COL_SPEED);
+ float acc = (float)points.getDouble(COL_ACC);
+
+ gpspoints.add(new CyclePoint(lat, lgt, time, acc, altitude, speed));
+
+ points.moveToNext();
+ }
+ points.close();
+ mDb.close();
+ }
+
+ private void createTripInDatabase(Context c) {
+ mDb.open();
+ tripid = mDb.createTrip();
+ mDb.close();
+ }
+
+ void dropTrip() {
+ mDb.open();
+ mDb.deleteAllCoordsForTrip(tripid);
+ mDb.deleteTrip(tripid);
+ mDb.close();
+ }
+
+ public long id() { return tripid; }
+ public boolean dataAvailable() { return gpspoints.size() != 0; }
+ public GeoPoint startLocation() { return gpspoints.get(0); }
+ public GeoPoint endLocation() { return gpspoints.get(gpspoints.size()-1); }
+ public BoundingBox boundingBox() {
+ double lathigh = Double.MIN_VALUE;
+ double lgthigh = Double.MIN_VALUE;
+ double latlow = Double.MAX_VALUE;
+ double lgtlow = Double.MAX_VALUE;
+
+ for (GeoPoint gp : gpspoints) {
+ lathigh = Math.max(gp.getLatitude(), lathigh);
+ latlow = Math.min(gp.getLatitude(), latlow);
+ lgthigh = Math.max(gp.getLongitude(), lgthigh);
+ lgtlow = Math.min(gp.getLongitude(), lgtlow);
+ }
+
+ return new BoundingBox(lathigh, lgtlow, latlow, lgthigh);
+ }
+ public List journey() { return gpspoints; }
+ public long startTime() { return startTime_; }
+ public long endTime() { return endTime_; }
+ public long secondsElapsed() {
+ if (status == STATUS_RECORDING)
+ return now() - startTime_;
+ return endTime_ - startTime_;
+ }
+ public long lastPointElapsed() {
+ if (!dataAvailable())
+ return secondsElapsed();
+ return now() - endTime_;
+ }
+ public float distanceTravelled() {
+ return (0.0006212f * distance);
+ }
+ public String notes() { return note_; }
+ public String purpose() { return purp_; }
+ public String info() { return info_; }
+ public String fancyStart() { return fancystart_; }
+ public String age() { return age_; }
+ public String gender() { return gender_; }
+ public String experience() { return experience_; }
+
+ private long now() { return System.currentTimeMillis()/1000; }
+
+ public void addPointNow(Location loc) {
+ double lat = loc.getLatitude();
+ double lgt = loc.getLongitude();
+
+ float accuracy = loc.getAccuracy();
+ double altitude = loc.getAltitude();
+ float speed = loc.getSpeed();
+
+ endTime_ = (loc.getTime()/1000);
+ CyclePoint pt = new CyclePoint(lat, lgt, endTime_, accuracy, altitude, speed);
+
+ if (gpspoints.size() > 1) {
+ CyclePoint gp = gpspoints.get(gpspoints.size()-1);
+
+ double segmentDistance = gp.distanceToAsDouble(pt);
+ if (segmentDistance == 0)
+ return; // we haven't gone anywhere
+
+ distance += (float)segmentDistance;
+ }
+
+ gpspoints.add(pt);
+
+ mDb.open();
+ mDb.addCoordToTrip(tripid, pt);
+ mDb.setDistance(tripid, distance);
+ mDb.close();
+ }
+
+ public void recordingStopped() {
+ endTime_ = now();
+ mDb.open();
+ mDb.updateTripStatus(tripid, STATUS_RECORDING_COMPLETE);
+ mDb.setEndTime(tripid, endTime_);
+ mDb.close();
+ }
+ public void metaDataComplete() { updateTripStatus(STATUS_COMPLETE_UNSENT);}
+ public void successfullyUploaded() { updateTripStatus(STATUS_COMPLETE); }
+ public void uploadFailed() { updateTripStatus(STATUS_COMPLETE_FAILED); }
+
+ private void updateTripStatus(int tripStatus) {
+ mDb.open();
+ mDb.updateTripStatus(tripid, tripStatus);
+ mDb.close();
+ }
+
+ public void updateTrip(String purpose,
+ String fancyStart,
+ String fancyInfo,
+ String notes,
+ String age,
+ String gender,
+ String experience) {
+ // Save the trip details to the phone database. W00t!
+ mDb.open();
+ mDb.updateNotes(tripid, purpose, fancyStart, fancyInfo, notes, age, gender, experience);
+ mDb.close();
+
+ purp_ = purpose;
+ note_ = notes;
+ age_ = age;
+ gender_ = gender;
+ experience_ = experience;
+ }
+
+}
diff --git a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripDataUploader.java b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripDataUploader.java
index 609b6fef4..4239ce311 100644
--- a/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripDataUploader.java
+++ b/libraries/cyclestreets-track/src/main/java/net/cyclestreets/track/TripDataUploader.java
@@ -6,20 +6,23 @@
import android.content.Context;
import android.content.Intent;
import android.os.AsyncTask;
-import android.provider.Settings.System;
-import net.cyclestreets.util.ListFactory;
+import net.cyclestreets.CycleStreetsNotifications;
+
+import static android.provider.Settings.Secure;
+import static net.cyclestreets.CycleStreetsNotifications.CHANNEL_TRACK_ID;
import org.json.JSONException;
import org.json.JSONObject;
+import java.util.Collections;
import java.util.List;
public class TripDataUploader extends AsyncTask {
private int NOTIFICATION_ID = 1;
public static void upload(final Context context, final TripData tripData) {
- upload(context, ListFactory.list(tripData));
+ upload(context, Collections.singletonList(tripData));
}
public static void upload(final Context context, final List tripData) {
@@ -33,7 +36,7 @@ public static void upload(final Context context, final List tripData)
private TripDataUploader(final Context context, final List tripData) {
context_ = context;
tripData_ = tripData;
- } // UploadDataTask
+ }
protected Boolean doInBackground(Void... p) {
for (final TripData td : tripData_) {
@@ -64,26 +67,25 @@ protected Boolean doInBackground(Void... p) {
td.uploadFailed();
warning("Upload failed.");
}
- } // for ...
+ }
return true;
- } // doInBackground
+ }
private String deviceId() {
- String androidId = System.getString(context_.getContentResolver(), System.ANDROID_ID);
+ String androidId = Secure.getString(context_.getContentResolver(), Secure.ANDROID_ID);
String androidBase = "androidDeviceId-";
if (androidId == null) { // This happens when running in the Emulator
final String emulatorId = "android-RunningAsTestingDeleteMe";
return emulatorId;
}
- String deviceId = androidBase.concat(androidId);
- return deviceId;
- } // deviceId
+ return androidBase.concat(androidId);
+ }
private JSONObject parse(final byte[] result) throws Exception {
final String s = new String(result, "UTF-8");
return new JSONObject(s);
- } // parse
+ }
private static final String TRIP_COORDS_TIME = "r"; // "rec";
private static final String TRIP_COORDS_LAT = "l"; // "lat";
@@ -96,14 +98,14 @@ private JSONObject parse(final byte[] result) throws Exception {
private String coordsAsJSON(final TripData tripData) throws JSONException {
final StringBuilder tripCoords = new StringBuilder();
- for(CyclePoint cp : tripData.journey()) {
+ for (CyclePoint cp : tripData.journey()) {
tripCoords.append(tripCoords.length() == 0 ? "{" : ",");
JSONObject coord = new JSONObject();
coord.put(TRIP_COORDS_TIME, cp.time);
- coord.put(TRIP_COORDS_LAT, cp.getLatitudeE6()/1e6);
- coord.put(TRIP_COORDS_LON, cp.getLongitudeE6()/1e6);
+ coord.put(TRIP_COORDS_LAT, cp.getLatitude());
+ coord.put(TRIP_COORDS_LON, cp.getLongitude());
coord.put(TRIP_COORDS_ALT, cp.getAltitude());
coord.put(TRIP_COORDS_SPEED, cp.speed);
coord.put(TRIP_COORDS_HACCURACY, cp.accuracy);
@@ -118,7 +120,7 @@ private String coordsAsJSON(final TripData tripData) throws JSONException {
tripCoords.append("}");
return tripCoords.toString();
- } // coordsAsJSON
+ }
private String userAsJSON(final TripData tripData) throws JSONException {
JSONObject user = new JSONObject();
@@ -130,7 +132,7 @@ private String userAsJSON(final TripData tripData) throws JSONException {
private void notification(final String text) {
showNotification(text, Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT);
- } // notification
+ }
private void warning(final String text) {
showNotification(text, Notification.FLAG_AUTO_CANCEL);
@@ -143,19 +145,26 @@ private void showNotification(final String text, final int flags) {
}
private Notification createNotification(final String text, final int flags) {
- final Notification notification = new Notification(R.drawable.icon25, text, java.lang.System.currentTimeMillis());
- notification.flags = flags;
final Intent notificationIntent = new Intent(context_, TripDataUploader.class);
final PendingIntent contentIntent = PendingIntent.getActivity(context_, 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
- notification.setLatestEventInfo(context_.getApplicationContext(), "Cycle Hackney", text, contentIntent);
+
+ Notification notification = CycleStreetsNotifications.INSTANCE.getBuilder(context_.getApplicationContext(), CHANNEL_TRACK_ID)
+ .setSmallIcon(R.drawable.icon25)
+ .setTicker(text)
+ .setWhen(java.lang.System.currentTimeMillis())
+ .setContentTitle("Cycle Hackney")
+ .setContentText(text)
+ .setContentIntent(contentIntent)
+ .build();
+ notification.flags = flags;
return notification;
}
private void cancelNotification() {
nm().cancel(NOTIFICATION_ID);
- } // cancelNotification
+ }
private NotificationManager nm() {
return (NotificationManager)context_.getSystemService(Context.NOTIFICATION_SERVICE);
- } // nm
-} // UploadDataTask
+ }
+}
diff --git a/libraries/cyclestreets-track/src/main/res/layout/completed_journey.xml b/libraries/cyclestreets-track/src/main/res/layout/completed_journey.xml
index 57c5fff45..5fff4d268 100644
--- a/libraries/cyclestreets-track/src/main/res/layout/completed_journey.xml
+++ b/libraries/cyclestreets-track/src/main/res/layout/completed_journey.xml
@@ -4,33 +4,45 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_alignParentLeft="true"
+ android:layout_marginLeft="2sp"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:text="@string/completedjourney_purpose" />
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentRight="true"
+ android:textSize="20sp"
+ android:textStyle="bold"
+ android:text="@string/completedjourney_start_time" />
-
+
+ android:padding="4.0dp">
+ android:text="" />
diff --git a/libraries/cyclestreets-track/src/main/res/layout/journey_in_progress.xml b/libraries/cyclestreets-track/src/main/res/layout/journey_in_progress.xml
index f550ae7fa..353813c04 100644
--- a/libraries/cyclestreets-track/src/main/res/layout/journey_in_progress.xml
+++ b/libraries/cyclestreets-track/src/main/res/layout/journey_in_progress.xml
@@ -11,14 +11,14 @@
android:textStyle="bold"
android:layout_marginTop="5sp"
android:textSize="26sp"
- android:text="Distance:" >
+ android:text="@string/journey_distance" />
+ android:layout_marginRight="5sp" android:textSize="26sp" />
+ android:text="@string/journey_time" />
+ android:layout_marginRight="5sp" android:textSize="26sp" />
+ android:text="@string/journey_current_speed" />
+ android:layout_marginRight="5sp"
+ android:textSize="26sp" />
+ android:padding="4.0dp">
+ android:text="" />
+ android:textStyle="bold"
+ android:textSize="18sp" />
diff --git a/libraries/cyclestreets-track/src/main/res/layout/save.xml b/libraries/cyclestreets-track/src/main/res/layout/save.xml
index 2cb9d14dc..20f6d892b 100644
--- a/libraries/cyclestreets-track/src/main/res/layout/save.xml
+++ b/libraries/cyclestreets-track/src/main/res/layout/save.xml
@@ -1,160 +1,212 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values-de/strings.xml b/libraries/cyclestreets-track/src/main/res/values-de/strings.xml
new file mode 100644
index 000000000..44266fc3c
--- /dev/null
+++ b/libraries/cyclestreets-track/src/main/res/values-de/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+ Aktuelle Geschwindigkeit:
+ Fahrtdauer:
+ Entfernung:
+ Ziel
+
+ Reiseinfo
+ Abfahrtszeit
+ Zweck der Fahrt
+
+ Aufnahme beendet. Bitte wähle den Zweck der Tour:
+ Arbeitsweg
+ Schule
+ Arbeitsbezogen
+ Training
+ Sozial
+ Einkaufen
+ Besorgung
+ Andere
+ Anmerkungen zu deiner Fahrt
+ Alter:
+ Geschlecht:
+
+
+ Level von
+ Radfahr-
+ Erfahrung:
+
+ senden
+ verwerfen
+ Fahrt verworfen.
+ Du musst einen Tourzweck vor der Übermittlung angeben! Wähle einen der obigen Zwecke.
+ Pendler: Diese Fahrradtour war primär dazu gedacht, zwischen deinen Wohnort und deinem Arbeitsplatz zu pendeln.]]>
+ Schule Diese Tour war primär dazu gedacht, zur Schule/Universität zu kommen oder zurück.]]>
+ Arbeitsbezogen: Diese Tour war primär dazu gedacht, von oder zu einem arbeitsbezogenen Meeting, anderen Funktion zu kommen oder Besorgungen für deinen Job zu erledigen.]]>
+ Training: Diese Tour war primär dazu gedacht, zu trainieren oder um des Radfahrens-wegen radzufahren.]]>
+ Sozial: Diese Tour war primär dazu gedacht, um von einer sozialen Aktivität zu kommen oder sich zu entfernen, bspw. zu dem Haus eines Freundes, in einen Park, Restaurant oder in ein Kinos.]]>
+ Shopping: Diese Tour war primär dazu gedacht, um Waren oder Lebensmittel zu kaufen oder nach Hause zu bringen.]]>
+ Errand:Diese Tour war primär dazu gedacht, um persönlichen Geschäften nachzugehen, wie Bankbesuch, Doktor-Besuch, Fitnessstudio-Besuch.]]>
+ Anderes: Wenn keiner der obigen Gründe auf die Tour zutreffen, kannst du unten einen Kommentar eingeben, um uns mehr zu erzählen.]]>
+ Bitte wähle
+ Bitte gebe dein Geschlecht an
+ männlich
+ weiblich
+ möchte es lieber nicht angeben
+ Bitte gebe die Erfahrung an
+ erfahren
+ unregelmäßig
+ Anfänger
+ %1$1.1f Meilen, %2$d Minuten. %3$s
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values-es/strings.xml b/libraries/cyclestreets-track/src/main/res/values-es/strings.xml
new file mode 100644
index 000000000..0ce6577c4
--- /dev/null
+++ b/libraries/cyclestreets-track/src/main/res/values-es/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+ Velocidad actual:
+ Tiempo viajado:
+ Distancia:
+ Terminado
+
+ Información de viaje
+ hora de comienzo
+ propósito del viaje
+
+ Grabación terminada. Elige un propósito de viaje:
+ Commutar
+ Escuela
+ Relacionado con el trabajo
+ Ejercicio
+ Social
+ Compra
+ Errando
+ Otro
+ Comentarios sobre el viaje.
+ Edad:
+ Género:
+
+
+ Nivel de
+ ciclismo
+ experiencia:
+
+ Enviar
+ Descartar
+ Viaje descartado.
+ Debes seleccionar un propósito para tu viaje antes de enviar. Elige uno de los propósitos anteriores.
+ Conmutar: este viaje es para ir básicamente de tu casa al trabajo.]]>
+ Colegio: este viaje es para ir a tu centro de estudios.]]>
+ Relacionado con el trabajo: este viaje es para ir a una reunión, convenció, conferencia... relacionados con el trabajo]]>
+ Ejercicio: este viaje es para hacer ejercicio, o por el amor al ciclismo.]]>
+ Social: este viaje es para moverse a una actividad social, como la casa de una amigo,parque, restaurante...]]>
+ Shopping: this bike trip was primarily to purchase or bring home goods or groceries.]]>
+ Errando: este viaje es para asistir a asuntos personales como el banco, el médico, gimnasio...]]>
+ Otro: si ninguna de las otras razones son aplicables, puedes comentarlo a continuación.]]>
+ Por favor selecciona
+ Por favor selecciona género
+ masculino
+ femenino
+ prefiero no decirlo
+ Por favor selecciona nivel de experiencia
+ experimentado
+ casual
+ principiante
+ %1$1.1f millas, %2$d minutos. %3$s
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values-fr/strings.xml b/libraries/cyclestreets-track/src/main/res/values-fr/strings.xml
new file mode 100644
index 000000000..4e01ace65
--- /dev/null
+++ b/libraries/cyclestreets-track/src/main/res/values-fr/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+ Vitesse actuelle:
+ Temps de parcours:
+ Distance:
+ Terminer
+
+ infos sur le parcours
+ Heure de départ
+ but du voyage
+
+ Terminé l\'enregistrement. Choisissez un but du voyage:
+ Commuer
+ École
+ Liée à l\'emploi
+ Exercice
+ Social
+ Achats
+ Course
+ Autre
+ Commentaires à propos de votre voyage
+ Âge:
+ Le genre:
+
+
+ Niveau de
+ Faire du vélo
+ experience:
+
+ Soumettre
+ Annuler
+ Trajet Annulé.
+ Vous devez sélectionner un motif de déplacement avant de soumettre! Choisissez parmi les objectifs ci-dessus.
+ Commuer: ce voyage à vélo était surtout d\'obtenir entre le domicile et votre lieu de travail principal.]]>
+ School: ce voyage à vélo est surtout d\'aller ou de l\'école ou collège.]]>
+ Liée à l\'emploi: ce voyage à vélo est principalement pour aller à ou d\'une réunion d\'affaires, fonction ou mission liée au travail.]]>
+ Exercice: ce voyage de vélo est principalement pour l\'exercice, ou faire du vélo pour le plaisir de faire du vélo.]]>
+ Social: ce voyage à vélo était principalement pour aller vers ou à partir d\'une activité sociale, par exemple à un ami(e)s, maison, le parc, un restaurant, cinéma.]]>
+ Shopping: ce voyage à vélo est principalement pour acheter ou pour apporter des marchandises à la maison ou l\'épicerie.]]>
+ Course: ce voyage à vélo est principalement pour assister à des affaires personnelles telles que la banque, une visite chez le médecin, aller à la gym, etc.]]>
+ Autre: si aucune des autres raisons appliquée à ce voyage, vous pouvez entrer des commentaires ci-dessous pour nous en dire plus.]]>
+ S\'il vous plaît sélectionnez
+ S\'il vous plaît sélectionner le sexe
+ mâle
+ femelle
+ je préfère ne rien dire
+ S\'il vous plaît sélectionner le niveau d\'expérience
+ expérimenté
+ peu fréquent
+ débutant
+ %1$1.1f miles, %2$d minutes. %3$s
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values-it/strings.xml b/libraries/cyclestreets-track/src/main/res/values-it/strings.xml
new file mode 100644
index 000000000..3aa15b99e
--- /dev/null
+++ b/libraries/cyclestreets-track/src/main/res/values-it/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+ Velocità Attuale:
+ Durata Spostamento:
+ Distanza:
+ Arrivo
+
+ info tragitto
+ scopo dello spostamento
+
+ Registrazione terminata. Scegli il motivo dello spostamento:
+ Lavoro
+ Scuola
+ Per Lavoro
+ Allenamento
+ Sociale
+ Shopping
+ Commenti sul tuo spostamento
+ Età :
+ Genere:
+
+
+ Livello di
+ ciclismo
+ esperienza:
+
+ Invia
+ Scarta
+ Spostamento scartato.
+ Devi selezionare lo scopo dello spostamento prima di inviare! Scegli tra le motivazioni qui sopra.
+ Scuola: questo spostamento in bici è stato effettuato principalmente per andare o tornare da scuola o dall\'università .]]>
+ Per Lavoro: questo spostamento in bici è stato effettuato principalmente per andare o tornare da una attività legata alla propria occupazione .]]>
+ Allenamento:questo spostamento in bici è stato effettuato principalmente per allenarsi o per fare semplicemente un giro in bici.]]>
+ Sociale: questo spostamento in bici è stato effettuato principalmente per andare o tornare da una attività sociale, per esempio la casa di un amico, il parco, un ristorante, il cinema.]]>
+ Shopping: questo spostamento in bici è stato effettuato principalmente per compare o portare a casa oggetti o la spesa.]]>
+ Passeggiata: questo spostamento in bici è stato effettuato principalmente per motivi personali come andare in banca, dal dottore, in palestra, ecc.]]>
+ Altro: se nessuna delle altre motivazioni rispecchia questo spostamento, puoi inserire un commento qui sotto per dirci di più.]]>
+ Seleziona
+ Seleziona il tuo sesso
+ uomo
+ donna
+ preferisco non dirlo
+ Seleziona il livello di esperienza
+ saltuario
+ principiante
+ %1$1.1f miglia, %2$d minuti. %3$s
+ ora di partenza
+ Passeggiata
+ Altro
+ esperto
+ Lavoro: questo sportamento in bici è stato effettuato principalmente per spostarsi tra casa e il luogo di lavoro.]]>
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values-pt/strings.xml b/libraries/cyclestreets-track/src/main/res/values-pt/strings.xml
new file mode 100644
index 000000000..0b05d9d31
--- /dev/null
+++ b/libraries/cyclestreets-track/src/main/res/values-pt/strings.xml
@@ -0,0 +1,53 @@
+
+
+ Velocidade atual:
+ Tempo decorrido:
+ Distancia:
+ Terminar
+
+ Informações de jornada
+ Iniciar tempo
+ razão da viagem
+
+ Parar de gravar.Escolha razão da viagem:
+ Trasporte
+ Estudos
+ Negócios
+ ExercÃcio
+ Social
+ Compras
+ Pessoal
+ Outra
+ Comentários sobre sua viagem
+ Idade:
+ Gênero:
+
+
+ NÃvel de
+ experiência com
+ ciclismo:
+
+ Enviar
+ Discartar
+ Viagem discartada.
+ Você deve escolher um propósito para a viagem antes de enviar! Escolha nas opções acima.
+ Transporte: sua viagem de bicicleta foi de transporte entre sua casa e seu local de trabalho.]]>
+ Estudos: sua viagem de bicicleta foi de transporte para a escola ou faculdade.]]>
+ Negócios: sua viagem de bicicleta foi com o propósito de negócios, transporte para reunião, ou transporte para realizar qualquer função relacioanda com trabalho.]]>
+ ExercÃcio: sua viagem de bicicleta foi primariamente para se exercitar, ou apenas para pedalar.]]>
+ Social: sua viagem de bicicleta foi pripariamente para transporte a uma atividade social, como ir para a casa de um amigo, ir ao parque, a um restaurante ou ao cinema.]]>
+ Compras: sua viagem de bicicleta foi primariamente para compras.]]>
+ Pessoal: sua viagem de bicicleta foi para atender a necessidaes pessoais, como ir ao banco, ao médico, ir para a academia, e etc.]]>
+ Outros: se nenhuma das razões acima se aplica, comente abaixo e nos diga mais.]]>
+ Favor selecionar
+ Favor selecionar seu gênero
+ Masculino
+ Feminino
+ Outro
+ Favor selecionar seu nÃvel de esperiência
+ Experiente
+ Médio
+ Iniciante
+ %1$1.1f milhas, %2$d minutos. %3$s
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values-ru/strings.xml b/libraries/cyclestreets-track/src/main/res/values-ru/strings.xml
new file mode 100644
index 000000000..6bb57e2f3
--- /dev/null
+++ b/libraries/cyclestreets-track/src/main/res/values-ru/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+ Ð¢ÐµÐºÑƒÑ‰Ð°Ñ ÑкороÑть:
+ Ð’Ñ€ÐµÐ¼Ñ Ð¿ÑƒÑ‚ÐµÑˆÐµÑтвий:
+ РаÑÑтоÑние:
+ Конец
+
+ Ð˜Ð½Ñ„Ð¾Ñ€Ð¼Ð°Ñ†Ð¸Ñ Ð¾ путешеÑтвие
+ Ð’Ñ€ÐµÐ¼Ñ Ð½Ð°Ñ‡Ð°Ð»Ð°
+ Цель поездки
+
+ ЗапиÑÑŒ окончена. Выберите цель поездки:
+ Ездить
+ Школа
+ СвÑÐ·Ð°Ð½Ð½Ð°Ñ Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð¾Ð¹
+ Упражнение
+ Социальное
+ Покупка
+ Поручение
+ Другое
+ Комментарии о вашей поездке
+ ВозраÑÑ‚:
+ Пол:
+
+
+ Уровень
+ езда на велоÑипеде
+ опыт:
+
+ Отправить
+ ОтброÑить
+ Поездка отброшена.
+ Вы должны выбрать цель поездки перед отправкой! Выберите один из указанных выше целей.
+ Ездить: Ñта поездка была в первую очередь, чтобы дойти до дом Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ñ‹ или наоборот.]]>
+ Школа: Ñта поездка была в первую очередь, чтобы прийти или уйти Ñ ÑˆÐºÐ¾Ð»Ð° или колледжа.]]>
+ СвÑÐ·Ð°Ð½Ð½Ð°Ñ Ñ Ñ€Ð°Ð±Ð¾Ñ‚Ð¾Ð¹: Ñта поездка была в первую очередь, чтобы прийти или уйти Ñ Ð´ÐµÐ»Ð¾Ð²Ð¾Ð¹ вÑтречи, функции или дело ÑвÑзанное Ñ Ð¼Ð¾ÐµÐ¹ работой.]]>
+ Упражнение: Ñто поездка была в первую очередь Ð´Ð»Ñ Ñ„Ð¸Ð·Ð¸Ñ‡ÐµÑких упражнений, или езда на велоÑипеде ради езды на велоÑипеде.]]>
+ Социальное: Ñто поездка была в первую очередь Ð´Ð»Ñ Ð¿ÐµÑ€ÐµÑ…Ð¾Ð´Ð° к или от Ñоциальной активноÑти, например, в другом доме, парк, реÑторан, кино.]]>
+ Покупка: Ñто поездка была в первую очередь, чтобы купить или привезти домой товары или продукты.]]>
+ Поручение: Ñто поездка была в первую очередь Ð´Ð»Ñ ÑƒÑ‡Ð°ÑÑ‚Ð¸Ñ Ð² личном бизнеÑе таких как банковÑкое дело, визит врача, поход в тренажерный зал и Ñ‚.д.]]>
+ Другое: еÑли ни одна из других причин, не применÑетÑÑ Ðº Ñтой поездке, вы можете ввеÑти комментарии ниже, чтобы раÑÑказать нам больше.]]>
+ ПожалуйÑта выберите
+ ПожалуйÑта, выберите пол
+ мужÑкой
+ женÑкий
+ Предпочитаю не говорить
+ ПожалуйÑта, выберите уровень опыта
+ опытный
+ нечаÑтый
+ начинающий
+ %1$1.1f мили, %2$d минут. %3$s
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values-tr/strings.xml b/libraries/cyclestreets-track/src/main/res/values-tr/strings.xml
new file mode 100644
index 000000000..03b2d3087
--- /dev/null
+++ b/libraries/cyclestreets-track/src/main/res/values-tr/strings.xml
@@ -0,0 +1,54 @@
+
+
+
+ Şuanki hız:
+ Seyahat zamanı:
+ Kat edilen yol:
+ BitiÅŸ
+
+ seyahat bilgisi
+ zamanı başlat
+ seyahat amacı
+
+ Kaydı bitir. Seyahat amacını seçiniz:
+ ev iş arası gidp gelmek
+ Okul
+ İş ile ilgili
+ Egzersiz
+ Sosyal
+ Alışveriş
+ Günlük işler
+ DiÄŸer
+ Seyahatiniz hakkında yorun giriniz
+ YaÅŸ:
+ Cinsiyet:
+
+
+ Seviye
+ bisiklete binme
+ deneyim:
+
+ Yayınla
+ İptal et
+ Seyahat iptal edildi.
+ Yayınlamadan önce seyahat amacınızı seçmelisiniz! Yukarıdan seyahat amacınızı seçiniz.
+ Commute: this bike trip was primarily to get between home and your main workplace.]]>
+ School: this bike trip was primarily to go to or from school or college.]]>
+ Work-Related: this bike trip was primarily to go to or from a business related meeting, function, or work-related errand for your job.]]>
+ Exercise: this bike trip was primarily for exercise, or biking for the sake of biking.]]>
+ Social: this bike trip was primarily for going to or from a social activity, e.g. at a friend\'s house, the park, a restaurant, the movies.]]>
+ Shopping: this bike trip was primarily to purchase or bring home goods or groceries.]]>
+ Errand: this bike trip was primarily to attend to personal business such as banking, a doctor visit, going to the gym, etc.]]>
+ Other: if none of the other reasons applied to this trip, you can enter comments below to tell us more.]]>
+ Lütfen seçiniz
+ Lütfen cinsiyetinizi seçiniz
+ erkek
+ kadın
+ bir şey söylememeyi tercih ediyorum
+ Lütfen deneyim seviyenizi seçiniz
+ deneyimli
+ nadir
+ başlangıç
+ %2$d dakikada, %1$1.1f mil. %3$s
+
+
diff --git a/libraries/cyclestreets-track/src/main/res/values/strings.xml b/libraries/cyclestreets-track/src/main/res/values/strings.xml
index 1885e0dff..47cde2477 100644
--- a/libraries/cyclestreets-track/src/main/res/values/strings.xml
+++ b/libraries/cyclestreets-track/src/main/res/values/strings.xml
@@ -1,6 +1,55 @@
- Saved Trips:
- Start Trip!
- Finish
+
+ Current Speed:
+ Journey Time:
+ Distance:
+ Finish
+
+ journey info
+ start time
+ purpose of trip
+
+ Finished recording. Choose a trip purpose:
+ Commute
+ School
+ Work-Related
+ Exercise
+ Social
+ Shopping
+ Errand
+ Other
+ " "
+ Comments about your trip
+ Age:
+ Gender:
+
+
+ Level of
+ cycling
+ experience:
+
+ Submit
+ Discard
+ Trip discarded.
+ You must select a trip purpose before submitting! Choose from the purposes above.
+ Commute: this bike trip was primarily to get between home and your main workplace.]]>
+ School: this bike trip was primarily to go to or from school or college.]]>
+ Work-Related: this bike trip was primarily to go to or from a business related meeting, function, or work-related errand for your job.]]>
+ Exercise: this bike trip was primarily for exercise, or biking for the sake of biking.]]>
+ Social: this bike trip was primarily for going to or from a social activity, e.g. at a friend\'s house, the park, a restaurant, the movies.]]>
+ Shopping: this bike trip was primarily to purchase or bring home goods or groceries.]]>
+ Errand: this bike trip was primarily to attend to personal business such as banking, a doctor visit, going to the gym, etc.]]>
+ Other: if none of the other reasons applied to this trip, you can enter comments below to tell us more.]]>
+ Please select
+ Please select gender
+ male
+ female
+ prefer not to say
+ Please select experience level
+ experienced
+ infrequent
+ beginner
+ %1$1.1f miles, %2$d minutes. %3$s
+
diff --git a/libraries/cyclestreets-view/build.gradle b/libraries/cyclestreets-view/build.gradle
index 87dcc8500..31ab4f208 100644
--- a/libraries/cyclestreets-view/build.gradle
+++ b/libraries/cyclestreets-view/build.gradle
@@ -1,12 +1,41 @@
evaluationDependsOn(':libraries:cyclestreets-core')
+android {
+ buildFeatures {
+ buildConfig true
+ }
+
+ testOptions {
+ unitTests {
+ includeAndroidResources = true
+ }
+ }
+ namespace 'net.cyclestreets.view'
+}
+
dependencies {
- compile project(':libraries:cyclestreets-core')
- compile files('libs/mapsforge-map-0.3.0-jar-with-dependencies.jar')
- compile 'com.android.support:appcompat-v7:20.+'
- compile 'com.getpebble:pebblekit:3.1.0@aar'
-
- // In the main app project, we're already pulling in 23.x via the `acra` dependency.
- compile 'com.android.support:support-v4:23.4.0'
- compile 'com.android.support:support-annotations:23.4.0'
+ api project(':libraries:cyclestreets-core')
+ implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4'
+ implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.9.0'
+ api "org.mapsforge:mapsforge-map-android:${rootProject.ext.mapsforgeVersion}"
+ api "org.mapsforge:mapsforge-themes:${rootProject.ext.mapsforgeVersion}"
+ // there is some suggestion (https://github.com/osmdroid/osmdroid/wiki/Mapsforge) that the below may be necessary
+// api "org.osmdroid:osmdroid-mapsforge:${rootProject.ext.osmdroidVersion}@aar"
+// Icons disappear with 5.2.8 so reverting to 5.0.3 for now
+ api 'com.mikepenz:iconics-core:5.0.3@aar'
+ api 'com.mikepenz:iconics-typeface-api:5.0.3@aar'
+ implementation 'com.mikepenz:iconics-views:5.0.3@aar'
+ api 'com.mikepenz:google-material-typeface:3.0.1.6.original-kotlin@aar'
+
+ api 'com.google.android.material:material:1.12.0'
+ api 'androidx.exifinterface:exifinterface:1.4.1'
+ api 'androidx.preference:preference:1.2.1'
+
+ testImplementation 'androidx.test:core:1.6.1'
+ testImplementation 'androidx.test.ext:junit:1.2.1'
+ testImplementation "junit:junit:${rootProject.ext.junitVersion}"
+ testImplementation "org.assertj:assertj-core:${rootProject.ext.assertjVersion}"
+ testImplementation "org.mockito:mockito-core:${rootProject.ext.mockitoVersion}"
+ testImplementation "org.robolectric:robolectric:${rootProject.ext.robolectricVersion}"
+ testImplementation 'commons-io:commons-io:2.16.1'
}
diff --git a/libraries/cyclestreets-view/gradle/wrapper/gradle-wrapper.jar b/libraries/cyclestreets-view/gradle/wrapper/gradle-wrapper.jar
deleted file mode 100644
index c97a8bdb9..000000000
Binary files a/libraries/cyclestreets-view/gradle/wrapper/gradle-wrapper.jar and /dev/null differ
diff --git a/libraries/cyclestreets-view/gradle/wrapper/gradle-wrapper.properties b/libraries/cyclestreets-view/gradle/wrapper/gradle-wrapper.properties
deleted file mode 100644
index 46a9213ec..000000000
--- a/libraries/cyclestreets-view/gradle/wrapper/gradle-wrapper.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-#Sat May 14 10:07:49 BST 2016
-distributionBase=GRADLE_USER_HOME
-distributionPath=wrapper/dists
-zipStoreBase=GRADLE_USER_HOME
-zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.13-bin.zip
diff --git a/libraries/cyclestreets-view/gradlew b/libraries/cyclestreets-view/gradlew
deleted file mode 100755
index 91a7e269e..000000000
--- a/libraries/cyclestreets-view/gradlew
+++ /dev/null
@@ -1,164 +0,0 @@
-#!/usr/bin/env bash
-
-##############################################################################
-##
-## Gradle start up script for UN*X
-##
-##############################################################################
-
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-
-# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
-
-warn ( ) {
- echo "$*"
-}
-
-die ( ) {
- echo
- echo "$*"
- echo
- exit 1
-}
-
-# OS specific support (must be 'true' or 'false').
-cygwin=false
-msys=false
-darwin=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
-esac
-
-# For Cygwin, ensure paths are in UNIX format before anything is touched.
-if $cygwin ; then
- [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
-fi
-
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >&-
-APP_HOME="`pwd -P`"
-cd "$SAVED" >&-
-
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-# Determine the Java command to use to start the JVM.
-if [ -n "$JAVA_HOME" ] ; then
- if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
- # IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
- else
- JAVACMD="$JAVA_HOME/bin/java"
- fi
- if [ ! -x "$JAVACMD" ] ; then
- die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
- fi
-else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-
-Please set the JAVA_HOME variable in your environment to match the
-location of your Java installation."
-fi
-
-# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
-fi
-
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
-
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
- # Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
- fi
- i=$((i+1))
- done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
-fi
-
-# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
-function splitJvmOpts() {
- JVM_OPTS=("$@")
-}
-eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
-JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
-
-exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/libraries/cyclestreets-view/gradlew.bat b/libraries/cyclestreets-view/gradlew.bat
deleted file mode 100644
index aec99730b..000000000
--- a/libraries/cyclestreets-view/gradlew.bat
+++ /dev/null
@@ -1,90 +0,0 @@
-@if "%DEBUG%" == "" @echo off
-@rem ##########################################################################
-@rem
-@rem Gradle startup script for Windows
-@rem
-@rem ##########################################################################
-
-@rem Set local scope for the variables with windows NT shell
-if "%OS%"=="Windows_NT" setlocal
-
-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-set DEFAULT_JVM_OPTS=
-
-set DIRNAME=%~dp0
-if "%DIRNAME%" == "" set DIRNAME=.
-set APP_BASE_NAME=%~n0
-set APP_HOME=%DIRNAME%
-
-@rem Find java.exe
-if defined JAVA_HOME goto findJavaFromJavaHome
-
-set JAVA_EXE=java.exe
-%JAVA_EXE% -version >NUL 2>&1
-if "%ERRORLEVEL%" == "0" goto init
-
-echo.
-echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:findJavaFromJavaHome
-set JAVA_HOME=%JAVA_HOME:"=%
-set JAVA_EXE=%JAVA_HOME%/bin/java.exe
-
-if exist "%JAVA_EXE%" goto init
-
-echo.
-echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
-echo.
-echo Please set the JAVA_HOME variable in your environment to match the
-echo location of your Java installation.
-
-goto fail
-
-:init
-@rem Get command-line arguments, handling Windowz variants
-
-if not "%OS%" == "Windows_NT" goto win9xME_args
-if "%@eval[2+2]" == "4" goto 4NT_args
-
-:win9xME_args
-@rem Slurp the command line arguments.
-set CMD_LINE_ARGS=
-set _SKIP=2
-
-:win9xME_args_slurp
-if "x%~1" == "x" goto execute
-
-set CMD_LINE_ARGS=%*
-goto execute
-
-:4NT_args
-@rem Get arguments from the 4NT Shell from JP Software
-set CMD_LINE_ARGS=%$
-
-:execute
-@rem Setup the command line
-
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
-
-@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
-
-:end
-@rem End local scope for the variables with windows NT shell
-if "%ERRORLEVEL%"=="0" goto mainEnd
-
-:fail
-rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
-rem the _cmd.exe /c_ return code!
-if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
-exit /b 1
-
-:mainEnd
-if "%OS%"=="Windows_NT" endlocal
-
-:omega
diff --git a/libraries/cyclestreets-view/libs/mapsforge-map-0.3.0-jar-with-dependencies.jar b/libraries/cyclestreets-view/libs/mapsforge-map-0.3.0-jar-with-dependencies.jar
deleted file mode 100644
index e8cdd622e..000000000
Binary files a/libraries/cyclestreets-view/libs/mapsforge-map-0.3.0-jar-with-dependencies.jar and /dev/null differ
diff --git a/libraries/cyclestreets-view/src/main/AndroidManifest.xml b/libraries/cyclestreets-view/src/main/AndroidManifest.xml
index 96cf41294..996272dfa 100644
--- a/libraries/cyclestreets-view/src/main/AndroidManifest.xml
+++ b/libraries/cyclestreets-view/src/main/AndroidManifest.xml
@@ -1,28 +1,37 @@
-
+
-
-
-
-
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
-
+
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/AccountDetailsActivity.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/AccountDetailsActivity.java
index 9641e1acc..66d21dad2 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/AccountDetailsActivity.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/AccountDetailsActivity.java
@@ -1,12 +1,13 @@
package net.cyclestreets;
-import net.cyclestreets.view.R;
-import net.cyclestreets.util.Dialog;
-import net.cyclestreets.util.MessageBox;
import net.cyclestreets.api.Registration;
+import net.cyclestreets.api.Result;
import net.cyclestreets.api.Signin;
+import net.cyclestreets.util.Dialog;
+import net.cyclestreets.util.MessageBox;
+import net.cyclestreets.util.ProgressDialog;
+import net.cyclestreets.view.R;
import android.app.Activity;
-import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
@@ -21,355 +22,329 @@
import android.widget.TextView;
public class AccountDetailsActivity extends Activity
- implements View.OnClickListener, TextWatcher
-{
- public enum RegisterStep
- {
- ACCOUNT(null),
-
- REGISTER_DETAILS(ACCOUNT),
-
- SIGNIN_DETAILS(ACCOUNT),
-
- EXISTING_SIGNIN_DETAILS(null);
-
- RegisterStep(final RegisterStep p)
- {
- prev_ = p;
- if(prev_ != null)
- prev_.next_ = this;
- } // AddStep
-
- public RegisterStep prev() { return prev_; }
- public RegisterStep next() { return next_; }
-
- private RegisterStep prev_;
- private RegisterStep next_;
- } // RegisterStep
-
- private RegisterStep step_;
-
- private View registerView_;
- private View registerDetails_;
- private View signinDetails_;
- private Button signinButton_;
-
- @Override
- public void onCreate(final Bundle saved)
- {
- super.onCreate(saved);
-
- final LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
- final InputFilter[] usernameFilters = new InputFilter[]{ new WhitespaceInputFilter() };
-
- registerView_ = inflater.inflate(R.layout.accountdetails, null);
- registerDetails_ = inflater.inflate(R.layout.accountregister, null);
- textView(registerDetails_, R.id.username).setFilters(usernameFilters);
- signinDetails_ = inflater.inflate(R.layout.accountsignin, null);
- signinButton_ = (Button)signinDetails_.findViewById(R.id.signin_button);
- TextView usernameTV = textView(signinDetails_, R.id.username);
- usernameTV.addTextChangedListener(this);
- usernameTV.setFilters(usernameFilters);
- textView(signinDetails_, R.id.password).addTextChangedListener(this);
- signinButton_.setEnabled(false);
-
- step_ = (CycleStreetsPreferences.accountOK()) ? RegisterStep.EXISTING_SIGNIN_DETAILS : RegisterStep.ACCOUNT;
-
- setupView();
- } // onCreate
-
- @Override
- public void onBackPressed()
- {
- step_ = step_.prev();
-
- if(step_ != null)
- setupView();
- else
- super.onBackPressed();
- } // onBackPressed
-
- private void setupView()
- {
- switch(step_)
- {
- case ACCOUNT:
- setContentView(registerView_);
- hookUpButton(registerView_, R.id.newaccount_button);
- hookUpButton(registerView_, R.id.existingaccount_button);
- break;
- case REGISTER_DETAILS:
- setContentView(registerDetails_);
- setText(registerDetails_, R.id.username, CycleStreetsPreferences.username());
- setText(registerDetails_, R.id.password, CycleStreetsPreferences.password());
- setText(registerDetails_, R.id.name, CycleStreetsPreferences.name());
- setText(registerDetails_, R.id.email, CycleStreetsPreferences.email());
- setText(registerDetails_, R.id.registration_message, registrationMessage());
- hookUpButton(registerDetails_, R.id.register_button);
- break;
- case SIGNIN_DETAILS:
- case EXISTING_SIGNIN_DETAILS:
- setContentView(signinDetails_);
- setText(signinDetails_, R.id.username, CycleStreetsPreferences.username());
- setText(signinDetails_, R.id.password, CycleStreetsPreferences.password());
- setText(signinDetails_, R.id.signin_message, signinMessage());
-
- hookUpButton(signinDetails_, R.id.signin_button);
- hookUpButton(signinDetails_, R.id.cleardetails_button);
- break;
- } // switch
- } // setupView
-
- private String signinMessage()
- {
- if(CycleStreetsPreferences.accountOK())
- return String.format("You are already signed in as\n%s, %s",
- CycleStreetsPreferences.name(),
- CycleStreetsPreferences.email());
- return "Please enter your account username and password to sign in.";
- } // signinMessage
-
- private String registrationMessage()
- {
- if(CycleStreetsPreferences.accountPending())
- return "You have already registered an account. Please check your email for the verification email.";
- return "Registration is free and registered users can add photos. To start " +
- "the registration process please enter your details in the form below.";
- } // registrationMessage
-
- private void hookUpButton(final View v, final int id)
- {
- final Button b = (Button)v.findViewById(id);
- if(b == null)
- return;
- b.setOnClickListener(this);
- } // hookUpNext
-
- private TextView textView(final View v, final int id)
- {
- return (TextView)v.findViewById(id);
- } // textView
-
- private void setText(final View v, final int id, final String value)
- {
- final TextView tv = textView(v, id);
- if(tv == null)
- return;
- tv.setText(value);
- } // setText
-
- private String getText(final View v, final int id)
- {
- final TextView tv = textView(v, id);
- return tv.getText().toString();
- } // getText
-
- @Override
- public void onClick(final View v)
- {
- final int clicked = v.getId();
-
- if(R.id.newaccount_button == clicked)
- step_ = RegisterStep.REGISTER_DETAILS;
- if(R.id.existingaccount_button == clicked)
- step_ = RegisterStep.SIGNIN_DETAILS;
- if(R.id.cleardetails_button == clicked)
- confirmClear();
- if(R.id.signin_button == clicked) {
- signin();
- return;
+ implements View.OnClickListener, TextWatcher {
+ public enum RegisterStep {
+ ACCOUNT(null),
+
+ REGISTER_DETAILS(ACCOUNT),
+
+ SIGNIN_DETAILS(ACCOUNT),
+
+ EXISTING_SIGNIN_DETAILS(null);
+
+ RegisterStep(final RegisterStep p) {
+ prev_ = p;
+ if (prev_ != null)
+ prev_.next_ = this;
+ }
+
+ public RegisterStep prev() {
+ return prev_;
+ }
+
+ public RegisterStep next() {
+ return next_;
+ }
+
+ private RegisterStep prev_;
+ private RegisterStep next_;
}
- if(R.id.register_button == clicked) {
- register();
- return;
+
+ private RegisterStep step_;
+
+ private View registerView_;
+ private View registerDetails_;
+ private View signinDetails_;
+ private Button signinButton_;
+
+ @Override
+ public void onCreate(final Bundle saved) {
+ super.onCreate(saved);
+
+ final LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+ final InputFilter[] usernameFilters = new InputFilter[]{new WhitespaceInputFilter()};
+
+ registerView_ = inflater.inflate(R.layout.accountdetails, null);
+ registerDetails_ = inflater.inflate(R.layout.accountregister, null);
+ textView(registerDetails_, R.id.username).setFilters(usernameFilters);
+ signinDetails_ = inflater.inflate(R.layout.accountsignin, null);
+ signinButton_ = (Button) signinDetails_.findViewById(R.id.signin_button);
+ TextView usernameTV = textView(signinDetails_, R.id.username);
+ usernameTV.addTextChangedListener(this);
+ usernameTV.setFilters(usernameFilters);
+ textView(signinDetails_, R.id.password).addTextChangedListener(this);
+ signinButton_.setEnabled(false);
+
+ step_ = (CycleStreetsPreferences.accountOK()) ? RegisterStep.EXISTING_SIGNIN_DETAILS : RegisterStep.ACCOUNT;
+
+ setupView();
}
-
- setupView();
- } // onClick
- ///////////////////////////////////////////////////////
-
- @Override
- public void afterTextChanged(Editable arg0) { }
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count)
- {
- final String username = getText(signinDetails_, R.id.username);
- final String password = getText(signinDetails_, R.id.password);
- signinButton_.setEnabled((username.length() != 0) && (password.length() != 0));
- } // onTextChanged
-
- ///////////////////////////////////////////////////////
- private void confirmClear()
- {
- MessageBox.YesNo(signinDetails_,
- "Are you sure you want to clear the stored account details?",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface arg0, int arg1) {
- CycleStreetsPreferences.clearUsernamePassword();
- setupView();
- }
- });
- } // confirmClear
-
- ////////////////////////////////////////////////////////
- private void MessageBox(final String message, final boolean finishOnOK)
- {
- MessageBox.OKAndFinish(signinDetails_, message, this, finishOnOK);
- } // MessageBox
-
- ////////////////////////////////////////////////////////
- private void signin()
- {
- final String username = getText(signinDetails_, R.id.username);
- final String password = getText(signinDetails_, R.id.password);
-
- if((username.length() == 0) || (password.length() == 0))
- {
- MessageBox("Please enter username and password.", false);
- return;
- } // if ...
- final SignInTask task = new SignInTask(this, username, password);
- task.execute();
- } // signin
-
- private class SignInTask extends AsyncTask
- {
- private final String username_;
- private final String password_;
- private final ProgressDialog progress_;
-
- SignInTask(final Context context,
- final String username,
- final String password)
- {
- username_ = username;
- password_ = password;
-
- progress_ = Dialog.createProgressDialog(context, R.string.signing_in);
- } // SigninTask
-
+
@Override
- protected void onPreExecute()
- {
- super.onPreExecute();
- progress_.show();
- } // onPreExecute
-
- protected Signin.Result doInBackground(Object... params)
- {
- return Signin.signin(username_, password_);
- } // doInBackground
-
+ public void onBackPressed() {
+ step_ = step_.prev();
+
+ if (step_ != null)
+ setupView();
+ else
+ super.onBackPressed();
+ }
+
+ private void setupView() {
+ switch (step_) {
+ case ACCOUNT:
+ setContentView(registerView_);
+ hookUpButton(registerView_, R.id.newaccount_button);
+ hookUpButton(registerView_, R.id.existingaccount_button);
+ break;
+ case REGISTER_DETAILS:
+ setContentView(registerDetails_);
+ setText(registerDetails_, R.id.username, CycleStreetsPreferences.username());
+ setText(registerDetails_, R.id.password, CycleStreetsPreferences.password());
+ setText(registerDetails_, R.id.name, CycleStreetsPreferences.name());
+ setText(registerDetails_, R.id.email, CycleStreetsPreferences.email());
+ setText(registerDetails_, R.id.registration_message, registrationMessage());
+ hookUpButton(registerDetails_, R.id.register_button);
+ break;
+ case SIGNIN_DETAILS:
+ case EXISTING_SIGNIN_DETAILS:
+ setContentView(signinDetails_);
+ setText(signinDetails_, R.id.username, CycleStreetsPreferences.username());
+ setText(signinDetails_, R.id.password, CycleStreetsPreferences.password());
+ setText(signinDetails_, R.id.signin_message, signinMessage());
+
+ hookUpButton(signinDetails_, R.id.signin_button);
+ hookUpButton(signinDetails_, R.id.cleardetails_button);
+ break;
+ }
+ }
+
+ private String signinMessage() {
+ if (CycleStreetsPreferences.accountOK())
+ return getString(R.string.account_already_signed_in_format,
+ CycleStreetsPreferences.name(),
+ CycleStreetsPreferences.email());
+ return getString(R.string.account_signin_message);
+ }
+
+ private String registrationMessage() {
+ if (CycleStreetsPreferences.accountPending())
+ return getString(R.string.account_pending);
+ return getString(R.string.account_registration_is_free_long);
+ }
+
+ private void hookUpButton(final View v, final int id) {
+ final Button b = (Button) v.findViewById(id);
+ if (b == null)
+ return;
+ b.setOnClickListener(this);
+ }
+
+ private TextView textView(final View v, final int id) {
+ return (TextView) v.findViewById(id);
+ }
+
+ private void setText(final View v, final int id, final String value) {
+ final TextView tv = textView(v, id);
+ if (tv == null)
+ return;
+ tv.setText(value);
+ }
+
+ private String getText(final View v, final int id) {
+ final TextView tv = textView(v, id);
+ return tv.getText().toString();
+ }
+
@Override
- protected void onPostExecute(final Signin.Result result)
- {
- progress_.dismiss();
-
- CycleStreetsPreferences.setUsernamePassword(username_,
- password_,
- result.name(),
- result.email(),
- result.ok());
- setText(signinDetails_, R.id.signin_message, signinMessage());
-
- String msg = "You have successfully signed into CycleStreets.";
- if(!result.ok())
- msg = result.error().startsWith("Error:") ? result.error() : "Could not sign into CycleStreets. Please check your username and password.";
- MessageBox(msg, result.ok());
- } // onPostExecute
- } // class SignInTask
-
- ////////////////////////////////////////////////////////
- private void register()
- {
- final String emailRegex = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$";
-
- final String username = getText(registerDetails_, R.id.username);
- final String password = getText(registerDetails_, R.id.password);
- final String password2 = getText(registerDetails_, R.id.confirm_password);
- final String name = getText(registerDetails_, R.id.name);
- final String email = getText(registerDetails_, R.id.email);
-
- String oops = null;
-
- if(!email.toLowerCase().matches(emailRegex))
- oops = "The email address entered is not a properly formatted address.";
- if(!password.equals(password2))
- oops = "Password and confirmation password do not match.";
- if(username.length() < 5)
- oops = "Username must be at least five letters/numbers long.";
-
- if(oops != null)
- {
- MessageBox(oops, false);
- return;
- } // if(oops)
-
- final RegisterTask task = new RegisterTask(this,
- username,
- password,
- name,
- email);
- task.execute();
- } // register
-
- private class RegisterTask extends AsyncTask
- {
- private final String username_;
- private final String password_;
- private final String name_;
- private final String email_;
- private final ProgressDialog progress_;
-
- RegisterTask(final Context context,
- final String username,
- final String password,
- final String name,
- final String email)
- {
- username_ = username;
- password_ = password;
- name_ = name;
- email_ = email;
-
- progress_ = Dialog.createProgressDialog(context, R.string.registering);
- } // RegisterTask
-
+ public void onClick(final View v) {
+ final int clicked = v.getId();
+
+ if (R.id.newaccount_button == clicked)
+ step_ = RegisterStep.REGISTER_DETAILS;
+ if (R.id.existingaccount_button == clicked)
+ step_ = RegisterStep.SIGNIN_DETAILS;
+ if (R.id.cleardetails_button == clicked)
+ confirmClear();
+ if (R.id.signin_button == clicked) {
+ signin();
+ return;
+ }
+ if (R.id.register_button == clicked) {
+ register();
+ return;
+ }
+
+ setupView();
+ }
+ ///////////////////////////////////////////////////////
+
@Override
- protected void onPreExecute()
- {
- super.onPreExecute();
- progress_.show();
- } // onPreExecute
-
- protected Registration.Result doInBackground(Object... params)
- {
- return Registration.register(username_,
- password_,
- name_,
- email_);
- } // doInBackground
-
+ public void afterTextChanged(Editable arg0) {
+ }
+
@Override
- protected void onPostExecute(final Registration.Result result)
- {
- progress_.dismiss();
- CycleStreetsPreferences.setPendingUsernamePassword(username_, password_, name_, email_, result.ok());
- MessageBox(result.message(), result.ok());
- } // onPostExecute
- } // class RegisterTask
-
- private class WhitespaceInputFilter extends LoginFilter.UsernameFilterGeneric {
- public WhitespaceInputFilter() {
- super(false);
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
- public boolean isAllowed(char c) {
- return !Character.isWhitespace(c);
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ final String username = getText(signinDetails_, R.id.username);
+ final String password = getText(signinDetails_, R.id.password);
+ signinButton_.setEnabled((username.length() != 0) && (password.length() != 0));
+ }
+
+ ///////////////////////////////////////////////////////
+ private void confirmClear() {
+ MessageBox.YesNo(signinDetails_,
+ getString(R.string.account_clear_details_confirm),
+ new DialogInterface.OnClickListener() {
+ public void onClick(DialogInterface arg0, int arg1) {
+ CycleStreetsPreferences.clearUsernamePassword();
+ setupView();
+ }
+ });
+ }
+
+ ////////////////////////////////////////////////////////
+ private void MessageBox(final String message, final boolean finishOnOK) {
+ MessageBox.OKAndFinish(signinDetails_, message, this, finishOnOK);
+ }
+
+ ////////////////////////////////////////////////////////
+ private void signin() {
+ final String username = getText(signinDetails_, R.id.username);
+ final String password = getText(signinDetails_, R.id.password);
+
+ if ((username.length() == 0) || (password.length() == 0)) {
+ MessageBox("Please enter username and password.", false);
+ return;
+ }
+ final SignInTask task = new SignInTask(this, username, password);
+ task.execute();
+ }
+
+ private class SignInTask extends AsyncTask {
+ private final String username_;
+ private final String password_;
+ private final ProgressDialog progress_;
+
+ SignInTask(final Context context,
+ final String username,
+ final String password) {
+ username_ = username;
+ password_ = password;
+
+ progress_ = Dialog.createProgressDialog(context, R.string.account_signing_in);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ progress_.show();
+ }
+
+ protected Signin.Result doInBackground(Object... params) {
+ return Signin.signin(username_, password_);
+ }
+
+ @Override
+ protected void onPostExecute(final Signin.Result result) {
+ progress_.dismiss();
+
+ CycleStreetsPreferences.setUsernamePassword(username_,
+ password_,
+ result.name(),
+ result.email(),
+ result.ok());
+ setText(signinDetails_, R.id.signin_message, signinMessage());
+
+ MessageBox(result.message(), result.ok());
+ }
+ }
+
+ ////////////////////////////////////////////////////////
+ private void register() {
+ final String emailRegex = "^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$";
+
+ final String username = getText(registerDetails_, R.id.username);
+ final String password = getText(registerDetails_, R.id.password);
+ final String password2 = getText(registerDetails_, R.id.confirm_password);
+ final String name = getText(registerDetails_, R.id.name);
+ final String email = getText(registerDetails_, R.id.email);
+
+ String oops = null;
+
+ if (!email.toLowerCase().matches(emailRegex))
+ oops = getString(R.string.account_email_format);
+ if (!password.equals(password2))
+ oops = getString(R.string.account_password_mismatch);
+ if (username.length() < 5)
+ oops = getString(R.string.account_username_too_short);
+
+ if (oops != null) {
+ MessageBox(oops, false);
+ return;
+ }
+
+ final RegisterTask task = new RegisterTask(this,
+ username,
+ password,
+ name,
+ email);
+ task.execute();
+ }
+
+ private class RegisterTask extends AsyncTask {
+ private final String username_;
+ private final String password_;
+ private final String name_;
+ private final String email_;
+ private final ProgressDialog progress_;
+
+ RegisterTask(final Context context,
+ final String username,
+ final String password,
+ final String name,
+ final String email) {
+ username_ = username;
+ password_ = password;
+ name_ = name;
+ email_ = email;
+
+ progress_ = Dialog.createProgressDialog(context, R.string.account_registering);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ progress_.show();
+ }
+
+ protected Result doInBackground(Object... params) {
+ return Registration.register(username_,
+ password_,
+ name_,
+ email_);
+ }
+
+ @Override
+ protected void onPostExecute(final Result result) {
+ progress_.dismiss();
+ CycleStreetsPreferences.setPendingUsernamePassword(username_, password_, name_, email_, result.ok());
+ MessageBox(result.message(), result.ok());
+ }
+ }
+
+ private class WhitespaceInputFilter extends LoginFilter.UsernameFilterGeneric {
+ public WhitespaceInputFilter() {
+ super(false);
+ }
+
+ @Override
+ public boolean isAllowed(char c) {
+ return !Character.isWhitespace(c);
+ }
}
- } // class WhitespaceInputFilter
-} // class AccountDetailsActivity
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/BlogState.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/BlogState.kt
new file mode 100644
index 000000000..f0d4a9e80
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/BlogState.kt
@@ -0,0 +1,71 @@
+package net.cyclestreets
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+
+import net.cyclestreets.api.Blog
+import net.cyclestreets.util.Logging
+
+import java.util.Timer
+import java.util.TimerTask
+
+private const val BLOG_SHARED_PREFERENCES_NAME = "net.cyclestreets.blog"
+private const val LAST_DATE_KEY = "lastDate"
+private const val UPDATE_AVAILABLE_KEY = "updateAvailable"
+private const val ONE_MINUTE_DELAY = (1000 * 60).toLong()
+private const val ONE_DAY_REPEAT = (1000 * 60 * 60 * 24).toLong()
+private val TAG: String = Logging.getTag(BlogState::class.java)
+
+object BlogState {
+
+ private val blogUpdateTimer: Timer = Timer()
+ private var initialised: Boolean = false
+
+ fun initialise(context: Context) {
+ if (!initialised) {
+ Log.d(TAG, "Starting blog update timer")
+ blogUpdateTimer.schedule(CheckBlogTask(context.applicationContext), ONE_MINUTE_DELAY, ONE_DAY_REPEAT)
+ initialised = true
+ }
+ }
+
+ fun isBlogUpdateAvailable(context: Context): Boolean {
+ return prefs(context).getBoolean(UPDATE_AVAILABLE_KEY, false)
+ }
+
+ fun markBlogAsRead(context: Context) {
+ prefs(context).edit()
+ .putBoolean(UPDATE_AVAILABLE_KEY, false)
+ .apply()
+ }
+
+ private fun prefs(context: Context): SharedPreferences {
+ return context.getSharedPreferences(BLOG_SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ }
+
+ private class CheckBlogTask(private val checkBlogContext: Context) : TimerTask() {
+ override fun run() {
+ Log.d(TAG, "Checking for blog updates")
+ val blog = Blog.load()
+
+ // check for new blog entries
+ if (blog.isNull || blog.mostRecent() == lastBlogUpdate(checkBlogContext))
+ return
+
+ Log.d(TAG, "New blog update found, date=${blog.mostRecent()}, title=${blog.mostRecentTitle()}")
+ blogUpdated(checkBlogContext, blog.mostRecent())
+ }
+
+ private fun lastBlogUpdate(context: Context): String? {
+ return prefs(context).getString(LAST_DATE_KEY, null)
+ }
+
+ private fun blogUpdated(context: Context, update: String?) {
+ prefs(context).edit()
+ .putString(LAST_DATE_KEY, update)
+ .putBoolean(UPDATE_AVAILABLE_KEY, true)
+ .apply()
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/CycleStreetsConstants.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/CycleStreetsConstants.java
deleted file mode 100644
index 8bb2cfc42..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/CycleStreetsConstants.java
+++ /dev/null
@@ -1,10 +0,0 @@
-package net.cyclestreets;
-
-public class CycleStreetsConstants {
- // Intent constants
- public static final String EXTRA_ROUTE_TYPE = "net.cyclestreets.extra.ROUTE_TYPE";
- public static final String EXTRA_ROUTE_SPEED = "net.cyclestreets.extra.ROUTE_SPEED";
- public static final String EXTRA_ROUTE_NUMBER = "net.cyclestreets.extra.ROUTE";
-
- public static final String ROUTE_ID = "net.cyclestreets.extra.ROUTE_ID";
-}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/CycleStreetsConstants.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/CycleStreetsConstants.kt
new file mode 100644
index 000000000..4ef5baa16
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/CycleStreetsConstants.kt
@@ -0,0 +1,37 @@
+package net.cyclestreets
+
+// Defaults for cycle maps
+const val DEFAULT_GPS_STATE = true
+const val DEFAULT_ZOOM_LEVEL = 14.0
+const val FINDPLACE_ZOOM_LEVEL = 16.0
+const val ITEM_ZOOM_LEVEL = 16.0
+const val MAX_ZOOM_LEVEL = 19.0
+const val MIN_ZOOM_LEVEL = 2.0
+// Greenwich!
+const val DEFAULT_MAP_CENTRE_LONGITUDE = 0
+const val DEFAULT_MAP_CENTRE_LATITUDE = 51477841
+// No cycling at the Poles
+const val MAX_LATITUDE_NORTH = 80
+const val MAX_LATITUDE_SOUTH = -80
+
+// Circular routing
+const val CIRCULAR_ROUTE_ACTIVITY_REQUEST_CODE = 1
+const val CIRCULAR_ROUTE_MIN_MINUTES = 5
+const val CIRCULAR_ROUTE_MAX_MINUTES = 200
+const val CIRCULAR_ROUTE_MIN_DISTANCE = 1
+const val CIRCULAR_ROUTE_MAX_DISTANCE_KM = 50
+const val CIRCULAR_ROUTE_MAX_DISTANCE_MILES = 30
+
+// Intent constants
+const val EXTRA_ROUTE_TYPE = "net.cyclestreets.extra.ROUTE_TYPE"
+const val EXTRA_ROUTE_SPEED = "net.cyclestreets.extra.ROUTE_SPEED"
+const val EXTRA_ROUTE_NUMBER = "net.cyclestreets.extra.ROUTE"
+const val EXTRA_CIRCULAR_ROUTE_DISTANCE = "net.cyclestreets.extra.CIRCULAR_ROUTE_DISTANCE"
+const val EXTRA_CIRCULAR_ROUTE_DURATION = "net.cyclestreets.extra.CIRCULAR_ROUTE_DURATION"
+const val EXTRA_CIRCULAR_ROUTE_POI_CATEGORIES = "net.cyclestreets.extra.CIRCULAR_ROUTE_POI_CATEGORIES"
+const val ROUTE_ID = "net.cyclestreets.extra.ROUTE_ID"
+
+// Permission request codes
+const val GENERIC_PERMISSION_REQUEST = 1
+const val LIVERIDE_LOCATION_PERMISSION_REQUEST = 2
+const val FOLLOW_LOCATION_PERMISSION_REQUEST = 3
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/DisplayPhoto.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/DisplayPhoto.java
index af29338a5..1370adbac 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/DisplayPhoto.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/DisplayPhoto.java
@@ -1,16 +1,18 @@
package net.cyclestreets;
+
import net.cyclestreets.api.Photo;
+import net.cyclestreets.util.ImageDownloader;
+import net.cyclestreets.util.ProgressDialog;
import net.cyclestreets.util.Screen;
import net.cyclestreets.view.R;
-import net.cyclestreets.util.ImageDownloader;
import android.app.AlertDialog;
-import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.graphics.Bitmap;
+import android.graphics.Point;
import android.graphics.drawable.BitmapDrawable;
import android.media.MediaPlayer;
import android.net.Uri;
@@ -30,6 +32,7 @@
import android.widget.VideoView;
import java.lang.ref.WeakReference;
+import java.text.DateFormat;
public final class DisplayPhoto {
public static void launch(final Photo photo, final Context context) {
@@ -38,7 +41,7 @@ public static void launch(final Photo photo, final Context context) {
photoDisplay(photo, context);
dd.show();
- } // launch
+ }
private DisplayPhoto() { }
@@ -49,30 +52,30 @@ private static ImageDisplay videoDisplay(
if (Screen.isSmall(context))
return new ExternalVideoPlayer(photo, context);
return new VideoDisplay(photo, context);
- } // VideoDisplay
+ }
- private static class VideoDisplay
- extends DisplayDialog
- implements MediaPlayer.OnPreparedListener {
+ private static class VideoDisplay extends DisplayDialog implements MediaPlayer.OnPreparedListener {
private VideoView vv_;
private VideoControllerView controller_;
private ProgressDialog pd_;
VideoDisplay(final Photo photo, final Context context) {
super(photo, context);
- } // VideoDisplay
+ }
@Override
protected String title() { return String.format("Video #%d", photo_.id()); }
@Override
protected String caption() { return photo_.caption().replace('\n', ' '); }
@Override
+ protected long datetime() {return photo_.datetime();}
+ @Override
protected View loadLayout() {
final View layout = View.inflate(context_, R.layout.showvideo, null);
controller_ = new VideoControllerView(layout.findViewById(R.id.videocontroller));
vv_ = (VideoView)layout.findViewById(R.id.video);
return layout;
- } // loadLayout
+ }
@Override
protected void postShowSetup(AlertDialog dialog) {
@@ -91,7 +94,7 @@ protected void postShowSetup(AlertDialog dialog) {
pd_.show();
vv_.setOnPreparedListener(this);
- } // postShowSetup
+ }
@Override
public void onPrepared(final MediaPlayer mediaPlayer) {
@@ -111,17 +114,17 @@ public void onPrepared(final MediaPlayer mediaPlayer) {
vv_.setLayoutParams(new LinearLayout.LayoutParams(newwidth, newheight));
}
- } // onPrepared
+ }
private static String videoUrl(final Photo photo) {
for (String format : new String[]{ "mp4", "mov", "3gp" }) {
Photo.Video v = photo.video(format);
if (v != null)
return v.url();
- } // for ...
+ }
return null;
- } // videoUrl
- } // VideoDisplay
+ }
+ }
private static class ExternalVideoPlayer implements ImageDisplay {
protected final Photo photo_;
@@ -130,7 +133,7 @@ private static class ExternalVideoPlayer implements ImageDisplay {
ExternalVideoPlayer(final Photo photo, final Context context) {
photo_ = photo;
context_ = context;
- } // VideoDisplay
+ }
public void show() {
final String videoUrl = videoUrl(photo_);
@@ -140,37 +143,39 @@ public void show() {
final Intent player = new Intent(Intent.ACTION_VIEW);
player.setDataAndType(Uri.parse(videoUrl), "video/*");
context_.startActivity(player);
- } // show
+ }
private static String videoUrl(final Photo photo) {
for (String format : new String[]{ "mp4", "mov", "3gp" }) {
Photo.Video v = photo.video(format);
if (v != null)
return v.url();
- } // for ...
+ }
return null;
- } // videoUrl
- } // class ExternalVideoPlayer
+ }
+ }
/////////////////////////////////////////////////////////////////////
private static ImageDisplay photoDisplay(
final Photo photo,
final Context context) {
return new PhotoDisplay(photo, context);
- } // photoDisplay
+ }
private static class PhotoDisplay extends DisplayDialog {
private ImageView iv_;
PhotoDisplay(final Photo photo, final Context context) {
super(photo, context);
- } // PhotoDisplay
+ }
@Override
protected String title() { return String.format("Photo #%d", photo_.id()); }
@Override
protected String caption() { return photo_.caption(); }
@Override
+ protected long datetime() { return photo_.datetime(); }
+ @Override
protected View loadLayout() {
final View layout = View.inflate(context_, R.layout.showphoto, null);
iv_ = (ImageView)layout.findViewById(R.id.photo);
@@ -182,7 +187,7 @@ protected View loadLayout() {
ImageDownloader.get(thumbnailUrl, iv_);
return layout;
- } // loadLayout
+ }
@Override
protected void preShowSetup(AlertDialog.Builder builder) {
@@ -191,10 +196,10 @@ protected void preShowSetup(AlertDialog.Builder builder) {
public void onCancel(DialogInterface dialogInterface) {
final Bitmap photo = ((BitmapDrawable)iv_.getDrawable()).getBitmap();
photo.recycle();
- } // onCancel
+ }
});
- } // preShowSetup
- } // class PhotoDisplay
+ }
+ }
///////////////////////////////////////////////////////////
private interface ImageDisplay {
@@ -211,7 +216,7 @@ protected DisplayDialog(final Photo photo, final Context context) {
photo_ = photo;
context_ = context;
gd_ = new GestureDetector(context_, this);
- } // Display
+ }
public void show() {
final AlertDialog.Builder builder = new AlertDialog.Builder(context_);
@@ -220,9 +225,16 @@ public void show() {
final View layout = loadLayout();
builder.setView(layout);
- final TextView text = (TextView)layout.findViewById(R.id.caption);
+ final TextView text = layout.findViewById(R.id.caption);
text.setText(caption());
+ final TextView textDate = layout.findViewById(R.id.datetime);
+ String stringDate = "";
+ if (datetime() != -1) {
+ stringDate = DateFormat.getDateInstance(DateFormat.LONG).format(datetime() * 1000);
+ }
+ textDate.setText(stringDate);
+
preShowSetup(builder);
ad_ = builder.create();
@@ -231,12 +243,12 @@ public void show() {
postShowSetup(ad_);
layout.setOnTouchListener(this);
- } // show
+ }
@Override
public boolean onTouch(View view, MotionEvent event) {
return gd_.onTouchEvent(event);
- } // onTouch
+ }
@Override public boolean onDown(MotionEvent motionEvent) { return false; }
@Override public void onShowPress(MotionEvent motionEvent) { }
@@ -248,26 +260,29 @@ public boolean onTouch(View view, MotionEvent event) {
public boolean onFling(MotionEvent motionEvent, MotionEvent motionEvent1, float v, float v1) {
ad_.cancel();
return true;
- } // onFling
+ }
protected abstract String title();
protected abstract String caption();
+ protected abstract long datetime();
protected abstract View loadLayout();
-
+
protected void preShowSetup(AlertDialog.Builder builder) { }
protected void postShowSetup(AlertDialog dialog) { }
protected static void sizeView(final View v, final Context context) {
final WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
- final int device_height = wm.getDefaultDisplay().getHeight();
- final int device_width = wm.getDefaultDisplay().getWidth();
+ final Point point = new Point();
+ wm.getDefaultDisplay().getSize(point);
+ final int device_height = point.y;
+ final int device_width = point.x;
final int height = (device_height > device_width)
? device_height / 10 * 5
: device_height / 10 * 6;
final int width = device_width;
v.setLayoutParams(new LinearLayout.LayoutParams(width, height));
- } // sizeView
- } // DisplayDialog
+ }
+ }
/////////////////////////////////////////////////////////////////////////////////////
private static class VideoControllerView {
@@ -286,14 +301,14 @@ private static class VideoControllerView {
public VideoControllerView(final View controllerView) {
controllerView_ = controllerView;
initControllerView(controllerView_);
- } // VideoControllerView
+ }
public int getWidth() { return controllerView_.getWidth(); }
public void setMediaPlayer(MediaController.MediaPlayerControl player) {
videoPlayer_ = player;
updatePausePlay();
- } // setMediaPlayer
+ }
private void initControllerView(View v) {
pauseBtn_ = (ImageButton)v.findViewById(R.id.pause);
@@ -311,7 +326,7 @@ private void initControllerView(View v) {
timeElapsed_ = (TextView)v.findViewById(R.id.time_current);
endTime_ = (TextView)v.findViewById(R.id.time);
- } // initControllerView
+ }
private void disableUnsupportedButtons() {
if (videoPlayer_ == null)
@@ -336,20 +351,20 @@ public void show() {
setProgress();
pauseBtn_.requestFocus();
disableUnsupportedButtons();
- } // if ...
+ }
controllerView_.setVisibility(View.VISIBLE);
updatePausePlay();
msgHandler_.sendEmptyMessage(SHOW_PROGRESS);
- } // show
+ }
private boolean isShowing() {
return (controllerView_.getVisibility() == View.VISIBLE);
- } // isShowing
+ }
public void hide() {
controllerView_.setVisibility(View.GONE);
- } // hide
+ }
private String formatTime(int timeMs) {
int totalSeconds = timeMs / 1000;
@@ -362,7 +377,7 @@ private String formatTime(int timeMs) {
return String.format("%d:%02d:%02d", hours, minutes, seconds);
else
return String.format("%02d:%02d", minutes, seconds);
- } // formatTime
+ }
private int setProgress() {
if (videoPlayer_ == null || isDragging_)
@@ -373,7 +388,7 @@ private int setProgress() {
if (duration > 0) {
long pos = 1000L * position / duration;
seekBar_.setProgress((int)pos);
- } // if ...
+ }
int percent = videoPlayer_.getBufferPercentage();
seekBar_.setSecondaryProgress(percent * 10);
@@ -381,7 +396,7 @@ private int setProgress() {
endTime_.setText(formatTime(duration));
return position;
- } // setProgress
+ }
private View.OnClickListener pauseListener_ = new View.OnClickListener() {
public void onClick(View v) { doPauseResume(); }
@@ -389,7 +404,7 @@ private int setProgress() {
public void updatePausePlay() {
pauseBtn_.setImageResource(videoPlayer_.isPlaying() ? R.drawable.ic_media_pause : R.drawable.ic_media_play);
- } // updatePausePlay
+ }
private void doPauseResume() {
if (videoPlayer_.isPlaying())
@@ -397,7 +412,7 @@ private void doPauseResume() {
else
videoPlayer_.start();
updatePausePlay();
- } // doPauseResume
+ }
private SeekBar.OnSeekBarChangeListener seekListener_ = new SeekBar.OnSeekBarChangeListener() {
public void onStartTrackingTouch(SeekBar bar) {
@@ -409,7 +424,7 @@ public void onStartTrackingTouch(SeekBar bar) {
// we will post one of these messages to the queue again and
// this ensures that there will be exactly one message queued up.
msgHandler_.removeMessages(SHOW_PROGRESS);
- } // onStartTrackingTouch
+ }
public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
if (!fromuser)
@@ -419,7 +434,7 @@ public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
long newposition = (duration * progress) / 1000L;
videoPlayer_.seekTo((int)newposition);
timeElapsed_.setText(formatTime((int) newposition));
- } // onProgressChanged
+ }
public void onStopTrackingTouch(SeekBar bar) {
isDragging_ = false;
@@ -429,7 +444,7 @@ public void onStopTrackingTouch(SeekBar bar) {
// the call to show() does not guarantee this because it is a
// no-op if we are already showing.
msgHandler_.sendEmptyMessage(SHOW_PROGRESS);
- } // onStopTrackingTouch
+ }
};
private View.OnClickListener rewListener_ = new View.OnClickListener() {
@@ -470,10 +485,9 @@ public void handleMessage(Message msg) {
sendMessageDelayed(msg, 1000 - (pos % 1000));
}
view.updatePausePlay();
- } // if ...
- } // handleMessage
- } // class MessageHandler
- } // class VideoViewController
-} // DisplayPhoto
-
+ }
+ }
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/FeedbackActivity.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/FeedbackActivity.java
index 989d81673..cf056a738 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/FeedbackActivity.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/FeedbackActivity.java
@@ -1,10 +1,15 @@
package net.cyclestreets;
-import net.cyclestreets.view.R;
import net.cyclestreets.api.Feedback;
+import net.cyclestreets.api.Result;
import net.cyclestreets.routing.Route;
+import net.cyclestreets.util.Dialog;
import net.cyclestreets.util.MessageBox;
+import net.cyclestreets.util.ProgressDialog;
+import net.cyclestreets.view.R;
import android.app.Activity;
+import android.content.Context;
+import android.os.AsyncTask;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
@@ -13,66 +18,94 @@
import android.widget.Button;
import android.widget.TextView;
-public class FeedbackActivity extends Activity implements TextWatcher, OnClickListener
-{
- private Button upload_;
-
- @Override
- protected void onCreate(final Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.routefeedback);
-
- upload_ = (Button)findViewById(R.id.upload);
-
- upload_.setEnabled(false);
- upload_.setOnClickListener(this);
-
- setText(R.id.name, CycleStreetsPreferences.name());
- setText(R.id.email, CycleStreetsPreferences.email());
-
- textView(R.id.comments).addTextChangedListener(this);
- } // onCreate
-
- private TextView textView(final int id)
- {
- return (TextView)findViewById(id);
- } // textView
-
- private void setText(final int id, final String value)
- {
- textView(id).setText(value);
- } // setText
-
- private String text(final int id)
- {
- return textView(id).getText().toString();
- } // getText
-
- @Override
- public void afterTextChanged(Editable s) { }
- @Override
- public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
-
- @Override
- public void onTextChanged(CharSequence s, int start, int before, int count)
- {
- upload_.setEnabled(s.length() != 0);
- } //onTextChanged
-
- @Override
- public void onClick(View v)
- {
- try {
- final Feedback.Result result = Feedback.send(Route.journey().itinerary(),
- text(R.id.comments),
- text(R.id.name),
- text(R.id.email));
- MessageBox.OKAndFinish(this.getCurrentFocus(), result.message(), this, result.ok());
- }
- catch(Exception e) {
- MessageBox.OK(v, "There was a problem sending your comments:\n" + e.getMessage());
- }
- } // onClick
-
-} // FeedbackActivity
+public class FeedbackActivity extends Activity implements TextWatcher, OnClickListener {
+ private Button upload_;
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.routefeedback);
+
+ upload_ = (Button)findViewById(R.id.upload);
+
+ upload_.setEnabled(false);
+ upload_.setOnClickListener(this);
+
+ setText(R.id.name, CycleStreetsPreferences.name());
+ setText(R.id.email, CycleStreetsPreferences.email());
+
+ textView(R.id.comments).addTextChangedListener(this);
+ }
+
+ private TextView textView(final int id) {
+ return (TextView)findViewById(id);
+ }
+
+ private void setText(final int id, final String value) {
+ textView(id).setText(value);
+ }
+
+ private String text(final int id) {
+ return textView(id).getText().toString();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {}
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before, int count) {
+ upload_.setEnabled(s.length() != 0);
+ }
+
+ @Override
+ public void onClick(View v) {
+ FeedbackTask task = new FeedbackTask(this, Route.journey().itinerary(), text(R.id.comments),
+ text(R.id.name), text(R.id.email));
+ task.execute();
+ }
+
+ private void messageBox(Result result) {
+ MessageBox.OKAndFinish(this.getCurrentFocus(), result.message(), this, result.ok());
+ }
+
+ private class FeedbackTask extends AsyncTask {
+ private final int itinerary;
+ private final String comments;
+ private final String name;
+ private final String email;
+ private final ProgressDialog progress;
+
+ public FeedbackTask(Context context,
+ int itinerary,
+ String comments,
+ String name,
+ String email) {
+ this.itinerary = itinerary;
+ this.comments = comments;
+ this.name = name;
+ this.email = email;
+
+ this.progress = Dialog.createProgressDialog(context, R.string.feedback_sending);
+ }
+
+ @Override
+ protected void onPreExecute() {
+ super.onPreExecute();
+ progress.show();
+ }
+
+ @Override
+ protected Result doInBackground(final Object... params) {
+ return Feedback.send(itinerary, comments, name, email);
+ }
+
+ @Override
+ protected void onPostExecute(final Result result) {
+ progress.dismiss();
+ messageBox(result);
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/LiveRideActivity.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/LiveRideActivity.java
deleted file mode 100644
index aa0069e60..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/LiveRideActivity.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.liveride.PebbleNotifier;
-import net.cyclestreets.util.GPS;
-import net.cyclestreets.util.MessageBox;
-import net.cyclestreets.views.CycleMapView;
-import net.cyclestreets.views.overlay.LiveRideOverlay;
-import net.cyclestreets.views.overlay.LockScreenOnOverlay;
-import net.cyclestreets.views.overlay.RouteOverlay;
-import android.app.Activity;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.os.Bundle;
-import android.widget.RelativeLayout;
-
-public class LiveRideActivity extends Activity
-{
- static public void launch(final Context context)
- {
- if(!GPS.isOn(context)) {
- MessageBox.YesNo(context,
- "LiveRide needs the GPS location service.\n\nWould you like to turn it on now?",
- new DialogInterface.OnClickListener() {
- public void onClick(DialogInterface arg0, int arg1) {
- GPS.showSettings(context);
- }
- });
- return;
- }
- launchActivity(context);
- } // launch
-
- static private void launchActivity(final Context context)
- {
- final Intent intent = new Intent(context, LiveRideActivity.class);
- context.startActivity(intent);
- } // launchActivity
-
- private CycleMapView map_;
-
- private PebbleNotifier notifier_;
-
- @Override
- public void onCreate(final Bundle saved)
- {
- super.onCreate(saved);
-
- // Map initialized in onResume
- } // onCreate
-
- //////////////////////////
- @Override
- public void onPause()
- {
- map_.disableFollowLocation();
- map_.onPause();
-
- super.onPause();
- } // onPause
-
- @Override
- public void onResume()
- {
- super.onResume();
-
- // Map needs to be recreated, because tile provider is shut down on CycleMapView.onPause
- initializeMapView();
- map_.onResume();
- map_.enableAndFollowLocation();
- } // onResume
-
- private void initializeMapView() {
- map_ = new CycleMapView(this, this.getClass().getName());
- map_.overlayPushBottom(new RouteOverlay(this));
- map_.overlayPushTop(new LockScreenOnOverlay(this, map_));
- map_.overlayPushTop(new LiveRideOverlay(this, map_));
- map_.lockOnLocation();
- map_.hideLocationButton();
-
- final RelativeLayout rl = new RelativeLayout(this);
- rl.addView(map_, new RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT, RelativeLayout.LayoutParams.MATCH_PARENT));
- setContentView(rl);
- }
-
-} // class LiveRideActivity
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/LiveRideActivity.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/LiveRideActivity.kt
new file mode 100644
index 000000000..fb49140e4
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/LiveRideActivity.kt
@@ -0,0 +1,113 @@
+package net.cyclestreets
+
+import android.Manifest
+import net.cyclestreets.liveride.LiveRideService
+import net.cyclestreets.util.GPS
+import net.cyclestreets.util.MessageBox
+import net.cyclestreets.views.CycleMapView
+
+import android.app.Activity
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.ServiceConnection
+import android.location.Location
+import android.os.Bundle
+import android.os.IBinder
+import android.util.Log
+import android.widget.RelativeLayout
+
+import net.cyclestreets.util.Logging
+import net.cyclestreets.util.hasPermission
+import net.cyclestreets.views.overlay.*
+
+
+private val TAG = Logging.getTag(LiveRideActivity::class.java)
+
+
+class LiveRideActivity : Activity(), ServiceConnection, LiveRideOverlay.Locator {
+ private lateinit var map: CycleMapView
+ private lateinit var liveride: LiveRideService.Binding
+
+ override fun onServiceConnected(className: ComponentName, binder: IBinder) {
+ liveride = binder as LiveRideService.Binding
+
+ if (!liveride.areRiding())
+ liveride.startRiding()
+ }
+
+ override fun onServiceDisconnected(className: ComponentName) {}
+
+ override fun lastLocation(): Location? {
+ return liveride.lastLocation()
+ }
+
+ public override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ val intent = Intent(this, LiveRideService::class.java)
+ this.bindService(intent, this, Context.BIND_AUTO_CREATE)
+ }
+
+ public override fun onDestroy() {
+ if (this::liveride.isInitialized) {
+ liveride.stopRiding()
+ }
+ this.unbindService(this)
+ super.onDestroy()
+ }
+
+ public override fun onPause() {
+ map.disableFollowLocation()
+ map.onPause()
+ super.onPause()
+ }
+
+ public override fun onResume() {
+ super.onResume()
+
+ // Map needs to be recreated, because tile provider is shut down on CycleMapView.onPause
+ initializeMapView()
+ map.onResume()
+ map.enableAndFollowLocation()
+ }
+
+ private fun initializeMapView() {
+ map = CycleMapView(this, this.javaClass.name, null).apply {
+ overlayPushBottom(RouteOverlay(this,false))
+ overlayPushTop(WaymarkOverlay(this))
+ overlayPushTop(LockScreenOnOverlay(this))
+ overlayPushTop(RotateMapOverlay(this))
+ overlayPushTop(LiveRideOverlay(this@LiveRideActivity, this@LiveRideActivity))
+ lockOnLocation()
+ hideLocationButton()
+ shiftAttribution()
+ }
+ RelativeLayout(this).apply {
+ addView(map,
+ RelativeLayout.LayoutParams(RelativeLayout.LayoutParams.MATCH_PARENT,
+ RelativeLayout.LayoutParams.MATCH_PARENT))
+ this@LiveRideActivity.setContentView(this)
+ }
+ }
+
+ companion object {
+ fun launch(context: Context) {
+ if (!hasPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)) {
+ // Should be unreachable but we're being defensive
+ Log.w(TAG, "Location permission is not granted. Bail out.")
+ return
+ }
+
+ if (!GPS.isOn(context)) {
+ MessageBox.YesNo(context,
+ "LiveRide needs the GPS location service.\n\nWould you like to turn it on now?")
+ { _, _ -> GPS.showSettings(context) }
+ return
+ }
+
+ // Proceed
+ context.startActivity(Intent(context, LiveRideActivity::class.java))
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/PhotoUploadActivity.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/PhotoUploadActivity.java
deleted file mode 100644
index 610a3e8cd..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/PhotoUploadActivity.java
+++ /dev/null
@@ -1,868 +0,0 @@
-package net.cyclestreets;
-
-import net.cyclestreets.view.R;
-import net.cyclestreets.api.PhotomapCategory;
-import net.cyclestreets.api.PhotomapCategories;
-import net.cyclestreets.api.Upload;
-import net.cyclestreets.util.Bitmaps;
-import net.cyclestreets.util.Dialog;
-import net.cyclestreets.util.MessageBox;
-import net.cyclestreets.util.Share;
-import net.cyclestreets.views.CycleMapView;
-import net.cyclestreets.views.overlay.ThereOverlay;
-import net.cyclestreets.views.overlay.ThereOverlay.LocationListener;
-import android.app.Activity;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.DialogInterface;
-import android.content.Intent;
-import android.content.SharedPreferences;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.PackageManager;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.media.ExifInterface;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.provider.MediaStore;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodManager;
-import android.widget.BaseAdapter;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.ImageView;
-import android.widget.LinearLayout;
-import android.widget.RelativeLayout;
-import android.widget.Spinner;
-import android.widget.TextView;
-import android.widget.Toast;
-import android.widget.RelativeLayout.LayoutParams;
-
-import java.io.File;
-import java.io.IOException;
-import java.text.DateFormat;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.util.GeoPoint;
-
-import static net.cyclestreets.util.MenuHelper.createMenuItem;
-import static net.cyclestreets.util.MenuHelper.enableMenuItem;
-
-public class PhotoUploadActivity extends Activity
- implements View.OnClickListener, LocationListener
-{
- public enum AddStep
- {
- PHOTO(null),
- CAPTION(PHOTO),
- CATEGORY(CAPTION),
- LOCATION(CATEGORY),
- VIEW(LOCATION),
- DONE(VIEW);
-
- AddStep(AddStep p)
- {
- prev_ = p;
- if(prev_ != null)
- prev_.next_ = this;
- save(this);
- } // AddStep
-
- public AddStep prev() { return prev_; }
- public AddStep next() { return next_; }
-
- public int value() { return Value_.get(this); }
-
- private AddStep prev_;
- private AddStep next_;
-
- public static AddStep fromInt(int a)
- {
- for(AddStep s : Value_.keySet())
- if(s.value() == a)
- return s;
- return null;
- } // AddStep
-
- private static void save(AddStep a)
- {
- if(Value_ == null)
- Value_ = new HashMap<>();
- Value_.put(a, Value_.size());
- } // save
-
- private static Map Value_;
- } // AddStep
-
- private static final int TakePhoto = 2;
- private static final int ChoosePhoto = 3;
- private static final int AccountDetails = 4;
-
- private LinearLayout photoRoot_;
- private View photoView_;
- private View photoCaption_;
- private View photoCategory_;
- private View photoLocation_;
- private View photoWebView_;
-
- private CycleMapView map_;
- private ThereOverlay there_;
- private static PhotomapCategories photomapCategories;
-
- private AddStep step_;
-
- private String photoFile_ = null;
- private Bitmap photo_ = null;
- private String caption_;
- private String dateTime_;
- private int metaCatId_;
- private int catId_;
- private String uploadedUrl_;
-
- private boolean allowUploadByKey_;
- private boolean allowTextOnly_;
- private boolean noShare_;
-
- private LayoutInflater inflater_;
- private InputMethodManager imm_;
-
- @Override
- protected void onCreate(Bundle savedInstanceState)
- {
- super.onCreate(savedInstanceState);
-
- final String metaData = photoUploadMetaData();
- allowUploadByKey_ = metaData.contains("ByKey");
- allowTextOnly_ = metaData.contains("AllowTextOnly");
- noShare_ = metaData.contains("NoShare");
-
- inflater_ = LayoutInflater.from(this);
- imm_ = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
-
- photoRoot_ = (LinearLayout)inflater_.inflate(R.layout.addphoto, null);
- setContentView(photoRoot_);
-
- step_ = AddStep.PHOTO;
- caption_ = "";
- dateTime_ = "";
- metaCatId_ = -1;
- catId_ = -1;
-
- photoView_ = inflater_.inflate(R.layout.addphotostart, null);
- {
- final Button takePhoto = (Button)photoView_.findViewById(R.id.takephoto_button);
- if(getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA))
- takePhoto.setOnClickListener(this);
- else
- takePhoto.setEnabled(false);
- }
- photoView_.findViewById(R.id.chooseexisting_button).setOnClickListener(this);
- {
- final Button textOnly = (Button)photoView_.findViewById(R.id.textonly_button);
- if (allowTextOnly_)
- textOnly.setOnClickListener(this);
- else
- textOnly.setVisibility(View.GONE);
- }
-
- photoCategory_ = inflater_.inflate(R.layout.addphotocategory, null);
- backNextButtons(photoCategory_, "Back", android.R.drawable.ic_media_rew, "Next", android.R.drawable.ic_media_ff);
-
- photoLocation_ = inflater_.inflate(R.layout.addphotolocation, null);
- backNextButtons(photoLocation_, "Back", android.R.drawable.ic_media_rew, "Upload!", android.R.drawable.ic_menu_upload);
-
- photoWebView_ = inflater_.inflate(R.layout.addphotoview, null);
- backNextButtons(photoWebView_, "Upload another", android.R.drawable.ic_menu_revert, "Close", android.R.drawable.ic_menu_close_clear_cancel);
-
- // start reading categories
- if(photomapCategories == null)
- new GetPhotomapCategoriesTask().execute();
- else
- setupSpinners();
-
- there_ = new ThereOverlay(this);
- there_.setLocationListener(this);
-
- setupView();
- } // PhotoUploadActivity
-
- private void backNextButtons(final View parentView,
- final String backText, final int backDrawable,
- final String nextText, final int nextDrawable) {
- final Button back = (Button)parentView.findViewById(R.id.back);
- back.setText(backText);
- back.setCompoundDrawablesWithIntrinsicBounds(backDrawable, 0, 0, 0);
- final Button next = (Button)parentView.findViewById(R.id.next);
- next.setText(nextText);
- next.setCompoundDrawablesWithIntrinsicBounds(0, 0, nextDrawable, 0);
- } // backNextButtons
-
- private String photoUploadMetaData() {
- try {
- final ApplicationInfo ai = getPackageManager().getApplicationInfo(getPackageName(), PackageManager.GET_META_DATA);
- final Bundle bundle = ai.metaData;
- final String upload = bundle.getString("CycleStreetsPhotoUpload");
- return upload != null ? upload : "";
- } catch(final Exception e) {
- return "";
- } // catch
- } // photoUploadMetaData
-
- private void setUploadView(final View child)
- {
- photoRoot_.removeAllViewsInLayout();
- photoRoot_.addView(child);
- } // setUploadView
-
- private void store()
- {
- final SharedPreferences.Editor edit = prefs().edit();
- edit.putInt("STEP", step_.value());
- edit.putString("PHOTOFILE", photoFile_);
- edit.putString("DATETIME", dateTime_);
- edit.putString("CAPTION", caption_);
- edit.commit();
- } // store
-
- @Override
- public void onPause()
- {
- final SharedPreferences.Editor edit = prefs().edit();
- edit.putString("CAPTION", captionText());
- edit.putInt("METACAT", metaCategoryId());
- edit.putInt("CATEGORY", categoryId());
- final IGeoPoint p = there_.there();
- if(p != null)
- {
- edit.putInt("THERE-LAT", p.getLatitudeE6());
- edit.putInt("THERE-LON", p.getLongitudeE6());
- }
- else
- edit.putInt("THERE-LAT", -1);
- edit.putLong("WHEN", new Date().getTime());
- edit.commit();
-
- if(map_ != null)
- map_.onPause();
- super.onPause();
- } // onPause
-
- private final long fiveMinutes = 5 * 60 * 1000;
-
- @Override
- public void onResume()
- {
- try {
- doOnResume();
- } // try
- catch(RuntimeException e) {
- step_ = AddStep.fromInt(0);
- } // catch
-
- super.onResume();
- setupView();
- } // onResume
-
- private void doOnResume()
- {
- final SharedPreferences prefs = prefs();
-
- step_ = AddStep.fromInt(prefs.getInt("STEP", 0));
- photoFile_ = prefs.getString("PHOTOFILE", photoFile_);
- if(photo_ == null && photoFile_ != null)
- photo_ = Bitmaps.loadFile(photoFile_);
- dateTime_ = prefs.getString("DATETIME", "");
- caption_ = prefs.getString("CAPTION", "");
-
- metaCatId_ = prefs.getInt("METACAT", -1);
- catId_ = prefs.getInt("CATEGORY", -1);
- setSpinnerSelections();
-
- final int tlat = prefs.getInt("THERE-LAT", -1);
- final int tlon = prefs.getInt("THERE-LON", -1);
- if((tlat != -1) && (tlon != -1))
- there_.noOverThere(new GeoPoint(tlat, tlon));
-
- if(map_ != null)
- map_.onResume();
-
- final long now = new Date().getTime();
- final long when = prefs.getLong("WHEN", now);
- if((now - when) > fiveMinutes)
- step_ = AddStep.fromInt(0);
- } // doOnResume
-
- private SharedPreferences prefs()
- {
- return getSharedPreferences("net.cyclestreets.AddPhotoActivity", Context.MODE_PRIVATE);
- } // prefs()
-
- ///////////////////////////////////////////////////////////////////
- @Override
- public boolean onCreateOptionsMenu(final Menu menu)
- {
- createMenuItem(menu, R.string.ic_menu_restart, Menu.NONE, R.drawable.ic_menu_rotate);
- createMenuItem(menu, R.string.ic_menu_back, Menu.NONE, R.drawable.ic_menu_revert);
- return true;
- } // onCreateOptionsMenu
-
- @Override
- public boolean onPrepareOptionsMenu(final Menu menu)
- {
- enableMenuItem(menu, R.string.ic_menu_restart, step_ != AddStep.PHOTO);
- enableMenuItem(menu, R.string.ic_menu_back, step_ != AddStep.PHOTO && step_ != AddStep.VIEW);
- return true;
- } // onPrepareOptionsMenu
-
- @Override
- public boolean onOptionsItemSelected(final MenuItem item)
- {
- final int menuItem = item.getItemId();
-
- if(R.string.ic_menu_restart == menuItem) {
- step_ = AddStep.PHOTO;
- setupView();
- return true;
- }
-
- if(R.string.ic_menu_back == menuItem) {
- onBackPressed();
- return true;
- }
-
- return false;
- } // onMenuItemSelected
-
- ///////////////////////////////////////////////////////////////////
- @Override
- public void onBackPressed()
- {
- if(step_ == AddStep.PHOTO) {
- super.onBackPressed();
- return;
- }
-
- if(step_ == AddStep.VIEW) {
- finish();
- return;
- }
-
- step_ = step_.prev();
- store();
- setupView();
- } // onBackPressed
-
- private void nextStep()
- {
- if((step_ == AddStep.LOCATION) && (there_.there() == null))
- {
- Toast.makeText(this, "Please set photo location", Toast.LENGTH_LONG).show();
- return;
- } // if ...
-
- step_ = step_.next();
-
- store();
- setupView();
- } // nextStep
-
- private void setupView()
- {
- switch(step_)
- {
- case PHOTO:
- metaCategorySpinner().setSelection(0);
- categorySpinner().setSelection(0);
- caption_ = "";
- there_.noOverThere(null);
- setUploadView(photoView_);
- break;
- case CAPTION:
- // why recreate this view each time - well *sigh* because we have to force the
- // keyboard to hide, if we don't recreate the view afresh, Android won't redisplay
- // the keyboard if we come back to this view
- photoCaption_ = inflater_.inflate(R.layout.addphotocaption, null);
- backNextButtons(photoCaption_, "Back", android.R.drawable.ic_media_rew, "Next", android.R.drawable.ic_media_ff);
- setUploadView(photoCaption_);
- captionEditor().setText(caption_);
- if (photo_ == null && allowTextOnly_) {
- ((TextView)photoRoot_.findViewById(R.id.label)).setText("Your Report");
- ((EditText)photoRoot_.findViewById(R.id.caption)).setLines(10);
- } // if ...
- break;
- case CATEGORY:
- caption_ = captionText();
- store();
- setUploadView(photoCategory_);
- break;
- case LOCATION:
- metaCatId_ = metaCategoryId();
- catId_ = categoryId();
- setupMap();
- setUploadView(photoLocation_);
- there_.recentre();
- if (photo_ == null && allowTextOnly_)
- ((TextView)photoRoot_.findViewById(R.id.label)).setText("Where is the location your report describes?");
- else
- ((TextView)photoRoot_.findViewById(R.id.label)).setText("Where was this photo taken?");
- break;
- case VIEW:
- setUploadView(photoWebView_);
- {
- final TextView text = (TextView)photoWebView_.findViewById(R.id.photo_text);
- text.setText(caption_);
-
- final TextView url = (TextView)photoWebView_.findViewById(R.id.photo_url);
- final Button share = (Button)photoWebView_.findViewById(R.id.photo_share);
- if (noShare_) {
- url.setVisibility(View.GONE);
- share.setVisibility(View.GONE);
- } else {
- url.setText(uploadedUrl_);
- share.setOnClickListener(this);
- }
- }
- break;
- case DONE:
- step_ = AddStep.PHOTO;
- setupView();
- break;
- } // switch ...
-
- previewPhoto();
- hookUpNext();
- } // setupView
-
- private void previewPhoto()
- {
- final ImageView iv = (ImageView)photoRoot_.findViewById(R.id.photo);
- if(iv == null)
- return;
-
- if (photo_ == null && allowTextOnly_) {
- iv.setVisibility(View.GONE);
- return;
- }
-
- iv.setImageBitmap(photo_);
- int newHeight = getWindowManager().getDefaultDisplay().getHeight() / 10 * 4;
- int newWidth = getWindowManager().getDefaultDisplay().getWidth();
-
- iv.setLayoutParams(new LinearLayout.LayoutParams(newWidth, newHeight));
- iv.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
- } // previewPhoto
-
- private void hookUpNext()
- {
- final Button b = (Button)photoRoot_.findViewById(R.id.back);
- if(b != null)
- b.setOnClickListener(this);
-
- final Button n = (Button)photoRoot_.findViewById(R.id.next);
- if(n != null)
- n.setOnClickListener(this);
-
- if(step_ == AddStep.LOCATION)
- n.setEnabled(there_.there() != null);
- } // hookUpNext
-
- private EditText captionEditor() { return (EditText)photoCaption_.findViewById(R.id.caption); }
- private String captionText()
- {
- if(photoCaption_ == null)
- return caption_;
- imm_.hideSoftInputFromWindow(captionEditor().getWindowToken(), 0);
- return captionEditor().getText().toString();
- } // captionText
- private int metaCategoryId() { return (int)metaCategorySpinner().getSelectedItemId(); }
- private int categoryId() { return (int)categorySpinner().getSelectedItemId(); }
- private Spinner metaCategorySpinner() { return (Spinner)photoCategory_.findViewById(R.id.metacat); }
- private Spinner categorySpinner() { return (Spinner)photoCategory_.findViewById(R.id.category); }
-
- private void setupSpinners()
- {
- metaCategorySpinner().setAdapter(new CategoryAdapter(this, photomapCategories.metaCategories()));
- categorySpinner().setAdapter(new CategoryAdapter(this, photomapCategories.categories()));
-
- setSpinnerSelections();
- } // setupSpinners
-
- private void setSpinnerSelections()
- {
- // ids == position
- if(metaCatId_ != -1)
- metaCategorySpinner().setSelection(metaCatId_);
- if(catId_ != -1)
- categorySpinner().setSelection(catId_);
- } // setSpinnerSelections
-
- private void setupMap()
- {
- final RelativeLayout v = (RelativeLayout)(photoLocation_.findViewById(R.id.mapholder));
-
- if(map_ != null) {
- map_.onPause();
- ((RelativeLayout)map_.getParent()).removeView(map_);
- }
- else
- {
- map_ = new CycleMapView(this, this.getClass().getName());
- map_.overlayPushTop(there_);
- }
-
- v.addView(map_, new RelativeLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT));
- map_.enableAndFollowLocation();
- map_.onResume();
- there_.setMapView(map_);
- } // setupMap
-
- @Override
- public void onClick(final View v)
- {
- int clicked = v.getId();
-
- if (R.id.takephoto_button == clicked)
- startActivityForResult(new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE),
- TakePhoto);
-
- if (R.id.chooseexisting_button == clicked)
- startActivityForResult(new Intent(Intent.ACTION_PICK, android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI),
- ChoosePhoto);
-
- if (R.id.textonly_button == clicked) {
- photo_ = null;
- photoFile_ = null;
- dateTime_ = null;
- nextStep();
- }
-
- if (R.id.photo_share == clicked)
- Share.Url(this, uploadedUrl_, caption_, "Photo on CycleStreets.net");
-
- if (R.id.back == clicked) {
- if (step_ == AddStep.VIEW) {
- step_ = AddStep.PHOTO;
- store();
- setupView();
- } else
- onBackPressed();
- }
-
- if (R.id.next == clicked) {
- if(step_ == AddStep.LOCATION) {
- final boolean needAccountDetails = !allowUploadByKey_ && !CycleStreetsPreferences.accountOK();
- if(needAccountDetails)
- startActivityForResult(new Intent(this, AccountDetailsActivity.class), AccountDetails);
- else
- upload();
- } else if (step_ == AddStep.VIEW) {
- finish();
- }
- else
- nextStep();
- } // switch
- } // onClick
-
- @Override
- public void onSetLocation(IGeoPoint point)
- {
- final Button u = (Button)photoLocation_.findViewById(R.id.next);
- u.setEnabled(point != null);
- } // onSetLocation
-
- @Override
- public void onActivityResult(final int requestCode,
- final int resultCode,
- final Intent data)
- {
- if (resultCode != Activity.RESULT_OK)
- return;
-
- try
- {
- /*
- String url = intent.getData().toString();
-Bitmap bitmap = null;
-InputStream is = null;
-if (url.startsWith("content://com.google.android.apps.photos.content")){
- is = getContentResolver().openInputStream(Uri.parse(url));
- bitmap = getBitmapFromInputStream(is);
-}
- */
-
- photoFile_ = getImageFilePath(data);
- if(photo_ != null)
- photo_.recycle();
- photo_ = Bitmaps.loadFile(photoFile_);
-
- final ExifInterface exif = new ExifInterface(photoFile_);
-
- dateTime_ = photoTimestamp(exif);
- there_.noOverThere(photoLocation(exif));
-
- nextStep();
- }
- catch(Exception e)
- {
- Toast.makeText(this, "There was a problem grabbing the photo : " + e.getMessage(), Toast.LENGTH_LONG).show();
- if(requestCode == TakePhoto)
- startActivityForResult(new Intent(Intent.ACTION_PICK,
- android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI),
- ChoosePhoto);
- }
- } // onActivityResult
-
- private String getImageFilePath(final Intent data)
- {
- final Uri selectedImage = data.getData();
- final String[] filePathColumn = { MediaStore.Images.Media.DATA };
-
- final Cursor cursor = getContentResolver().query(selectedImage, filePathColumn, null, null, null);
- try
- {
- cursor.moveToFirst();
- return cursor.getString(cursor.getColumnIndex(filePathColumn[0]));
- } // try
- finally
- {
- cursor.close();
- } // finally
- } // getImageFilePath
-
- private void upload()
- {
- try
- {
- doUpload();
- }
- catch(Exception e)
- {
- Toast.makeText(this, "Could not upload photo. Please check your network connection.", Toast.LENGTH_LONG).show();
- step_ = AddStep.LOCATION;
- }
- } // upload
-
- private void doUpload() throws Exception
- {
- final String filename = photoFile_;
- final String username = CycleStreetsPreferences.username();
- final String password = CycleStreetsPreferences.password();
- final IGeoPoint location = there_.there();
- final String metaCat = photomapCategories.metaCategories().get(metaCatId_).getTag();
- final String category = photomapCategories.categories().get(catId_).getTag();
- final String dateTime = dateTime_ != null ? dateTime_ : Long.toString(new Date().getTime() / 1000);
- final String caption = caption_;
-
- final UploadPhotoTask uploader = new UploadPhotoTask(this,
- filename,
- username,
- password,
- location,
- metaCat,
- category,
- dateTime,
- caption);
- uploader.execute();
- } // upload
-
- private void uploadComplete(final String photo_url)
- {
- uploadedUrl_ = photo_url;
- nextStep();
- } // uploadComplete
-
- private void uploadFailed(final String msg)
- {
- MessageBox.OK(photoLocation_,
- msg,
- new DialogInterface.OnClickListener() {
- @Override
- public void onClick(DialogInterface dialog, int which) {
- step_ = AddStep.LOCATION;
- setupView();
- }
- });
- } // uploadFailed
-
- private GeoPoint photoLocation(final ExifInterface photoExif)
- {
- final float[] coords = new float[2];
- if(!photoExif.getLatLong(coords))
- return null;
- int lat = (int)(((double)coords[0]) * 1E6);
- int lon = (int)(((double)coords[1]) * 1E6);
- return new GeoPoint(lat, lon);
- } // photoLocation
-
- private String photoTimestamp(final ExifInterface photoExif)
- {
- Date date = new Date();
-
- try
- {
- final DateFormat df = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
- final String dateString = photoExif.getAttribute(ExifInterface.TAG_DATETIME);
- if(dateString != null && dateString.length() > 0)
- date = df.parse(dateString);
- } // try
- catch(Exception e)
- {
- // ah well
- } // catch
-
- return Long.toString(date.getTime() / 1000);
- } // photoTimestamp
-
- ///////////////////////////////////////////////////////////////////////////
- private class GetPhotomapCategoriesTask extends AsyncTask
- {
- protected PhotomapCategories doInBackground(Object... params)
- {
- PhotomapCategories photomapCategories = null;
- try {
- photomapCategories = PhotomapCategories.get();
- }
- catch (Exception ex) {
- }
- return photomapCategories;
- } // PhotomapCategories
-
- @Override
- protected void onPostExecute(PhotomapCategories photomapCategories)
- {
- if(photomapCategories == null)
- {
- Toast.makeText(PhotoUploadActivity.this, "Could not load photomap categories. Please check network connection.", Toast.LENGTH_LONG).show();
- return;
- } // if ...
- PhotoUploadActivity.photomapCategories = photomapCategories;
- setupSpinners();
- } // onPostExecute
- } // class GetPhotomapCategoriesTask
-
- //////////////////////////////////////////////////////////////////////////
- private class UploadPhotoTask extends AsyncTask
- {
- private final String filename_;
- private final String username_;
- private final String password_;
- private final IGeoPoint location_;
- private final String metaCat_;
- private final String category_;
- private final String dateTime_;
- private final String caption_;
- private final ProgressDialog progress_;
- private final boolean smallImage_;
-
- UploadPhotoTask(final Context context,
- final String filename,
- final String username,
- final String password,
- final IGeoPoint location,
- final String metaCat,
- final String category,
- final String dateTime,
- final String caption)
- {
- smallImage_ = CycleStreetsPreferences.uploadSmallImages();
- filename_ = smallImage_ ? Bitmaps.resizePhoto(filename) : filename;
- username_ = username;
- password_ = password;
- location_ = location;
- metaCat_ = metaCat;
- category_ = category;
- dateTime_ = dateTime;
- caption_ = caption;
-
- progress_ = Dialog.createProgressDialog(context, R.string.uploading_photo);
- } // UploadPhotoTask
-
- @Override
- protected void onPreExecute()
- {
- super.onPreExecute();
- progress_.show();
- } // onPreExecute
-
- protected Upload.Result doInBackground(Object... params)
- {
- try {
- return Upload.photo(filename_,
- username_,
- password_,
- location_,
- metaCat_,
- category_,
- dateTime_,
- caption_);
- } // try
- catch (IOException e) {
- return Upload.Result.forError("There was a problem uploading your photo: \n" + e.getMessage());
- }
- } // doInBackground
-
- @Override
- protected void onPostExecute(final Upload.Result result)
- {
- if(smallImage_)
- new File(filename_).delete();
- progress_.dismiss();
- if(result.ok())
- uploadComplete(result.url());
- else
- uploadFailed(result.error());
- } // onPostExecute
- } // class UploadPhotoTask
-
- //////////////////////////////////////////////////////////
- static private class CategoryAdapter extends BaseAdapter
- {
- private final LayoutInflater inflater_;
- private final List list_;
-
- public CategoryAdapter(final Context context,
- final List list)
- {
- inflater_ = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- list_ = list;
- } // CategoryAdapter
-
- @Override
- public int getCount()
- {
- return list_.size();
- } // getCount
-
- @Override
- public String getItem(final int position)
- {
- final PhotomapCategory c = list_.get(position);
- return c.getName();
- } // getItem
-
- @Override
- public long getItemId(final int position)
- {
- return position;
- } // getItemId
-
- @Override
- public View getView(final int position, final View convertView, final ViewGroup parent)
- {
- final int id = (parent instanceof Spinner) ? android.R.layout.simple_spinner_item : android.R.layout.simple_spinner_dropdown_item;
- final TextView tv = (TextView)inflater_.inflate(id, parent, false);
- tv.setText(getItem(position));
- return tv;
- } // getView
- } // CategoryAdapter
-} // class AddPhotoActivity
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/Undoable.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/Undoable.java
index 0232a65c4..c7a0a5c41 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/Undoable.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/Undoable.java
@@ -2,5 +2,11 @@
public interface Undoable
{
+ /**
+ * Interface which our main app fragments may choose to implement, allowing them to handle the "Back" button action.
+ *
+ * @return True if the fragment has processed and wishes to swallow the "Back" action;
+ * False otherwise (in which case the main activity "Back" action will be performed).
+ */
boolean onBackPressed();
} // Undoable
\ No newline at end of file
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contact.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contact.java
index f1253f7d2..ee472e094 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contact.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contact.java
@@ -2,55 +2,50 @@
import java.util.Comparator;
-public class Contact
+public class Contact
{
- private final String name_;
- private final String address_;
- private final String neighbourhood_;
- private final String city_;
- private final String postcode_;
-
- public Contact(final String name,
- final String address,
- final String street,
- final String neighbourhood,
- final String city,
- final String postcode)
- {
- name_ = name;
- address_ = address;
- neighbourhood_ = neighbourhood;
- city_ = city;
- postcode_ = postcode;
- } // Contact
-
- public String name() { return name_; }
- public String address() { return address_; }
- public String neighbourhood() { return neighbourhood_; }
- public String city() { return city_; }
- public String postcode() { return postcode_; }
-
- public String toString() { return name_; }
-
- static public Comparator comparator()
- {
- return ContactsComparator.instance();
- } // Comparator
-
- static private class ContactsComparator implements Comparator
- {
- static private ContactsComparator instance_;
- static public ContactsComparator instance()
- {
- if(instance_ == null)
- instance_ = new ContactsComparator();
- return instance_;
- } // instance
-
- @Override
- public int compare(final Contact lhs, final Contact rhs)
- {
- return lhs.name().compareToIgnoreCase(rhs.name());
- } // compare
- } // class ContactsComparator
-} // class Contact
+ private final String name_;
+ private final String address_;
+ private final String neighbourhood_;
+ private final String city_;
+ private final String postcode_;
+
+ public Contact(final String name,
+ final String address,
+ final String street,
+ final String neighbourhood,
+ final String city,
+ final String postcode) {
+ name_ = name;
+ address_ = address;
+ neighbourhood_ = neighbourhood;
+ city_ = city;
+ postcode_ = postcode;
+ }
+
+ public String name() { return name_; }
+ public String address() { return address_; }
+ public String neighbourhood() { return neighbourhood_; }
+ public String city() { return city_; }
+ public String postcode() { return postcode_; }
+
+ public String toString() { return name_; }
+
+ public static Comparator comparator() {
+ return ContactsComparator.instance();
+ }
+
+ private static class ContactsComparator implements Comparator {
+ private static ContactsComparator instance_;
+ public static ContactsComparator instance() {
+ if (instance_ == null)
+ instance_ = new ContactsComparator();
+ return instance_;
+ }
+
+ @Override
+ public int compare(final Contact lhs, final Contact rhs) {
+ return lhs.name().compareToIgnoreCase(rhs.name());
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contacts.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contacts.java
index 25dbf3965..2aa5e82c7 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contacts.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/contacts/Contacts.java
@@ -10,90 +10,87 @@
public class Contacts
{
- static public List load(final Context context)
- {
- final List contacts = new ArrayList<>();
-
- final String[] projection = new String[] {
- ContactsContract.Data.CONTACT_ID,
- ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS,
- ContactsContract.CommonDataKinds.StructuredPostal.STREET,
- ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD,
- ContactsContract.CommonDataKinds.StructuredPostal.CITY,
- ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE
- };
-
- final String where = ContactsContract.Data.MIMETYPE + " = ?";
- final String[] whereParameters = new String[] { ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE };
-
- final Cursor addrCur = context.getContentResolver().query(
- ContactsContract.Data.CONTENT_URI,
- projection,
- where,
- whereParameters,
- null);
-
- try {
- final int idIndex = addrCur.getColumnIndex(ContactsContract.Data.CONTACT_ID);
- final int addressIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS);
- final int streetIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.STREET);
- final int neighbourhoodIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD);
- final int cityIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.CITY);
- final int postcodeIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE);
-
-
- if(addrCur.moveToFirst()) { // move the cursor to the first entry
- while(!addrCur.isAfterLast()) { // still a valid entry left?
- final String id = addrCur.getString(idIndex);
- final String address = addrCur.getString(addressIndex);
- final String street = addrCur.getString(streetIndex);
- final String neighbourhood = addrCur.getString(neighbourhoodIndex);
- final String city = addrCur.getString(cityIndex);
- final String postcode = addrCur.getString(postcodeIndex);
-
- final String name = displayName(context, id);
-
- if(name != null && address != null)
- contacts.add(new Contact(name,
- address,
- street,
- neighbourhood,
- city,
- postcode));
-
- addrCur.moveToNext(); // move to the next entry
- } // while ...
- } // if ...
- } // try
- finally {
- addrCur.close();
- } // finally
-
- Collections.sort(contacts, Contact.comparator());
-
- return contacts;
- } // queryContacts
-
- private static String displayName(final Context context, final String contactId)
- {
- final String[] projection = new String[] {
- ContactsContract.Contacts.DISPLAY_NAME
- };
-
- final Cursor contact = context.getContentResolver().query(
- ContactsContract.Contacts.CONTENT_URI,
- projection,
- ContactsContract.Contacts._ID + "=?",
- new String[] { contactId },
- null);
- try {
- if(contact.moveToFirst())
- return contact.getString(contact.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
- }
- finally {
- contact.close();
- } // finally
-
- return null;
- } // displayName
-} // class Contacts
+ public static List load(final Context context) {
+ final List contacts = new ArrayList<>();
+
+ final String[] projection = new String[] {
+ ContactsContract.Data.CONTACT_ID,
+ ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS,
+ ContactsContract.CommonDataKinds.StructuredPostal.STREET,
+ ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD,
+ ContactsContract.CommonDataKinds.StructuredPostal.CITY,
+ ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE
+ };
+
+ final String where = ContactsContract.Data.MIMETYPE + " = ?";
+ final String[] whereParameters = new String[] { ContactsContract.CommonDataKinds.StructuredPostal.CONTENT_ITEM_TYPE };
+
+ final Cursor addrCur = context.getContentResolver().query(
+ ContactsContract.Data.CONTENT_URI,
+ projection,
+ where,
+ whereParameters,
+ null);
+
+ try {
+ final int idIndex = addrCur.getColumnIndex(ContactsContract.Data.CONTACT_ID);
+ final int addressIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.FORMATTED_ADDRESS);
+ final int streetIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.STREET);
+ final int neighbourhoodIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.NEIGHBORHOOD);
+ final int cityIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.CITY);
+ final int postcodeIndex = addrCur.getColumnIndex(ContactsContract.CommonDataKinds.StructuredPostal.POSTCODE);
+
+ if (addrCur.moveToFirst()) { // move the cursor to the first entry
+ while (!addrCur.isAfterLast()) { // still a valid entry left?
+ final String id = addrCur.getString(idIndex);
+ final String address = addrCur.getString(addressIndex);
+ final String street = addrCur.getString(streetIndex);
+ final String neighbourhood = addrCur.getString(neighbourhoodIndex);
+ final String city = addrCur.getString(cityIndex);
+ final String postcode = addrCur.getString(postcodeIndex);
+
+ final String name = displayName(context, id);
+
+ if (name != null && address != null)
+ contacts.add(new Contact(name,
+ address,
+ street,
+ neighbourhood,
+ city,
+ postcode));
+
+ addrCur.moveToNext(); // move to the next entry
+ }
+ }
+ }
+ finally {
+ addrCur.close();
+ }
+
+ Collections.sort(contacts, Contact.comparator());
+
+ return contacts;
+ }
+
+ private static String displayName(final Context context, final String contactId) {
+ final String[] projection = new String[] {
+ ContactsContract.Contacts.DISPLAY_NAME
+ };
+
+ final Cursor contact = context.getContentResolver().query(
+ ContactsContract.Contacts.CONTENT_URI,
+ projection,
+ ContactsContract.Contacts._ID + "=?",
+ new String[] { contactId },
+ null);
+ try {
+ if (contact.moveToFirst())
+ return contact.getString(contact.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
+ }
+ finally {
+ contact.close();
+ }
+
+ return null;
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/DatabaseHelper.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/DatabaseHelper.java
index 5657eb14c..4cedcc939 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/DatabaseHelper.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/DatabaseHelper.java
@@ -4,66 +4,90 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
import android.provider.BaseColumns;
+import android.util.Log;
+import net.cyclestreets.api.client.JourneyStringTransformerKt;
+import net.cyclestreets.util.Logging;
+import net.cyclestreets.view.BuildConfig;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.util.GeoPoint;
+
+import java.util.ArrayList;
+import java.util.List;
class DatabaseHelper extends SQLiteOpenHelper {
- private static final String DATABASE_NAME = "cyclestreets.db";
- private static final int DATABASE_VERSION = 3;
- public static final String ROUTE_TABLE = "route";
- public static final String LOCATION_TABLE = "location";
-
-
- private static final String ROUTE_TABLE_CREATE =
- "CREATE TABLE route (" + BaseColumns._ID + " INTEGER PRIMARY KEY, " +
- " journey INTEGER, " +
- " last_used DATE, " +
- " name TEXT, " +
- " plan TEXT, " +
- " distance INTEGER, " +
- " waypoints TEXT, " +
- " xml TEXT " +
- " ) ";
-
- private static final String LOCATIONS_TABLE_CREATE =
+ private static final String TAG = Logging.getTag(DatabaseHelper.class);
+
+ static final String DATABASE_NAME = "cyclestreets.db";
+ // If you're upgrading the DB version, then be sure to grab a snapshot - see the DatabaseUpgradeTest.
+ static final int DATABASE_VERSION = 4;
+ static final String ROUTE_TABLE = "route";
+ static final String LOCATION_TABLE = "location";
+ static final String ROUTE_TABLE_OLD = "_" + ROUTE_TABLE + "_old";
+ static final String LOCATION_TABLE_OLD = "_" + LOCATION_TABLE + "_old";
+
+ private static final String ROUTE_TABLE_CREATE =
+ "CREATE TABLE route (" + BaseColumns._ID + " INTEGER PRIMARY KEY, " +
+ " journey INTEGER, " +
+ " last_used DATE, " +
+ " name TEXT, " +
+ " plan TEXT, " +
+ " distance INTEGER, " +
+ " waypoints TEXT, " +
+ " journey_json TEXT " +
+ " ) ";
+
+ private static final String LOCATION_TABLE_CREATE =
"CREATE TABLE location (" + BaseColumns._ID + " INTEGER PRIMARY KEY, " +
" name TEXT, " +
- " lat INTEGER, " +
- " lon INTEGER " +
+ " lat TEXT, " +
+ " lon TEXT " +
" ) ";
DatabaseHelper(final Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
- } // DatabaseHelper
-
- @Override
- public void onCreate(final SQLiteDatabase db) {
- db.execSQL(ROUTE_TABLE_CREATE);
- db.execSQL(LOCATIONS_TABLE_CREATE);
- } // onCreate
-
- @Override
- public void onOpen(final SQLiteDatabase db) {
- } // onOpen
-
- @Override
- public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
- if (oldVersion < 2)
- upgradeTo2(db);
+
+ // From Android 9 (API version 28, SQLite version 3.22.0) Compatibility WAL is enabled by default.
+ // This breaks our testing mechanism of pasting over a new version of the database, so disable
+ // it when we're in debug mode. See:
+ // - https://source.android.com/devices/tech/perf/compatibility-wal
+ // - https://www.sqlite.org/wal.html
+ // - https://www.sqlite.org/tempfiles.html
+ if (BuildConfig.DEBUG) {
+ this.setWriteAheadLoggingEnabled(false);
+ }
+ }
+
+ @Override
+ public void onCreate(final SQLiteDatabase db) {
+ db.execSQL(ROUTE_TABLE_CREATE);
+ db.execSQL(LOCATION_TABLE_CREATE);
+ }
+
+ @Override
+ public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
+ Log.d(TAG, "onUpgrade - from " + oldVersion + " to " + newVersion);
+ if (oldVersion < 2)
+ upgradeTo2(db);
if (oldVersion < 3)
upgradeTo3(db);
- } // onUpgrade
-
- private void upgradeTo2(final SQLiteDatabase db) {
- try {
- db.execSQL("ALTER TABLE route ADD COLUMN waypoints TEXT");
+ if (oldVersion < 4)
+ upgradeTo4(db);
+ }
+
+ private void upgradeTo2(final SQLiteDatabase db) {
+ try {
+ db.execSQL("ALTER TABLE route ADD COLUMN waypoints TEXT");
final Cursor cursor = db.query(ROUTE_TABLE,
new String[] { BaseColumns._ID, "start_lat", "start_long", "end_lat", "end_long" },
- null,
- null,
- null,
- null,
+ null,
+ null,
+ null,
+ null,
null);
- if(cursor.moveToFirst())
+ if (cursor.moveToFirst())
do {
final StringBuilder sb = new StringBuilder();
sb.append("UPDATE route SET waypoints='")
@@ -76,19 +100,128 @@ private void upgradeTo2(final SQLiteDatabase db) {
.append(cursor.getInt(4))
.append("' WHERE ")
.append(BaseColumns._ID).append(" = ").append(cursor.getInt(0));
-
+
final String updateStmt = sb.toString();
db.compileStatement(updateStmt).execute();
} while (cursor.moveToNext());
-
- if(!cursor.isClosed())
+
+ if (!cursor.isClosed())
cursor.close();
- } catch(Exception e) {
- System.out.println(e.getMessage());
- }
- } // upgradeTo2
+ } catch (Exception e) {
+ System.out.println(e.getMessage());
+ }
+ }
private void upgradeTo3(final SQLiteDatabase db) {
- db.execSQL(LOCATIONS_TABLE_CREATE);
- } // upgradeTo3
-} // class DatabaseHelper
+ db.execSQL(LOCATION_TABLE_CREATE);
+ }
+
+ private void upgradeTo4(final SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + ROUTE_TABLE + " RENAME TO " + ROUTE_TABLE_OLD);
+ db.execSQL(ROUTE_TABLE_CREATE);
+ db.execSQL("INSERT INTO " + ROUTE_TABLE + " (" + BaseColumns._ID + ", journey, last_used, name, plan, distance, waypoints, journey_json)\n" +
+ " SELECT " + BaseColumns._ID + ", journey, last_used, name, plan, distance, waypoints, xml\n" +
+ " FROM " + ROUTE_TABLE_OLD + ";");
+
+ try {
+ final Cursor cursor = db.query(ROUTE_TABLE,
+ new String[] { BaseColumns._ID, "journey_json", "waypoints"},
+ null,
+ null,
+ null,
+ null,
+ null);
+ if (cursor.moveToFirst()) {
+ do {
+ final String v1ApiJourneyXml = cursor.getString(1);
+ final String journeyJson = JourneyStringTransformerKt.fromV1ApiXml(v1ApiJourneyXml);
+ final String e6Waypoints = cursor.getString(2);
+ final String newWaypoints = RouteDatabase.serializeWaypoints(deserializeE6Waypoints(e6Waypoints));
+
+ final String updateStmt = "UPDATE " + ROUTE_TABLE + " SET journey_json = ?, waypoints = ? " +
+ " WHERE " + BaseColumns._ID + " = " + cursor.getInt(0);
+ final SQLiteStatement update = db.compileStatement(updateStmt);
+ update.bindString(1, journeyJson);
+ update.bindString(2, newWaypoints);
+ update.execute();
+ } while (cursor.moveToNext());
+ }
+ if (!cursor.isClosed())
+ cursor.close();
+ } catch (RuntimeException e) {
+ System.out.println(e.getMessage());
+ }
+
+ db.execSQL("ALTER TABLE " + LOCATION_TABLE + " RENAME TO " + LOCATION_TABLE_OLD);
+ db.execSQL(LOCATION_TABLE_CREATE);
+ db.execSQL("INSERT INTO " + LOCATION_TABLE + " (" + BaseColumns._ID + ", name, lat, lon)\n" +
+ " SELECT " + BaseColumns._ID + ", name, lat, lon\n" +
+ " FROM " + LOCATION_TABLE_OLD + ";");
+
+ try {
+ final Cursor cursor = db.query(LOCATION_TABLE,
+ new String[] { BaseColumns._ID, "lat", "lon"},
+ null,
+ null,
+ null,
+ null,
+ null);
+ if (cursor.moveToFirst()) {
+ do {
+ double lat = Long.parseLong(cursor.getString(1)) / 1E6;
+ double lon = Long.parseLong(cursor.getString(2)) / 1E6;
+
+ final String updateStmt = "UPDATE " + LOCATION_TABLE + " SET lat = ?, lon = ? " +
+ " WHERE " + BaseColumns._ID + " = " + cursor.getInt(0);
+ final SQLiteStatement update = db.compileStatement(updateStmt);
+ update.bindString(1, String.valueOf(lat));
+ update.bindString(2, String.valueOf(lon));
+ update.execute();
+ } while (cursor.moveToNext());
+ }
+ if (!cursor.isClosed())
+ cursor.close();
+ } catch (RuntimeException e) {
+ System.out.println(e.getMessage());
+ }
+ }
+
+ /**
+ * Helper function that parses a given table into a string
+ * and returns it for easy printing. The string consists of
+ * the table name and then each row is iterated through with
+ * column_name: value pairs printed out.
+ *
+ * Courtesy of https://stackoverflow.com/a/27003490/2108057
+ *
+ * @param db the database to get the table from
+ * @param tableName the the name of the table to parse
+ */
+ static void logTableContents(SQLiteDatabase db, String tableName) {
+ Log.i(TAG, String.format("Table %s:\n", tableName));
+ Cursor allRows = db.rawQuery("SELECT * FROM " + tableName, null);
+ if (allRows.moveToFirst() ){
+ String[] columnNames = allRows.getColumnNames();
+ do {
+ StringBuilder rowSb = new StringBuilder();
+ for (String name: columnNames) {
+ rowSb.append(String.format("%s: %s\n", name, allRows.getString(allRows.getColumnIndex(name))));
+ }
+ Log.d(TAG, rowSb.toString());
+ } while (allRows.moveToNext());
+ }
+ allRows.close();
+ }
+
+ private static List deserializeE6Waypoints(String serializedWaypoints) {
+ List waypoints = new ArrayList<>();
+ for (final String coords : serializedWaypoints.split("\\|")) {
+ final String[] latlon = coords.split(",", 2);
+ double lat = Long.parseLong(latlon[0]) / 1E6;
+ double lon = Long.parseLong(latlon[1]) / 1E6;
+ Log.d(TAG, "dWE6: lat=" + lat + ", lon=" + lon);
+ waypoints.add(new GeoPoint(lat, lon));
+ }
+ return waypoints;
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/LocationDatabase.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/LocationDatabase.java
index a6dd398b7..147817ebd 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/LocationDatabase.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/LocationDatabase.java
@@ -17,11 +17,11 @@ public class LocationDatabase {
public LocationDatabase(final Context context) {
DatabaseHelper dh = new DatabaseHelper(context);
db_ = dh.getWritableDatabase();
- } // LocationDatabase
+ }
public void close() {
db_.close();
- } // close
+ }
public int locationCount() {
final Cursor cursor = db_.query(DatabaseHelper.LOCATION_TABLE,
@@ -32,16 +32,16 @@ public int locationCount() {
null,
null);
int c = 0;
- if(cursor.moveToFirst())
+ if (cursor.moveToFirst())
do {
c = cursor.getInt(0);
} while (cursor.moveToNext());
- if(!cursor.isClosed())
+ if (!cursor.isClosed())
cursor.close();
return c;
- } // count
+ }
public void addLocation(final String name, final IGeoPoint where) {
final String LOCATION_TABLE_INSERT =
@@ -50,10 +50,10 @@ public void addLocation(final String name, final IGeoPoint where) {
final SQLiteStatement insert = db_.compileStatement(LOCATION_TABLE_INSERT);
insert.bindString(1, name);
- insert.bindLong(2, where.getLatitudeE6());
- insert.bindLong(3, where.getLongitudeE6());
+ insert.bindString(2, String.valueOf(where.getLatitude()));
+ insert.bindString(3, String.valueOf(where.getLongitude()));
insert.executeInsert();
- } // addLocation
+ }
public void updateLocation(final int localId, final String name, final IGeoPoint where) {
final String LOCATION_TABLE_UPDATE =
@@ -61,11 +61,11 @@ public void updateLocation(final int localId, final String name, final IGeoPoint
final SQLiteStatement update = db_.compileStatement(LOCATION_TABLE_UPDATE);
update.bindString(1, name);
- update.bindLong(2, where.getLatitudeE6());
- update.bindLong(3, where.getLongitudeE6());
+ update.bindString(2, String.valueOf(where.getLatitude()));
+ update.bindString(3, String.valueOf(where.getLongitude()));
update.bindLong(4, localId);
update.execute();
- } // updateLocation
+ }
public void deleteLocation(final int localId) {
final String LOCATION_TABLE_DELETE =
@@ -74,16 +74,16 @@ public void deleteLocation(final int localId) {
final SQLiteStatement delete = db_.compileStatement(LOCATION_TABLE_DELETE);
delete.bindLong(1, localId);
delete.execute();
- } // deleteRoute
+ }
public SavedLocation savedLocation(int localId) {
List locs = locations(BaseColumns._ID + " = ?", new String[] { Integer.toString(localId)});
return locs.size() != 0 ? locs.get(0) : null;
- } // savedLocation
+ }
public List savedLocations() {
return locations(null, null);
- } // savedLocations
+ }
private List locations(String where, String[] whereArgs) {
final List locations = new ArrayList<>();
@@ -94,18 +94,18 @@ private List locations(String where, String[] whereArgs) {
null,
null,
"name");
- if(cursor.moveToFirst())
+ if (cursor.moveToFirst())
do {
locations.add(new SavedLocation(
cursor.getInt(0),
cursor.getString(1),
- cursor.getInt(2),
- cursor.getInt(3)));
+ Double.parseDouble(cursor.getString(2)),
+ Double.parseDouble(cursor.getString(3))));
} while (cursor.moveToNext());
- if(!cursor.isClosed())
+ if (!cursor.isClosed())
cursor.close();
return locations;
- } // locations
-} // class LocationDatabase
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteData.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteData.java
index 843abda43..cf8e2b97b 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteData.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteData.java
@@ -2,22 +2,25 @@
import net.cyclestreets.routing.Waypoints;
-public class RouteData
+public class RouteData
{
- final String name_;
- final String xml_;
- final Waypoints points_;
-
- public RouteData(final String xml,
- final Waypoints points,
- final String name)
- {
- xml_ = xml;
- points_ = points;
- name_ = name;
- } // RouteData
-
- public String name() { return name_; }
- public String xml() { return xml_; }
- public Waypoints points() { return points_; }
-} // class RouteData
+ private final String name;
+ private final String json;
+ private final Waypoints points;
+ private final boolean saveRoute;
+
+ public RouteData(final String json,
+ final Waypoints points,
+ final String name,
+ final boolean saveRoute) {
+ this.json = json;
+ this.points = points;
+ this.name = name;
+ this.saveRoute = saveRoute;
+ }
+
+ public String name() { return name; }
+ public String json() { return json; }
+ public Waypoints points() { return points; }
+ public boolean saveRoute() { return saveRoute; }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteDatabase.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteDatabase.java
index 0044d4017..43b87a0e5 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteDatabase.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteDatabase.java
@@ -6,195 +6,184 @@
import net.cyclestreets.routing.Journey;
import net.cyclestreets.routing.Waypoints;
-import org.osmdroid.api.IGeoPoint;
-
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteStatement;
import android.provider.BaseColumns;
+import android.util.Log;
+import net.cyclestreets.util.Logging;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.util.GeoPoint;
-public class RouteDatabase
+public class RouteDatabase
{
- private final SQLiteDatabase db_;
-
- public RouteDatabase(final Context context)
- {
+ private static final String TAG = Logging.getTag(RouteDatabase.class);
+ private final SQLiteDatabase db;
+
+ public RouteDatabase(final Context context) {
DatabaseHelper dh = new DatabaseHelper(context);
- db_ = dh.getWritableDatabase();
- } // RouteDatabase
-
- public int routeCount()
- {
- final Cursor cursor = db_.query(DatabaseHelper.ROUTE_TABLE,
+ db = dh.getWritableDatabase();
+ }
+
+ public int routeCount() {
+ final Cursor cursor = db.query(DatabaseHelper.ROUTE_TABLE,
new String[] { "count(" + BaseColumns._ID +")" },
- null,
+ null,
null,
null,
null,
null);
int c = 0;
- if(cursor.moveToFirst())
- do
- {
+ if (cursor.moveToFirst())
+ do {
c = cursor.getInt(0);
- }
+ }
while (cursor.moveToNext());
-
- if(!cursor.isClosed())
+
+ if (!cursor.isClosed())
cursor.close();
-
+
return c;
- } // count
-
+ }
+
public void saveRoute(final Journey journey,
- final String xml)
- {
- if(route(journey.itinerary(), journey.plan()) == null)
- addRoute(journey, xml);
+ final String json) {
+ if (route(journey.itinerary(), journey.plan()) == null)
+ addRoute(journey, json);
else
updateRoute(journey);
- } // saveRoute
-
+ }
+
private void addRoute(final Journey journey,
- final String xml)
- {
- final String ROUTE_TABLE_INSERT =
- "INSERT INTO route (journey, name, plan, distance, waypoints, xml, last_used) " +
+ final String json) {
+ final String ROUTE_TABLE_INSERT =
+ "INSERT INTO route (journey, name, plan, distance, waypoints, journey_json, last_used) " +
" VALUES(?, ?, ?, ?, ?, ?, datetime())";
-
- final SQLiteStatement insertRoute = db_.compileStatement(ROUTE_TABLE_INSERT);
+
+ final SQLiteStatement insertRoute = db.compileStatement(ROUTE_TABLE_INSERT);
insertRoute.bindLong(1, journey.itinerary());
insertRoute.bindString(2, journey.name());
insertRoute.bindString(3, journey.plan());
- insertRoute.bindLong(4, journey.total_distance());
- insertRoute.bindString(5, flattenWaypoints(journey.waypoints()));
- insertRoute.bindString(6, xml);
+ insertRoute.bindLong(4, journey.totalDistance());
+ insertRoute.bindString(5, serializeWaypoints(journey.getWaypoints()));
+ insertRoute.bindString(6, json);
insertRoute.executeInsert();
- } // addRoute
-
- private void updateRoute(final Journey journey)
- {
- final String ROUTE_TABLE_UPDATE =
+ }
+
+ private void updateRoute(final Journey journey) {
+ final String ROUTE_TABLE_UPDATE =
"UPDATE route SET last_used = datetime() WHERE journey = ? and plan = ?";
-
- final SQLiteStatement update = db_.compileStatement(ROUTE_TABLE_UPDATE);
+
+ final SQLiteStatement update = db.compileStatement(ROUTE_TABLE_UPDATE);
update.bindLong(1, journey.itinerary());
update.bindString(2, journey.plan());
update.execute();
- } // updateRoute
-
- public void renameRoute(final int localId, final String newName)
- {
+ }
+
+ public void renameRoute(final int localId, final String newName) {
final String ROUTE_TABLE_RENAME =
"UPDATE route SET name = ? WHERE " + BaseColumns._ID + " = ?";
- final SQLiteStatement update = db_.compileStatement(ROUTE_TABLE_RENAME);
+ final SQLiteStatement update = db.compileStatement(ROUTE_TABLE_RENAME);
update.bindString(1, newName);
update.bindLong(2, localId);
- update.execute();
- } // renameRoute
-
- public void deleteRoute(final int localId)
- {
- final String ROUTE_TABLE_DELETE =
+ update.execute();
+ }
+
+ public void deleteRoute(final int localId) {
+ final String ROUTE_TABLE_DELETE =
"DELETE FROM route WHERE " + BaseColumns._ID + " = ?";
-
- final SQLiteStatement delete = db_.compileStatement(ROUTE_TABLE_DELETE);
+
+ final SQLiteStatement delete = db.compileStatement(ROUTE_TABLE_DELETE);
delete.bindLong(1, localId);
delete.execute();
- } // deleteRoute
-
- public List savedRoutes()
- {
+ }
+
+ public List savedRoutes() {
final List routes = new ArrayList<>();
- final Cursor cursor = db_.query(DatabaseHelper.ROUTE_TABLE,
+ final Cursor cursor = db.query(DatabaseHelper.ROUTE_TABLE,
new String[] { BaseColumns._ID, "journey", "name", "plan", "distance" },
- null,
- null,
- null,
- null,
- "last_used desc");
- if(cursor.moveToFirst())
- do
- {
+ null,
+ null,
+ null,
+ null,
+ "last_used desc");
+ if (cursor.moveToFirst())
+ do {
routes.add(new RouteSummary(cursor.getInt(0),
- cursor.getInt(1),
+ cursor.getInt(1),
cursor.getString(2),
cursor.getString(3),
cursor.getInt(4)));
- }
+ }
while (cursor.moveToNext());
-
- if(!cursor.isClosed())
+
+ if (!cursor.isClosed())
cursor.close();
-
+
return routes;
- } // savedRoutes
-
- public RouteData route(final int localId)
- {
- return fetchRoute(BaseColumns._ID + "=?",
+ }
+
+ public RouteData route(final int localId) {
+ return fetchRoute(BaseColumns._ID + "=?",
new String[] { Integer.toString(localId) });
- } // route
-
- public RouteData route(final int itinerary, final String plan)
- {
- return fetchRoute("journey=? and plan=?",
+ }
+
+ public RouteData route(final int itinerary, final String plan) {
+ return fetchRoute("journey=? and plan=?",
new String[] { Integer.toString(itinerary), plan });
- } // route
+ }
- private RouteData fetchRoute(final String filter, final String[] bindParams)
- {
+ private RouteData fetchRoute(final String filter, final String[] bindParams) {
RouteData r = null;
- final Cursor cursor = db_.query(DatabaseHelper.ROUTE_TABLE,
- new String[] { "xml",
+ final Cursor cursor = db.query(DatabaseHelper.ROUTE_TABLE,
+ new String[] { "journey_json",
"waypoints",
"name"},
- filter,
- bindParams,
- null,
- null,
+ filter,
+ bindParams,
+ null,
+ null,
null);
- if(cursor.moveToFirst())
- do
- {
+ if (cursor.moveToFirst())
+ do {
r = new RouteData(cursor.getString(0),
- expandWaypoints(cursor.getString(1)),
- cursor.getString(2));
- }
+ new Waypoints(deserializeWaypoints(cursor.getString(1))),
+ cursor.getString(2),
+ true);
+ }
while (cursor.moveToNext());
-
- if(!cursor.isClosed())
+
+ if (!cursor.isClosed())
cursor.close();
-
+
return r;
- } // fetchRoute
-
- private String flattenWaypoints(final Waypoints waypoints)
- {
+ }
+
+ public static String serializeWaypoints(Iterable waypoints) {
final StringBuilder sb = new StringBuilder();
- for(final IGeoPoint waypoint : waypoints)
- {
- if(sb.length() != 0)
+ for (final IGeoPoint waypoint : waypoints) {
+ if (sb.length() != 0)
sb.append('|');
- sb.append(waypoint.getLatitudeE6())
- .append(',')
- .append(waypoint.getLongitudeE6());
- } // for ...
- return sb.toString();
- } // flattenWaypoints
-
- private Waypoints expandWaypoints(final String str)
- {
- final Waypoints points = new Waypoints();
- for(final String coords : str.split("\\|"))
- {
- final String[] latlon = coords.split(",");
- final double lat = Long.parseLong(latlon[0])/1E6;
- final double lon = Long.parseLong(latlon[1])/1E6;
-
- points.add(lat, lon);
- } // for ...
- return points;
- } // expandWaypoints
-} // class RouteDatabase
+ sb.append(waypoint.getLatitude())
+ .append(",")
+ .append(waypoint.getLongitude());
+ }
+ String wpString = sb.toString();
+ Log.d(TAG, "sW: " + wpString);
+ return wpString;
+ }
+
+ public static List deserializeWaypoints(String serializedWaypoints) {
+ List waypoints = new ArrayList<>();
+ for (final String coords : serializedWaypoints.split("\\|")) {
+ final String[] latlon = coords.split(",", 2);
+ double lat = Double.parseDouble(latlon[0]);
+ double lon = Double.parseDouble(latlon[1]);
+ Log.d(TAG, "dW: lat=" + lat + ", lon=" + lon);
+ waypoints.add(new GeoPoint(lat, lon));
+ }
+ return waypoints;
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteSummary.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteSummary.java
index 377ada019..ce31fccb4 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteSummary.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/RouteSummary.java
@@ -1,29 +1,28 @@
package net.cyclestreets.content;
-public class RouteSummary
+public class RouteSummary
{
- private int localId_;
- private int itinerary_;
- private String title_;
- private String plan_;
- private int distance_;
-
- RouteSummary(final int localId,
- final int itinerary,
- final String title,
- final String plan,
- final int distance)
- {
- localId_ = localId;
- itinerary_ = itinerary;
- title_ = title;
- plan_ = plan;
- distance_ = distance;
- } // RouteSummary
-
- public int localId() { return localId_; }
- public int itinerary() { return itinerary_; }
- public String title() { return title_; }
- public String plan() { return plan_; }
- public int distance() { return distance_; }
-} // class RouteSummary
+ private int localId_;
+ private int itinerary_;
+ private String title_;
+ private String plan_;
+ private int distance_;
+
+ RouteSummary(final int localId,
+ final int itinerary,
+ final String title,
+ final String plan,
+ final int distance) {
+ localId_ = localId;
+ itinerary_ = itinerary;
+ title_ = title;
+ plan_ = plan;
+ distance_ = distance;
+ }
+
+ public int localId() { return localId_; }
+ public int itinerary() { return itinerary_; }
+ public String title() { return title_; }
+ public String plan() { return plan_; }
+ public int distance() { return distance_; }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/SavedLocation.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/SavedLocation.java
index 63865fab7..3a9a69973 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/SavedLocation.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/content/SavedLocation.java
@@ -10,16 +10,16 @@ public class SavedLocation {
SavedLocation(final int id,
final String name,
- final int whereLat,
- final int whereLon) {
+ final double whereLat,
+ final double whereLon) {
id_ = id;
name_ = name;
where_ = new GeoPoint(whereLat, whereLon);
- } // SavedLocation
+ }
public int localId() { return id_; }
public String name() { return name_; }
public IGeoPoint where() { return where_; }
public String toString() { return name_; }
-} // SavedLocation
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/iconics/IconicsHelper.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/iconics/IconicsHelper.kt
new file mode 100644
index 000000000..7bd13c4e9
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/iconics/IconicsHelper.kt
@@ -0,0 +1,71 @@
+package net.cyclestreets.iconics
+
+import android.content.Context
+import android.util.Log
+import android.view.Menu
+import android.view.MenuInflater
+import com.mikepenz.iconics.IconicsDrawable
+import com.mikepenz.iconics.typeface.IIcon
+
+import com.mikepenz.iconics.utils.IconicsMenuInflaterUtil
+import com.mikepenz.iconics.utils.colorInt
+import com.mikepenz.iconics.utils.sizeDp
+import net.cyclestreets.util.Logging
+
+
+private val TAG = Logging.getTag(IconicsHelper::class.java)
+
+
+object IconicsHelper {
+
+ fun materialIcon(context: Context, iconId: IIcon, color: Int? = null, size: Int = 24): IconicsDrawable {
+ return materialIcons(context, listOf(iconId), color, size).first()
+ }
+
+ fun materialIcons(context: Context, iconIds: List, color: Int? = null, size: Int = 24): List {
+ return iconIds.map {
+ iconId -> IconicsDrawable(context, iconId)
+ .apply {
+ sizeDp = size
+ color?.let { this.colorInt = color}
+ }
+ }
+ }
+
+ // Derive Context from the inflater, and then delegate to the Iconics inflater.
+ fun inflate(inflater: MenuInflater, menuId: Int, menu: Menu) {
+ inflate(inflater, menuId, menu, true)
+ }
+
+ // Derive Context from the inflater, and then delegate to the Iconics inflater.
+ fun inflate(inflater: MenuInflater, menuId: Int, menu: Menu, checkSubMenus: Boolean) {
+ val context = getContext(inflater)
+
+ if (context != null) {
+ IconicsMenuInflaterUtil.inflate(inflater, context, menuId, menu, checkSubMenus)
+ } else {
+ // In the worst case (e.g. on Google implementation change), we fall back to the default
+ // inflater; we'll lose the icons but won't fall over.
+ inflater.inflate(menuId, menu)
+ }
+ }
+
+ // Derive the Context from a MenuInflater (using reflection).
+ //
+ // In some fragment transitions, menu inflation is performed before the fragment's context
+ // is initialised, so we can't just do a `getContext()`; the internal `mContext` field is used
+ // in this scope by the native inflater.inflate(), so we should be safe.
+ private fun getContext(inflater: MenuInflater): Context? {
+ return try {
+ val f = inflater.javaClass.getDeclaredField("mContext")
+ f.isAccessible = true
+ f.get(inflater) as Context
+ } catch (e: IllegalAccessException) {
+ Log.w(TAG, "IllegalAccessException: Failed to find mContext on ${inflater.javaClass.canonicalName}")
+ null
+ } catch (e: NoSuchFieldException) {
+ Log.w(TAG, "NoSuchFieldException: Failed to find mContext on ${inflater.javaClass.canonicalName}")
+ null
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AdvanceToSegment.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AdvanceToSegment.java
deleted file mode 100644
index 359e96411..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AdvanceToSegment.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Segment;
-
-import org.osmdroid.util.GeoPoint;
-
-final class AdvanceToSegment extends LiveRideState
-{
- AdvanceToSegment(final LiveRideState previous,
- final Journey journey)
- {
- this(previous, journey, journey.segments().get(journey.activeSegmentIndex()+1));
- } // AdvanceToSegment
-
- AdvanceToSegment(final LiveRideState previous,
- final Journey journey,
- final Segment segment)
- {
- super(previous);
- journey.setActiveSegment(segment);
- notify(segment);
- } // AdvanceToSegment
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- if(journey.atWaypoint())
- return new PassingWaypoint(this);
- if(journey.atEnd())
- return new Arrivee(this);
-
- return new OnTheMove(this);
- } // update
-
- @Override
- public boolean isStopped() { return false; }
- @Override
- public boolean arePedalling() { return true; }
-}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AdvanceToSegment.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AdvanceToSegment.kt
new file mode 100644
index 000000000..19cfd92a2
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AdvanceToSegment.kt
@@ -0,0 +1,30 @@
+package net.cyclestreets.liveride
+
+import android.util.Log
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Segment
+import org.osmdroid.util.GeoPoint
+
+internal class AdvanceToSegment @JvmOverloads constructor(previous: LiveRideState,
+ journey: Journey,
+ segment: Segment? = journey.segments[journey.activeSegmentIndex() + 1]) : LiveRideState(previous) {
+ init {
+ journey.setActiveSegment(segment!!)
+ // this might not be an important message
+ Log.d("importantTest", "AdvanceToSegment Init: ${segment.toString()}")
+ notify(segment, true)
+ }
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ if (journey.atWaypoint()) {
+ return PassingWaypoint(this)
+ }
+ if (journey.atEnd()) {
+ return Arrivee(this)
+ }
+ return OnTheMove(this)
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return true }
+}
\ No newline at end of file
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Arrivee.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Arrivee.java
deleted file mode 100644
index c7aff0e1c..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Arrivee.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.routing.Journey;
-
-import org.osmdroid.util.GeoPoint;
-
-final class Arrivee extends LiveRideState
-{
- Arrivee(final LiveRideState previous)
- {
- super(previous);
- notify("Arreeve eh", "Arriv\u00e9e");
- } // Arrivee
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- getPebbleNotifier().notifyStopped();
- return new Stopped(context(), getPebbleNotifier());
- } // update
-
- @Override
- public boolean isStopped() { return false; }
- @Override
- public boolean arePedalling() { return false; }
-}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Arrivee.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Arrivee.kt
new file mode 100644
index 000000000..e504e5bdb
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Arrivee.kt
@@ -0,0 +1,23 @@
+package net.cyclestreets.liveride
+
+import android.util.Log
+import net.cyclestreets.routing.Journey
+import org.osmdroid.util.GeoPoint
+
+internal class Arrivee(previous: LiveRideState) : LiveRideState(previous) {
+
+ companion object {
+ const val ARRIVEE = "Arrivée"
+ }
+
+ init {
+ notify(ARRIVEE, important = true)
+ }
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ return Stopped(context)
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return false }
+}
\ No newline at end of file
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AudioFocuser.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AudioFocuser.kt
new file mode 100644
index 000000000..34bed2175
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/AudioFocuser.kt
@@ -0,0 +1,60 @@
+package net.cyclestreets.liveride
+
+import android.content.Context
+import android.media.AudioFocusRequest
+import android.media.AudioManager
+import android.os.Build
+
+import android.media.AudioAttributes.CONTENT_TYPE_SPEECH
+import android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE
+import android.media.AudioManager.*
+import android.speech.tts.UtteranceProgressListener
+import android.util.Log
+import net.cyclestreets.util.Logging
+import kotlin.collections.HashMap
+
+private val TAG = Logging.getTag(AudioFocuser::class.java)
+
+class AudioFocuser(context: Context) : UtteranceProgressListener(), AudioManager.OnAudioFocusChangeListener {
+
+ private val audioAttributes = android.media.AudioAttributes.Builder()
+ .setUsage(USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
+ .setContentType(CONTENT_TYPE_SPEECH)
+ .build()
+ private val am: AudioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
+ private val audioFocusRequests: MutableMap = HashMap()
+
+ @Suppress("deprecation")
+ override fun onStart(utteranceId: String) {
+ val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val afr = AudioFocusRequest.Builder(AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK).setAudioAttributes(audioAttributes).build()
+ audioFocusRequests[utteranceId] = afr
+ am.requestAudioFocus(afr)
+ } else {
+ am.requestAudioFocus(this, CONTENT_TYPE_SPEECH, AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
+ }
+ Log.d(TAG, "Audio focus request for utterance $utteranceId: result $result ($AUDIOFOCUS_REQUEST_GRANTED=granted, $AUDIOFOCUS_REQUEST_FAILED=failed)")
+ }
+
+ @Suppress("deprecation")
+ override fun onDone(utteranceId: String) {
+ val result: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ audioFocusRequests.remove(utteranceId)?.let {
+ am.abandonAudioFocusRequest(it)
+ } ?: -1
+ } else {
+ am.abandonAudioFocus(this)
+ }
+ Log.d(TAG, "Audio focus release for utterance $utteranceId: result $result ($AUDIOFOCUS_REQUEST_GRANTED=granted, $AUDIOFOCUS_REQUEST_FAILED=failed, -1=utterance unknown)")
+ }
+
+ @Deprecated("")
+ override fun onError(utteranceId: String) {
+ Log.d(TAG, "TTS error occurred processing utterance $utteranceId")
+ audioFocusRequests.remove(utteranceId)
+ }
+
+ override fun onAudioFocusChange(focusChange: Int) {
+ // Nothing to do - it's not the end of the world if we can't get audio focus
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/GoingOffCourse.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/GoingOffCourse.java
deleted file mode 100644
index 7c57a144a..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/GoingOffCourse.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Segment;
-
-import org.osmdroid.util.GeoPoint;
-
-final class GoingOffCourse extends LiveRideState
-{
- GoingOffCourse(final LiveRideState previous)
- {
- super(previous);
- notify("Moving away from route");
- getPebbleNotifier().notify(this);
- }
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- Segment nearestSeg = null;
- int distance = Integer.MAX_VALUE;
-
- for(final Segment seg : journey.segments())
- {
- int from = seg.distanceFrom(whereIam);
- if(from < distance)
- {
- distance = from;
- nearestSeg = seg;
- } // if ...
- } // for ...
-
- distance -= accuracy;
-
- if(distance > CycleStreetsPreferences.replanDistance())
- return new ReplanFromHere(this, whereIam);
-
- if(nearestSeg != journey.activeSegment())
- return new AdvanceToSegment(this, journey, nearestSeg);
-
- if(distance <= CycleStreetsPreferences.offtrackDistance() - 5) {
- notify("Getting back on track");
- return new OnTheMove(this);
- }
-
- return this;
- } // update
-
- @Override
- public boolean isStopped() { return false; }
-
- @Override
- public boolean arePedalling() { return true; }
-} // class GoingOffCourse
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/GoingOffCourse.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/GoingOffCourse.kt
new file mode 100644
index 000000000..6e560e0d8
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/GoingOffCourse.kt
@@ -0,0 +1,44 @@
+package net.cyclestreets.liveride
+
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Segment
+import org.osmdroid.util.GeoPoint
+
+internal class GoingOffCourse(previous: LiveRideState) : LiveRideState(previous) {
+
+ init {
+ notify("Moving away from route", important = true)
+ }
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ var nearestSeg: Segment = journey.segments.first()
+ var distance = Int.MAX_VALUE
+
+ for (seg in journey.segments) {
+ val from = seg.distanceFrom(myLocation)
+ if (from < distance) {
+ distance = from
+ nearestSeg = seg
+ }
+ }
+
+ distance -= accuracy
+
+ if (distance > CycleStreetsPreferences.replanDistance()) {
+ return ReplanFromHere(this, myLocation)
+ }
+ if (nearestSeg !== journey.activeSegment()) {
+ return AdvanceToSegment(this, journey, nearestSeg)
+ }
+ if (distance <= CycleStreetsPreferences.offtrackDistance() - 5) {
+ notify("Getting back on track", important = true)
+ return OnTheMove(this)
+ }
+
+ return this
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return true }
+}
\ No newline at end of file
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/HuntForSegment.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/HuntForSegment.java
deleted file mode 100644
index 3d011093c..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/HuntForSegment.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Segment;
-
-import org.osmdroid.util.GeoPoint;
-
-final class HuntForSegment extends LiveRideState
-{
- private int waitToSettle_;
-
- HuntForSegment(final LiveRideState state)
- {
- super(state);
- waitToSettle_ = 5;
- } // HuntForSegment
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- if(waitToSettle_ > 0) {
- --waitToSettle_;
- return this;
- }
-
- Segment nearestSeg = null;
- int distance = Integer.MAX_VALUE;
-
- for(final Segment seg : journey.segments())
- {
- int from = seg.distanceFrom(whereIam);
- if(from < distance)
- {
- distance = from;
- nearestSeg = seg;
- } // if ...
- } // for ...
-
- distance -= accuracy;
-
- if(distance > CycleStreetsPreferences.replanDistance())
- return new ReplanFromHere(this, whereIam);
-
- if(nearestSeg == journey.activeSegment())
- return new OnTheMove(this);
-
- return new AdvanceToSegment(this, journey, nearestSeg);
- } // update
-
- @Override
- public boolean isStopped() { return false; }
- @Override
- public boolean arePedalling() { return true; }
-}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/HuntForSegment.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/HuntForSegment.kt
new file mode 100644
index 000000000..f627ef3d6
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/HuntForSegment.kt
@@ -0,0 +1,41 @@
+package net.cyclestreets.liveride
+
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Segment
+import org.osmdroid.util.GeoPoint
+
+internal class HuntForSegment(state: LiveRideState) : LiveRideState(state) {
+
+ private var waitToSettle = 5
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ if (waitToSettle > 0) {
+ --waitToSettle
+ return this
+ }
+
+ var nearestSeg: Segment = journey.segments.first()
+ var distance = Int.MAX_VALUE
+
+ for (seg in journey.segments) {
+ val from = seg.distanceFrom(myLocation)
+ if (from < distance) {
+ distance = from
+ nearestSeg = seg
+ }
+ }
+ distance -= accuracy
+
+ if (distance > CycleStreetsPreferences.replanDistance()) {
+ return ReplanFromHere(this, myLocation)
+ }
+ if (nearestSeg === journey.activeSegment()) {
+ return OnTheMove(this)
+ }
+ return AdvanceToSegment(this, journey, nearestSeg)
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return true }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideService.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideService.java
deleted file mode 100644
index 59a638240..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideService.java
+++ /dev/null
@@ -1,118 +0,0 @@
-package net.cyclestreets.liveride;
-
-import org.osmdroid.util.GeoPoint;
-
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Route;
-import android.app.Service;
-import android.content.Context;
-import android.content.Intent;
-import android.location.Location;
-import android.location.LocationListener;
-import android.location.LocationManager;
-import android.os.Binder;
-import android.os.Bundle;
-import android.os.IBinder;
-
-public class LiveRideService extends Service
- implements LocationListener
-{
- private IBinder binder_;
- private LocationManager locationManager_;
- private Location lastLocation_;
- private LiveRideState stage_;
- private PebbleNotifier pebbleNotifier_;
-
- private static int updateDistance = 5; // metres
- private static int updateTime = 500; // milliseconds
-
- @Override
- public void onCreate()
- {
- binder_ = new Binding();
- locationManager_ = (LocationManager)getSystemService(Context.LOCATION_SERVICE);
- pebbleNotifier_ = new PebbleNotifier(this);
- stage_ = LiveRideState.StoppedState(this, pebbleNotifier_);
- } // onCreate
-
- @Override
- public int onStartCommand(final Intent intent, final int flags, final int startId)
- {
- return Service.START_NOT_STICKY;
- } // onStartCommand
-
- @Override
- public void onDestroy()
- {
- super.onDestroy();
- } // onDestroy
-
- @Override
- public IBinder onBind(final Intent intent)
- {
- return binder_;
- } // onBind
-
- public void startRiding()
- {
- if(!stage_.isStopped())
- return;
- stage_ = LiveRideState.InitialState(this, pebbleNotifier_);
- locationManager_.requestLocationUpdates(LocationManager.GPS_PROVIDER, updateTime, updateDistance, this);
- } // startRiding
-
- public void stopRiding()
- {
- stage_ = LiveRideState.StoppedState(this, pebbleNotifier_);
- locationManager_.removeUpdates(this);
- pebbleNotifier_.notifyStopped();
- } // stopRiding
-
- public boolean areRiding()
- {
- return stage_.arePedalling();
- } // onRide
-
- public Location lastLocation()
- {
- return lastLocation_;
- } // lastLocation
-
- public class Binding extends Binder
- {
- private LiveRideService service() { return LiveRideService.this; }
- public void startRiding() { service().startRiding(); }
- public void stopRiding() { service().stopRiding(); }
- public boolean areRiding() { return service().areRiding(); }
- public String stage() { return stage_.getClass().getSimpleName(); }
- public Location lastLocation() { return service().lastLocation(); }
- } // class LocalBinder
-
- // ///////////////////////////////////////////////
- // location listener
- @Override
- public void onLocationChanged(final Location location)
- {
- if(!Route.available())
- {
- stopRiding();
- return;
- } // if ...
-
- lastLocation_ = location;
-
- final GeoPoint whereIam = new GeoPoint(location);
- final float accuracy = location.hasAccuracy() ? location.getAccuracy() : 2;
-
- final Journey journey = Route.journey();
-
- stage_ = stage_.update(journey, whereIam, (int)accuracy);
- } // onLocationChanged
-
- @Override
- public void onProviderDisabled(String arg0) { }
- @Override
- public void onProviderEnabled(String arg0) { }
- @Override
- public void onStatusChanged(String arg0, int arg1, Bundle arg2) { }
-} // class LiveRideService
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideService.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideService.kt
new file mode 100644
index 000000000..e7edd73b3
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideService.kt
@@ -0,0 +1,114 @@
+package net.cyclestreets.liveride
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.util.Log
+import net.cyclestreets.util.Logging
+import net.cyclestreets.util.*
+import org.osmdroid.util.GeoPoint
+
+import net.cyclestreets.routing.Route
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.location.Location
+import android.location.LocationListener
+import android.location.LocationManager
+import android.os.Binder
+import android.os.Bundle
+import android.os.IBinder
+import android.speech.tts.TextToSpeech
+import android.speech.tts.TextToSpeech.ERROR
+import android.speech.tts.TextToSpeech.SUCCESS
+
+private const val UPDATE_DISTANCE = 5f // metres
+private const val UPDATE_TIME = 500L // milliseconds
+private val TAG = Logging.getTag(LiveRideService::class.java)
+
+class LiveRideService : Service(), LocationListener, TextToSpeech.OnInitListener {
+ private lateinit var binder: IBinder
+ private lateinit var locationManager: LocationManager
+ private var stage: LiveRideState? = null
+ private var lastLocation: Location? = null
+
+ override fun onCreate() {
+ binder = Binding()
+ locationManager = getSystemService(Context.LOCATION_SERVICE) as LocationManager
+ stage = Stopped(this)
+ }
+
+ override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
+ return Service.START_NOT_STICKY
+ }
+
+ override fun onDestroy() {
+ stopRiding()
+ super.onDestroy()
+ }
+
+ override fun onBind(intent: Intent): IBinder? {
+ return binder
+ }
+
+ @SuppressLint("MissingPermission") // We handle this with the hasPermission() check
+ fun startRiding() {
+ if (!stage!!.isStopped())
+ return
+
+ if (!hasPermission(this, Manifest.permission.ACCESS_FINE_LOCATION)) {
+ // Should be unreachable, but we're being defensive
+ Log.w(TAG, "Location permission is not granted. Bail out.")
+ return
+ }
+
+ val tts = TextToSpeech(this, this)
+ tts.setOnUtteranceProgressListener(AudioFocuser(this))
+ stage = LiveRideStart(this, tts).setServiceForeground(this)
+ locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, UPDATE_TIME, UPDATE_DISTANCE, this)
+ Log.d(TAG, "startRiding")
+ }
+
+ fun stopRiding() {
+ if (stage!!.isStopped())
+ return
+ stage!!.tts!!.stop()
+ stage!!.tts!!.shutdown()
+ stage = Stopped(this)
+ locationManager.removeUpdates(this)
+ Log.d(TAG, "stopRiding")
+ }
+
+ inner class Binding : Binder() {
+ private fun service(): LiveRideService { return this@LiveRideService }
+ fun startRiding() { service().startRiding() }
+ fun stopRiding() { service().stopRiding() }
+ fun areRiding(): Boolean { return service().stage!!.arePedalling() }
+ fun lastLocation(): Location? { return service().lastLocation }
+ }
+
+ // TextToSpeech init listener
+ @SuppressLint("MissingPermission")
+ override fun onInit(status: Int) {
+ Log.i(TAG, "TextToSpeech init returned $status ($SUCCESS=success, $ERROR=error)")
+ locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER)?.let { onLocationChanged(it) }
+ }
+
+ // Location listener
+ override fun onLocationChanged(location: Location) {
+ if (!Route.routeAvailable()) {
+ stopRiding()
+ return
+ }
+
+ lastLocation = location
+
+ val whereIam = GeoPoint(location)
+ val accuracy: Int = if (location.hasAccuracy()) location.accuracy.toInt() else 2
+ stage = stage!!.update(Route.journey(), whereIam, accuracy)
+ }
+
+ override fun onProviderDisabled(arg0: String) {}
+ override fun onProviderEnabled(arg0: String) {}
+ @Deprecated("Deprecated in Java")
+ override fun onStatusChanged(arg0: String, arg1: Int, arg2: Bundle) {}
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideStart.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideStart.java
deleted file mode 100644
index 48a8ead42..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideStart.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.routing.Journey;
-
-import org.osmdroid.util.GeoPoint;
-
-import android.content.Context;
-import android.speech.tts.TextToSpeech;
-
-final class LiveRideStart extends LiveRideState
-{
- LiveRideStart(final Context context, final PebbleNotifier pebbleNotifier, final TextToSpeech tts)
- {
- super(context, pebbleNotifier, tts);
- notify("Starting LiveRide", "Starting LiveRide");
- } // LiveRideStart
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- notify("LiveRide", "LiveRide");
- journey.setActiveSegmentIndex(0);
- notify(journey.activeSegment());
- getPebbleNotifier().notifyStart(this, journey.activeSegment());
- return new HuntForSegment(this);
- } // update
-
- @Override
- public boolean isStopped() { return false; }
- @Override
- public boolean arePedalling() { return false; }
-} // class LiveRideStart
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideStart.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideStart.kt
new file mode 100644
index 000000000..93b57e907
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideStart.kt
@@ -0,0 +1,26 @@
+package net.cyclestreets.liveride
+
+import android.app.Service
+import android.content.Context
+import android.speech.tts.TextToSpeech
+import android.util.Log
+import net.cyclestreets.routing.Journey
+import org.osmdroid.util.GeoPoint
+
+internal class LiveRideStart(context: Context, tts: TextToSpeech?) : LiveRideState(context, tts) {
+
+ fun setServiceForeground(liveRideService: Service): LiveRideStart {
+ notifyAndSetServiceForeground(liveRideService, "Starting LiveRide")
+ return this
+ }
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ journey.setActiveSegmentIndex(0)
+ Log.d("importantTest", "LiveRideStart Update: ${journey.activeSegment()!!.toString()}")
+ notify(journey.activeSegment()!!, true)
+ return HuntForSegment(this)
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return false }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideState.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideState.java
deleted file mode 100644
index f621aea5c..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideState.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.LiveRideActivity;
-import net.cyclestreets.view.R;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Segment;
-
-import org.osmdroid.util.GeoPoint;
-
-import android.app.Notification;
-import android.app.NotificationManager;
-import android.app.PendingIntent;
-import android.content.Context;
-import android.content.Intent;
-import android.speech.tts.TextToSpeech;
-import android.util.Log;
-
-public abstract class LiveRideState
-{
- private static final int NOTIFICATION_ID = 1;
- private final PebbleNotifier pebbleNotifier_;
-
- static public LiveRideState InitialState(final Context context, final PebbleNotifier pebbleNotifier)
- {
- final TextToSpeech tts = new TextToSpeech(context,
- new TextToSpeech.OnInitListener() { public void onInit(int arg0) { } }
- );
- return new LiveRideStart(context, pebbleNotifier, tts);
- } // InitialState
-
- static public LiveRideState StoppedState(final Context context, PebbleNotifier pebbleNotifier)
- {
- return new Stopped(context, pebbleNotifier);
- } // StoppedState
- //////////////////////////////////////////
-
- private Context context_;
- private String title_;
- private TextToSpeech tts_;
-
- protected LiveRideState(final Context context, final PebbleNotifier pebbleNotifier, final TextToSpeech tts)
- {
- context_ = context;
- pebbleNotifier_ = pebbleNotifier;
- tts_ = tts;
- title_ = context.getString(context.getApplicationInfo().labelRes);
- Log.d("CS_PEBBLE LRS", "New State: " + this.getClass().getSimpleName());
- } // LiveRideState
-
- protected LiveRideState(final LiveRideState state)
- {
- context_ = state.context();
- pebbleNotifier_ = state.getPebbleNotifier();
- tts_ = state.tts();
- Log.d("CS_PEBBLE LRS", "State: " + this.getClass().getSimpleName());
- } // LiveRideState
-
-
- public abstract LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy);
- public abstract boolean isStopped();
- public abstract boolean arePedalling();
-
- protected Context context() { return context_; }
- protected TextToSpeech tts() { return tts_; }
- protected PebbleNotifier getPebbleNotifier() {
- return pebbleNotifier_;
- }
-
- protected void notify(final Segment seg)
- {
- notification(seg.street() + " " + seg.distance(), seg.toString());
-
- final StringBuilder instruction = new StringBuilder();
- if(seg.turn().length() != 0)
- instruction.append(seg.turn()).append(" into ");
- instruction.append(seg.street().replace("un-", "un").replace("Un-", "un"));
- instruction.append(". Continue ").append(seg.distance());
- speak(instruction.toString());
- getPebbleNotifier().notify(this, seg);
- } // notify
-
- protected void notify(final String text)
- {
- notify(text, text);
- } // notify
-
- protected void notify(final String text, final String ticker)
- {
- notification(text, ticker);
- speak(text);
- } // notify
-
- private void notification(final String text, final String ticker)
- {
- final NotificationManager nm = nm();
- final Notification notification = new Notification(R.drawable.ic_launcher, ticker, System.currentTimeMillis());
- notification.flags = Notification.FLAG_AUTO_CANCEL | Notification.FLAG_ONGOING_EVENT;
- final Intent notificationIntent = new Intent(context(), LiveRideActivity.class);
- final PendingIntent contentIntent = PendingIntent.getActivity(context(), 0, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
- notification.setLatestEventInfo(context(), title_, text, contentIntent);
- nm.notify(NOTIFICATION_ID, notification);
- } // notify
-
- protected void cancelNotification()
- {
- nm().cancel(NOTIFICATION_ID);
- } // cancelNotification
-
- private NotificationManager nm()
- {
- return (NotificationManager)context().getSystemService(Context.NOTIFICATION_SERVICE);
- } // nm
-
- private void speak(final String words)
- {
- String toSpeak = words.replace("LiveRide", "Live Ride");
-
- if (getPebbleNotifier().isConnected()) {
- if (CycleStreetsPreferences.pebbleVoice())
- tts().speak(toSpeak, TextToSpeech.QUEUE_ADD, null);
- } else
- tts().speak(toSpeak, TextToSpeech.QUEUE_ADD, null);
- } // speak
-} // interface LiveRideState
-
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideState.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideState.kt
new file mode 100644
index 000000000..dd4fea017
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/LiveRideState.kt
@@ -0,0 +1,130 @@
+package net.cyclestreets.liveride
+
+import android.app.Notification
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Icon
+import android.speech.tts.TextToSpeech
+import android.speech.tts.UtteranceProgressListener
+import android.util.Log
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import com.mikepenz.iconics.utils.toAndroidIconCompat
+import net.cyclestreets.CycleStreetsNotifications
+import net.cyclestreets.CycleStreetsNotifications.CHANNEL_LIVERIDE_ID
+import net.cyclestreets.LiveRideActivity
+import net.cyclestreets.iconics.IconicsHelper.materialIcon
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Segment
+import net.cyclestreets.util.Logging
+import net.cyclestreets.view.R
+import org.osmdroid.util.GeoPoint
+import java.util.*
+
+private val TAG = Logging.getTag(LiveRideState::class.java)
+private const val NOTIFICATION_ID = 1
+
+
+internal abstract class LiveRideState(protected val context: Context,
+ val tts: TextToSpeech?,
+ private val title: String) {
+ init {
+ Log.d(TAG, "New State: " + this.javaClass.simpleName)
+ }
+
+ protected constructor(context: Context, tts: TextToSpeech?):
+ this(context, tts, context.getString(R.string.app_name))
+
+ protected constructor(state: LiveRideState):
+ this(state.context, state.tts, state.title)
+
+ abstract fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState
+ abstract fun isStopped(): Boolean
+ abstract fun arePedalling(): Boolean
+
+ protected fun notify(seg: Segment, important: Boolean = false) {
+ notification(seg.street() + " " + seg.formattedDistance(), seg.toString())
+
+ val instruction = turnInto(seg)
+ if (seg.turnInstruction().isNotEmpty()) {
+ instruction.append(". Continue ").append(seg.formattedDistance())
+ }
+
+ speak(instruction.toString(), important)
+ }
+
+ protected fun turnInto(seg: Segment): StringBuilder {
+ val instruction = StringBuilder()
+ if (seg.turnInstruction().isNotEmpty()) {
+ instruction.append(seg.turnInstruction()).append(" into ")
+ }
+ instruction.append(fixStreet(seg.street()))
+ return instruction
+ }
+
+ // checked
+ protected fun notify(text: String, directionIcon: Int, important: Boolean = false) {
+ notification(text, text, directionIcon)
+ speak(text, important)
+ }
+
+ @JvmOverloads
+ protected fun notify(text: String, ticker: String = text, important: Boolean = false) {
+ notification(text, ticker)
+ speak(text, important)
+ }
+
+ protected fun notifyAndSetServiceForeground(service: Service, text: String) {
+ val notification = getNotification(text, text, null)
+ service.startForeground(NOTIFICATION_ID, notification)
+ speak(text, true)
+ }
+
+ private fun notification(text: String, ticker: String, directionIcon: Int? = null) {
+ val notification = getNotification(text, ticker, directionIcon)
+ nm().notify(NOTIFICATION_ID, notification)
+ }
+
+ private fun getNotification(text: String, ticker: String, directionIcon: Int? = null): Notification {
+ val notificationIntent = Intent(context, LiveRideActivity::class.java)
+ val contentIntent = PendingIntent.getActivity(
+ context,
+ 0,
+ notificationIntent,
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+
+ val notificationBuilder = CycleStreetsNotifications.getBuilder(context, CHANNEL_LIVERIDE_ID)
+ .setSmallIcon(materialIcon(context, GoogleMaterial.Icon.gmd_directions_bike).toAndroidIconCompat().toIcon(context))
+ .setTicker(ticker)
+ .setWhen(System.currentTimeMillis())
+ .setAutoCancel(true)
+ .setOngoing(true)
+ .setContentTitle(title)
+ .setContentText(text)
+ .setContentIntent(contentIntent)
+
+ directionIcon?.let {
+ notificationBuilder.setLargeIcon(Icon.createWithResource(context, it))
+ }
+
+ return notificationBuilder.build()
+ }
+
+ protected fun cancelNotification() {
+ nm().cancel(NOTIFICATION_ID)
+ }
+
+ private fun nm(): NotificationManager {
+ return context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ }
+
+ private fun speak(words: String, important: Boolean = false) {
+ if (important) {
+ tts?.speak(speechify(words), TextToSpeech.QUEUE_FLUSH, null, UUID.randomUUID().toString())
+ } else {
+ tts?.speak(speechify(words), TextToSpeech.QUEUE_ADD, null, UUID.randomUUID().toString())
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/MovingState.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/MovingState.java
deleted file mode 100644
index 1029f48e6..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/MovingState.java
+++ /dev/null
@@ -1,55 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.routing.Journey;
-
-import org.osmdroid.util.GeoPoint;
-
-abstract class MovingState extends LiveRideState
-{
- private final int transition_;
-
- private boolean notifiedPebble_ = false;
-
- MovingState(final LiveRideState previous, final int transitionThreshold)
- {
- super(previous);
- transition_ = transitionThreshold;
- } // OnTheMove
-
- @Override
- public final LiveRideState update(final Journey journey, final GeoPoint whereIam, final int accuracy)
- {
- if (! this.notifiedPebble_) {
- this.notifiedPebble_ = true;
- getPebbleNotifier().notify(this, journey.nextSegment());
- }
- int distanceFromEnd = journey.activeSegment().distanceFromEnd(whereIam);
- distanceFromEnd -= accuracy;
- if(distanceFromEnd < transition_)
- return transitionState(journey);
-
- return checkCourse(journey, whereIam, accuracy);
- } // update
-
- protected abstract LiveRideState transitionState(final Journey journey);
-
- private LiveRideState checkCourse(final Journey journey, final GeoPoint whereIam, final int accuracy)
- {
- int distance = journey.activeSegment().distanceFrom(whereIam);
- distance -= accuracy;
-
- if(distance > CycleStreetsPreferences.replanDistance())
- return new ReplanFromHere(this, whereIam);
-
- if(distance > CycleStreetsPreferences.offtrackDistance())
- return new GoingOffCourse(this);
-
- return this;
- } // update
-
- @Override
- public boolean isStopped() { return false; }
- @Override
- public boolean arePedalling() { return true; }
-} // class OnTheMove
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/MovingState.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/MovingState.kt
new file mode 100644
index 000000000..6d3981185
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/MovingState.kt
@@ -0,0 +1,37 @@
+package net.cyclestreets.liveride
+
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.routing.Journey
+import org.osmdroid.util.GeoPoint
+
+internal abstract class MovingState(previous: LiveRideState, private val transition_: Int) :
+ LiveRideState(previous) {
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ var distanceFromEnd = journey.activeSegment()!!.distanceFromEnd(myLocation)
+ distanceFromEnd -= accuracy
+
+ if (distanceFromEnd < transition_) {
+ return transitionState(journey)
+ }
+ return checkCourse(journey, myLocation, accuracy)
+ }
+
+ protected abstract fun transitionState(journey: Journey): LiveRideState
+
+ private fun checkCourse(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ var distance = journey.activeSegment()!!.distanceFrom(myLocation)
+ distance -= accuracy
+
+ if (distance > CycleStreetsPreferences.replanDistance()) {
+ return ReplanFromHere(this, myLocation)
+ }
+ if (distance > CycleStreetsPreferences.offtrackDistance()) {
+ return GoingOffCourse(this)
+ }
+ return this
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return true }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/NearingTurn.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/NearingTurn.java
deleted file mode 100644
index f4d57feb4..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/NearingTurn.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.routing.Segment;
-import net.cyclestreets.CycleStreetsPreferences;
-
-import net.cyclestreets.routing.Journey;
-
-final class NearingTurn extends MovingState
-{
- NearingTurn(final LiveRideState previous, final Journey journey)
- {
- super(previous, CycleStreetsPreferences.turnNowDistance());
-
- final Segment segment = journey.segments().get(journey.activeSegmentIndex()+1);
- notify("Get ready to " + segment.turn());
- } // NearingEnd
-
- @Override
- protected LiveRideState transitionState(final Journey journey)
- {
- return new AdvanceToSegment(this, journey);
- } // transitionStatue
-} // class NearingTurn
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/NearingTurn.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/NearingTurn.kt
new file mode 100644
index 000000000..2943a6ff0
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/NearingTurn.kt
@@ -0,0 +1,24 @@
+package net.cyclestreets.liveride
+
+import net.cyclestreets.CycleStreetsPreferences
+
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.util.TurnIcons
+
+internal class NearingTurn(previous: LiveRideState, journey: Journey) :
+ MovingState(previous, CycleStreetsPreferences.turnNowDistance()) {
+
+ init {
+ val segment = journey.segments.get(journey.activeSegmentIndex() + 1)
+
+ if (!segment.turnInstruction().isNullOrEmpty()) {
+ notify("Get ready to ${turnInto(segment)}", TurnIcons.iconId(segment.turn()))
+ } else {
+ notify("You are approaching the ${Arrivee.ARRIVEE}", important = true)
+ }
+ }
+
+ override fun transitionState(journey: Journey): LiveRideState {
+ return AdvanceToSegment(this, journey)
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/OnTheMove.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/OnTheMove.java
deleted file mode 100644
index f361a49ba..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/OnTheMove.java
+++ /dev/null
@@ -1,19 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.routing.Journey;
-
-final class OnTheMove extends MovingState
-{
- OnTheMove(final LiveRideState previous)
- {
- super(previous, CycleStreetsPreferences.nearingTurnDistance());
- } // OnTheMove
-
- @Override
- protected LiveRideState transitionState(final Journey journey)
- {
- return new NearingTurn(this, journey);
- } // transitionStatue
-} // class OnTheMove
-
\ No newline at end of file
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/OnTheMove.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/OnTheMove.kt
new file mode 100644
index 000000000..adc485d65
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/OnTheMove.kt
@@ -0,0 +1,12 @@
+package net.cyclestreets.liveride
+
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.routing.Journey
+
+internal class OnTheMove(previous: LiveRideState) :
+ MovingState(previous, CycleStreetsPreferences.nearingTurnDistance()) {
+
+ override fun transitionState(journey: Journey): LiveRideState {
+ return NearingTurn(this, journey)
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PassingWaypoint.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PassingWaypoint.java
deleted file mode 100644
index 68a15439d..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PassingWaypoint.java
+++ /dev/null
@@ -1,25 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.routing.Journey;
-
-import org.osmdroid.util.GeoPoint;
-
-final class PassingWaypoint extends LiveRideState
-{
- PassingWaypoint(final LiveRideState previous)
- {
- super(previous);
- notify("Passing waypoint");
- } // PassingWaypoint
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- return new AdvanceToSegment(this, journey);
- } // update
-
- @Override
- public boolean isStopped() { return false; }
- @Override
- public boolean arePedalling() { return true; }
-} // class PassingWaypoint
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PassingWaypoint.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PassingWaypoint.kt
new file mode 100644
index 000000000..5ad8f2606
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PassingWaypoint.kt
@@ -0,0 +1,18 @@
+package net.cyclestreets.liveride
+
+import net.cyclestreets.routing.Journey
+import org.osmdroid.util.GeoPoint
+
+internal class PassingWaypoint(previous: LiveRideState?) : LiveRideState(previous!!) {
+
+ init {
+ notify("Passing waypoint", important = true)
+ }
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ return AdvanceToSegment(this, journey)
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return true }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PebbleNotifier.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PebbleNotifier.java
deleted file mode 100644
index bbd18cef0..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/PebbleNotifier.java
+++ /dev/null
@@ -1,196 +0,0 @@
-package net.cyclestreets.liveride;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.IntentFilter;
-import android.util.Log;
-
-import com.getpebble.android.kit.PebbleKit;
-import com.getpebble.android.kit.util.PebbleDictionary;
-
-import net.cyclestreets.routing.Segment;
-
-import java.util.LinkedList;
-import java.util.Queue;
-import java.util.UUID;
-
-/**
- * Created by jsinglet on 20/02/2015.
- */
-public class PebbleNotifier {
-
- private static final String BROADCAST_PEBBLE_MESSAGE=PebbleNotifier.class.getName() + ".message";
- // UUID generated by pebble app creation
- private static final UUID APP_UID = UUID.fromString("7b99db93-2503-4d6e-a503-67353132a90c");
- public static final String TAG = "CS_PEBBLE";
- private final Context context;
- private int transactionId = 1;
-
-
-
- private boolean isSending = false;
-
-
- private Queue messageQueue = new LinkedList<>();
- private BroadcastReceiver pebbleMessageReceiver;
- private PebbleKit.PebbleAckReceiver pebbleAckReceiver;
- private PebbleKit.PebbleNackReceiver pebbleNackReceiver;
-
- private enum PebbleMessages {
- turn(0),
- street(1),
- distance(2),
- running(3),
- instruction(4),
- stateType(5);
-
- private final int key;
-
- PebbleMessages(int key) {
- this.key = key;
- }
-
- public int getKey() {
- return key;
- }
-
- }
-
- public PebbleNotifier(Context context) {
- this.context = context;
- pebbleMessageReceiver = new BroadcastReceiver() {
-
- @Override
- public void onReceive(Context context, Intent intent) {
- if (!isSending() && !messageQueue.isEmpty()) {
- setSending(true);
- PebbleDictionary dictionary = messageQueue.peek();
- if (dictionary != null) {
- int txnId = nextTransactionId();
- Log.d(TAG, "sending message " + txnId + " Queue size: " + messageQueue.size());
- PebbleKit.sendDataToPebbleWithTransactionId(context, APP_UID, dictionary, txnId);
- }
- }
- }
- };
-
- pebbleAckReceiver = new PebbleKit.PebbleAckReceiver(APP_UID) {
-
- @Override
- public void receiveAck(Context context, int transactionId) {
- Log.i(TAG, "Received ack for transaction " + transactionId);
- messageQueue.remove();
- setSending(false);
- context.sendBroadcast(new Intent(BROADCAST_PEBBLE_MESSAGE));
- }
-
- };
-
- pebbleNackReceiver = new PebbleKit.PebbleNackReceiver(APP_UID) {
-
- @Override
- public void receiveNack(Context context, int transactionId) {
- Log.i(TAG, "Received nack for transaction " + transactionId + ", resending");
- setSending(false);
- context.sendBroadcast(new Intent(BROADCAST_PEBBLE_MESSAGE));
- }
-
- };
- }
-
- private void registerReceivers() {
- Log.d(TAG, "register");
- context.registerReceiver(pebbleMessageReceiver, new IntentFilter(BROADCAST_PEBBLE_MESSAGE));
- PebbleKit.registerReceivedAckHandler(context, pebbleAckReceiver);
- PebbleKit.registerReceivedNackHandler(context, pebbleNackReceiver);
- }
-
- private void unregisterReceivers() {
- Log.d(TAG, "unregister");
- context.unregisterReceiver(pebbleMessageReceiver);
- context.unregisterReceiver(pebbleAckReceiver);
- context.unregisterReceiver(pebbleNackReceiver);
-
- }
-
- private synchronized void addMessage(PebbleDictionary dictionary) {
- messageQueue.add(dictionary);
- }
-
-
- public boolean isConnected() {
- return PebbleKit.isWatchConnected(context);
- }
-
- public void connectIfNeeded() {
- if (!isConnected()) {
- Log.i(TAG, "Received ack for transaction " + transactionId);
- }
- }
-
- public void notifyStopped() {
- Log.d(TAG, "Stopping App");
- if (isConnected()) {
- PebbleKit.closeAppOnPebble(this.context, APP_UID);
- unregisterReceivers();
- }
- }
-
- public void notifyStart(LiveRideState state, Segment seg) {
- Log.d(TAG, "Starting App");
- registerReceivers();
- messageQueue.clear();
- PebbleKit.startAppOnPebble(this.context, APP_UID);
- PebbleDictionary dictionary = new PebbleDictionary();
- dictionary.addString(PebbleMessages.street.getKey(), "Starting Ride");
- dictionary.addString(PebbleMessages.stateType.getKey(), state.getClass().getSimpleName());
-
- Log.i(TAG, "NotifyStart " + state.getClass().getSimpleName() + " :: "+ seg.turn() + " into " + seg.street());
- messageQueue.add(dictionary);
- context.sendBroadcast(new Intent(BROADCAST_PEBBLE_MESSAGE));
- }
-
- public void notify(LiveRideState state) {
- if (isConnected()) {
- PebbleDictionary dictionary = new PebbleDictionary();
- dictionary.addString(PebbleMessages.stateType.getKey(), state.getClass().getSimpleName());
-
- Log.i(TAG, "Notifying State " + state.getClass().getSimpleName());
- messageQueue.add(dictionary);
- context.sendBroadcast(new Intent(BROADCAST_PEBBLE_MESSAGE));
- }
- }
-
- public void notify(LiveRideState state, Segment seg) {
- if (isConnected()) {
- PebbleDictionary dictionary = new PebbleDictionary();
- if (seg != null) {
- dictionary.addString(PebbleMessages.turn.getKey(), seg.turn());
- dictionary.addString(PebbleMessages.street.getKey(), seg.street());
- dictionary.addString(PebbleMessages.running.getKey(), seg.runningDistance());
- dictionary.addString(PebbleMessages.distance.getKey(), seg.distance());
- Log.i(TAG, "Notifying " + state.getClass().getSimpleName() + " :: " + seg.turn() + " into " + seg.street());
- } else {
- Log.i(TAG, "Notifying " + state.getClass().getSimpleName() + " :: with null segment");
- }
- dictionary.addString(PebbleMessages.stateType.getKey(), state.getClass().getSimpleName());
-
- messageQueue.add(dictionary);
- context.sendBroadcast(new Intent(BROADCAST_PEBBLE_MESSAGE));
- }
- }
-
- private int nextTransactionId() {
- return this.transactionId++;
- }
-
- public synchronized boolean isSending() {
- return isSending;
- }
-
- public synchronized void setSending(boolean isSending) {
- this.isSending = isSending;
- }
-
-}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/ReplanFromHere.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/ReplanFromHere.java
deleted file mode 100644
index a84375c49..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/ReplanFromHere.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.routing.Journey;
-import net.cyclestreets.routing.Route;
-import net.cyclestreets.routing.Waypoints;
-
-import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.util.GeoPoint;
-
-final class ReplanFromHere extends LiveRideState
- implements Route.Listener
-{
- private LiveRideState next_;
-
- ReplanFromHere(final LiveRideState previous, final GeoPoint whereIam)
- {
- super(previous);
- notify("Too far away. Re-planning the journey.");
- getPebbleNotifier().notify(this);
- next_ = this;
-
- final IGeoPoint finish = Route.waypoints().last();
- Route.softRegisterListener(this);
- Route.PlotRoute(CycleStreetsPreferences.routeType(),
- CycleStreetsPreferences.speed(),
- context(),
- Waypoints.fromTo(whereIam, finish));
- } // ReplanFromHere
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- return next_;
- } // update
-
- @Override
- public boolean isStopped() { return false; }
-
- @Override
- public boolean arePedalling() { return true; }
-
- @Override
- public void onNewJourney(Journey journey, Waypoints waypoints)
- {
- next_ = new HuntForSegment(this);
- Route.unregisterListener(this);
- } // onNewJourney
-
- @Override
- public void onResetJourney()
- {
- } // onResetJourney
-} // class ReplanFromHere
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/ReplanFromHere.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/ReplanFromHere.kt
new file mode 100644
index 000000000..7be6b64cd
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/ReplanFromHere.kt
@@ -0,0 +1,64 @@
+package net.cyclestreets.liveride
+
+import android.util.Log
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.routing.Journey
+import net.cyclestreets.routing.Route
+import net.cyclestreets.routing.Segment
+import net.cyclestreets.routing.Waypoints
+import net.cyclestreets.util.Logging
+
+import org.osmdroid.util.GeoPoint
+
+private val TAG = Logging.getTag(ReplanFromHere::class.java)
+
+internal class ReplanFromHere(previous: LiveRideState, whereIam: GeoPoint) : LiveRideState(previous), Route.Listener {
+ private var next: LiveRideState? = null
+
+ init {
+ notify("Too far away. Re-planning the journey.", important = true)
+
+ next = this
+
+ val activeSegment = Route.journey().activeSegment()
+ // if waypoints size is 1, it's a circular route, so need to get waypoints from Segment.Waymark's
+ if (Route.waypoints().count() == 1) {
+ for (waymark in Route.journey().segments) {
+ if (waymark is Segment.Waymark) {
+ Log.d(TAG, "Waymark points " + waymark.points().toString())
+ Route.waypoints().add(waymark.start().latitude, waymark.start().longitude)
+ }
+ }
+ // Add final waypoint (which is same as starting point):
+ Route.waypoints().first()?.let { Route.waypoints().add(it) }
+ }
+ val remainingWaypoints: Waypoints = when (activeSegment) {
+ is Segment.Start -> Route.waypoints().startingWith(whereIam)
+ is Segment.End -> Waypoints.fromTo(whereIam, Route.waypoints().last())
+ is Segment.Waymark -> Route.waypoints().fromLeg(activeSegment.legNumber()).startingWith(whereIam)
+ is Segment.Step -> Route.waypoints().fromLeg(activeSegment.legNumber()).startingWith(whereIam)
+ else -> {
+ Log.w(TAG, "Unexpected segment type ${activeSegment?.javaClass ?: "'null'"}")
+ throw IllegalStateException("Unexpected segment type ${activeSegment?.javaClass ?: "'null'"}")
+ }
+ }
+ Route.softRegisterListener(this)
+ Route.LiveReplanRoute(CycleStreetsPreferences.speed(),
+ context,
+ remainingWaypoints)
+ }
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ return next!!
+ }
+
+ override fun isStopped(): Boolean { return false }
+ override fun arePedalling(): Boolean { return true }
+
+ override fun onNewJourney(journey: Journey, waypoints: Waypoints) {
+ next = HuntForSegment(this)
+ Route.unregisterListener(this)
+ }
+
+ override fun onResetJourney() {}
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/SpeechFixer.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/SpeechFixer.kt
new file mode 100644
index 000000000..1f46fa067
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/SpeechFixer.kt
@@ -0,0 +1,87 @@
+package net.cyclestreets.liveride
+
+private val REGEX_METRES = "(\\d+)m".toRegex()
+private val REGEX_KILOMETRES = "(\\d+)km".toRegex()
+
+private val REGEX_METRIC_DISTANCES: Map String> = mapOf(
+ REGEX_METRES to { m -> "${m.groupValues[1]} metres" },
+ REGEX_KILOMETRES to { m -> "${m.groupValues[1]} kilometres" }
+)
+
+private val REGEX_3_DIGIT_TENS = "([a-zA-Z])(\\d)([1-9]0)(\\D|\$)".toRegex() // e.g. A230
+private val REGEX_3_DIGIT_MIDDLE_ZERO = "([a-zA-Z])(\\d)0([1-9])(\\D|\$)".toRegex() // e.g. A404
+private val REGEX_3_DIGIT_OTHER = "([a-zA-Z])(\\d)([1-9])([1-9])(\\D|\$)".toRegex() // e.g. A467
+
+private val REGEX_3_DIGIT_ROADS: Map String> = mapOf(
+ REGEX_3_DIGIT_TENS to { m -> "${m.groupValues[1]}${m.groupValues[2]} ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_3_DIGIT_MIDDLE_ZERO to { m -> "${m.groupValues[1]}${m.groupValues[2]}-oh ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_3_DIGIT_OTHER to { m -> "${m.groupValues[1]}${m.groupValues[2]} ${m.groupValues[3]} ${m.groupValues[4]}${m.groupValues[5]}" }
+)
+
+private val REGEX_4_DIGIT_HUNDREDS = "([a-zA-Z])(\\d[1-9])00(\\D|\$)".toRegex() // e.g. B1200
+private val REGEX_4_DIGIT_TENS_AND_TENS = "([a-zA-Z])(\\d0)([1-9]0)(\\D|\$)".toRegex() // e.g. A4030
+private val REGEX_4_DIGIT_DIGITS_AND_TENS = "([a-zA-Z])(\\d[1-9])([1-9]0)(\\D|\$)".toRegex() // e.g. A4130
+private val REGEX_4_DIGIT_TENS_AND_DIGITS = "([a-zA-Z])(\\d0)([1-9][1-9])(\\D|\$)".toRegex() // e.g. A4032
+private val REGEX_4_DIGIT_MIDDLE_ZEROES = "([a-zA-Z])(\\d)00([1-9])(\\D|\$)".toRegex() // e.g. B5001
+private val REGEX_4_DIGIT_TREBLE_AT_START = "([a-zA-Z])(\\d)\\2\\2(\\d)(\\D|\$)".toRegex() // e.g. B1113
+private val REGEX_4_DIGIT_TREBLE_AT_END = "([a-zA-Z])(\\d)([1-9])\\3\\3(\\D|\$)".toRegex() // e.g. A2111
+private val REGEX_4_DIGIT_THIRD_DIGIT_0 = "([a-zA-Z])(\\d)([1-9])0([1-9])(\\D|\$)".toRegex() // e.g. A2103
+private val REGEX_4_DIGIT_OTHER = "([a-zA-Z])(\\d)([1-9])([1-9])([1-9])(\\D|\$)".toRegex() // e.g. A4123
+
+private val REGEX_4_DIGIT_ROADS: Map String> = mapOf(
+ REGEX_4_DIGIT_HUNDREDS to { m -> "${m.groupValues[1]}${m.groupValues[2]}-hundred${m.groupValues[3]}" },
+ REGEX_4_DIGIT_MIDDLE_ZEROES to { m -> "${m.groupValues[1]}${m.groupValues[2]}-double-oh ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_4_DIGIT_TENS_AND_TENS to { m -> "${m.groupValues[1]}${m.groupValues[2]} ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_4_DIGIT_DIGITS_AND_TENS to { m -> "${m.groupValues[1]}${m.groupValues[2]} ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_4_DIGIT_TENS_AND_DIGITS to { m -> "${m.groupValues[1]}${m.groupValues[2]} ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_4_DIGIT_TREBLE_AT_START to { m -> "${m.groupValues[1]}-treble ${m.groupValues[2]} ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_4_DIGIT_TREBLE_AT_END to { m -> "${m.groupValues[1]}${m.groupValues[2]}-treble ${m.groupValues[3]}${m.groupValues[4]}" },
+ REGEX_4_DIGIT_THIRD_DIGIT_0 to { m -> "${m.groupValues[1]}${m.groupValues[2]} ${m.groupValues[3]}-oh ${m.groupValues[4]}${m.groupValues[5]}" },
+ REGEX_4_DIGIT_OTHER to { m -> "${m.groupValues[1]}${m.groupValues[2]} ${m.groupValues[3]} ${m.groupValues[4]} ${m.groupValues[5]}${m.groupValues[6]}" }
+)
+
+/**
+ * This rather complex helper method manipulates the Android speech engine so instead of saying e.g.
+ * "Bee four thousand one hundred and twenty three" it will correctly call the road "B4123", as
+ * humans would.
+ *
+ * To live test, edit LiveRideState and substitute something like the following for the input to the `speechify()` method.
+ * val testWords = "Testing... B3000 and A200 and A230 and A404 and A467 and B1200 and A4030. New: A4130 and A4032. Then B5001 and B1113 and A2111 and A2103 and A4123"
+ */
+fun speechify(words: String): String {
+
+ var updatedWords = words
+ .replace("LiveRide", "Live Ride")
+ .replace(Arrivee.ARRIVEE, "arreev eh")
+
+ for (entry in REGEX_METRIC_DISTANCES) {
+ updatedWords = entry.key.replace(updatedWords, entry.value)
+ }
+
+ for (entry in REGEX_3_DIGIT_ROADS) {
+ updatedWords = entry.key.replace(updatedWords, entry.value)
+ }
+
+ for (entry in REGEX_4_DIGIT_ROADS) {
+ updatedWords = entry.key.replace(updatedWords, entry.value)
+ }
+
+ return updatedWords;
+}
+
+
+fun fixStreet(streetWords: String): String {
+
+ // handling "un-named link" etc
+ var updatedWords = streetWords
+ .replace("un-", "un")
+ .replace("Un-", "un")
+
+ // some Android speech engines get this right; others don't (https://github.com/cyclestreets/android/issues/442)
+ val wordsList = updatedWords.split(" ")
+ if (wordsList.size > 1 && setOf("St.", "st.", "St", "st").contains(wordsList.first())) {
+ updatedWords = "Saint ${wordsList.subList(1, wordsList.size).joinToString(" ")}";
+ }
+
+ return updatedWords;
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Stopped.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Stopped.java
deleted file mode 100644
index 57654bf32..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Stopped.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package net.cyclestreets.liveride;
-
-import net.cyclestreets.routing.Journey;
-
-import org.osmdroid.util.GeoPoint;
-
-import android.content.Context;
-
-final class Stopped extends LiveRideState
-{
- Stopped(final Context context, final PebbleNotifier pebbleNotifier)
- {
- super(context, pebbleNotifier, null);
- cancelNotification();
- } // Stopped
-
- @Override
- public LiveRideState update(Journey journey, GeoPoint whereIam, int accuracy)
- {
- return this;
- } // update
-
- @Override
- public boolean isStopped() { return true; }
- @Override
- public boolean arePedalling() { return false; }
-} // class Stopped
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Stopped.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Stopped.kt
new file mode 100644
index 000000000..a8fe4c047
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/liveride/Stopped.kt
@@ -0,0 +1,19 @@
+package net.cyclestreets.liveride
+
+import android.content.Context
+import net.cyclestreets.routing.Journey
+import org.osmdroid.util.GeoPoint
+
+internal class Stopped(context: Context) : LiveRideState(context, null) {
+
+ init {
+ cancelNotification()
+ }
+
+ override fun update(journey: Journey, myLocation: GeoPoint, accuracy: Int): LiveRideState {
+ return this
+ }
+
+ override fun isStopped(): Boolean { return true }
+ override fun arePedalling(): Boolean { return false }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/photos/FetchIndividualPhotoTask.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/photos/FetchIndividualPhotoTask.kt
new file mode 100644
index 000000000..d2d1d80b3
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/photos/FetchIndividualPhotoTask.kt
@@ -0,0 +1,31 @@
+package net.cyclestreets.photos
+
+import android.os.AsyncTask
+import android.util.Log
+import net.cyclestreets.api.ApiClient
+import net.cyclestreets.api.Photo
+import net.cyclestreets.util.Logging
+
+private val TAG = Logging.getTag(FetchIndividualPhotoTask::class.java)
+
+internal class FetchIndividualPhotoTask constructor() : AsyncTask() {
+
+ @Deprecated("Deprecated in Java")
+ override fun doInBackground(vararg params: Long?): Photo? {
+ val photoId = params[0]!!
+ return try {
+ Log.d(TAG, "Querying API for photo $photoId")
+ val photos = ApiClient.getPhoto(photoId)
+ photos.first()
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to get photo $photoId", e)
+ null
+ }
+ }
+
+ @Deprecated("Deprecated in Java")
+ override fun onPostExecute(photo: Photo?) {
+ if (photo != null)
+ IndividualPhoto.onPhotoLoaded(photo)
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/photos/IndividualPhoto.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/photos/IndividualPhoto.kt
new file mode 100644
index 000000000..f62bb6b93
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/photos/IndividualPhoto.kt
@@ -0,0 +1,33 @@
+package net.cyclestreets.photos
+
+import net.cyclestreets.api.Photo
+import net.cyclestreets.util.Logging
+
+import java.util.ArrayList
+
+private val TAG = Logging.getTag(IndividualPhoto::class.java)
+
+object IndividualPhoto {
+
+ interface Listener {
+ fun onPhotoLoaded(photo: Photo)
+ }
+
+ private val listeners = ArrayList()
+ fun registerListener(listener: Listener) {
+ if (!listeners.contains(listener))
+ listeners.add(listener)
+ }
+ fun unregisterListener(listener: Listener) {
+ listeners.remove(listener)
+ }
+ fun onPhotoLoaded(photo: Photo) {
+ for (l in listeners)
+ l.onPhotoLoaded(photo)
+ }
+
+ fun fetchPhoto(photoId: Long) {
+ val query = FetchIndividualPhotoTask()
+ query.execute(photoId)
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/CycleStreetsRoutingTask.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/CycleStreetsRoutingTask.java
deleted file mode 100644
index 93eada741..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/CycleStreetsRoutingTask.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package net.cyclestreets.routing;
-
-import net.cyclestreets.view.R;
-import net.cyclestreets.content.RouteData;
-
-import android.content.Context;
-
-class CycleStreetsRoutingTask extends RoutingTask
-{
- /////////////////////////////////////////////////////
- private final String routeType_;
- private final int speed_;
-
- CycleStreetsRoutingTask(final String routeType,
- final int speed,
- final Context context)
- {
- super(R.string.finding_route, context);
- routeType_ = routeType;
- speed_ = speed;
- } // NewRouteTask
-
- @Override
- protected RouteData doInBackground(final Waypoints... waypoints)
- {
- final Waypoints wp = waypoints[0];
- return fetchRoute(routeType_, speed_, wp);
- } // doInBackgroud
-} // NewRouteTask
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/CycleStreetsRoutingTask.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/CycleStreetsRoutingTask.kt
new file mode 100644
index 000000000..83ee2119a
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/CycleStreetsRoutingTask.kt
@@ -0,0 +1,28 @@
+package net.cyclestreets.routing
+
+import android.content.Context
+
+import net.cyclestreets.content.RouteData
+import net.cyclestreets.view.R
+
+internal open class CycleStreetsRoutingTask(private val routeType: String,
+ private val speed: Int,
+ context: Context,
+ private val distance: Int? = null,
+ private val duration: Int? = null,
+ private val poiTypes: String? = null,
+ private val saveRoute: Boolean = true,
+ pAltRoute: Boolean = false) : RoutingTask(R.string.route_finding_new, context, pAltRoute) {
+
+ @Deprecated("Deprecated in Java")
+ override fun doInBackground(vararg waypoints: Waypoints): RouteData? {
+ val wp = waypoints[0]
+ return fetchRoute(routeType,
+ speed = speed,
+ waypoints = wp,
+ distance = distance,
+ duration = duration,
+ poiTypes = poiTypes,
+ saveRoute = saveRoute)
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/DistanceFormatter.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/DistanceFormatter.java
index 8b69980b1..d56e4df64 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/DistanceFormatter.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/DistanceFormatter.java
@@ -1,93 +1,81 @@
package net.cyclestreets.routing;
-public abstract class DistanceFormatter
+public abstract class DistanceFormatter
{
public abstract String distance(int metres);
- public abstract String total_distance(int metres);
+ public abstract String totalDistance(int metres);
public abstract String speed(float metresPerSec);
public abstract String speedUnit();
-
- static public DistanceFormatter formatter(final String name)
- {
- if("miles".equals(name))
- return milesFormatter;
+
+ public static DistanceFormatter formatter(final String name) {
+ if ("miles".equals(name))
+ return milesFormatter;
return kmFormatter;
- } // formatter
+ }
+
+ private static DistanceFormatter kmFormatter = new KmFormatter();
+ private static DistanceFormatter milesFormatter = new MilesFormatter();
- static private DistanceFormatter kmFormatter = new KmFormatter();
- static private DistanceFormatter milesFormatter = new MilesFormatter();
+ private static class KmFormatter extends DistanceFormatter {
+ public String distance(int metres) {
+ if (metres < 2000)
+ return String.format("%dm", roundDistance(metres));
+ return totalDistance(metres);
+ }
- static private class KmFormatter extends DistanceFormatter
- {
- public String distance(int metres)
- {
- if(metres < 2000)
- return String.format("%dm", round_distance(metres));
- return total_distance(metres);
- } // distance
-
- public String total_distance(int metres)
- {
+ public String totalDistance(int metres) {
int km = metres / 1000;
int frackm = (int)((metres % 1000) / 10.0);
return String.format("%d.%02dkm", km, frackm);
- } // total_distance
-
- public String speed(float metresPerSec)
- {
+ }
+
+ public String speed(float metresPerSec) {
final double kph = metresPerSec * 60.0 * 60.0 / 1000.0;
- if(kph < 10)
+ if (kph < 10)
return String.format("%.1f", kph);
return String.format("%d", (int)kph);
- } // speed
-
- public String speedUnit()
- {
+ }
+
+ public String speedUnit() {
return "km/h";
- } // speedUnit
- } // class KmFormatter
-
- static private class MilesFormatter extends DistanceFormatter
- {
+ }
+ }
+
+ private static class MilesFormatter extends DistanceFormatter {
private int metresToYards(int metres) { return (int)(metres * 1.0936133); }
-
- public String distance(int metres)
- {
+
+ public String distance(int metres) {
int yards = metresToYards(metres);
- if(yards <= 750)
- return String.format("%d yards", round_distance(yards));
- return total_distance(metres);
- } // distance
-
- public String total_distance(int metres)
- {
+ if (yards <= 750)
+ return String.format("%d yards", roundDistance(yards));
+ return totalDistance(metres);
+ }
+
+ public String totalDistance(int metres) {
int yards = metresToYards(metres);
int miles = yards / 1760;
int frackm = (int)((yards % 1760) / 17.6);
return String.format("%d.%02d miles", miles, frackm);
- } // total_distance
-
- public String speed(float metresPerSec)
- {
+ }
+
+ public String speed(float metresPerSec) {
final double metresPerHour = metresPerSec * 60.0 * 60.0;
final int yardsPerHour = metresToYards((int)metresPerHour);
final double mph = yardsPerHour / 1760.0;
- if(mph < 10)
+ if (mph < 10)
return String.format("%.1f", mph);
return String.format("%d", (int)mph);
- } // speed
-
- public String speedUnit()
- {
+ }
+
+ public String speedUnit() {
return "mph";
}
- } // class MilesFormatter
-
- static protected int round_distance(int units)
- {
- if(units < 500)
+ }
+
+ static protected int roundDistance(int units) {
+ if (units < 500)
return (int)
(units/5.0) * 5;
return (int)(units/10.0) * 10;
- } // round_distance
-} // DistanceFormatter
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Elevation.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Elevation.java
index f2d2e5a73..8c22440cb 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Elevation.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Elevation.java
@@ -1,14 +1,14 @@
package net.cyclestreets.routing;
public class Elevation {
- final int distanceFromStart_;
- final int elevation_;
+ private final int distanceFromStart;
+ private final int elevation;
- Elevation(final int d, final int e) {
- distanceFromStart_ = d;
- elevation_ = e;
- } // Elevation
+ public Elevation(final int d, final int e) {
+ distanceFromStart = d;
+ elevation = e;
+ }
- public int distance() { return distanceFromStart_; }
- public int elevation() { return elevation_; }
-} // class Elevation
+ public int distance() { return distanceFromStart; }
+ public int elevation() { return elevation; }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationFormatter.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationFormatter.java
index 2bb571725..cb03d5db1 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationFormatter.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationFormatter.java
@@ -1,56 +1,111 @@
package net.cyclestreets.routing;
+import java.util.Locale;
+
public abstract class ElevationFormatter {
public abstract String height(int metres);
+ public abstract String roundedHeight(int metres);
public abstract String distance(int metres);
+ public abstract double roundHeightBelow(int metres);
+ public abstract double roundHeightAbove(int metres);
- static public ElevationFormatter formatter(final String name) {
- if("miles".equals(name))
+ public static ElevationFormatter formatter(final String name) {
+ if ("miles".equals(name))
return imperialFormatter;
return metricFormatter;
- } // formatter
+ }
- static private ElevationFormatter metricFormatter = new MetricFormatter();
- static private ElevationFormatter imperialFormatter = new ImperialFormatter();
+ private static ElevationFormatter metricFormatter = new MetricFormatter();
+ private static ElevationFormatter imperialFormatter = new ImperialFormatter();
- static private class MetricFormatter extends ElevationFormatter {
+ private static class MetricFormatter extends ElevationFormatter {
@Override
public String height(int metres) {
- return String.format("%dm", metres);
- } // height
+ return String.format(Locale.getDefault(), "%dm", metres);
+ }
+
+ @Override
+ public String roundedHeight(int metres) {
+ return height(metres);
+ }
@Override
public String distance(int metres) {
- if(metres < 2000)
- return String.format("%dm", round_distance(metres));
+ if (metres < 1000)
+ return String.format(Locale.getDefault(), "%dm", roundDistance(metres));
+
+ float km = metres / 1000f;
+ if (km < 5)
+ return String.format(Locale.getDefault(), "%.2fkm", km);
+ else if (km < 20)
+ return String.format(Locale.getDefault(), "%.1fkm", km);
+ else
+ return String.format(Locale.getDefault(), "%dkm", (int)km);
+ }
- int km = metres / 1000;
- return String.format("%dkm", km);
- } // distance
- } // class MetricFormatter
+ @Override
+ public double roundHeightBelow(int metres) {
+ return metres - (metres % 100);
+ }
+
+ @Override
+ public double roundHeightAbove(int metres) {
+ return metres + 100 - (metres % 100);
+ }
+ }
static private class ImperialFormatter extends ElevationFormatter {
+ private static final double YARDS_PER_METRE = 1.0936133d;
+ private static final double FEET_PER_METRE = 3.2808399d;
+
@Override
public String height(int metres) {
- int yards = metresToYards(metres);
- return String.format("%d yards", yards);
- } // height
+ int feet = (int)(metres * FEET_PER_METRE);
+ return String.format(Locale.getDefault(), "%d ft", feet);
+ }
+
+ @Override
+ public String roundedHeight(int metres) {
+ // Everything's stored in metres. If we want to show a height in feet, then nearest-100
+ // gridlines calculated from metre values may end up being e.g. 898ft instead of 900ft.
+ // Therefore when labelling graph axes we round (N.B. not floor!) to the nearest 5.
+ int feet = (int)(Math.round(metres * FEET_PER_METRE / 5.0) * 5);
+ return String.format(Locale.getDefault(), "%d ft", feet);
+ }
@Override
public String distance(int metres) {
- int yards = metresToYards(metres);
- if(yards <= 750)
- return String.format("%d yards", round_distance(yards));
- int miles = yards / 1760;
- return String.format("%d miles", miles);
- } // distance
+ int yards = (int)(metres * YARDS_PER_METRE);
+ if (yards <= 750)
+ return String.format(Locale.getDefault(), "%d yards", roundDistance(yards));
+
+ float miles = yards / 1760f;
+ if (miles < 5)
+ return String.format(Locale.getDefault(), "%.2f miles", miles);
+ else if (miles < 20)
+ return String.format(Locale.getDefault(), "%.1f miles", miles);
+ else
+ return String.format(Locale.getDefault(), "%d miles", (int)miles);
+ }
- private int metresToYards(int metres) { return (int)(metres * 1.0936133); }
- } // class MilesFormatter
+ @Override
+ public double roundHeightBelow(int metres) {
+ int feet = (int)(metres * FEET_PER_METRE);
+ feet -= (feet % 100);
+ return feet / FEET_PER_METRE;
+ }
+
+ @Override
+ public double roundHeightAbove(int metres) {
+ int feet = (int)(metres * FEET_PER_METRE);
+ feet += 100 - (feet % 100);
+ return feet / FEET_PER_METRE;
+ }
+ }
- static private int round_distance(int units) {
+ private static int roundDistance(int units) {
return (units < 500) ?
(int)(units/5.0) * 5 :
(int)(units/10.0) * 10;
- } // round_distance
-} // ElevationFormatter
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationProfile.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationProfile.java
index bcd1ccbe7..4611e3c91 100644
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationProfile.java
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ElevationProfile.java
@@ -1,36 +1,55 @@
package net.cyclestreets.routing;
-import java.util.ArrayList;
+import java.util.LinkedList;
import java.util.List;
public class ElevationProfile {
- private List profile_;
- private int min_;
- private int max_;
-
- ElevationProfile() {
- profile_ = new ArrayList<>();
- min_ = Integer.MAX_VALUE;
- max_ = Integer.MIN_VALUE;
- } // ElevationProfile
+ private final LinkedList profile = new LinkedList<>();
+ private int min = Integer.MAX_VALUE;
+ private int max = Integer.MIN_VALUE;
+ private int totalElevationGain = 0;
+ private int totalElevationLoss = 0;
void add(List segmentProfile) {
- int cumulativeDistance = profile_.size() != 0 ? profile_.get(profile_.size() - 1).distance() : 0;
+ int distanceUpToSegmentStart = profile.size() != 0 ? profile.getLast().distance() : 0;
for (Elevation e : segmentProfile) {
- if ((e.distance() == 0) && (cumulativeDistance != 0))
+ if ((e.distance() == 0) && (distanceUpToSegmentStart != 0))
continue;
- profile_.add(new Elevation(e.distance() + cumulativeDistance, e.elevation()));
- min_ = Math.min(min_, e.elevation());
- max_ = Math.max(max_, e.elevation());
- } // append
- } // add
+ addProfileEntry(new Elevation(distanceUpToSegmentStart + e.distance(), e.elevation()));
+ }
+ }
+
+ private void addProfileEntry(Elevation e) {
+ min = Math.min(min, e.elevation());
+ max = Math.max(max, e.elevation());
+
+ if (!profile.isEmpty()) {
+ Elevation last = profile.getLast();
+ int elevationGain = e.elevation() - last.elevation();
+ if (elevationGain > 0) {
+ totalElevationGain += elevationGain;
+ } else {
+ totalElevationLoss -= elevationGain;
+ }
+ }
+
+ profile.add(e);
+ }
public Iterable profile() {
- return profile_;
- } // profile
+ return profile;
+ }
+
+ public int totalElevationGain() {
+ return totalElevationGain;
+ }
+
+ public int totalElevationLoss() {
+ return totalElevationLoss;
+ }
- public int minimum() { return min_; }
- public int maximum() { return max_; }
-} // Elevationprofile
+ public int minimum() { return min; }
+ public int maximum() { return max; }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/FetchCycleStreetsRouteTask.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/FetchCycleStreetsRouteTask.java
deleted file mode 100644
index 090188d12..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/FetchCycleStreetsRouteTask.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package net.cyclestreets.routing;
-
-import net.cyclestreets.view.R;
-import net.cyclestreets.content.RouteData;
-import android.content.Context;
-
-public class FetchCycleStreetsRouteTask extends RoutingTask
-{
- private final String routeType_;
- private final int speed_;
-
- FetchCycleStreetsRouteTask(final String routeType,
- final int speed,
- final Context context)
- {
- super(R.string.fetching_route, context);
- routeType_ = routeType;
- speed_ = speed;
- } // FetchCycleStreetsRouteTask
-
- @Override
- protected RouteData doInBackground(Long... params)
- {
- return fetchRoute(routeType_, params[0], speed_);
- } // doInBackground
-} // class FetchCycleStreetsRouteTask
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/FetchCycleStreetsRouteTask.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/FetchCycleStreetsRouteTask.kt
new file mode 100644
index 000000000..85a13d2dc
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/FetchCycleStreetsRouteTask.kt
@@ -0,0 +1,15 @@
+package net.cyclestreets.routing
+
+import android.content.Context
+
+import net.cyclestreets.content.RouteData
+import net.cyclestreets.view.R
+
+internal class FetchCycleStreetsRouteTask(private val routeType: String,
+ private val speed: Int,
+ context: Context) : RoutingTask(R.string.route_fetching_existing, context) {
+ @Deprecated("Deprecated in Java")
+ override fun doInBackground(vararg params: Long?): RouteData? {
+ return fetchRoute(routeType, params[0]!!, speed)
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Journey.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Journey.java
deleted file mode 100644
index 89248a958..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Journey.java
+++ /dev/null
@@ -1,335 +0,0 @@
-package net.cyclestreets.routing;
-
-import java.util.Iterator;
-import java.util.List;
-import java.util.ArrayList;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.util.Collections;
-
-import org.osmdroid.api.IGeoPoint;
-import org.osmdroid.util.GeoPoint;
-import org.xml.sax.Attributes;
-import org.xml.sax.ContentHandler;
-
-import android.sax.Element;
-import android.sax.EndElementListener;
-import android.sax.RootElement;
-import android.sax.StartElementListener;
-import android.util.Xml;
-
-public class Journey
-{
- private Waypoints waypoints_;
- private Segments segments_;
- private ElevationProfile elevations_;
- private int activeSegment_;
-
- static public final Journey NULL_JOURNEY;
- static {
- NULL_JOURNEY = new Journey();
- NULL_JOURNEY.activeSegment_ = -1;
- }
-
- private Journey()
- {
- waypoints_ = new Waypoints();
- segments_ = new Segments();
- activeSegment_ = 0;
- elevations_ = new ElevationProfile();
- } // PlannedRoute
-
- private Journey(final Waypoints waypoints)
- {
- this();
- if(waypoints != null)
- waypoints_ = waypoints;
- } // Journey
-
- public boolean isEmpty() { return segments_.isEmpty(); }
- public Segments segments() { return segments_; }
- public ElevationProfile elevation() { return elevations_; }
-
- private Segment.Start s() { return segments_.first(); }
- private Segment.End e() { return segments_.last(); }
-
- public Waypoints waypoints() { return waypoints_; }
-
- public String url() { return "http://cycle.st/j" + itinerary(); }
- public int itinerary() { return s().itinerary(); }
- public String name() { return s().name(); }
- public String plan() { return s().plan(); }
- public int speed() { return s().speed(); }
- public int total_distance() { return e().total_distance(); }
-
- /////////////////////////////////////////
- public void setActiveSegmentIndex(int index) { activeSegment_ = index; }
- public void setActiveSegment(final Segment seg)
- {
- for(int i = 0; i != segments_.count(); ++i)
- if(seg == segments_.get(i))
- {
- setActiveSegmentIndex(i);
- break;
- }
- } // setActiveSegment
- public int activeSegmentIndex() { return activeSegment_; }
-
- public Segment activeSegment() { return activeSegment_ >= 0 ? segments_.get(activeSegment_) : null; }
- public Segment nextSegment()
- {
- if(atEnd())
- return activeSegment();
- return segments_.get(activeSegment_+1);
- } // nextSegment
-
- public boolean atStart() { return activeSegment_ <= 0; }
- public boolean atWaypoint() { return activeSegment() instanceof Segment.Waymark; }
- public boolean atEnd() { return activeSegment_ == segments_.count()-1; }
-
- public void regressActiveSegment()
- {
- if(!atStart())
- --activeSegment_;
- } // regressActiveSegment
- public void advanceActiveSegment()
- {
- if(!atEnd())
- ++activeSegment_;
- } // advanceActiveSegment
-
- public Iterator points()
- {
- return segments_.pointsIterator();
- } // points
-
- ////////////////////////////////////////////////////////////////
- static private IGeoPoint pD(final IGeoPoint a1, final IGeoPoint a2)
- {
- return a1 != null ? a1 : a2;
- } // pD
-
- static Journey loadFromXml(final String xml,
- final Waypoints points,
- final String name)
- throws Exception
- {
- final JourneyFactory factory = factory(points, name);
-
- try {
- Xml.parse(xml, factory.contentHandler());
- } // try
- catch(final Exception e) {
- throw new RuntimeException(e);
- } // catch
-
- return factory.get();
- } // loadFromXml
-
- ////////////////////////////////////////////////////////////////////////////////
- /*
-As at 16 October 2012
-
-
-
-
-
-
-
-
-
-
-
-
- */
-
- static private JourneyFactory factory(final Waypoints waypoints,
- final String name)
- {
- return new JourneyFactory(waypoints, name);
- } // factory
-
- static private class JourneyFactory
- {
- private final Journey journey_;
- private final String name_;
- private int total_time = 0;
- private int total_distance = 0;
- private int itinerary_ = 0;
- private int grammesCO2saved_ = 0;
- private int calories_ = 0;
- private String plan_;
- private int speed_;
- private String start_;
- private String finish_;
- private int leg_ = 1;
-
- public JourneyFactory(final Waypoints waypoints,
- final String name)
- {
- journey_ = new Journey(waypoints);
- name_ = name;
- } // JourneyFactory
-
- private ContentHandler contentHandler()
- {
- Segment.formatter = DistanceFormatter.formatter(CycleStreetsPreferences.units());
-
- final RootElement root = new RootElement("markers");
- final Element marker = root.getChild("marker");
- marker.setStartElementListener(new StartElementListener() {
- @Override
- public void start(final Attributes attr)
- {
- final String type = s(attr, "type");
- final String name = s(attr, "name");
-
- if(type.equals("segment"))
- {
- final String packedPoints = s(attr, "points");
-
- final String turn = s(attr, "turn");
-
- final int distance = i(attr, "distance");
- final int time = i(attr, "time");
- final boolean shouldWalk = "1".equals(s(attr, "walk"));
- final int currentLeg = i(attr, "legNumber");
-
- final List points = pointsList(packedPoints);
-
- if(currentLeg != leg_)
- {
- journey_.segments_.add(new Segment.Waymark(leg_, total_distance, points.get(0)));
- leg_ = currentLeg;
- } // if ...
-
- total_time += time;
- total_distance += distance;
- final Segment seg = new Segment.Step(name,
- turn,
- shouldWalk,
- total_time,
- distance,
- total_distance,
- points);
- journey_.segments_.add(seg);
-
- final String distances = s(attr, "distances");
- final String elevations = s(attr, "elevations");
-
- List segmentProfile = elevationsList(distances, elevations);
- journey_.elevations_.add(segmentProfile);
- } // if ...
- if(type.equals("route"))
- {
- grammesCO2saved_ = i(attr, "grammesCO2saved");
- calories_ = i(attr, "calories");
- plan_ = s(attr, "plan");
- speed_ = i(attr, "speed");
- itinerary_ = i(attr, "itinerary");
- start_ = s(attr, "name");
- finish_ = s(attr, "finish");
- } // if ...
- } // start
-
- private String s(final Attributes attr, final String name) { return attr.getValue(name); }
- private int i(final Attributes attr, final String name)
- {
- final String v = s(attr, name);
- return v != null ? Integer.parseInt(v) : 0;
- } // i
- });
-
- if(journey_.waypoints().count() == 0)
- root.getChild("waypoint").setStartElementListener(new StartElementListener() {
- @Override
- public void start(final Attributes attr)
- {
- final double lat = d(attr, "latitude");
- final double lon = d(attr, "longitude");
-
- journey_.waypoints().add(lat, lon);
- } // start
-
- private double d(final Attributes attr, final String name)
- {
- final String v = attr.getValue(name);
- return v != null ? Double.parseDouble(v) : 0;
- } // i
- });
-
- root.setEndElementListener(new EndElementListener() {
- @Override
- public void end()
- {
- final IGeoPoint from = journey_.waypoints().first();
- final IGeoPoint to = journey_.waypoints().last();
-
- final IGeoPoint pstart = journey_.segments_.startPoint();
- final IGeoPoint pend = journey_.segments_.finishPoint();
- final Segment startSeg = new Segment.Start(itinerary_,
- name_ != null ? name_ : start_,
- plan_,
- speed_,
- total_time,
- total_distance,
- calories_,
- grammesCO2saved_,
- Collections.list(pD(from, pstart), pstart));
- final Segment endSeg = new Segment.End(finish_,
- total_time,
- total_distance,
- Collections.list(pend, pD(to, pend)));
- journey_.segments_.add(startSeg);
- journey_.segments_.add(endSeg);
- } // end
- });
-
- return root.getContentHandler();
- } // contentHandler
-
- public Journey get()
- {
- return journey_;
- } // get
-
- private List pointsList(final String points)
- {
- final List pl = new ArrayList<>();
- final String[] coords = points.split(" ");
- for (final String coord : coords)
- {
- final String[] yx = coord.split(",");
- final GeoPoint p = new GeoPoint(Double.parseDouble(yx[1]), Double.parseDouble(yx[0]));
- pl.add(p);
- } // for ...
- return pl;
- } // points
-
- private List elevationsList(final String distances,
- final String elevations) {
- final List list = new ArrayList<>();
- final String[] dists = distances.split(",");
- final String[] els = elevations.split(",");
- int cumulativeDistance = 0;
- for (int i = 0; i != dists.length; ++i) {
- int distance = Integer.parseInt(dists[i]);
- int elevation = Integer.parseInt(els[i]);
-
- cumulativeDistance += distance;
- list.add(new Elevation(cumulativeDistance, elevation));
- } // for ...
- return list;
- } // elevationsList
- } // class JourneyFactory
-
-} // class Journey
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Journey.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Journey.kt
new file mode 100644
index 000000000..82ad08fb2
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Journey.kt
@@ -0,0 +1,279 @@
+package net.cyclestreets.routing
+
+import android.content.Context
+import java.io.IOException
+
+import android.text.TextUtils
+import android.util.Log
+import com.fasterxml.jackson.databind.DeserializationFeature
+import com.fasterxml.jackson.databind.ObjectMapper
+import com.fasterxml.jackson.databind.module.SimpleModule
+import net.cyclestreets.CycleStreetsPreferences
+import net.cyclestreets.api.POI
+import net.cyclestreets.api.POICategories
+import net.cyclestreets.api.POICategory
+import net.cyclestreets.api.client.dto.poiIcon
+
+import net.cyclestreets.routing.domain.GeoPointDeserializer
+import net.cyclestreets.routing.domain.JourneyDomainObject
+import net.cyclestreets.routing.domain.SegmentDomainObject
+import net.cyclestreets.util.Logging
+import net.cyclestreets.util.Turn
+import org.osmdroid.api.IGeoPoint
+
+private val TAG = Logging.getTag(Journey::class.java)
+
+class Journey private constructor(wp: Waypoints? = null) {
+ val waypoints: Waypoints = wp ?: Waypoints.none()
+ val segments: Segments = Segments()
+ val elevation: ElevationProfile = ElevationProfile()
+ val circularRoutePois = mutableSetOf()
+ private var activeSegment: Int = 0
+
+ companion object {
+ val NULL_JOURNEY: Journey = Journey()
+ init { NULL_JOURNEY.activeSegment = -1 }
+
+ fun loadFromJson(domainJson: String, waypoints: Waypoints?, name: String?, context: Context): Journey {
+ return JourneyFactory(waypoints, name).parse(domainJson, context)
+ }
+ }
+
+ fun isEmpty(): Boolean { return segments.isEmpty() }
+
+ private fun start(): Segment.Start { return segments.first() }
+ private fun end(): Segment.End { return segments.last() }
+
+ fun url(): String { return "https://cycle.st/j" + itinerary() }
+ fun itinerary(): Int { return start().itinerary() }
+ fun name(): String { return start().name() }
+ fun plan(): String { return start().plan() }
+ fun speed(): Int { return start().speed() }
+ fun totalDistance(): Int { return end().totalDistance() }
+ fun totalTime(): Int { return end().totalTime() }
+ fun otherRoutes(): String {return start().otherRoutes()}
+
+ fun remainingDistance(distanceUntilTurn: Int): Int {
+ val actSeg = activeSegment()
+
+ if (actSeg != null && activeSegment > 0) {
+ return (totalDistance() - actSeg.cumulativeDistance + distanceUntilTurn)
+ }
+ // segments[0] is a summary of the whole journey and its Cumulative distance is same as Total distance.
+ // Don't try and calculate remaining distance until actual segment has been found.
+ return totalDistance()
+ }
+
+ fun remainingTime(distanceUntilTurn: Int): Int {
+ val actSeg = activeSegment()
+ val prevSeg = previousSegment()
+ val prevSegCumulativeTime : Int = if (prevSeg != null) prevSeg.cumulativeTime else 0
+ val timeToEndOfSeg : Int
+
+ if (actSeg != null && activeSegment > 0) {
+ // Time to cover whole of active segment
+ val segTime = actSeg.cumulativeTime - prevSegCumulativeTime
+
+ if (actSeg.distance != 0) {
+ // This is an approximation of the time to the end of the active segment from current location
+ timeToEndOfSeg = ((segTime * distanceUntilTurn).toFloat() / actSeg.distance.toFloat()).toInt()
+ }
+ else {
+ timeToEndOfSeg = 0
+ }
+
+ return (totalTime() - actSeg.cumulativeTime + timeToEndOfSeg)
+ }
+ // Don't try and calculate remaining time until actual segment has been found
+ return (totalTime())
+ }
+
+ /////////////////////////////////////////
+ fun setActiveSegmentIndex(index: Int) { activeSegment = index }
+ fun setActiveSegment(seg: Segment) {
+ for (i in 0 until segments.count())
+ if (seg === segments[i]) {
+ setActiveSegmentIndex(i)
+ break
+ }
+ }
+
+ fun activeSegmentIndex(): Int { return activeSegment }
+
+ fun previousSegment(): Segment? {
+ return if (activeSegment > 1) segments[activeSegment - 1] else null
+ }
+ fun activeSegment(): Segment? {
+ return if (activeSegment >= 0) segments[activeSegment] else null
+ }
+ fun nextSegment(): Segment? {
+ return if (atEnd()) activeSegment() else segments[activeSegment + 1]
+ }
+
+ fun atStart(): Boolean { return activeSegment <= 0 }
+ fun atWaypoint(): Boolean { return activeSegment() is Segment.Waymark }
+ fun atEnd(): Boolean { return activeSegment == segments.count() - 1 }
+
+ fun regressActiveSegment() {
+ if (!atStart())
+ --activeSegment
+ }
+ fun advanceActiveSegment() {
+ if (!atEnd())
+ ++activeSegment
+ }
+
+ fun points(): Iterator {
+ return segments.pointsIterator()
+ }
+
+ ////////////////////////////////////////////////////////////////////////////////
+ private class JourneyFactory internal constructor(waypoints: Waypoints?, private val name: String?) {
+ private val objectMapper = ObjectMapper()
+ private val journey: Journey = Journey(waypoints)
+
+ // Variables to maintain state as we process the JourneyDomainObject
+ private var leg = 1
+ private var totalDistance = 0
+ private var totalTime = 0
+
+ init {
+ val module = SimpleModule()
+ module.addDeserializer(IGeoPoint::class.java, GeoPointDeserializer())
+ objectMapper.registerModule(module)
+ objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
+ }
+
+ internal fun parse(domainJson: String, context: Context): Journey {
+ // I guess this is in case the units have changed without the app restarting
+ Segment.formatter = DistanceFormatter.formatter(CycleStreetsPreferences.units())
+
+ val jdo: JourneyDomainObject
+ try {
+ jdo = objectMapper.readValue(domainJson, JourneyDomainObject::class.java)
+ } catch (e: IOException) {
+ throw RuntimeException("Coding error - unable to parse domain JSON", e)
+ }
+
+
+ populateWaypoints(jdo)
+ populateSegments(jdo)
+ // Currently (June 2021) the API returns POIs whether or not they were requested.
+ // https://github.com/cyclestreets/android/issues/465#issuecomment-818049555
+ // For simplicity's sake, Will display these, even if not requested,
+ // because, in the event of opening a circular route by number
+ // there is no way of telling whether POIs were requested originally
+ populatePois(jdo, context)
+
+ generateStartAndFinishSegments(jdo)
+
+ return journey
+ }
+
+ private fun populateWaypoints(jdo: JourneyDomainObject) {
+ // waypoints will have the points tapped on the screen. But if we are retrieving an existing route
+ // (e.g. by number) then it will be empty, so needs to be populated from the json - the route
+ // retrieved from the server.
+ if (journey.waypoints.count() == 0) {
+ for (gp in jdo.waypoints) {
+ journey.waypoints.add(gp)
+ }
+ }
+ }
+
+ private fun populateSegments(jdo: JourneyDomainObject) {
+ for (sdo: SegmentDomainObject in jdo.segments) {
+ if (sdo.legNumber != leg) {
+ journey.segments.add(Segment.Waymark(leg, totalDistance, sdo.points[0]))
+ leg = sdo.legNumber
+ }
+
+ totalTime += sdo.time
+ totalDistance += sdo.distance
+ journey.segments.add(
+ // Format time for display in Itinerary
+ Segment.Step(
+ getStreetName(sdo.name),
+ sdo.legNumber,
+ Turn.turnFor(sdo.turn),
+ sdo.turn,
+ sdo.shouldWalk,
+ totalTime,
+ sdo.distance,
+ totalDistance,
+ sdo.points
+ )
+ )
+ journey.elevation.add(sdo.segmentProfile)
+ }
+ }
+ // For circular routes.
+ private fun populatePois(jdo: JourneyDomainObject, context: Context) {
+ // For circular route pois, the id isn't returned in the API, so will create an artificial one.
+ var id = 0
+ for (poi in jdo.pois) {
+ val circularRoutePoi = POI(id.toString(),
+ poi.name,
+ "",
+ poi.website,
+ "",
+ "",
+ poi.latitude.toDouble(),
+ poi.longitude.toDouble())
+ // Shouldn't ever have a poi which doesn't have a type,
+ // but if it does happen, don't add it to the list to be displayed:
+ val type = poi.poitypeId
+ if (type != null) {
+ circularRoutePoi.setCategory(POICategory(type, "", poiIcon(context, type)))
+ journey.circularRoutePois.add(circularRoutePoi)
+ }
+ id++
+ }
+ }
+
+ private fun getStreetName(name: String): String {
+ // If "Link" appears three or more times, abbreviate! (https://github.com/cyclestreets/android/issues/305)
+ val split = name.split(", Link")
+ val numberOfLinks = split.size + 1
+ return if (numberOfLinks >= 3) {
+ val abbreviatedName = "${split.first()} and other streets"
+ Log.d(TAG, "Abbreviating long street name $name to $abbreviatedName")
+ abbreviatedName
+ } else name
+ }
+
+ private fun generateStartAndFinishSegments(jdo: JourneyDomainObject) {
+ val from = journey.waypoints.first()
+ val to = if (journey.waypoints.isEmpty()) null else journey.waypoints.last()
+
+ val pStart = journey.segments.startPoint()
+ val pEnd = journey.segments.finishPoint()
+
+ val startSeg = Segment.Start(
+ jdo.route.itinerary,
+ if (TextUtils.isEmpty(name)) jdo.route.name else name,
+ jdo.route.plan,
+ jdo.route.speed,
+ totalTime,
+ totalDistance,
+ jdo.route.calories,
+ jdo.route.grammesCO2saved,
+ jdo.route.otherRoutes,
+ listOf(pD(from, pStart), pStart)
+ )
+ val endSeg = Segment.End(
+ jdo.route.finish,
+ totalTime,
+ totalDistance,
+ listOf(pEnd, pD(to, pEnd))
+ )
+
+ journey.segments.add(startSeg)
+ journey.segments.add(endSeg)
+ }
+
+ private fun pD(a1: IGeoPoint?, a2: IGeoPoint): IGeoPoint {
+ return a1 ?: a2
+ }
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/LiveRideReplanRoutingTask.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/LiveRideReplanRoutingTask.kt
new file mode 100644
index 000000000..bef8c1478
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/LiveRideReplanRoutingTask.kt
@@ -0,0 +1,16 @@
+package net.cyclestreets.routing
+
+import android.content.Context
+
+import net.cyclestreets.content.RouteData
+
+internal class LiveRideReplanRoutingTask(routeType: String,
+ speed: Int,
+ context: Context) : CycleStreetsRoutingTask(routeType, speed, context, saveRoute = false) {
+ @Deprecated("Deprecated in Java")
+ override fun onPostExecute(route: RouteData?) {
+ super.onPostExecute(route)
+ if (route != null)
+ Route.waypoints().firstWaypointEphemeral = true
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ReplanRoutingTask.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ReplanRoutingTask.java
deleted file mode 100644
index ab84d0df2..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ReplanRoutingTask.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package net.cyclestreets.routing;
-
-import android.content.Context;
-import net.cyclestreets.view.R;
-import net.cyclestreets.content.RouteData;
-import net.cyclestreets.content.RouteDatabase;
-
-public class ReplanRoutingTask
- extends RoutingTask
-{
- private final RouteDatabase db_;
- private final String newPlan_;
-
- ReplanRoutingTask(final String newPlan,
- final RouteDatabase db,
- final Context context)
- {
- super(R.string.loading_route, context);
- db_ = db;
- newPlan_ = newPlan;
- } // ReplanRouteTask
-
- @Override
- protected RouteData doInBackground(Journey... params)
- {
- final Journey pr = params[0];
- final RouteData rd = db_.route(pr.itinerary(), newPlan_);
- if(rd != null)
- return rd;
-
- publishProgress(R.string.finding_route);
- return fetchRoute(newPlan_, pr.itinerary(), 0, pr.waypoints());
- } // doInBackground
-} // class ReplanRoutingTask
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ReplanRoutingTask.kt b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ReplanRoutingTask.kt
new file mode 100644
index 000000000..35d680fe4
--- /dev/null
+++ b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/ReplanRoutingTask.kt
@@ -0,0 +1,22 @@
+package net.cyclestreets.routing
+
+import android.content.Context
+
+import net.cyclestreets.content.RouteData
+import net.cyclestreets.content.RouteDatabase
+import net.cyclestreets.view.R
+
+internal class ReplanRoutingTask(private val newPlan: String,
+ private val db: RouteDatabase,
+ context: Context) : RoutingTask(R.string.route_loading, context) {
+ @Deprecated("Deprecated in Java")
+ override fun doInBackground(vararg params: Journey): RouteData? {
+ val pr = params[0]
+ val rd = db.route(pr.itinerary(), newPlan)
+ if (rd != null)
+ return rd
+
+ publishProgress(R.string.route_finding_new)
+ return fetchRoute(newPlan, pr.itinerary().toLong(), 0, pr.waypoints)
+ }
+}
diff --git a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Route.java b/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Route.java
deleted file mode 100644
index 633224676..000000000
--- a/libraries/cyclestreets-view/src/main/java/net/cyclestreets/routing/Route.java
+++ /dev/null
@@ -1,199 +0,0 @@
-package net.cyclestreets.routing;
-
-import java.util.ArrayList;
-import java.util.List;
-
-
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.widget.Toast;
-
-import net.cyclestreets.CycleStreetsPreferences;
-import net.cyclestreets.view.R;
-import net.cyclestreets.content.RouteData;
-import net.cyclestreets.content.RouteDatabase;
-import net.cyclestreets.content.RouteSummary;
-
-public class Route
-{
- public interface Listener {
- void onNewJourney(final Journey journey, final Waypoints waypoints);
- void onResetJourney();
- } // Listener
-
- private static class Listeners {
- private List listeners_ = new ArrayList<>();
-
- public void register(final Listener listener) {
- if(!doRegister(listener))
- return;
-
- if((Route.journey() != Journey.NULL_JOURNEY) || (Route.waypoints() != Waypoints.NULL_WAYPOINTS))
- listener.onNewJourney(Route.journey(), Route.waypoints());
- else
- listener.onResetJourney();
- } // registerListener
-
- public void softRegister(final Listener listener)
- {
- doRegister(listener);
- } // softRegister
-
- private boolean doRegister(final Listener listener) {
- if(listeners_.contains(listener))
- return false;
- listeners_.add(listener);
- return true;
- } // doRegister
- public void unregister(final Listener listener) {
- listeners_.remove(listener);
- } // unregisterListener
-
- public void onNewJourney(final Journey journey, final Waypoints waypoints) {
- for(final Listener l : listeners_)
- l.onNewJourney(journey, waypoints);
- } // onNewJourney
-
- public void onReset() {
- for(final Listener l : listeners_)
- l.onResetJourney();
- } // onReset
- } // Listeners
-
- private static final Listeners listeners_ = new Listeners();
-
- public static void registerListener(final Listener l) { listeners_.register(l); }
- public static void softRegisterListener(final Listener l) { listeners_.softRegister(l); }
- public static void unregisterListener(final Listener l) { listeners_.unregister(l); }
-
- public static void PlotRoute(final String plan,
- final int speed,
- final Context context,
- final Waypoints waypoints) {
- final CycleStreetsRoutingTask query = new CycleStreetsRoutingTask(plan, speed, context);
- query.execute(waypoints);
- } // PlotRoute
-
- public static void FetchRoute(final String plan,
- final long itinerary,
- final int speed,
- final Context context) {
- final FetchCycleStreetsRouteTask query = new FetchCycleStreetsRouteTask(plan, speed, context);
- query.execute(itinerary);
- } // FetchRoute
-
- public static void RePlotRoute(final String plan,
- final Context context) {
- final ReplanRoutingTask query = new ReplanRoutingTask(plan, db_, context);
- query.execute(plannedRoute_);
- } // PlotRoute
-
- public static void PlotStoredRoute(final int localId,
- final Context context) {
- final StoredRoutingTask query = new StoredRoutingTask(db_, context);
- query.execute(localId);
- } // PlotRoute
-
- public static void RenameRoute(final int localId, final String newName) {
- db_.renameRoute(localId, newName);
- } // RenameRoute
-
- public static void DeleteRoute(final int localId) {
- db_.deleteRoute(localId);
- } // DeleteRoute
-
- /////////////////////////////////////////
- private static Journey plannedRoute_ = Journey.NULL_JOURNEY;
- private static Waypoints waypoints_ = plannedRoute_.waypoints();
- private static RouteDatabase db_;
- private static Context context_;
-
- public static void initialise(final Context context) {
- context_ = context;
- db_ = new RouteDatabase(context);
-
- if (isLoaded())
- loadLastJourney();
- } // initialise
-
- public static void setWaypoints(final Waypoints waypoints) {
- waypoints_ = waypoints;
- } // setTerminals
-
- public static void resetJourney() {
- onNewJourney(null);
- } // resetJourney
-
- public static void onResume() {
- Segment.formatter = DistanceFormatter.formatter(CycleStreetsPreferences.units());
- } // onResult
-
- /////////////////////////////////////
- public static int storedCount() {
- return db_.routeCount();
- } // storedCount
-
- public static List