From 9652fc2f56ac807f7e7554dff9115d5d31712b48 Mon Sep 17 00:00:00 2001 From: ClemensF Date: Tue, 3 Jul 2012 18:03:56 +0200 Subject: [PATCH] Replaced local file caching by persistent ObjectCache based on FileDb. Removed IsCached and ImageType properties from TileLayer. --- Caches/FileDbCache/FileDb/FileDb.XML | 1560 +++++++++++++++++ Caches/FileDbCache/FileDb/FileDb.dll | Bin 0 -> 70144 bytes Caches/FileDbCache/FileDb/FileDb.txt | 5 + Caches/FileDbCache/FileDbCache.cs | 444 +++++ Caches/FileDbCache/FileDbCache.csproj | 58 + Caches/FileDbCache/Properties/AssemblyInfo.cs | 36 + MapControl.sln | 12 + MapControl/Tile.cs | 6 - MapControl/TileImageLoader.cs | 272 +-- MapControl/TileLayer.cs | 8 +- TestApplication/MainWindow.xaml | 28 +- TestApplication/MainWindow.xaml.cs | 11 + TestApplication/TestApplication.csproj | 5 + 13 files changed, 2297 insertions(+), 148 deletions(-) create mode 100644 Caches/FileDbCache/FileDb/FileDb.XML create mode 100644 Caches/FileDbCache/FileDb/FileDb.dll create mode 100644 Caches/FileDbCache/FileDb/FileDb.txt create mode 100644 Caches/FileDbCache/FileDbCache.cs create mode 100644 Caches/FileDbCache/FileDbCache.csproj create mode 100644 Caches/FileDbCache/Properties/AssemblyInfo.cs diff --git a/Caches/FileDbCache/FileDb/FileDb.XML b/Caches/FileDbCache/FileDb/FileDb.XML new file mode 100644 index 00000000..47c4a542 --- /dev/null +++ b/Caches/FileDbCache/FileDb/FileDb.XML @@ -0,0 +1,1560 @@ + + + + FileDb + + + + + Represents an open FileDb database file. None of the FileDb classes are re-entrant - + access to the class objects must be syncronised by the calling application. + + + + + + Return a List of custom objects filtered by the filter parameter. + + A FilterExpression representing the desired filter. + A new List of custom objects with the requested Records + + + + + Return a List of custom objects filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A new List of custom objects with the requested Records and Fields + + + + + Return a List of custom objects filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new List of custom objects with the requested Records and Fields ordered by the specified fields. + + + + + Get all records matching the search expression in the indicated order, if any. + + Represents a single search expression, such as ID = 3 + The list of fields to return or null for all fields + If true, an additional Field named "index" will be returned + which is the ordinal index of the Record in the database, which can be used in + GetRecordByIndex and UpdateRecordByIndex. + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new List of custom objects with the requested Records and Fields + + + + + Return a List of custom objects filtered by the filter parameter. + + A FilterExpressionGroup representing the desired filter. + A new List of custom objects with the requested Records + + + + + Return a List of custom objects filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A new List of custom objects with the requested Records and Fields + + + + + Return a List of custom objects filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new List of custom objects with the requested Records and Fields in the specified order + + + + + Get all records matching the FilterExpressionGroup in the indicated order, if any. + + Represents a compound search expression, such as FirstName = "John" AND LastName = "Smith" + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new List of custom objects with the requested Records and Fields + + + + + Return a List of custom objects filtered by the filter parameter. + + A string representing the desired filter, eg. LastName = 'Fuller' + A new List of custom objects with the requested Records + + + + + Return a List of custom objects filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + A new List of custom objects with the requested Records and Fields + + + + + Return a List of custom objects filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new List of custom objects with the requested Records and Fields ordered by the specified list + + + + + Return a List of custom objects filtered by the filter parameter. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + If true, an additional Field named "index" will be returned + which is the ordinal index of the Record in the database, which can be used in + GetRecordByIndex and UpdateRecordByIndex + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order + A new List of custom objects with the requested Records and Fields + + + + + Return all records in the database (table). + + A table containing all Records and Fields. + + + + + Return all records in the database (table). + + The list of Fields to return or null for all Fields + A table containing all rows. + + + + + Return all records in the database (table). + + The list of fields to return or null for all Fields + A list of one or more fields to order the returned table by, + or null for default order + A table containing all rows. + + + + + Return all records in the database (table). + + Specify whether to include the Record index as one of the Fields + A table containing all rows. + + + + + Return all records in the database (table). + + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A list of one or more fields to order the returned table by, + or null for default order + A table containing all Records and the specified Fields. + + + + + Returns a single custom object at the current location. Meant to be used ONLY in conjunction + with the MoveFirst/MoveNext methods. + + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A new object or null + + + + + + Returns a single custom object specified by the index. + + The index of the record to return. This value can be obtained from + Record returning queries by specifying true for the includeIndex parameter. + The list of fields to return or null for all fields + A new object or null + + + + + Returns a single custom object specified by the primary key value or record number. + + The primary key value. For databases without a primary key, + 'key' is the zero-based record number in the table. + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A new object or null + + + + + Open the indicated database file. + + The filename of the database file to open. It can be a fully qualified + path or, if no path is specified the current folder will be used. + + + + + Open the indicated database file for encryption. Encryption is "all or nothing", + meaning all records are either encrypted or not. + + The filename of the database file to open. It can be a fully qualified + path or, if no path is specified the current folder will be used. + A string value to use as the encryption key + + + + + Close an open database. + + + + + + Create a new database file. If the file exists, it will be overwritten. + + The full pathname of the file. + Array of Fields for the new database. + + + + + Delete an existing database. + + The pathname of the file to delete. + + + + + Add a new record to the database using the name-value pairs in the FieldValues object. + Note that not all fields must be represented. Missing fields will be set to default + values (0, empty or null). Note that only Array datatypes can NULL. + + The name-value pairs to add. + The volatile index of the newly added record. + + + + + Return a Table of Records filtered by the filter parameter. + + A FilterExpression representing the desired filter. + A new Table with the requested Records + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields ordered by the specified fields. + + + + + Get all records matching the search expression in the indicated order, if any. + + Represents a single search expression, such as ID = 3 + The list of fields to return or null for all fields + If true, an additional Field named "index" will be returned + which is the ordinal index of the Record in the database, which can be used in + GetRecordByIndex and UpdateRecordByIndex. + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. + + A FilterExpressionGroup representing the desired filter. + A new Table with the requested Records + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields in the specified order + + + + + Get all records matching the FilterExpressionGroup in the indicated order, if any. + + Represents a compound search expression, such as FirstName = "John" AND LastName = "Smith" + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. + + A string representing the desired filter, eg. LastName = 'Fuller' + A new Table with the requested Records + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields ordered by the specified list + + + + + Return a Table of Records filtered by the filter parameter. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + If true, an additional Field named "index" will be returned + which is the ordinal index of the Record in the database, which can be used in + GetRecordByIndex and UpdateRecordByIndex + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order + A new Table with the requested Records and Fields + + + + + Return all records in the database (table). + + A table containing all Records and Fields. + + + + + Return all records in the database (table). + + The list of Fields to return or null for all Fields + A table containing all rows. + + + + + Return all records in the database (table). + + The list of fields to return or null for all Fields + A list of one or more fields to order the returned table by, + or null for default order + A table containing all rows. + + + + + Return all records in the database (table). + + Specify whether to include the Record index as one of the Fields + A table containing all rows. + + + + + Return all records in the database (table). + + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A list of one or more fields to order the returned table by, + or null for default order + A table containing all Records and the specified Fields. + + + + + Returns a single Record object at the current location. Meant to be used ONLY in conjunction + with the MoveFirst/MoveNext methods. + + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A Record object or null + + + + + + Returns a single Record object specified by the index. + + The index of the record to return. This value can be obtained from + Record returning queries by specifying true for the includeIndex parameter. + The list of fields to return or null for all fields + A Record object or null + + + + + Returns a single Record object specified by the primary key value or record number. + + The primary key value. For databases without a primary key, + 'key' is the zero-based record number in the table. + The list of fields to return or null for all fields + Specify whether to include the record index as one of the Fields + A Record object or null + + + + + Update the record at the indicated index. To get the index, you would need to first + get a record from the database then use the index field from it. The index is only + valid until a database operation which would invalidate it, such as adding/deleting + a record, or changing the value of a primary key. + + The record values to update + The index of the record to update + + + + + Update the record with the indicated primary key value. + + The record values to update + The primary key value of the record to update + + + + + Update all records which match the search criteria using the values in record. + + The search expression, e.g. ID = 100 + A list of name-value pairs to use to update the matching records + The number of records which were updated. + + + + + Update all records which match the compound search criteria using the values in record. + + The compound search expression, e.g. FirstName = "John" AND LastName = "Smith" + A list of name-value pairs to use to update the matching records + The number of records which were updated. + + + + + Update all records which match the filter expression using the values in record. + + The filter to use, eg. "~LastName = 'peacock' OR ~FirstName = 'nancy'". + This filter string will be parsed using FilterExpressionGroup.Parse. + A list of name-value pairs to use to update the matching records + The number of records which were updated. + + + + + Delete the record at the specified index. You would normally get the index from a previous + query. This index is only valid until a record has been deleted. + + The zero-based index of the record to delete. + true if the record was deleted, false otherwise + + + + + Delete the record with the specified primary key value + + The primary key value of the record to delete + true if the record was deleted, false otherwise + + + + + Delete all records which match the search criteria. + + The search expression, e.g. ID = 100 + The number of records deleted + + + + + Delete all records which match the compound search criteria. + + The compound search expression, e.g. FirstName = "John" AND LastName = "Smith" + The number of records deleted + + + + + Delete all records which match the filter criteria. + + The filter to use, eg. "~LastName = 'peacock' OR ~FirstName = 'nancy'". + This filter string will be parsed using FilterExpressionGroup.Parse. + The number of records deleted + + + + + Move to the first record in the index. Use this in conjunction with MoveNext and GetCurrentRecord + + + + + + Move to the next record in the index. Use this in conjunction with MoveFirst and GetCurrentRecord + + + + + Call this to remove deleted records from the file (compact). + + + + + + Call this to write the index and flush the stream buffer to disk. + Flushing will be done automatically if AutoFlush is On (and only writes the index + if necessary), whereas this call always writes the index. + You can use this to periodically write everything to disk rather than each time + as with AutoFlush. Flush is always called when the file is closed, however in that + case the index is only written if AutoFlush is set to Off. + + + + + + Call this method to reindex the database if your index file should be deleted or corrupted. + + + + + + Allows you to set an encryption key after the database has been opened. You must set + the encryption key before reading or writing to the database. Encryption is "all or nothing", + meaning all records are either encrypted or not. + + A string value to use as the encryption key + + + + + Encrypt a string value. + Not syncronized. + + The key to use for encryption + The value to encrypt + The encrypted value as a string + + + + + Decrypt a string value. + Not syncronized. + + The key to use for decryption + The value to decrypt + The decrypted value as a string + + + + + A string value which can be used to keep track of the database version for changes + + + + + The fields of the database (table). + + + + + + The number of records in the database (table). Doesn't include deleted records. + + + + + + The number of deleted records which not yet cleaned from the file. Call the + Clean method to remove all deleted records and compact the file. + + + + + + Configures autoclean. When an edit or delete is made, the + record is normally not removed from the data file - only the index. + After repeated edits/deletions, the data file may become very big with + deleted (non-removed) records. A cleanup is normally done with the + cleanup() method. Autoclean will do this automatically, keeping the + number of deleted records to under the threshold value. + To turn off autoclean, set threshold to a negative value. + + + + + + Specifies whether to automatically flush data buffers and write the index after each + operation in which the file was updated. When AutoFlush is not On, the index is not + written until the file is closed or Flush is called. + You can set AutoFlush to Off just before performing a bulk operation to dramatically + increase performance, then set it back On after. When you set it back On after it was + Off, everything is flushed immediately because the assumption is that it was needed, + so you don't need to call Flush in this case. + + Setting AutoFlush On is most useful for when you aren't able to guarantee that you will + be able to call Close before the program closes. This way the file won't become corrupt + in this case. + + + + + + Tests to see if a database is currently open. + + + + + + Allow ability to store meta data in the DB file. MetaData must be one of the supported + DataTypes: String, Byte, Int, UInt, Float, Double, Bool, DateTime and also Byte[] + + + + + + Specifies the data type for database Fields + + + + + + Specifies the type of match for FilterExpressions with String data types + + + + + Specifies the comparison operator to use for FilterExpressions + + + + + Boolean operands to use to join FilterExpressions + + + + + Use this class for single field searches. + + + + + + Create a FilterExpression with the indicated values + + The name of the Field to filter on + The Field value to filter on + The Equality operator to use in the value comparison + + + + + Create a FilterExpression with the indicated values + + The name of the Field to filter on + The Field value to filter on + The Equality operator to use in the value comparison + The match type, eg. MatchType.Exact + + + + + Parse the expression string to create a FilterExpressionGroup representing a simple expression. + + The string expression. Example: LastName = 'Fuller' + A new FilterExpression representing the simple expression + + + + + Create a FilterExpression of type "IN". This type is a HashSet of the values which will be used + to filter the query. + + The name of the Field which will be used in the FilterExpressions + A Table to use to build the IN FilterExpressions + The name of the Field in the Table which holds the value to be used to build the IN FilterExpressions + A new FilterExpression + + + + + Create a FilterExpression of type "IN". This type is a HashSet of the values which will be used + to filter the query. + + The name of the Field which will be used in the FilterExpressions + A List of custom objects to use to build the IN FilterExpression + The name of the Property of the custom class which holds the value to be + used to build the IN FilterExpression + A new FilterExpression + + + + + Use this class to group FilterExpression and FilterExpressionGroup to form compound search expressions. + All expressions in the group will be evaluated by the same boolean And/Or operation. Use multiple + FilterExpressionGroups to form any combination of And/Or logic. + + + + + + Parse the expression string to create a FilterExpressionGroup representing a compound expression. + + The string compound expression. Example: (FirstName ~= 'andrew' OR FirstName ~= 'nancy') AND LastName = 'Fuller' + A new FilterExpressionGroup representing the compound expression + + + + + Constructor + + + + + Open the database files + + + + + + + ---------------------------------------------------------------------------------------- + + record must have all fields + + + + + ---------------------------------------------------------------------------------------- + + record must have all fields + + + + + + Configures autoclean. When an edit or delete is made, the + record is normally not removed from the data file - only the index. + After repeated edits/deletions, the data file may become very big with + deleted (non-removed) records. A cleanup is normally done with the + cleanup() method. Autoclean will do this automatically, keeping the + number of deleted records to under the threshold value. + To turn off autoclean, set threshold to a negative value. + + number of deleted records to have at any one time + + + + ---------------------------------------------------------------------------------------- + + Read all records to create new index. + + + + + + Remove all deleted records + + + + + + Removes an entry from the database INDEX only - it appears + deleted, but the actual data is only removed from the file when a + cleanup() is called. + + Int32 or string primary key used to identify record to remove. For + databases without primary keys, it is the record number (zero based) in + the table. + true if a record was removed, false otherwise + + + + + Removes an entry from the database INDEX only - it appears + deleted, but the actual data is only removed from the file when a + cleanup() is called. + + The record number (zero based) in the table to remove + true on success, false otherwise + + + + + Removes entries from the database INDEX only, based on the + result of a regular expression match on a given field - records appear + deleted, but the actual data is only removed from the file when a + cleanup() is called. + + + number of records removed + + + + move to the first index position + + + + + + Move the current index position to the next database item. + + true if advanced to a new item, false if there are none left + + + + + Return the current record in the database. Note that the current iterator pointer is not moved in any way. + + + + + + + retrieves a record based on the specified key + + primary key used to identify record to retrieve. For + databases without primary keys, it is the record number (zero based) in + the table. + + if true, an extra field called 'IFIELD' will + be added to each record returned. It will contain an Int32 that specifies + the original position in the database (zero based) that the record is + positioned. It might be useful when an orderby is used, and a future + operation on a record is required, given it's index in the table. + record if found, or false otherwise + + + + + retrieves a record based on the record number in the table + (zero based) + + zero based record number to retrieve + + + + + + + + Searches the database for an item, and returns true if found, false otherwise. + + rimary key of record to search for, or the record + number (zero based) for databases without a primary key + true if found, false otherwise + + + + + Returns the number of records in the database + + the number of records in the database + + + + + Returns the number of deleted records in the database, that would be removed if cleanup() is called. + + the number of deleted records in the database + + + + + Returns the current database schema in the same form + as that used in the parameter for the create(...) method. + + + + + + + Flush the in-memory buffers to disk + + + + + + Helper + + + + + + + + Helper + + + + + + + + Use this version if you will need to add/remove indices + + + + + + function to write the index values. We assume the + database has been locked before calling this function. + + + + + + + + + + + to keep track of null fields in written record + + + + + + Use this version only if all of the fields are present and in the correct order in the array + + + + + + + + + Use this version only if all of the fields are present and in the correct order in the array + + + + + + + Write a single field to the file + + + + + + + + ------------------------------------------------------------------------------ + + Private function to perform a binary search + + file offsets into the .dat file, it must be ordered + by primary key. + the left most index to start searching from + the right most index to start searching from + the search target we're looking for + -[insert pos+1] when not found, or the array index+1 + when found. Note that we don't return the normal position, because we + can't differentiate between -0 and +0. + + + + + Helper + + + + + + + + Private function to read a record from the database + + + + + + size does not include the 4 byte record length, but only the total size of the fields + + + + + + + + + function to read a record KEY from the database. Note + that this function relies on the fact that they key is ALWAYS the first + item in the database record as stored on disk. + + + + + + + + + Reads a data type from a file. Note that arrays can only + consist of other arrays, ints, and strings. + + + + + + + + + Write the database schema and other meta information. + + + + + + + Helper + + + + + + + + Summary description for HexEncoding. + + + + + Creates a byte array from the hexadecimal string. Each two characters are combined + to create one byte. First two hexadecimal characters become first byte in returned array. + Non-hexadecimal characters are ignored. + + string to convert to byte array + number of characters in string ignored + byte array, in the same left-to-right order as the hexString + + + + Determines if given string is in proper hexadecimal string format + + + + + + + Returns true is c is a hexadecimal digit (A-F, a-f, 0-9) + + Character to test + true if hex digit, false if not + + + + Converts 1 or 2 character string into equivalant byte value + + 1 or 2 character string + byte + + + + Represents a column of the database (table). + + + + + Use this constructor when creating a new database. The ordinal index of the field will be the + order it was added to the field list, unless a primary key field is specified and wasn't the + first in the list. In this case the primary key field will be moved to the first in the list + so that its always first. + + The name of the field + The data type of the field + + + + + Use this constructor when you need to create a Records list manually rather than + using one retured from a query. In this case, you must set the field ordinal index + for each field, starting at zero. + + The name of the field + The data type of the field + The zero-based ordinal index of the field + + + + + The name of the field. + + + + + The type of the field. + + + + + The zero-based ordinal index of the field. + + + + + Indicates if this is the one and only primary key field + + + + + Indicate if this is an Array type field + + + + + Used for auto-increment fields. Set to the number which you want incrementing to begin. + Set to -1 if not an auto-increment field. + + + + + Returns true if this is an auto-increment field, false otherwise + + + + + Comment for the field + + + + + User property to associate a value with this Field + + + + + Represents data for a row, a Record consists of name-value pairs. + Used when adding data to the database and by the Record object + to store data returned from queries. + + + + + Represents a single row returned from a query. It will + contain one or more fields of the database (table). The last column may be the index of the row (if requested), + which is the zero-based position of the record in the index. If there is no primary key specified for + the database (table), then the index is the record number in the order in which the row was added to the database. + + + + + + Create a Record object with the indicated Fields and values. If creating a list of Record objects + (for a Records list) be sure to use the same Fields list for each Record. + + + + + + + + Tests to see if the indicated field is in this Record + + + true if the field is in this Record + + + + + Return the integer field value. + + The name of the field + The integer field value + + + + + Return the integer field value. + + The ordinal index of the field + The integer field value + + + + + Return the unsigned integer field value. + + The name of the field + The unsigned integer field value + + + + + Return the unsigned integer field value. + + The ordinal index of the field + The unsigned integer field value + + + + + Return the String field value. + + The name of the field + The String field value + + + + + Return the String field value. + + The ordinal index of the field + The String field value + + + + + Return the Byte field value. + + The name of the field + The Byte field value + + + + + Return the Byte field value. + + The ordinal index of the field + The Byte field value + + + + + Return the Single field value. + + The name of the field + The Single field value + + + + + Return the Single field value. + + The ordinal index of the field + The Single field value + + + + + Return the Double field value. + + The name of the field + The Double field value + + + + + Return the Double field value. + + The ordinal index of the field + The Double field value + + + + + Return the Boolean field value. + + The name of the field + The Boolean field value + + + + + Return the Boolean field value. + + The ordinal index of the field + The Boolean field value + + + + + Return the DateTime field value. + + The name of the field + The DateTime field value + + + + + Return the DateTime field value. + + The ordinal index of the field + The DateTime field value + + + + + The number of fields in the Record + + + + + + A property which is used for integrating with the binding framework. + + + + + + Used by the Record class for enumerating in foreach contructs + + + + + + A List of Records + + + + + + A list of Fields + + + + + + Represents a data table returned from a query. A table is made up of Fields and Records. + + + + + + Create a table with the indicated Fields and records. If copyFields is true, a new + Fields list is created and a copy of each field is made and its ordinal adjusted. + Otherwise the original Fields object is adopted. You should pass false for copyFields + only if you created the Fields list and its Field objects yourself. + + The Fields list to use + The record data + Indicates whether to make a copy of the Fields object and each Field. + + + + + Create a table with the indicated Fields and records. If copyFields is true, a new + Fields list is created and a copy of each field is made and its ordinal adjusted. + Otherwise the original Fields object is adopted. You should pass false for copyFields + only if you created the Fields list and its Field objects yourself and the Fields in + the Record objects match the data in the Record. + + The Fields list to use + The record data + Indicates whether to make a copy of the Fields object and each Field. + + + + + Create a new Record with all of the fields of the Table. The Record is not + added to the Table. To do this, call Records.Add. + + + + + + + Save this Table to the indicated file as a new database. If the file exists + it will be overwritten. The new database will be just as if you had created + it from scratch. + + The full path and filename of the new database + + + + + Return a Table of Records filtered by the filter parameter. + + A string representing the desired filter, eg. LastName = 'Fuller' + A new Table with the requested Records + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A string representing the desired filter, eg. LastName = 'Fuller' + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields ordered by the specified list + + + + + Return a Table of Records filtered by the filter parameter. + + A FilterExpression representing the desired filter. + A new Table with the requested Records + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields ordered by the specified fields. + + + + + Return a Table of Records filtered by the filter parameter. + + A FilterExpressionGroup representing the desired filter. + A new Table with the requested Records + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A new Table with the requested Records and Fields + + + + + Return a Table of Records filtered by the filter parameter. Only the specified Fields + will be in the Table. + + A FilterExpression representing the desired filter. + The desired fields to be in the returned Table + A list of one or more fields to order the returned table by, + or null for default order. If an orderByField is prefixed with "!", that field will sorted + in reverse order. + A new Table with the requested Records and Fields in the specified order + + + + + The Fields of the Table + + + + + + A simple class used to communicate property value changes to a Record. + Used by WPF databinding. + + + + + diff --git a/Caches/FileDbCache/FileDb/FileDb.dll b/Caches/FileDbCache/FileDb/FileDb.dll new file mode 100644 index 0000000000000000000000000000000000000000..2022a6199d64e05f46b57e9a9b67913475ab1ae7 GIT binary patch literal 70144 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 literal 0 HcmV?d00001 diff --git a/Caches/FileDbCache/FileDb/FileDb.txt b/Caches/FileDbCache/FileDb/FileDb.txt new file mode 100644 index 00000000..f3d49597 --- /dev/null +++ b/Caches/FileDbCache/FileDb/FileDb.txt @@ -0,0 +1,5 @@ +FileDbCache uses FileDb by Brett Goodman (aka EzTools), a simple No-SQL database for .NET. +FileDb is Open Source on Google Code Hosting at http://code.google.com/p/filedb-database/ +and licensed under the Apache License 2.0, http://www.apache.org/licenses/LICENSE-2.0. +See the product homepage at http://www.eztools-software.com/tools/filedb/. +Download FileDb from http://www.eztools-software.com/downloads/filedb.exe. diff --git a/Caches/FileDbCache/FileDbCache.cs b/Caches/FileDbCache/FileDbCache.cs new file mode 100644 index 00000000..bcd052e8 --- /dev/null +++ b/Caches/FileDbCache/FileDbCache.cs @@ -0,0 +1,444 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Caching; +using System.Runtime.Serialization.Formatters.Binary; +using FileDbNs; + +namespace Caching +{ + /// + /// ObjectCache implementation based on EzTools FileDb - http://www.eztools-software.com/tools/filedb/. + /// + public class FileDbCache : ObjectCache, IDisposable + { + private const string keyField = "Key"; + private const string valueField = "Value"; + private const string expiresField = "Expires"; + + private readonly BinaryFormatter formatter = new BinaryFormatter(); + private readonly FileDb fileDb = new FileDb(); + private readonly string name; + private readonly string path; + + public FileDbCache(string name, string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentException("The parameter path must not be null or empty."); + } + + if (string.IsNullOrEmpty(Path.GetExtension(path))) + { + path += ".fdb"; + } + + this.name = name; + this.path = path; + + try + { + fileDb.Open(path, false); + } + catch + { + CreateDatebase(); + } + + Trace.TraceInformation("FileDbCache created with {0} cached items", fileDb.NumRecords); + } + + public bool AutoFlush + { + get { return fileDb.AutoFlush; } + set { fileDb.AutoFlush = value; } + } + + public int AutoCleanThreshold + { + get { return fileDb.AutoCleanThreshold; } + set { fileDb.AutoCleanThreshold = value; } + } + + public override string Name + { + get { return name; } + } + + public override DefaultCacheCapabilities DefaultCacheCapabilities + { + get { return DefaultCacheCapabilities.InMemoryProvider | DefaultCacheCapabilities.AbsoluteExpirations | DefaultCacheCapabilities.SlidingExpirations; } + } + + public override object this[string key] + { + get { return Get(key); } + set { Set(key, value, null); } + } + + protected override IEnumerator> GetEnumerator() + { + throw new NotSupportedException("FileDbCache does not support the ability to enumerate items."); + } + + public override CacheEntryChangeMonitor CreateCacheEntryChangeMonitor(IEnumerable keys, string regionName = null) + { + throw new NotSupportedException("FileDbCache does not support the ability to create change monitors."); + } + + public override long GetCount(string regionName = null) + { + if (regionName != null) + { + throw new NotSupportedException("The parameter regionName must be null."); + } + + long count = 0; + + try + { + count = fileDb.NumRecords; + } + catch + { + if (CheckReindex()) + { + try + { + count = fileDb.NumRecords; + } + catch + { + CreateDatebase(); + } + } + } + + return count; + } + + public override bool Contains(string key, string regionName = null) + { + if (regionName != null) + { + throw new NotSupportedException("The parameter regionName must be null."); + } + + if (key == null) + { + throw new ArgumentNullException("The parameter key must not be null."); + } + + bool contains = false; + + try + { + contains = fileDb.GetRecordByKey(key, new string[0], false) != null; + } + catch + { + if (CheckReindex()) + { + try + { + contains = fileDb.GetRecordByKey(key, new string[0], false) != null; + } + catch + { + CreateDatebase(); + } + } + } + + return contains; + } + + public override object Get(string key, string regionName = null) + { + if (regionName != null) + { + throw new NotSupportedException("The parameter regionName must be null."); + } + + if (key == null) + { + throw new ArgumentNullException("The parameter key must not be null."); + } + + object value = null; + Record record = null; + + try + { + record = fileDb.GetRecordByKey(key, new string[] { valueField }, false); + } + catch + { + if (CheckReindex()) + { + try + { + record = fileDb.GetRecordByKey(key, new string[] { valueField }, false); + } + catch + { + CreateDatebase(); + } + } + } + + if (record != null) + { + try + { + using (MemoryStream stream = new MemoryStream((byte[])record[0])) + { + value = formatter.Deserialize(stream); + } + } + catch (Exception exc) + { + Trace.TraceWarning("FileDbCache.Get({0}): {1}", key, exc.Message); + + try + { + fileDb.DeleteRecordByKey(key); + } + catch + { + } + } + } + + return value; + } + + public override CacheItem GetCacheItem(string key, string regionName = null) + { + var value = Get(key, regionName); + return value != null ? new CacheItem(key, value) : null; + } + + public override IDictionary GetValues(IEnumerable keys, string regionName = null) + { + if (regionName != null) + { + throw new NotSupportedException("The parameter regionName must be null."); + } + + var values = new Dictionary(); + + foreach (string key in keys) + { + values[key] = Get(key); + } + + return values; + } + + public override void Set(string key, object value, CacheItemPolicy policy, string regionName = null) + { + if (regionName != null) + { + throw new NotSupportedException("The parameter regionName must be null."); + } + + if (value == null) + { + throw new ArgumentNullException("The parameter value must not be null."); + } + + if (key == null) + { + throw new ArgumentNullException("The parameter key must not be null."); + } + + byte[] valueBuffer = null; + + try + { + using (MemoryStream stream = new MemoryStream()) + { + formatter.Serialize(stream, value); + valueBuffer = stream.ToArray(); + } + } + catch (Exception exc) + { + Trace.TraceWarning("FileDbCache.Set({0}): {1}", key, exc.Message); + } + + if (valueBuffer != null) + { + DateTime expires = DateTime.MaxValue; + + if (policy.AbsoluteExpiration != InfiniteAbsoluteExpiration) + { + expires = policy.AbsoluteExpiration.DateTime; + } + else if (policy.SlidingExpiration != NoSlidingExpiration) + { + expires = DateTime.UtcNow + policy.SlidingExpiration; + } + + try + { + AddOrUpdateRecord(key, valueBuffer, expires); + } + catch + { + if (CheckReindex()) + { + try + { + AddOrUpdateRecord(key, valueBuffer, expires); + } + catch + { + CreateDatebase(); + AddOrUpdateRecord(key, valueBuffer, expires); + } + } + } + } + } + + public override void Set(CacheItem item, CacheItemPolicy policy) + { + Set(item.Key, item.Value, policy, item.RegionName); + } + + public override void Set(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null) + { + Set(key, value, new CacheItemPolicy { AbsoluteExpiration = absoluteExpiration }, regionName); + } + + public override object AddOrGetExisting(string key, object value, CacheItemPolicy policy, string regionName = null) + { + var oldValue = Get(key, regionName); + Set(key, value, policy); + return oldValue; + } + + public override CacheItem AddOrGetExisting(CacheItem item, CacheItemPolicy policy) + { + var oldItem = GetCacheItem(item.Key, item.RegionName); + Set(item, policy); + return oldItem; + } + + public override object AddOrGetExisting(string key, object value, DateTimeOffset absoluteExpiration, string regionName = null) + { + return AddOrGetExisting(key, value, new CacheItemPolicy { AbsoluteExpiration = absoluteExpiration }, regionName); + } + + public override object Remove(string key, string regionName = null) + { + var oldValue = Get(key, regionName); + + if (oldValue != null) + { + try + { + fileDb.DeleteRecordByKey(key); + } + catch + { + } + } + + return oldValue; + } + + public void Flush() + { + try + { + fileDb.Flush(); + } + catch + { + CheckReindex(); + } + } + + public void Clean() + { + try + { + fileDb.Clean(); + } + catch + { + CheckReindex(); + } + } + + public void Dispose() + { + try + { + fileDb.DeleteRecords(new FilterExpression(expiresField, DateTime.UtcNow, EqualityEnum.LessThanOrEqual)); + Trace.TraceInformation("FileDbCache has deleted {0} expired items", fileDb.NumDeleted); + fileDb.Clean(); + fileDb.Close(); + } + catch + { + if (CheckReindex()) + { + fileDb.Close(); + } + } + } + + private bool CheckReindex() + { + if (fileDb.IsOpen) + { + Trace.TraceWarning("FileDbCache is reindexing database"); + fileDb.Reindex(); + return true; + } + + return false; + } + + private void CreateDatebase() + { + if (File.Exists(path)) + { + File.Delete(path); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(path)); + } + + fileDb.Create(path, new Field[] + { + new Field(keyField, DataTypeEnum.String) { IsPrimaryKey = true }, + new Field(valueField, DataTypeEnum.Byte) { IsArray = true }, + new Field(expiresField, DataTypeEnum.DateTime) + }); + } + + private void AddOrUpdateRecord(string key, object value, DateTime expires) + { + var fieldValues = new FieldValues(3); // capacity + fieldValues.Add(valueField, value); + fieldValues.Add(expiresField, expires); + + if (fileDb.GetRecordByKey(key, new string[0], false) == null) + { + fieldValues.Add(keyField, key); + fileDb.AddRecord(fieldValues); + } + else + { + fileDb.UpdateRecordByKey(key, fieldValues); + } + } + } +} diff --git a/Caches/FileDbCache/FileDbCache.csproj b/Caches/FileDbCache/FileDbCache.csproj new file mode 100644 index 00000000..8889f87b --- /dev/null +++ b/Caches/FileDbCache/FileDbCache.csproj @@ -0,0 +1,58 @@ + + + + Debug + AnyCPU + 8.0.30703 + 2.0 + {EF44F661-B98A-4676-927F-85D138F82300} + Library + Properties + Caching + FileDbCache + v4.0 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + none + true + bin\Release\ + TRACE + prompt + 4 + + + + False + FileDb\FileDb.dll + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Caches/FileDbCache/Properties/AssemblyInfo.cs b/Caches/FileDbCache/Properties/AssemblyInfo.cs new file mode 100644 index 00000000..77a22a9e --- /dev/null +++ b/Caches/FileDbCache/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("FileDbCache")] +[assembly: AssemblyDescription("ObjectCache implementation based on EzTools FileDb")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("FileDbCache")] +[assembly: AssemblyCopyright("Copyright © 2012 Clemens Fischer")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("c1fc4f52-e47c-4f62-9807-7e096db69851")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/MapControl.sln b/MapControl.sln index cdede0a3..62028eaa 100644 --- a/MapControl.sln +++ b/MapControl.sln @@ -5,6 +5,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MapControl", "MapControl\Ma EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApplication", "TestApplication\TestApplication.csproj", "{CCBCDAE5-E68F-43A8-930A-0749E476D29D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FileDbCache", "Caches\FileDbCache\FileDbCache.csproj", "{EF44F661-B98A-4676-927F-85D138F82300}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -35,6 +37,16 @@ Global {CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|Mixed Platforms.Build.0 = Release|x86 {CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|x86.ActiveCfg = Release|x86 {CCBCDAE5-E68F-43A8-930A-0749E476D29D}.Release|x86.Build.0 = Release|x86 + {EF44F661-B98A-4676-927F-85D138F82300}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Release|Any CPU.Build.0 = Release|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {EF44F661-B98A-4676-927F-85D138F82300}.Release|x86.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/MapControl/Tile.cs b/MapControl/Tile.cs index 4464c208..84f5f213 100644 --- a/MapControl/Tile.cs +++ b/MapControl/Tile.cs @@ -3,7 +3,6 @@ // Licensed under the Microsoft Public License (Ms-PL) using System; -using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; @@ -49,10 +48,5 @@ namespace MapControl Brush.ImageSource = value; } } - - public override string ToString() - { - return string.Format("{0}.{1}.{2}", ZoomLevel, X, Y); - } } } diff --git a/MapControl/TileImageLoader.cs b/MapControl/TileImageLoader.cs index f30958f2..dae97125 100644 --- a/MapControl/TileImageLoader.cs +++ b/MapControl/TileImageLoader.cs @@ -4,11 +4,13 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Runtime.Caching; using System.Threading; +using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Threading; @@ -16,28 +18,70 @@ using System.Windows.Threading; namespace MapControl { /// - /// Loads map tiles by their URIs and optionally caches their image files in a folder - /// defined by the static TileCacheFolder property. + /// Loads map tile images by their URIs and optionally caches the images in an ObjectCache. /// public class TileImageLoader : DispatcherObject { - public static string TileCacheFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "MapControl Cache"); - public static TimeSpan TileCacheExpiryAge = TimeSpan.FromDays(1d); + [Serializable] + private class CachedImage + { + public readonly DateTime CreationTime = DateTime.UtcNow; + public readonly byte[] ImageBuffer; + + public CachedImage(byte[] imageBuffer) + { + ImageBuffer = imageBuffer; + } + } private readonly TileLayer tileLayer; private readonly Queue pendingTiles = new Queue(); + private readonly HashSet currentRequests = new HashSet(); private int numDownloads; + /// + /// The ObjectCache used to cache tile images. + /// The default is System.Runtime.Caching.MemoryCache.Default. + /// + public static ObjectCache Cache { get; set; } + + /// + /// The time interval after which cached images expire. The default value is 30 days. + /// When an image is not retrieved from the cache during this interval it is considered + /// as expired and will be removed from the cache. If an image is retrieved from the cache + /// and the CacheUpdateAge time interval has expired, the image is downloaded again and + /// rewritten to the cache with new expiration time. + /// + public static TimeSpan CacheExpiration { get; set; } + + /// + /// The time interval after which a cached image is updated and rewritten to the cache. + /// The default value is one day. This time interval should be shorter than the value of + /// the CacheExpiration property. + /// + public static TimeSpan CacheUpdateAge { get; set; } + + static TileImageLoader() + { + Cache = MemoryCache.Default; + CacheExpiration = TimeSpan.FromDays(30d); + CacheUpdateAge = TimeSpan.FromDays(1d); + + Application.Current.Exit += (o, e) => + { + IDisposable disposableCache = Cache as IDisposable; + if (disposableCache != null) + { + disposableCache.Dispose(); + } + }; + } + public TileImageLoader(TileLayer tileLayer) { this.tileLayer = tileLayer; } - private bool IsCached - { - get { return tileLayer.IsCached && !string.IsNullOrEmpty(TileCacheFolder); } - } - internal void StartDownloadTiles(ICollection tiles) { ThreadPool.QueueUserWorkItem(StartDownloadTilesAsync, new List(tiles.Where(t => t.Image == null && t.Uri == null))); @@ -49,46 +93,54 @@ namespace MapControl { pendingTiles.Clear(); } + + lock (currentRequests) + { + foreach (HttpWebRequest request in currentRequests) + { + request.Abort(); + } + } } private void StartDownloadTilesAsync(object newTilesList) { List newTiles = (List)newTilesList; - List expiredTiles = new List(newTiles.Count); lock (pendingTiles) { - newTiles.ForEach(tile => + if (Cache == null) { - ImageSource image = GetMemoryCachedImage(tile); + newTiles.ForEach(tile => pendingTiles.Enqueue(tile)); + } + else + { + List outdatedTiles = new List(newTiles.Count); - if (image == null && IsCached) + newTiles.ForEach(tile => { - bool fileCacheExpired; - image = GetFileCachedImage(tile, out fileCacheExpired); + string key = CacheKey(tile); + CachedImage cachedImage = Cache.Get(key) as CachedImage; - if (image != null) + if (cachedImage == null) { - SetMemoryCachedImage(tile, image); - - if (fileCacheExpired) - { - expiredTiles.Add(tile); // enqueue later - } + pendingTiles.Enqueue(tile); } - } + else if (!CreateTileImage(tile, cachedImage.ImageBuffer)) + { + // got garbage from cache + Cache.Remove(key); + pendingTiles.Enqueue(tile); + } + else if (cachedImage.CreationTime + CacheUpdateAge < DateTime.UtcNow) + { + // update cached image + outdatedTiles.Add(tile); + } + }); - if (image != null) - { - Dispatcher.BeginInvoke((Action)(() => tile.Image = image)); - } - else - { - pendingTiles.Enqueue(tile); - } - }); - - expiredTiles.ForEach(tile => pendingTiles.Enqueue(tile)); + outdatedTiles.ForEach(tile => pendingTiles.Enqueue(tile)); + } DownloadNextTiles(null); } @@ -109,13 +161,13 @@ namespace MapControl private void DownloadTileAsync(object t) { Tile tile = (Tile)t; - ImageSource image = DownloadImage(tile); + byte[] imageBuffer = DownloadImage(tile); - if (image != null) + if (imageBuffer != null && + CreateTileImage(tile, imageBuffer) && + Cache != null) { - SetMemoryCachedImage(tile, image); - - Dispatcher.BeginInvoke((Action)(() => tile.Image = image)); + Cache.Set(CacheKey(tile), new CachedImage(imageBuffer), new CacheItemPolicy { SlidingExpiration = CacheExpiration }); } lock (pendingTiles) @@ -125,98 +177,38 @@ namespace MapControl } } - private string MemoryCacheKey(Tile tile) + private string CacheKey(Tile tile) { - return string.Format("{0}/{1}/{2}/{3}", tileLayer.Name, tile.ZoomLevel, tile.XIndex, tile.Y); + return string.Format("{0}-{1}-{2}-{3}", tileLayer.Name, tile.ZoomLevel, tile.XIndex, tile.Y); } - private string CacheFilePath(Tile tile) + private byte[] DownloadImage(Tile tile) { - return string.Format("{0}.{1}", - Path.Combine(TileCacheFolder, tileLayer.Name, tile.ZoomLevel.ToString(), tile.XIndex.ToString(), tile.Y.ToString()), - tileLayer.ImageType); - } - - private ImageSource GetMemoryCachedImage(Tile tile) - { - string key = MemoryCacheKey(tile); - ImageSource image = MemoryCache.Default.Get(key) as ImageSource; - - if (image != null) - { - TraceInformation("{0} - Memory Cached", key); - } - - return image; - } - - private void SetMemoryCachedImage(Tile tile, ImageSource image) - { - MemoryCache.Default.Set(MemoryCacheKey(tile), image, - new CacheItemPolicy { SlidingExpiration = TimeSpan.FromMinutes(10d) }); - } - - private ImageSource GetFileCachedImage(Tile tile, out bool expired) - { - string path = CacheFilePath(tile); - ImageSource image = null; - expired = false; - - if (File.Exists(path)) - { - try - { - using (Stream fileStream = File.OpenRead(path)) - { - image = BitmapFrame.Create(fileStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); - } - - expired = File.GetLastWriteTime(path) + TileCacheExpiryAge <= DateTime.Now; - TraceInformation(expired ? "{0} - File Cache Expired" : "{0} - File Cached", path); - } - catch (Exception exc) - { - TraceWarning("{0} - {1}", path, exc.Message); - File.Delete(path); - } - } - - return image; - } - - private ImageSource DownloadImage(Tile tile) - { - ImageSource image = null; + HttpWebRequest request = null; + byte[] buffer = null; try { TraceInformation("{0} - Requesting", tile.Uri); - HttpWebRequest webRequest = (HttpWebRequest)WebRequest.Create(tile.Uri); - webRequest.UserAgent = typeof(TileImageLoader).ToString(); - webRequest.KeepAlive = true; + request = (HttpWebRequest)WebRequest.Create(tile.Uri); + request.UserAgent = typeof(TileImageLoader).ToString(); + request.KeepAlive = true; - using (HttpWebResponse response = (HttpWebResponse)webRequest.GetResponse()) + lock (currentRequests) + { + currentRequests.Add(request); + } + + using (HttpWebResponse response = (HttpWebResponse)request.GetResponse()) { using (Stream responseStream = response.GetResponseStream()) { - using (Stream memoryStream = new MemoryStream((int)response.ContentLength)) + buffer = new byte[(int)response.ContentLength]; + + using (MemoryStream memoryStream = new MemoryStream(buffer)) { responseStream.CopyTo(memoryStream); - memoryStream.Position = 0; - image = BitmapFrame.Create(memoryStream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); - - if (IsCached) - { - string path = CacheFilePath(tile); - Directory.CreateDirectory(Path.GetDirectoryName(path)); - - using (Stream fileStream = File.OpenWrite(path)) - { - memoryStream.Position = 0; - memoryStream.CopyTo(fileStream); - } - } } } } @@ -225,10 +217,16 @@ namespace MapControl } catch (WebException exc) { + buffer = null; + if (exc.Status == WebExceptionStatus.ProtocolError) { TraceInformation("{0} - {1}", tile.Uri, ((HttpWebResponse)exc.Response).StatusCode); } + else if (exc.Status == WebExceptionStatus.RequestCanceled) + { + TraceInformation("{0} - {1}", tile.Uri, exc.Status); + } else { TraceWarning("{0} - {1}", tile.Uri, exc.Status); @@ -236,20 +234,56 @@ namespace MapControl } catch (Exception exc) { + buffer = null; + TraceWarning("{0} - {1}", tile.Uri, exc.Message); } - return image; + if (request != null) + { + lock (currentRequests) + { + currentRequests.Remove(request); + } + } + + return buffer; + } + + private bool CreateTileImage(Tile tile, byte[] buffer) + { + try + { + BitmapImage bitmap = new BitmapImage(); + + using (Stream stream = new MemoryStream(buffer)) + { + bitmap.BeginInit(); + bitmap.CacheOption = BitmapCacheOption.OnLoad; + bitmap.StreamSource = stream; + bitmap.EndInit(); + bitmap.Freeze(); + } + + Dispatcher.BeginInvoke((Action)(() => tile.Image = bitmap)); + } + catch (Exception exc) + { + TraceWarning("Creating tile image failed: {0}", exc.Message); + return false; + } + + return true; } private static void TraceWarning(string format, params object[] args) { - System.Diagnostics.Trace.TraceWarning("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args)); + Trace.TraceWarning("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args)); } private static void TraceInformation(string format, params object[] args) { - //System.Diagnostics.Trace.TraceInformation("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args)); + Trace.TraceInformation("[{0:00}] {1}", Thread.CurrentThread.ManagedThreadId, string.Format(format, args)); } } } diff --git a/MapControl/TileLayer.cs b/MapControl/TileLayer.cs index aefa105a..a0735a5e 100644 --- a/MapControl/TileLayer.cs +++ b/MapControl/TileLayer.cs @@ -12,8 +12,7 @@ using System.Windows.Media; namespace MapControl { /// - /// Fills a rectangular area with map tiles from a TileSource. If the IsCached property is true, - /// map tiles are cached in a folder defined by the TileImageLoader.TileCacheFolder property. + /// Fills a rectangular area with map tiles from a TileSource. /// [ContentProperty("TileSource")] public class TileLayer : DrawingVisual @@ -30,19 +29,16 @@ namespace MapControl VisualEdgeMode = EdgeMode.Aliased; VisualTransform = new MatrixTransform(); Name = string.Empty; - ImageType = "png"; MinZoomLevel = 1; MaxZoomLevel = 18; MaxDownloads = 8; } public string Name { get; set; } - public string ImageType { get; set; } public TileSource TileSource { get; set; } public int MinZoomLevel { get; set; } public int MaxZoomLevel { get; set; } public int MaxDownloads { get; set; } - public bool IsCached { get; set; } public bool HasDarkBackground { get; set; } public string Description @@ -139,7 +135,7 @@ namespace MapControl drawingContext.DrawRectangle(tile.Brush, null, tileRect); //if (tile.ZoomLevel == zoomLevel) - // drawingContext.DrawText(new FormattedText(tile.ToString(), System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Segoe UI"), 14, Brushes.Black), tileRect.TopLeft); + // drawingContext.DrawText(new FormattedText(string.Format("{0}-{1}-{2}", tile.ZoomLevel, tile.X, tile.Y), System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight, new Typeface("Segoe UI"), 14, Brushes.Black), tileRect.TopLeft); }); } } diff --git a/TestApplication/MainWindow.xaml b/TestApplication/MainWindow.xaml index 3a63fdd8..5499baa8 100644 --- a/TestApplication/MainWindow.xaml +++ b/TestApplication/MainWindow.xaml @@ -7,8 +7,7 @@ + TileSource="http://tiles.openseamap.org/seamark/{z}/{x}/{y}.png" MinZoomLevel="10" MaxZoomLevel="18"/> @@ -120,30 +119,25 @@ + TileSource="http://{c}.tile.openstreetmap.org/{z}/{x}/{y}.png"/> + TileSource="http://{c}.tile.opencyclemap.org/cycle/{z}/{x}/{y}.png"/> + TileSource="http://{c}.tile2.opencyclemap.org/transport/{z}/{x}/{y}.png"/> + TileSource="http://{c}.tile3.opencyclemap.org/landscape/{z}/{x}/{y}.png"/> + TileSource="http://otile{n}.mqcdn.com/tiles/1.0.0/osm/{z}/{x}/{y}.png"/> + TileSource="http://ecn.t{i}.tiles.virtualearth.net/tiles/h{q}.jpeg?g=0&stl=h" MaxZoomLevel="20" HasDarkBackground="True"/>--> diff --git a/TestApplication/MainWindow.xaml.cs b/TestApplication/MainWindow.xaml.cs index 0733b060..1086637a 100644 --- a/TestApplication/MainWindow.xaml.cs +++ b/TestApplication/MainWindow.xaml.cs @@ -1,11 +1,13 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using MapControl; +using Caching; namespace MapControlTestApp { @@ -13,6 +15,15 @@ namespace MapControlTestApp { public MainWindow() { + bool usePersistentCache = false; + + if (usePersistentCache) + { + string appDataFolder = Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData); + string cachePath = Path.Combine(appDataFolder, "MapControl", "Cache.fdb"); + TileImageLoader.Cache = new FileDbCache("MapControlCache", cachePath) { AutoFlush = false }; + } + InitializeComponent(); ICollection polylines = (ICollection)Resources["Polylines"]; diff --git a/TestApplication/TestApplication.csproj b/TestApplication/TestApplication.csproj index 56948b58..4df35161 100644 --- a/TestApplication/TestApplication.csproj +++ b/TestApplication/TestApplication.csproj @@ -41,6 +41,7 @@ + 4.0 @@ -74,6 +75,10 @@ + + {EF44F661-B98A-4676-927F-85D138F82300} + FileDbCache + {06481252-2310-414A-B9FC-D5739FDF6BD3} MapControl