diff --git a/tests/tests.js b/tests/tests.js
index 75a71a76..07465f0a 100644
--- a/tests/tests.js
+++ b/tests/tests.js
@@ -325,11 +325,11 @@ define(function(require) {
localArchive.getTitlesInCoords(rectFranceGermany, 10, callbackTitlesNearbyFound);
});
- /*
asyncTest("check articles found nearby London", function() {
- expect(2);
+ expect(3);
var callbackTitlesNearbyLondonFound = function(titleList) {
ok(titleList !== null, "Some titles should be found");
+ ok(titleList.length > 0, "At least one title should be found");
var titleLondon = null;
for (var i=0; i
- */
-define(function(require) {
-
- // Module dependencies
- var normalize_string = require('normalize_string');
- var util = require('util');
- var geometry = require('geometry');
- var jQuery = require('jquery');
- var titleIterators = require('titleIterators');
-
- // Declare the webworker that can uncompress with bzip2 algorithm
- var webworkerBzip2;
- try {
- // When using the application normally
- webworkerBzip2 = new Worker("js/lib/webworker_bzip2.js");
- }
- catch(e) {
- // When using unit tests
- webworkerBzip2 = new Worker("www/js/lib/webworker_bzip2.js");
- }
-
- // Size of chunks read in the dump files : 128 KB
- var CHUNK_SIZE = 131072;
- // A rectangle representing all the earth globe
- var GLOBE_RECTANGLE = new geometry.rect(-181, -90, 361, 181);
-
- /**
- * LocalArchive class : defines a wikipedia dump on the filesystem
- */
- function LocalArchive() {
- this.dataFiles = new Array();
- this.coordinateFiles = new Array();
- this.titleFile = null;
- this.mathIndexFile = null;
- this.mathDataFile = null;
- this.date = null;
- this.language = null;
- this.titleSearchFile = null;
- this.normalizedTitles = true;
- };
-
-
- /**
- * Read the title Files in the given directory, and assign them to the
- * current LocalArchive
- *
- * @param storage
- * @param directory
- */
- LocalArchive.prototype.readTitleFilesFromStorage = function(storage, directory) {
- var currentLocalArchiveInstance = this;
- storage.get(directory + 'titles.idx').then(function(file) {
- currentLocalArchiveInstance.titleFile = file;
- }, function(error) {
- alert("Error reading title file in directory " + directory + " : " + error);
- });
- storage.get(directory + 'titles_search.idx').then(function(file) {
- currentLocalArchiveInstance.titleSearchFile = file;
- }, function(error) {
- // Do nothing : this file is not mandatory in an archive
- });
- };
-
- /**
- * Read the data Files in the given directory (starting at given index), and
- * assign them to the current LocalArchive
- *
- * @param storage
- * @param directory
- * @param index
- */
- LocalArchive.prototype.readDataFilesFromStorage = function(storage, directory, index) {
- var currentLocalArchiveInstance = this;
-
- var prefixedFileNumber = "";
- if (index < 10) {
- prefixedFileNumber = "0" + index;
- } else {
- prefixedFileNumber = index;
- }
- storage.get(directory + 'wikipedia_' + prefixedFileNumber + '.dat')
- .then(function(file) {
- currentLocalArchiveInstance.dataFiles[index] = file;
- currentLocalArchiveInstance.readDataFilesFromStorage(storage, directory,
- index + 1);
- }, function(error) {
- // TODO there must be a better way to detect a FileNotFound
- if (error != "NotFoundError") {
- alert("Error reading data file " + index + " in directory "
- + directory + " : " + error);
- }
- });
- };
-
- /**
- * Read the coordinate Files in the given directory (starting at given index), and
- * assign them to the current LocalArchive
- *
- * @param storage
- * @param directory
- * @param index
- */
- LocalArchive.prototype.readCoordinateFilesFromStorage = function(storage, directory, index) {
- var currentLocalArchiveInstance = this;
-
- var prefixedFileNumber = "";
- if (index < 10) {
- prefixedFileNumber = "0" + index;
- } else {
- prefixedFileNumber = index;
- }
- storage.get(directory + 'coordinates_' + prefixedFileNumber
- + '.idx').then(function(file) {
- currentLocalArchiveInstance.coordinateFiles[index] = file;
- currentLocalArchiveInstance.readCoordinateFilesFromStorage(storage, directory,
- index + 1);
- }, function(error) {
- // TODO there must be a better way to detect a FileNotFound
- if (error != "NotFoundError") {
- alert("Error reading coordinates file " + index + " in directory "
- + directory + " : " + error);
- }
- });
- };
-
- /**
- * Read the metadata.txt file in the given directory, and store its content
- * in the current instance
- *
- * @param storage
- * @param directory
- */
- LocalArchive.prototype.readMetadataFileFromStorage = function(storage, directory) {
- var currentLocalArchiveInstance = this;
-
- storage.get(directory + 'metadata.txt').then(function(file) {
- var metadataFile = file;
- currentLocalArchiveInstance.readMetadataFile(metadataFile);
- }, function(error) {
- alert("Error reading metadata.txt file in directory "
- + directory + " : " + error);
- });
- };
-
- /**
- * Read the metadata file, in order to populate its values in the current
- * instance
- * @param {File} file metadata.txt file
- */
- LocalArchive.prototype.readMetadataFile = function(file) {
- var currentLocalArchiveInstance = this;
- var reader = new FileReader();
- reader.onload = function(e) {
- var metadata = e.target.result;
- currentLocalArchiveInstance.language = /\nlanguage ?\= ?([^ \n]+)/.exec(metadata)[1];
- currentLocalArchiveInstance.date = /\ndate ?\= ?([^ \n]+)/.exec(metadata)[1];
- var normalizedTitlesRegex = /\nnormalized_titles ?\= ?([^ \n]+)/;
- if (normalizedTitlesRegex.exec(metadata)) {
- var normalizedTitlesInt = normalizedTitlesRegex.exec(metadata)[1];
- if (normalizedTitlesInt === "0") {
- currentLocalArchiveInstance.normalizedTitles = false;
- }
- else {
- currentLocalArchiveInstance.normalizedTitles = true;
- }
- }
- else {
- currentLocalArchiveInstance.normalizedTitles = true;
- }
- };
- reader.readAsText(file);
- };
-
- /**
- * Initialize the localArchive from given archive files
- * @param {type} archiveFiles
- */
- LocalArchive.prototype.initializeFromArchiveFiles = function(archiveFiles) {
- var dataFileRegex = /^wikipedia_(\d\d).dat$/;
- var coordinateFileRegex = /^coordinates_(\d\d).idx$/;
- this.dataFiles = new Array();
- this.coordinateFiles = new Array();
- for (var i=0; i 0) {
- var intFileNr = 1 * coordinateFileNr[1];
- this.coordinateFiles[intFileNr - 1] = file;
- }
- else {
- var dataFileNr = dataFileRegex.exec(file.name);
- if (dataFileNr && dataFileNr.length > 0) {
- var intFileNr = 1 * dataFileNr[1];
- this.dataFiles[intFileNr] = file;
- }
- }
- }
- }
- }
-
- };
-
- /**
- * Initialize the localArchive from given directory, using DeviceStorage
- * @param {type} storages List of DeviceStorages available
- * @param {type} archiveDirectory
- */
- LocalArchive.prototype.initializeFromDeviceStorage = function(storages, archiveDirectory) {
- // First, we have to find which DeviceStorage has been selected by the user
- // It is the prefix of the archive directory
- var storageNameRegex = /^\/([^\/]+)\//;
- var regexResults = storageNameRegex.exec(archiveDirectory);
- var selectedStorage = null;
- if (regexResults && regexResults.length>0) {
- var selectedStorageName = regexResults[1];
- for (var i=0; i= titleCount) {
- return titles;
- }
- return iterator.advance().then(function(title) {
- if (title === null)
- return titles;
- titles.push(title);
- return addNext();
- });
- }
- return addNext();
- }).then(callbackFunction, errorHandler);
- };
-
- /**
- * Look for a title by its name, and call the callbackFunction with this Title
- * If the title is not found, the callbackFunction is called with parameter null
- * @param titleName
- * @param callbackFunction
- */
- LocalArchive.prototype.getTitleByName = function(titleName, callbackFunction) {
- var that = this;
- var normalize = this.getNormalizeFunction();
- var normalizedTitleName = normalize(titleName);
-
- titleIterators.findPrefixOffset(this.titleFile, titleName, normalize).then(function(offset) {
- var iterator = new titleIterators.SequentialTitleIterator(that, offset);
- function check(title) {
- if (title == null || normalize(title.name) !== normalizedTitleName) {
- return null;
- } else if (title.name === titleName) {
- return title;
- } else {
- return iterator.advance().then(check);
- }
- }
- return iterator.advance().then(check);
- }).then(callbackFunction, errorHandler);
- };
-
- /**
- * Get a random title, and call the callbackFunction with this Title
- * @param callbackFunction
- */
- LocalArchive.prototype.getRandomTitle = function(callbackFunction) {
- var that = this;
- var offset = Math.floor(Math.random() * this.titleFile.size);
- jQuery.when().then(function() {
- return util.readFileSlice(that.titleFile, offset,
- offset + titleIterators.MAX_TITLE_LENGTH).then(function(byteArray) {
- // Let's find the next newLine
- var newLineIndex = 0;
- while (newLineIndex < byteArray.length && byteArray[newLineIndex] !== 10) {
- newLineIndex++;
- }
- var iterator = new titleIterators.SequentialTitleIterator(that, offset + newLineIndex + 1);
- return iterator.advance();
- });
- }).then(callbackFunction, errorHandler);
- };
-
- /**
- * Find titles that start with the given prefix, and call the callbackFunction with this list of Titles
- * @param prefix
- * @param maxSize Maximum number of titles to read
- * @param callbackFunction
- */
- LocalArchive.prototype.findTitlesWithPrefix = function(prefix, maxSize, callbackFunction) {
- var that = this;
- var titles = [];
- var normalize = this.getNormalizeFunction();
- prefix = normalize(prefix);
-
- titleIterators.findPrefixOffset(this.titleFile, prefix, normalize).then(function(offset) {
- var iterator = new titleIterators.SequentialTitleIterator(that, offset);
- function addNext() {
- if (titles.length >= maxSize) {
- return titles;
- }
- return iterator.advance().then(function(title) {
- if (title == null)
- return titles;
- // check whether this title really starts with the prefix
- var name = normalize(title.name);
- if (name.length < prefix.length || name.substring(0, prefix.length) != prefix)
- return titles;
- titles.push(title);
- return addNext();
- });
- }
- return addNext();
- }).then(callbackFunction, errorHandler);
- };
-
-
- /**
- * Read an article from the title instance, and call the
- * callbackFunction with the article HTML String
- *
- * @param title
- * @param callbackFunction
- */
- LocalArchive.prototype.readArticle = function(title, callbackFunction) {
- var dataFile = null;
-
- var prefixedFileNumber = "";
- if (title.fileNr < 10) {
- prefixedFileNumber = "0" + title.fileNr;
- } else {
- prefixedFileNumber = title.fileNr;
- }
- var expectedFileName = "wikipedia_" + prefixedFileNumber + ".dat";
-
- // Find the good dump file
- for (var i = 0; i < this.dataFiles.length; i++) {
- var fileName = this.dataFiles[i].name;
- // Check if the fileName ends with the expected file name (in case
- // of DeviceStorage usage, the fileName is prefixed by the
- // directory)
- if (fileName.match(expectedFileName + "$") == expectedFileName) {
- dataFile = this.dataFiles[i];
- }
- }
- if (!dataFile) {
- // TODO can probably be replaced by some error handler at window level
- alert("Oops : some files seem to be missing in your archive. Please report this problem to us by email (see About section), with the names of the archive and article, and the following info : "
- + "File number " + title.fileNr + " not found");
- throw new Error("File number " + title.fileNr + " not found");
- } else {
- var reader = new FileReader();
- // Read the article in the dataFile, starting with a chunk of CHUNK_SIZE
- this.readArticleChunk(title, dataFile, reader, CHUNK_SIZE, callbackFunction);
- }
-
- };
-
- /**
- * Read a chunk of the dataFile (of the given length) to try to read the
- * given article.
- * If the bzip2 algorithm works and articleLength of the article is reached,
- * call the callbackFunction with the article HTML String.
- * Else, recursively call this function with readLength + CHUNK_SIZE
- *
- * @param title
- * @param dataFile
- * @param reader
- * @param readLength
- * @param callbackFunction
- */
- LocalArchive.prototype.readArticleChunk = function(title, dataFile, reader,
- readLength, callbackFunction) {
- var currentLocalArchiveInstance = this;
- reader.onerror = errorHandler;
- reader.onabort = function(e) {
- alert('Data file read cancelled');
- };
- reader.onload = function(e) {
- try {
- var compressedArticles = e.target.result;
- webworkerBzip2.onerror = function(event){
- // TODO can probably be replaced by some error handler at window level
- alert("An unexpected error occured during bzip2 decompression. Please report it to us by email or through Github (see About section), with the names of the archive and article, and the following info : message="
- + event.message + " filename=" + event.filename + " line number=" + event.lineno );
- throw new Error("Error during bzip2 decompression : " + event.message + " (" + event.filename + ":" + event.lineno + ")");
- };
- webworkerBzip2.onmessage = function(event){
- switch (event.data.cmd){
- case "result":
- var htmlArticles = event.data.msg;
- // Start reading at offset, and keep length characters
- var htmlArticle = htmlArticles.substring(title.blockOffset,
- title.blockOffset + title.articleLength);
- if (htmlArticle.length >= title.articleLength) {
- // Keep only length characters
- htmlArticle = htmlArticle.substring(0, title.articleLength);
- // Decode UTF-8 encoding
- htmlArticle = decodeURIComponent(escape(htmlArticle));
- callbackFunction(title, htmlArticle);
- } else {
- // TODO : throw exception if we reach the end of the file
- currentLocalArchiveInstance.readArticleChunk(title, dataFile, reader, readLength + CHUNK_SIZE,
- callbackFunction);
- }
- break;
- case "recurse":
- currentLocalArchiveInstance.readArticleChunk(title, dataFile, reader, readLength + CHUNK_SIZE, callbackFunction);
- break;
- case "debug":
- console.log(event.data.msg);
- break;
- case "error":
- // TODO can probably be replaced by some error handler at window level
- alert("An unexpected error occured during bzip2 decompression. Please report it to us by email or through Github (see About section), with the names of the archive and article, and the following info : message="
- + event.data.msg );
- throw new Error("Error during bzip2 decompression : " + event.data.msg);
- break;
- }
- };
- webworkerBzip2.postMessage({cmd : 'uncompress', msg :
- new Uint8Array(compressedArticles)});
-
- }
- catch (e) {
- callbackFunction("Error : " + e);
- }
- };
- var blob = dataFile.slice(title.blockStart, title.blockStart
- + readLength);
-
- // Read in the image file as a binary string.
- reader.readAsArrayBuffer(blob);
- };
-
- /**
- * Load the math image specified by the hex string and call the
- * callbackFunction with a base64 encoding of its data.
- *
- * @param hexString
- * @param callbackFunction
- */
- LocalArchive.prototype.loadMathImage = function(hexString, callbackFunction) {
- var entrySize = 16 + 4 + 4;
- var lo = 0;
- var hi = this.mathIndexFile.size / entrySize;
-
- var mathDataFile = this.mathDataFile;
-
- this.findMathDataPosition(hexString, lo, hi, function(pos, length) {
- var reader = new FileReader();
- reader.onerror = errorHandler;
- reader.onabort = function(e) {
- alert('Math image file read cancelled');
- };
- var blob = mathDataFile.slice(pos, pos + length);
- reader.onload = function(e) {
- var byteArray = new Uint8Array(e.target.result);
- callbackFunction(util.uint8ArrayToBase64(byteArray));
- };
- reader.readAsArrayBuffer(blob);
- });
- };
-
-
- /**
- * Recursive algorithm to find the position of the Math image in the data file
- * @param {type} hexString
- * @param {type} lo
- * @param {type} hi
- * @param {type} callbackFunction
- */
- LocalArchive.prototype.findMathDataPosition = function(hexString, lo, hi, callbackFunction) {
- var entrySize = 16 + 4 + 4;
- if (lo >= hi) {
- /* TODO error - not found */
- return;
- }
- var reader = new FileReader();
- reader.onerror = errorHandler;
- reader.onabort = function(e) {
- alert('Math image file read cancelled');
- };
- var mid = Math.floor((lo + hi) / 2);
- var blob = this.mathIndexFile.slice(mid * entrySize, (mid + 1) * entrySize);
- var currentLocalArchiveInstance = this;
- reader.onload = function(e) {
- var byteArray = new Uint8Array(e.target.result);
- var hash = util.uint8ArrayToHex(byteArray.subarray(0, 16));
- if (hash == hexString) {
- var pos = util.readIntegerFrom4Bytes(byteArray, 16);
- var length = util.readIntegerFrom4Bytes(byteArray, 16 + 4);
- callbackFunction(pos, length);
- return;
- } else if (hexString < hash) {
- hi = mid;
- } else {
- lo = mid + 1;
- }
-
- currentLocalArchiveInstance.findMathDataPosition(hexString, lo, hi, callbackFunction);
- };
- // Read the file as a binary string
- reader.readAsArrayBuffer(blob);
- };
-
-
- /**
- * Resolve the redirect of the given title instance, and call the callbackFunction with the redirected Title instance
- * @param title
- * @param callbackFunction
- */
- LocalArchive.prototype.resolveRedirect = function(title, callbackFunction) {
- var reader = new FileReader();
- reader.onerror = errorHandler;
- reader.onabort = function(e) {
- alert('Title file read cancelled');
- };
- reader.onload = function(e) {
- var binaryTitleFile = e.target.result;
- var byteArray = new Uint8Array(binaryTitleFile);
-
- if (byteArray.length === 0) {
- // TODO can probably be replaced by some error handler at window level
- alert("Oops : there seems to be something wrong in your archive. Please report it to us by email or through Github (see About section), with the names of the archive and article and the following info : "
- + "Unable to find redirected article for title " + title.name + " : offset " + title.blockStart + " not found in title file");
- throw new Error("Unable to find redirected article for title " + title.name + " : offset " + title.blockStart + " not found in title file");
- }
-
- var redirectedTitle = title;
- redirectedTitle.fileNr = 1 * byteArray[2];
- redirectedTitle.blockStart = util.readIntegerFrom4Bytes(byteArray, 3);
- redirectedTitle.blockOffset = util.readIntegerFrom4Bytes(byteArray, 7);
- redirectedTitle.articleLength = util.readIntegerFrom4Bytes(byteArray, 11);
-
- callbackFunction(redirectedTitle);
- };
- // Read only the 16 necessary bytes, starting at title.blockStart
- var blob = this.titleFile.slice(title.blockStart, title.blockStart + 16);
- // Read in the file as a binary string
- reader.readAsArrayBuffer(blob);
- };
-
- /**
- * Finds titles that are located inside the given rectangle
- * This is the main function, that has to be called from the application
- *
- * @param {type} rect Rectangle where to look for titles
- * @param {type} maxTitles Maximum number of titles to find
- * @param callbackFunction Function to call with the list of titles found
- */
- LocalArchive.prototype.getTitlesInCoords = function(rect, maxTitles, callbackFunction) {
- var normalizedRectangle = rect.normalized();
- var i = 0;
- LocalArchive.getTitlesInCoordsInt(this, i, 0, normalizedRectangle, GLOBE_RECTANGLE, maxTitles, new Array(), callbackFunction, LocalArchive.callbackGetTitlesInCoordsInt);
- };
-
- /**
- * Callback function called by getTitlesInCoordsInt (or by itself), in order
- * to loop through every coordinate file, and search titles nearby in each
- * of them.
- * When all the coordinate files are searched, or when enough titles are
- * found, another function is called to convert the title positions found
- * into Title instances (asynchronously)
- *
- * @param {type} localArchive
- * @param {type} titlePositionsFound
- * @param {type} i : index of the coordinate file
- * @param {type} maxTitles
- * @param {type} normalizedRectangle
- * @param {type} callbackFunction
- */
- LocalArchive.callbackGetTitlesInCoordsInt = function(localArchive, titlePositionsFound, i, maxTitles, normalizedRectangle, callbackFunction) {
- i++;
- if (titlePositionsFound.length < maxTitles && i < localArchive.coordinateFiles.length) {
- LocalArchive.getTitlesInCoordsInt(localArchive, i, 0, normalizedRectangle, GLOBE_RECTANGLE, maxTitles, titlePositionsFound, callbackFunction, LocalArchive.callbackGetTitlesInCoordsInt);
- }
- else {
- // Search is over : now let's convert the title positions into Title instances
- if (titlePositionsFound && titlePositionsFound.length > 0) {
- // TODO find out why there are duplicates, and why the maxTitles is not respected
- // The statement below removes duplicates and limits its size
- // (not correctly because based on indexes of the original array, instead of target array)
- // This should be removed when the cause is found
- var filteredTitlePositions = titlePositionsFound.filter(function (e, i, arr) {
- return arr.lastIndexOf(e) === i && i<=maxTitles;
- });
- LocalArchive.readTitlesFromTitleCoordsInTitleFile(localArchive, filteredTitlePositions, 0, new Array(), callbackFunction);
- }
- else {
- callbackFunction(titlePositionsFound);
- }
- }
- };
-
- /**
- * This function reads a list of title positions, and converts it into a list or Title instances.
- * It handles index i, then recursively calls itself for index i+1
- * When all the list is processed, the callbackFunction is called with the Title list
- *
- * @param {type} localArchive
- * @param {type} titlePositionsFound
- * @param {type} i
- * @param {type} titlesFound
- * @param {type} callbackFunction
- */
- LocalArchive.readTitlesFromTitleCoordsInTitleFile = function (localArchive, titlePositionsFound, i, titlesFound, callbackFunction) {
- var titleOffset = titlePositionsFound[i];
- localArchive.getTitlesStartingAtOffset(titleOffset, 1, function(titleList) {
- if (titleList && titleList.length === 1) {
- titlesFound.push(titleList[0]);
- i++;
- if (i= 0 && titlePositionsFound.length >= maxTitles) {
- callbackGetTitlesInCoordsInt(localArchive, titlePositionsFound, coordinateFileIndex, maxTitles, targetRect, callbackFunction);
- return;
- }
- }
- callbackGetTitlesInCoordsInt(localArchive, titlePositionsFound, coordinateFileIndex, maxTitles, targetRect, callbackFunction);
- }
-
- };
- // Read 22 bytes in the coordinate files, at coordFilePos index, in order to read the selector and the coordinates
- // 2 + 4 + 4 + 3 * 4 = 22
- // As there can be up to 65535 different coordinates, we have to read 22*65535 bytes = 1.44MB
- // TODO : This should be improved by reading the file in 2 steps :
- // - first read the selector
- // - then read the coordinates (reading only the exact necessary bytes)
- var blob = localArchive.coordinateFiles[coordinateFileIndex].slice(coordFilePos, coordFilePos + 22*65535);
-
- // Read in the file as a binary string
- reader.readAsArrayBuffer(blob);
- };
-
- /**
- * Scans the DeviceStorage for archives
- *
- * @param storages List of DeviceStorage instances
- * @param callbackFunction Function to call with the list of directories where archives are found
- */
- LocalArchive.scanForArchives = function(storages, callbackFunction) {
- var directories = [];
- var promises = jQuery.map(storages, function(storage) {
- return storage.scanForDirectoriesContainingFile('titles.idx')
- .then(function(dirs) {
- jQuery.merge(directories, dirs);
- return true;
- });
- });
- jQuery.when.apply(null, promises).then(function() {
- callbackFunction(directories);
- }, function(error) {
- alert("Error scanning your SD card : " + error
- + ". If you're using the Firefox OS Simulator, please put the archives in "
- + "a 'fake-sdcard' directory inside your Firefox profile "
- + "(ex : ~/.mozilla/firefox/xxxx.default/extensions/fxos_1_x_simulator@mozilla.org/"
- + "profile/fake-sdcard/wikipedia_small_2010-08-14)");
- callbackFunction(null);
- });
- };
-
- /**
- * Normalize the given String, if the current Archive is compatible.
- * If it's not, return the given String, as is.
- * @param string : string to normalized
- * @returns normalized string, or same string if archive is not compatible
- */
- LocalArchive.prototype.normalizeStringIfCompatibleArchive = function(string) {
- if (this.normalizedTitles === true) {
- return normalize_string.normalizeString(string);
- }
- else {
- return string;
- }
- };
-
- /**
- * Returns a function that normalizes strings if the current archive is compatible.
- * If it is not, returns the identity function.
- */
- LocalArchive.prototype.getNormalizeFunction = function() {
- if (this.normalizedTitles === true) {
- return normalize_string.normalizeString;
- } else {
- return function(string) { return string; };
- }
- };
-
- /**
- * ErrorHandler for FileReader
- * @param {type} evt
- * @returns {undefined}
- */
- function errorHandler(evt) {
- switch (evt.target.error.code) {
- case evt.target.error.NOT_FOUND_ERR:
- alert('File Not Found!');
- break;
- case evt.target.error.NOT_READABLE_ERR:
- alert('File is not readable');
- break;
- case evt.target.error.ABORT_ERR:
- break; // noop
- default:
- alert('An error occurred reading this file.');
- };
- }
-
-
- /**
- * Functions and classes exposed by this module
- */
- return {
- LocalArchive: LocalArchive
- };
-});
+/**
+ * archive.js : Class for a local Evopedia archive, with the algorithms to read it
+ * This file handles finding a title in an archive, reading an article in an archive etc
+ *
+ * Copyright 2013-2014 Mossroy and contributors
+ * License GPL v3:
+ *
+ * This file is part of Evopedia.
+ *
+ * Evopedia is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Evopedia is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Evopedia (file LICENSE-GPLv3.txt). If not, see
+ */
+define(function(require) {
+
+ // Module dependencies
+ var normalize_string = require('normalize_string');
+ var util = require('util');
+ var geometry = require('geometry');
+ var jQuery = require('jquery');
+ var titleIterators = require('titleIterators');
+
+ // Declare the webworker that can uncompress with bzip2 algorithm
+ var webworkerBzip2;
+ try {
+ // When using the application normally
+ webworkerBzip2 = new Worker("js/lib/webworker_bzip2.js");
+ }
+ catch(e) {
+ // When using unit tests
+ webworkerBzip2 = new Worker("www/js/lib/webworker_bzip2.js");
+ }
+
+ // Size of chunks read in the dump files : 128 KB
+ var CHUNK_SIZE = 131072;
+ // A rectangle representing all the earth globe
+ var GLOBE_RECTANGLE = new geometry.rect(-181, -90, 361, 181);
+
+ /**
+ * LocalArchive class : defines a wikipedia dump on the filesystem
+ */
+ function LocalArchive() {
+ this.dataFiles = new Array();
+ this.coordinateFiles = new Array();
+ this.titleFile = null;
+ this.mathIndexFile = null;
+ this.mathDataFile = null;
+ this.date = null;
+ this.language = null;
+ this.titleSearchFile = null;
+ this.normalizedTitles = true;
+ };
+
+
+ /**
+ * Read the title Files in the given directory, and assign them to the
+ * current LocalArchive
+ *
+ * @param storage
+ * @param directory
+ */
+ LocalArchive.prototype.readTitleFilesFromStorage = function(storage, directory) {
+ var currentLocalArchiveInstance = this;
+ storage.get(directory + 'titles.idx').then(function(file) {
+ currentLocalArchiveInstance.titleFile = file;
+ }, function(error) {
+ alert("Error reading title file in directory " + directory + " : " + error);
+ });
+ storage.get(directory + 'titles_search.idx').then(function(file) {
+ currentLocalArchiveInstance.titleSearchFile = file;
+ }, function(error) {
+ // Do nothing : this file is not mandatory in an archive
+ });
+ };
+
+ /**
+ * Read the data Files in the given directory (starting at given index), and
+ * assign them to the current LocalArchive
+ *
+ * @param storage
+ * @param directory
+ * @param index
+ */
+ LocalArchive.prototype.readDataFilesFromStorage = function(storage, directory, index) {
+ var currentLocalArchiveInstance = this;
+
+ var prefixedFileNumber = "";
+ if (index < 10) {
+ prefixedFileNumber = "0" + index;
+ } else {
+ prefixedFileNumber = index;
+ }
+ storage.get(directory + 'wikipedia_' + prefixedFileNumber + '.dat')
+ .then(function(file) {
+ currentLocalArchiveInstance.dataFiles[index] = file;
+ currentLocalArchiveInstance.readDataFilesFromStorage(storage, directory,
+ index + 1);
+ }, function(error) {
+ // TODO there must be a better way to detect a FileNotFound
+ if (error != "NotFoundError") {
+ alert("Error reading data file " + index + " in directory "
+ + directory + " : " + error);
+ }
+ });
+ };
+
+ /**
+ * Read the coordinate Files in the given directory (starting at given index), and
+ * assign them to the current LocalArchive
+ *
+ * @param storage
+ * @param directory
+ * @param index
+ */
+ LocalArchive.prototype.readCoordinateFilesFromStorage = function(storage, directory, index) {
+ var currentLocalArchiveInstance = this;
+
+ var prefixedFileNumber = "";
+ if (index < 10) {
+ prefixedFileNumber = "0" + index;
+ } else {
+ prefixedFileNumber = index;
+ }
+ storage.get(directory + 'coordinates_' + prefixedFileNumber
+ + '.idx').then(function(file) {
+ currentLocalArchiveInstance.coordinateFiles[index] = file;
+ currentLocalArchiveInstance.readCoordinateFilesFromStorage(storage, directory,
+ index + 1);
+ }, function(error) {
+ // TODO there must be a better way to detect a FileNotFound
+ if (error != "NotFoundError") {
+ alert("Error reading coordinates file " + index + " in directory "
+ + directory + " : " + error);
+ }
+ });
+ };
+
+ /**
+ * Read the metadata.txt file in the given directory, and store its content
+ * in the current instance
+ *
+ * @param storage
+ * @param directory
+ */
+ LocalArchive.prototype.readMetadataFileFromStorage = function(storage, directory) {
+ var currentLocalArchiveInstance = this;
+
+ storage.get(directory + 'metadata.txt').then(function(file) {
+ var metadataFile = file;
+ currentLocalArchiveInstance.readMetadataFile(metadataFile);
+ }, function(error) {
+ alert("Error reading metadata.txt file in directory "
+ + directory + " : " + error);
+ });
+ };
+
+ /**
+ * Read the metadata file, in order to populate its values in the current
+ * instance
+ * @param {File} file metadata.txt file
+ */
+ LocalArchive.prototype.readMetadataFile = function(file) {
+ var currentLocalArchiveInstance = this;
+ var reader = new FileReader();
+ reader.onload = function(e) {
+ var metadata = e.target.result;
+ currentLocalArchiveInstance.language = /\nlanguage ?\= ?([^ \n]+)/.exec(metadata)[1];
+ currentLocalArchiveInstance.date = /\ndate ?\= ?([^ \n]+)/.exec(metadata)[1];
+ var normalizedTitlesRegex = /\nnormalized_titles ?\= ?([^ \n]+)/;
+ if (normalizedTitlesRegex.exec(metadata)) {
+ var normalizedTitlesInt = normalizedTitlesRegex.exec(metadata)[1];
+ if (normalizedTitlesInt === "0") {
+ currentLocalArchiveInstance.normalizedTitles = false;
+ }
+ else {
+ currentLocalArchiveInstance.normalizedTitles = true;
+ }
+ }
+ else {
+ currentLocalArchiveInstance.normalizedTitles = true;
+ }
+ };
+ reader.readAsText(file);
+ };
+
+ /**
+ * Initialize the localArchive from given archive files
+ * @param {type} archiveFiles
+ */
+ LocalArchive.prototype.initializeFromArchiveFiles = function(archiveFiles) {
+ var dataFileRegex = /^wikipedia_(\d\d).dat$/;
+ var coordinateFileRegex = /^coordinates_(\d\d).idx$/;
+ this.dataFiles = new Array();
+ this.coordinateFiles = new Array();
+ for (var i=0; i 0) {
+ var intFileNr = 1 * coordinateFileNr[1];
+ this.coordinateFiles[intFileNr - 1] = file;
+ }
+ else {
+ var dataFileNr = dataFileRegex.exec(file.name);
+ if (dataFileNr && dataFileNr.length > 0) {
+ var intFileNr = 1 * dataFileNr[1];
+ this.dataFiles[intFileNr] = file;
+ }
+ }
+ }
+ }
+ }
+
+ };
+
+ /**
+ * Initialize the localArchive from given directory, using DeviceStorage
+ * @param {type} storages List of DeviceStorages available
+ * @param {type} archiveDirectory
+ */
+ LocalArchive.prototype.initializeFromDeviceStorage = function(storages, archiveDirectory) {
+ // First, we have to find which DeviceStorage has been selected by the user
+ // It is the prefix of the archive directory
+ var storageNameRegex = /^\/([^\/]+)\//;
+ var regexResults = storageNameRegex.exec(archiveDirectory);
+ var selectedStorage = null;
+ if (regexResults && regexResults.length>0) {
+ var selectedStorageName = regexResults[1];
+ for (var i=0; i= titleCount) {
+ return titles;
+ }
+ return iterator.advance().then(function(title) {
+ if (title === null)
+ return titles;
+ titles.push(title);
+ return addNext();
+ });
+ }
+ return addNext();
+ }).then(callbackFunction, errorHandler);
+ };
+
+ /**
+ * Look for a title by its name, and call the callbackFunction with this Title
+ * If the title is not found, the callbackFunction is called with parameter null
+ * @param titleName
+ * @param callbackFunction
+ */
+ LocalArchive.prototype.getTitleByName = function(titleName, callbackFunction) {
+ var that = this;
+ var normalize = this.getNormalizeFunction();
+ var normalizedTitleName = normalize(titleName);
+
+ titleIterators.findPrefixOffset(this.titleFile, titleName, normalize).then(function(offset) {
+ var iterator = new titleIterators.SequentialTitleIterator(that, offset);
+ function check(title) {
+ if (title == null || normalize(title.name) !== normalizedTitleName) {
+ return null;
+ } else if (title.name === titleName) {
+ return title;
+ } else {
+ return iterator.advance().then(check);
+ }
+ }
+ return iterator.advance().then(check);
+ }).then(callbackFunction, errorHandler);
+ };
+
+ /**
+ * Get a random title, and call the callbackFunction with this Title
+ * @param callbackFunction
+ */
+ LocalArchive.prototype.getRandomTitle = function(callbackFunction) {
+ var that = this;
+ var offset = Math.floor(Math.random() * this.titleFile.size);
+ jQuery.when().then(function() {
+ return util.readFileSlice(that.titleFile, offset,
+ offset + titleIterators.MAX_TITLE_LENGTH).then(function(byteArray) {
+ // Let's find the next newLine
+ var newLineIndex = 0;
+ while (newLineIndex < byteArray.length && byteArray[newLineIndex] !== 10) {
+ newLineIndex++;
+ }
+ var iterator = new titleIterators.SequentialTitleIterator(that, offset + newLineIndex + 1);
+ return iterator.advance();
+ });
+ }).then(callbackFunction, errorHandler);
+ };
+
+ /**
+ * Find titles that start with the given prefix, and call the callbackFunction with this list of Titles
+ * @param prefix
+ * @param maxSize Maximum number of titles to read
+ * @param callbackFunction
+ */
+ LocalArchive.prototype.findTitlesWithPrefix = function(prefix, maxSize, callbackFunction) {
+ var that = this;
+ var titles = [];
+ var normalize = this.getNormalizeFunction();
+ prefix = normalize(prefix);
+
+ titleIterators.findPrefixOffset(this.titleFile, prefix, normalize).then(function(offset) {
+ var iterator = new titleIterators.SequentialTitleIterator(that, offset);
+ function addNext() {
+ if (titles.length >= maxSize) {
+ return titles;
+ }
+ return iterator.advance().then(function(title) {
+ if (title == null)
+ return titles;
+ // check whether this title really starts with the prefix
+ var name = normalize(title.name);
+ if (name.length < prefix.length || name.substring(0, prefix.length) != prefix)
+ return titles;
+ titles.push(title);
+ return addNext();
+ });
+ }
+ return addNext();
+ }).then(callbackFunction, errorHandler);
+ };
+
+
+ /**
+ * Read an article from the title instance, and call the
+ * callbackFunction with the article HTML String
+ *
+ * @param title
+ * @param callbackFunction
+ */
+ LocalArchive.prototype.readArticle = function(title, callbackFunction) {
+ var dataFile = null;
+
+ var prefixedFileNumber = "";
+ if (title.fileNr < 10) {
+ prefixedFileNumber = "0" + title.fileNr;
+ } else {
+ prefixedFileNumber = title.fileNr;
+ }
+ var expectedFileName = "wikipedia_" + prefixedFileNumber + ".dat";
+
+ // Find the good dump file
+ for (var i = 0; i < this.dataFiles.length; i++) {
+ var fileName = this.dataFiles[i].name;
+ // Check if the fileName ends with the expected file name (in case
+ // of DeviceStorage usage, the fileName is prefixed by the
+ // directory)
+ if (fileName.match(expectedFileName + "$") == expectedFileName) {
+ dataFile = this.dataFiles[i];
+ }
+ }
+ if (!dataFile) {
+ // TODO can probably be replaced by some error handler at window level
+ alert("Oops : some files seem to be missing in your archive. Please report this problem to us by email (see About section), with the names of the archive and article, and the following info : "
+ + "File number " + title.fileNr + " not found");
+ throw new Error("File number " + title.fileNr + " not found");
+ } else {
+ var reader = new FileReader();
+ // Read the article in the dataFile, starting with a chunk of CHUNK_SIZE
+ this.readArticleChunk(title, dataFile, reader, CHUNK_SIZE, callbackFunction);
+ }
+
+ };
+
+ /**
+ * Read a chunk of the dataFile (of the given length) to try to read the
+ * given article.
+ * If the bzip2 algorithm works and articleLength of the article is reached,
+ * call the callbackFunction with the article HTML String.
+ * Else, recursively call this function with readLength + CHUNK_SIZE
+ *
+ * @param title
+ * @param dataFile
+ * @param reader
+ * @param readLength
+ * @param callbackFunction
+ */
+ LocalArchive.prototype.readArticleChunk = function(title, dataFile, reader,
+ readLength, callbackFunction) {
+ var currentLocalArchiveInstance = this;
+ reader.onerror = errorHandler;
+ reader.onabort = function(e) {
+ alert('Data file read cancelled');
+ };
+ reader.onload = function(e) {
+ try {
+ var compressedArticles = e.target.result;
+ webworkerBzip2.onerror = function(event){
+ // TODO can probably be replaced by some error handler at window level
+ alert("An unexpected error occured during bzip2 decompression. Please report it to us by email or through Github (see About section), with the names of the archive and article, and the following info : message="
+ + event.message + " filename=" + event.filename + " line number=" + event.lineno );
+ throw new Error("Error during bzip2 decompression : " + event.message + " (" + event.filename + ":" + event.lineno + ")");
+ };
+ webworkerBzip2.onmessage = function(event){
+ switch (event.data.cmd){
+ case "result":
+ var htmlArticles = event.data.msg;
+ // Start reading at offset, and keep length characters
+ var htmlArticle = htmlArticles.substring(title.blockOffset,
+ title.blockOffset + title.articleLength);
+ if (htmlArticle.length >= title.articleLength) {
+ // Keep only length characters
+ htmlArticle = htmlArticle.substring(0, title.articleLength);
+ // Decode UTF-8 encoding
+ htmlArticle = decodeURIComponent(escape(htmlArticle));
+ callbackFunction(title, htmlArticle);
+ } else {
+ // TODO : throw exception if we reach the end of the file
+ currentLocalArchiveInstance.readArticleChunk(title, dataFile, reader, readLength + CHUNK_SIZE,
+ callbackFunction);
+ }
+ break;
+ case "recurse":
+ currentLocalArchiveInstance.readArticleChunk(title, dataFile, reader, readLength + CHUNK_SIZE, callbackFunction);
+ break;
+ case "debug":
+ console.log(event.data.msg);
+ break;
+ case "error":
+ // TODO can probably be replaced by some error handler at window level
+ alert("An unexpected error occured during bzip2 decompression. Please report it to us by email or through Github (see About section), with the names of the archive and article, and the following info : message="
+ + event.data.msg );
+ throw new Error("Error during bzip2 decompression : " + event.data.msg);
+ break;
+ }
+ };
+ webworkerBzip2.postMessage({cmd : 'uncompress', msg :
+ new Uint8Array(compressedArticles)});
+
+ }
+ catch (e) {
+ callbackFunction("Error : " + e);
+ }
+ };
+ var blob = dataFile.slice(title.blockStart, title.blockStart
+ + readLength);
+
+ // Read in the image file as a binary string.
+ reader.readAsArrayBuffer(blob);
+ };
+
+ /**
+ * Load the math image specified by the hex string and call the
+ * callbackFunction with a base64 encoding of its data.
+ *
+ * @param hexString
+ * @param callbackFunction
+ */
+ LocalArchive.prototype.loadMathImage = function(hexString, callbackFunction) {
+ var entrySize = 16 + 4 + 4;
+ var lo = 0;
+ var hi = this.mathIndexFile.size / entrySize;
+
+ var mathDataFile = this.mathDataFile;
+
+ this.findMathDataPosition(hexString, lo, hi, function(pos, length) {
+ var reader = new FileReader();
+ reader.onerror = errorHandler;
+ reader.onabort = function(e) {
+ alert('Math image file read cancelled');
+ };
+ var blob = mathDataFile.slice(pos, pos + length);
+ reader.onload = function(e) {
+ var byteArray = new Uint8Array(e.target.result);
+ callbackFunction(util.uint8ArrayToBase64(byteArray));
+ };
+ reader.readAsArrayBuffer(blob);
+ });
+ };
+
+
+ /**
+ * Recursive algorithm to find the position of the Math image in the data file
+ * @param {type} hexString
+ * @param {type} lo
+ * @param {type} hi
+ * @param {type} callbackFunction
+ */
+ LocalArchive.prototype.findMathDataPosition = function(hexString, lo, hi, callbackFunction) {
+ var entrySize = 16 + 4 + 4;
+ if (lo >= hi) {
+ /* TODO error - not found */
+ return;
+ }
+ var reader = new FileReader();
+ reader.onerror = errorHandler;
+ reader.onabort = function(e) {
+ alert('Math image file read cancelled');
+ };
+ var mid = Math.floor((lo + hi) / 2);
+ var blob = this.mathIndexFile.slice(mid * entrySize, (mid + 1) * entrySize);
+ var currentLocalArchiveInstance = this;
+ reader.onload = function(e) {
+ var byteArray = new Uint8Array(e.target.result);
+ var hash = util.uint8ArrayToHex(byteArray.subarray(0, 16));
+ if (hash == hexString) {
+ var pos = util.readIntegerFrom4Bytes(byteArray, 16);
+ var length = util.readIntegerFrom4Bytes(byteArray, 16 + 4);
+ callbackFunction(pos, length);
+ return;
+ } else if (hexString < hash) {
+ hi = mid;
+ } else {
+ lo = mid + 1;
+ }
+
+ currentLocalArchiveInstance.findMathDataPosition(hexString, lo, hi, callbackFunction);
+ };
+ // Read the file as a binary string
+ reader.readAsArrayBuffer(blob);
+ };
+
+
+ /**
+ * Resolve the redirect of the given title instance, and call the callbackFunction with the redirected Title instance
+ * @param title
+ * @param callbackFunction
+ */
+ LocalArchive.prototype.resolveRedirect = function(title, callbackFunction) {
+ var reader = new FileReader();
+ reader.onerror = errorHandler;
+ reader.onabort = function(e) {
+ alert('Title file read cancelled');
+ };
+ reader.onload = function(e) {
+ var binaryTitleFile = e.target.result;
+ var byteArray = new Uint8Array(binaryTitleFile);
+
+ if (byteArray.length === 0) {
+ // TODO can probably be replaced by some error handler at window level
+ alert("Oops : there seems to be something wrong in your archive. Please report it to us by email or through Github (see About section), with the names of the archive and article and the following info : "
+ + "Unable to find redirected article for title " + title.name + " : offset " + title.blockStart + " not found in title file");
+ throw new Error("Unable to find redirected article for title " + title.name + " : offset " + title.blockStart + " not found in title file");
+ }
+
+ var redirectedTitle = title;
+ redirectedTitle.fileNr = 1 * byteArray[2];
+ redirectedTitle.blockStart = util.readIntegerFrom4Bytes(byteArray, 3);
+ redirectedTitle.blockOffset = util.readIntegerFrom4Bytes(byteArray, 7);
+ redirectedTitle.articleLength = util.readIntegerFrom4Bytes(byteArray, 11);
+
+ callbackFunction(redirectedTitle);
+ };
+ // Read only the 16 necessary bytes, starting at title.blockStart
+ var blob = this.titleFile.slice(title.blockStart, title.blockStart + 16);
+ // Read in the file as a binary string
+ reader.readAsArrayBuffer(blob);
+ };
+
+ /**
+ * Finds titles that are located inside the given rectangle
+ * This is the main function, that has to be called from the application
+ *
+ * @param {type} rect Rectangle where to look for titles
+ * @param {type} maxTitles Maximum number of titles to find
+ * @param callbackFunction Function to call with the list of titles found
+ */
+ LocalArchive.prototype.getTitlesInCoords = function(rect, maxTitles, callbackFunction) {
+ var normalizedRectangle = rect.normalized();
+ var i = 0;
+ LocalArchive.getTitlesInCoordsInt(this, i, 0, normalizedRectangle, GLOBE_RECTANGLE, maxTitles, new Array(), callbackFunction, LocalArchive.callbackGetTitlesInCoordsInt);
+ };
+
+ /**
+ * Callback function called by getTitlesInCoordsInt (or by itself), in order
+ * to loop through every coordinate file, and search titles nearby in each
+ * of them.
+ * When all the coordinate files are searched, or when enough titles are
+ * found, another function is called to convert the title positions found
+ * into Title instances (asynchronously)
+ *
+ * @param {type} localArchive
+ * @param {type} titlePositionsFound
+ * @param {type} i : index of the coordinate file
+ * @param {type} maxTitles
+ * @param {type} normalizedRectangle
+ * @param {type} callbackFunction
+ */
+ LocalArchive.callbackGetTitlesInCoordsInt = function(localArchive, titlePositionsFound, i, maxTitles, normalizedRectangle, callbackFunction) {
+ i++;
+ if (titlePositionsFound.length < maxTitles && i < localArchive.coordinateFiles.length) {
+ LocalArchive.getTitlesInCoordsInt(localArchive, i, 0, normalizedRectangle, GLOBE_RECTANGLE, maxTitles, titlePositionsFound, callbackFunction, LocalArchive.callbackGetTitlesInCoordsInt);
+ }
+ else {
+ // Search is over : now let's convert the title positions into Title instances
+ if (titlePositionsFound && titlePositionsFound.length > 0) {
+ LocalArchive.readTitlesFromTitleCoordsInTitleFile(localArchive, titlePositionsFound, 0, new Array(), callbackFunction);
+ }
+ else {
+ callbackFunction(titlePositionsFound);
+ }
+ }
+ };
+
+ /**
+ * This function reads a list of title positions, and converts it into a list or Title instances.
+ * It handles index i, then recursively calls itself for index i+1
+ * When all the list is processed, the callbackFunction is called with the Title list
+ *
+ * @param {type} localArchive
+ * @param {type} titlePositionsFound
+ * @param {type} i
+ * @param {type} titlesFound
+ * @param {type} callbackFunction
+ */
+ LocalArchive.readTitlesFromTitleCoordsInTitleFile = function (localArchive, titlePositionsFound, i, titlesFound, callbackFunction) {
+ var titleOffset = titlePositionsFound[i];
+ localArchive.getTitlesStartingAtOffset(titleOffset, 1, function(titleList) {
+ if (titleList && titleList.length === 1) {
+ titlesFound.push(titleList[0]);
+ i++;
+ if (i= 0 && titlePositionsFound.length >= maxTitles) {
+ callbackGetTitlesInCoordsInt(localArchive, titlePositionsFound, coordinateFileIndex, maxTitles, targetRect, callbackFunction);
+ return;
+ }
+ }
+ callbackGetTitlesInCoordsInt(localArchive, titlePositionsFound, coordinateFileIndex, maxTitles, targetRect, callbackFunction);
+ }
+
+ };
+ // Read 22 bytes in the coordinate files, at coordFilePos index, in order to read the selector and the coordinates
+ // 2 + 4 + 4 + 3 * 4 = 22
+ // As there can be up to 65535 different coordinates, we have to read 22*65535 bytes = 1.44MB
+ // TODO : This should be improved by reading the file in 2 steps :
+ // - first read the selector
+ // - then read the coordinates (reading only the exact necessary bytes)
+ var blob = localArchive.coordinateFiles[coordinateFileIndex].slice(coordFilePos, coordFilePos + 22*65535);
+
+ // Read in the file as a binary string
+ reader.readAsArrayBuffer(blob);
+ };
+
+ /**
+ * Scans the DeviceStorage for archives
+ *
+ * @param storages List of DeviceStorage instances
+ * @param callbackFunction Function to call with the list of directories where archives are found
+ */
+ LocalArchive.scanForArchives = function(storages, callbackFunction) {
+ var directories = [];
+ var promises = jQuery.map(storages, function(storage) {
+ return storage.scanForDirectoriesContainingFile('titles.idx')
+ .then(function(dirs) {
+ jQuery.merge(directories, dirs);
+ return true;
+ });
+ });
+ jQuery.when.apply(null, promises).then(function() {
+ callbackFunction(directories);
+ }, function(error) {
+ alert("Error scanning your SD card : " + error
+ + ". If you're using the Firefox OS Simulator, please put the archives in "
+ + "a 'fake-sdcard' directory inside your Firefox profile "
+ + "(ex : ~/.mozilla/firefox/xxxx.default/extensions/fxos_1_x_simulator@mozilla.org/"
+ + "profile/fake-sdcard/wikipedia_small_2010-08-14)");
+ callbackFunction(null);
+ });
+ };
+
+ /**
+ * Normalize the given String, if the current Archive is compatible.
+ * If it's not, return the given String, as is.
+ * @param string : string to normalized
+ * @returns normalized string, or same string if archive is not compatible
+ */
+ LocalArchive.prototype.normalizeStringIfCompatibleArchive = function(string) {
+ if (this.normalizedTitles === true) {
+ return normalize_string.normalizeString(string);
+ }
+ else {
+ return string;
+ }
+ };
+
+ /**
+ * Returns a function that normalizes strings if the current archive is compatible.
+ * If it is not, returns the identity function.
+ */
+ LocalArchive.prototype.getNormalizeFunction = function() {
+ if (this.normalizedTitles === true) {
+ return normalize_string.normalizeString;
+ } else {
+ return function(string) { return string; };
+ }
+ };
+
+ /**
+ * ErrorHandler for FileReader
+ * @param {type} evt
+ * @returns {undefined}
+ */
+ function errorHandler(evt) {
+ switch (evt.target.error.code) {
+ case evt.target.error.NOT_FOUND_ERR:
+ alert('File Not Found!');
+ break;
+ case evt.target.error.NOT_READABLE_ERR:
+ alert('File is not readable');
+ break;
+ case evt.target.error.ABORT_ERR:
+ break; // noop
+ default:
+ alert('An error occurred reading this file.');
+ };
+ }
+
+
+ /**
+ * Functions and classes exposed by this module
+ */
+ return {
+ LocalArchive: LocalArchive
+ };
+});
diff --git a/www/js/lib/geometry.js b/www/js/lib/geometry.js
index 5edf764b..9e54703a 100644
--- a/www/js/lib/geometry.js
+++ b/www/js/lib/geometry.js
@@ -1,628 +1,628 @@
-/* This Source Code Form is subject to the terms of the Mozilla Public
- * License, v. 2.0. If a copy of the MPL was not distributed with this
- * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
-
-// Geometry library.
-// (c) 2011-2013 client IO
-// Copied from https://github.com/DavidDurman/joint/blob/master/src/geometry.js
-
-define(function(require) {
-
-
-
- // Declare shorthands to the most used math functions.
- var math = Math;
- var abs = math.abs;
- var cos = math.cos;
- var sin = math.sin;
- var sqrt = math.sqrt;
- var mmin = math.min;
- var mmax = math.max;
- var atan = math.atan;
- var atan2 = math.atan2;
- var acos = math.acos;
- var round = math.round;
- var floor = math.floor;
- var PI = math.PI;
- var random = math.random;
- var toDeg = function(rad) { return (180*rad / PI) % 360; };
- var toRad = function(deg) { return (deg % 360) * PI / 180; };
- var snapToGrid = function(val, gridSize) { return gridSize * Math.round(val/gridSize); };
- var normalizeAngle = function(angle) { return (angle % 360) + (angle < 0 ? 360 : 0); };
-
- // Point
- // -----
-
- // Point is the most basic object consisting of x/y coordinate,.
-
- // Possible instantiations are:
-
- // * `point(10, 20)`
- // * `new point(10, 20)`
- // * `point('10 20')`
- // * `point(point(10, 20))`
- function point(x, y) {
- if (!(this instanceof point))
- return new point(x, y);
- var xy;
- if (y === undefined && Object(x) !== x) {
- xy = x.split(_.indexOf(x, "@") === -1 ? " " : "@");
- this.x = parseInt(xy[0], 10);
- this.y = parseInt(xy[1], 10);
- } else if (Object(x) === x) {
- this.x = x.x;
- this.y = x.y;
- } else {
- this.x = x;
- this.y = y;
- }
- }
-
- point.prototype = {
- toString: function() {
- return this.x + "@" + this.y;
- },
- // If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`,
- // otherwise return point itself.
- // (see Squeak Smalltalk, Point>>adhereTo:)
- adhereToRect: function(r) {
- if (r.containsPoint(this)){
- return this;
- }
- this.x = mmin(mmax(this.x, r.x), r.x + r.width);
- this.y = mmin(mmax(this.y, r.y), r.y + r.height);
- return this;
- },
- // Compute the angle between me and `p` and the x axis.
- // (cartesian-to-polar coordinates conversion)
- // Return theta angle in degrees.
- theta: function(p) {
- p = point(p);
- // Invert the y-axis.
- var y = -(p.y - this.y);
- var x = p.x - this.x;
- // Makes sure that the comparison with zero takes rounding errors into account.
- var PRECISION = 10;
- // Note that `atan2` is not defined for `x`, `y` both equal zero.
- var rad = (y.toFixed(PRECISION) == 0 && x.toFixed(PRECISION) == 0) ? 0 : atan2(y, x);
-
- // Correction for III. and IV. quadrant.
- if (rad < 0) {
- rad = 2*PI + rad;
- }
- return 180*rad / PI;
- },
- // Returns distance between me and point `p`.
- distance: function(p) {
- return line(this, p).length();
- },
- // Returns a manhattan (taxi-cab) distance between me and point `p`.
- manhattanDistance: function(p) {
- return abs(p.x - this.x) + abs(p.y - this.y);
- },
- // Offset me by the specified amount.
- offset: function(dx, dy) {
- this.x += dx || 0;
- this.y += dy || 0;
- return this;
- },
- magnitude: function() {
- return sqrt((this.x*this.x) + (this.y*this.y)) || 0.01;
- },
- update: function(x, y) {
- this.x = x || 0;
- this.y = y || 0;
- return this;
- },
- round: function(decimals) {
- this.x = decimals ? this.x.toFixed(decimals) : round(this.x);
- this.y = decimals ? this.y.toFixed(decimals) : round(this.y);
- return this;
- },
- // Scale the line segment between (0,0) and me to have a length of len.
- normalize: function(len) {
- var s = (len || 1) / this.magnitude();
- this.x = s * this.x;
- this.y = s * this.y;
- return this;
- },
- difference: function(p) {
- return point(this.x - p.x, this.y - p.y);
- },
- // Converts rectangular to polar coordinates.
- // An origin can be specified, otherwise it's 0@0.
- toPolar: function(o) {
- o = (o && point(o)) || point(0,0);
- var x = this.x;
- var y = this.y;
- this.x = sqrt((x-o.x)*(x-o.x) + (y-o.y)*(y-o.y)); // r
- this.y = toRad(o.theta(point(x,y)));
- return this;
- },
- // Rotate point by angle around origin o.
- rotate: function(o, angle) {
- angle = (angle + 360) % 360;
- this.toPolar(o);
- this.y += toRad(angle);
- var p = point.fromPolar(this.x, this.y, o);
- this.x = p.x;
- this.y = p.y;
- return this;
- },
- // Move point on line starting from ref ending at me by
- // distance distance.
- move: function(ref, distance) {
- var theta = toRad(point(ref).theta(this));
- return this.offset(cos(theta) * distance, -sin(theta) * distance);
- },
- // Returns change in angle from my previous position (-dx, -dy) to my new position
- // relative to ref point.
- changeInAngle: function(dx, dy, ref) {
- // Revert the translation and measure the change in angle around x-axis.
- return point(this).offset(-dx, -dy).theta(ref) - this.theta(ref);
- },
- equals: function(p) {
- return this.x === p.x && this.y === p.y;
- }
- };
- // Alternative constructor, from polar coordinates.
- // @param {number} r Distance.
- // @param {number} angle Angle in radians.
- // @param {point} [optional] o Origin.
- point.fromPolar = function(r, angle, o) {
- o = (o && point(o)) || point(0,0);
- var x = abs(r * cos(angle));
- var y = abs(r * sin(angle));
- var deg = normalizeAngle(toDeg(angle));
-
- if (deg < 90) y = -y;
- else if (deg < 180) { x = -x; y = -y; }
- else if (deg < 270) x = -x;
-
- return point(o.x + x, o.y + y);
- };
-
- // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`.
- point.random = function(x1, x2, y1, y2) {
- return point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1));
- };
-
- // Line.
- // -----
- function line(p1, p2) {
- if (!(this instanceof line))
- return new line(p1, p2);
- this.start = point(p1);
- this.end = point(p2);
- }
-
- line.prototype = {
- toString: function() {
- return this.start.toString() + ' ' + this.end.toString();
- },
- // @return {double} length of the line
- length: function() {
- return sqrt(this.squaredLength());
- },
- // @return {integer} length without sqrt
- // @note for applications where the exact length is not necessary (e.g. compare only)
- squaredLength: function() {
- var x0 = this.start.x;
- var y0 = this.start.y;
- var x1 = this.end.x;
- var y1 = this.end.y;
- return (x0 -= x1)*x0 + (y0 -= y1)*y0;
- },
- // @return {point} my midpoint
- midpoint: function() {
- return point((this.start.x + this.end.x) / 2,
- (this.start.y + this.end.y) / 2);
- },
- // @return {point} Point where I'm intersecting l.
- // @see Squeak Smalltalk, LineSegment>>intersectionWith:
- intersection: function(l) {
- var pt1Dir = point(this.end.x - this.start.x, this.end.y - this.start.y);
- var pt2Dir = point(l.end.x - l.start.x, l.end.y - l.start.y);
- var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x);
- var deltaPt = point(l.start.x - this.start.x, l.start.y - this.start.y);
- var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x);
- var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x);
-
- if (det === 0 ||
- alpha * det < 0 ||
- beta * det < 0) {
- // No intersection found.
- return null;
- }
- if (det > 0){
- if (alpha > det || beta > det){
- return null;
- }
- } else {
- if (alpha < det || beta < det){
- return null;
- }
- }
- return point(this.start.x + (alpha * pt1Dir.x / det),
- this.start.y + (alpha * pt1Dir.y / det));
- }
- };
-
- // Rectangle.
- // ----------
- function rect(x, y, w, h) {
- if (!(this instanceof rect))
- return new rect(x, y, w, h);
- if (y === undefined) {
- y = x.y;
- w = x.width;
- h = x.height;
- x = x.x;
- }
- if (w === undefined && h === undefined) {
- // The rectangle is built from topLeft and bottomRight points
- var topLeft = x;
- var bottomRight = y;
- this.x = topLeft.x;
- this.y = bottomRight.y;
- this.width = bottomRight.x - topLeft.x;
- this.height = topLeft.y - bottomRight.y;
- }
- else {
- this.x = x;
- this.y = y;
- this.width = w;
- this.height = h;
- }
- }
-
- rect.prototype = {
- toString: function() {
- return this.origin().toString() + ' ' + this.corner().toString();
- },
- origin: function() {
- return point(this.x, this.y);
- },
- corner: function() {
- return point(this.x + this.width, this.y + this.height);
- },
- topRight: function() {
- return point(this.x + this.width, this.y);
- },
- bottomLeft: function() {
- return point(this.x, this.y + this.height);
- },
- center: function() {
- return point(this.x + this.width/2, this.y + this.height/2);
- },
- // @return {boolean} true if rectangles intersect
- intersect: function(r) {
- var myOrigin = this.origin();
- var myCorner = this.corner();
- var rOrigin = r.origin();
- var rCorner = r.corner();
-
- if (rCorner.x <= myOrigin.x ||
- rCorner.y <= myOrigin.y ||
- rOrigin.x >= myCorner.x ||
- rOrigin.y >= myCorner.y) return false;
- return true;
- },
- // @return {string} (left|right|top|bottom) side which is nearest to point
- // @see Squeak Smalltalk, Rectangle>>sideNearestTo:
- sideNearestToPoint: function(p) {
- p = point(p);
- var distToLeft = p.x - this.x;
- var distToRight = (this.x + this.width) - p.x;
- var distToTop = p.y - this.y;
- var distToBottom = (this.y + this.height) - p.y;
- var closest = distToLeft;
- var side = 'left';
-
- if (distToRight < closest) {
- closest = distToRight;
- side = 'right';
- }
- if (distToTop < closest) {
- closest = distToTop;
- side = 'top';
- }
- if (distToBottom < closest) {
- closest = distToBottom;
- side = 'bottom';
- }
- return side;
- },
- // @return {bool} true if point p is insight me
- containsPoint: function(p) {
- p = point(p);
- if (p.x > this.x && p.x < this.x + this.width &&
- p.y > this.y && p.y < this.y + this.height) {
- return true;
- }
- return false;
- },
- // Algorithm copied from java.awt.Rectangle from OpenJDK
- // @return {bool} true if rectangle r is inside me
- contains: function(r) {
- var nr = r.normalized();
- var W = nr.width;
- var H = nr.height;
- var X = nr.x;
- var Y = nr.y;
- var w = this.width;
- var h = this.height;
- if ((w | h | W | H) < 0) {
- // At least one of the dimensions is negative...
- return false;
- }
- // Note: if any dimension is zero, tests below must return false...
- var x = this.x;
- var y = this.y;
- if (X < x || Y < y) {
- return false;
- }
- w += x;
- W += X;
- if (W <= X) {
- // X+W overflowed or W was zero, return false if...
- // either original w or W was zero or
- // x+w did not overflow or
- // the overflowed x+w is smaller than the overflowed X+W
- if (w >= x || W > w) return false;
- } else {
- // X+W did not overflow and W was not zero, return false if...
- // original w was zero or
- // x+w did not overflow and x+w is smaller than X+W
- if (w >= x && W > w) return false;
- }
- h += y;
- H += Y;
- if (H <= Y) {
- if (h >= y || H > h) return false;
- } else {
- if (h >= y && H > h) return false;
- }
- return true;
- },
- // @return {point} a point on my boundary nearest to p
- // @see Squeak Smalltalk, Rectangle>>pointNearestTo:
- pointNearestToPoint: function(p) {
- p = point(p);
- if (this.containsPoint(p)) {
- var side = this.sideNearestToPoint(p);
- switch (side){
- case "right": return point(this.x + this.width, p.y);
- case "left": return point(this.x, p.y);
- case "bottom": return point(p.x, this.y + this.height);
- case "top": return point(p.x, this.y);
- }
- }
- return p.adhereToRect(this);
- },
- // Find point on my boundary where line starting
- // from my center ending in point p intersects me.
- // @param {number} angle If angle is specified, intersection with rotated rectangle is computed.
- intersectionWithLineFromCenterToPoint: function(p, angle) {
- p = point(p);
- var center = point(this.x + this.width/2, this.y + this.height/2);
- var result;
- if (angle) p.rotate(center, angle);
-
- // (clockwise, starting from the top side)
- var sides = [
- line(this.origin(), this.topRight()),
- line(this.topRight(), this.corner()),
- line(this.corner(), this.bottomLeft()),
- line(this.bottomLeft(), this.origin())
- ];
- var connector = line(center, p);
-
- for (var i = sides.length - 1; i >= 0; --i){
- var intersection = sides[i].intersection(connector);
- if (intersection !== null){
- result = intersection;
- break;
- }
- }
- if (result && angle) result.rotate(center, -angle);
- return result;
- },
- // Move and expand me.
- // @param r {rectangle} representing deltas
- moveAndExpand: function(r) {
- this.x += r.x;
- this.y += r.y;
- this.width += r.width;
- this.height += r.height;
- return this;
- },
- round: function(decimals) {
- this.x = decimals ? this.x.toFixed(decimals) : round(this.x);
- this.y = decimals ? this.y.toFixed(decimals) : round(this.y);
- this.width = decimals ? this.width.toFixed(decimals) : round(this.width);
- this.height = decimals ? this.height.toFixed(decimals) : round(this.height);
- return this;
- },
- // Returns a normalized rectangle; i.e., a rectangle that has a non-negative width and height.
- // If width < 0 the function swaps the left and right corners,
- // and it swaps the top and bottom corners if height < 0
- // like in http://harmattan-dev.nokia.com/docs/library/html/qt4/qrectf.html#normalized
- normalized: function() {
- var newx = this.x;
- var newy = this.y;
- var newwidth = this.width;
- var newheight = this.height;
- if (this.width < 0) {
- newx = this.x + this.width;
- newwidth = - this.width;
- }
- if (this.height < 0) {
- newy = this.y + this.height;
- newheight = - this.height;
- }
- return new rect(newx, newy, newwidth, newheight);
- }
- };
-
- // Ellipse.
- // --------
- function ellipse(c, a, b) {
- if (!(this instanceof ellipse))
- return new ellipse(c, a, b);
- c = point(c);
- this.x = c.x;
- this.y = c.y;
- this.a = a;
- this.b = b;
- }
-
- ellipse.prototype = {
- toString: function() {
- return point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b;
- },
- bbox: function() {
- return rect(this.x - this.a, this.y - this.b, 2*this.a, 2*this.b);
- },
- // Find point on me where line from my center to
- // point p intersects my boundary.
- // @param {number} angle If angle is specified, intersection with rotated ellipse is computed.
- intersectionWithLineFromCenterToPoint: function(p, angle) {
- p = point(p);
- if (angle) p.rotate(point(this.x, this.y), angle);
- var dx = p.x - this.x;
- var dy = p.y - this.y;
- var result;
- if (dx === 0) {
- result = this.bbox().pointNearestToPoint(p);
- if (angle) return result.rotate(point(this.x, this.y), -angle);
- return result;
- }
- var m = dy / dx;
- var mSquared = m * m;
- var aSquared = this.a * this.a;
- var bSquared = this.b * this.b;
- var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared)));
-
- x = dx < 0 ? -x : x;
- var y = m * x;
- result = point(this.x + x, this.y + y);
- if (angle) return result.rotate(point(this.x, this.y), -angle);
- return result;
- }
- };
-
- // Bezier curve.
- // -------------
- var bezier = {
- // Cubic Bezier curve path through points.
- // Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx).
- // @param {array} points Array of points through which the smooth line will go.
- // @return {array} SVG Path commands as an array
- curveThroughPoints: function(points) {
- var controlPoints = this.getCurveControlPoints(points);
- var path = ['M', points[0].x, points[0].y];
-
- for (var i = 0; i < controlPoints[0].length; i++) {
- path.push('C', controlPoints[0][i].x, controlPoints[0][i].y, controlPoints[1][i].x, controlPoints[1][i].y, points[i+1].x, points[i+1].y);
- }
- return path;
- },
-
- // Get open-ended Bezier Spline Control Points.
- // @param knots Input Knot Bezier spline points (At least two points!).
- // @param firstControlPoints Output First Control points. Array of knots.length - 1 length.
- // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length.
- getCurveControlPoints: function(knots) {
- var firstControlPoints = [];
- var secondControlPoints = [];
- var n = knots.length - 1;
- var i;
-
- // Special case: Bezier curve should be a straight line.
- if (n == 1) {
- // 3P1 = 2P0 + P3
- firstControlPoints[0] = point((2 * knots[0].x + knots[1].x) / 3,
- (2 * knots[0].y + knots[1].y) / 3);
- // P2 = 2P1 – P0
- secondControlPoints[0] = point(2 * firstControlPoints[0].x - knots[0].x,
- 2 * firstControlPoints[0].y - knots[0].y);
- return [firstControlPoints, secondControlPoints];
- }
-
- // Calculate first Bezier control points.
- // Right hand side vector.
- var rhs = [];
-
- // Set right hand side X values.
- for (i = 1; i < n - 1; i++) {
- rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x;
- }
- rhs[0] = knots[0].x + 2 * knots[1].x;
- rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0;
- // Get first control points X-values.
- var x = this.getFirstControlPoints(rhs);
-
- // Set right hand side Y values.
- for (i = 1; i < n - 1; ++i) {
- rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y;
- }
- rhs[0] = knots[0].y + 2 * knots[1].y;
- rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0;
- // Get first control points Y-values.
- var y = this.getFirstControlPoints(rhs);
-
- // Fill output arrays.
- for (i = 0; i < n; i++) {
- // First control point.
- firstControlPoints.push(point(x[i], y[i]));
- // Second control point.
- if (i < n - 1) {
- secondControlPoints.push(point(2 * knots [i + 1].x - x[i + 1],
- 2 * knots[i + 1].y - y[i + 1]));
- } else {
- secondControlPoints.push(point((knots[n].x + x[n - 1]) / 2,
- (knots[n].y + y[n - 1]) / 2));
- }
- }
- return [firstControlPoints, secondControlPoints];
- },
-
- // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points.
- // @param rhs Right hand side vector.
- // @return Solution vector.
- getFirstControlPoints: function(rhs) {
- var n = rhs.length;
- // `x` is a solution vector.
- var x = [];
- var tmp = [];
- var b = 2.0;
-
- x[0] = rhs[0] / b;
- // Decomposition and forward substitution.
- for (var i = 1; i < n; i++) {
- tmp[i] = 1 / b;
- b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
- x[i] = (rhs[i] - x[i - 1]) / b;
- }
- for (i = 1; i < n; i++) {
- // Backsubstitution.
- x[n - i - 1] -= tmp[n - i] * x[n - i];
- }
- return x;
- }
- };
-
- return {
-
- toDeg: toDeg,
- toRad: toRad,
- snapToGrid: snapToGrid,
- normalizeAngle: normalizeAngle,
- point: point,
- line: line,
- rect: rect,
- ellipse: ellipse,
- bezier: bezier
- };
-});
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Geometry library.
+// (c) 2011-2013 client IO
+// Copied from https://github.com/DavidDurman/joint/blob/master/src/geometry.js
+
+define(function(require) {
+
+
+
+ // Declare shorthands to the most used math functions.
+ var math = Math;
+ var abs = math.abs;
+ var cos = math.cos;
+ var sin = math.sin;
+ var sqrt = math.sqrt;
+ var mmin = math.min;
+ var mmax = math.max;
+ var atan = math.atan;
+ var atan2 = math.atan2;
+ var acos = math.acos;
+ var round = math.round;
+ var floor = math.floor;
+ var PI = math.PI;
+ var random = math.random;
+ var toDeg = function(rad) { return (180*rad / PI) % 360; };
+ var toRad = function(deg) { return (deg % 360) * PI / 180; };
+ var snapToGrid = function(val, gridSize) { return gridSize * Math.round(val/gridSize); };
+ var normalizeAngle = function(angle) { return (angle % 360) + (angle < 0 ? 360 : 0); };
+
+ // Point
+ // -----
+
+ // Point is the most basic object consisting of x/y coordinate,.
+
+ // Possible instantiations are:
+
+ // * `point(10, 20)`
+ // * `new point(10, 20)`
+ // * `point('10 20')`
+ // * `point(point(10, 20))`
+ function point(x, y) {
+ if (!(this instanceof point))
+ return new point(x, y);
+ var xy;
+ if (y === undefined && Object(x) !== x) {
+ xy = x.split(_.indexOf(x, "@") === -1 ? " " : "@");
+ this.x = parseInt(xy[0], 10);
+ this.y = parseInt(xy[1], 10);
+ } else if (Object(x) === x) {
+ this.x = x.x;
+ this.y = x.y;
+ } else {
+ this.x = x;
+ this.y = y;
+ }
+ }
+
+ point.prototype = {
+ toString: function() {
+ return this.x + "@" + this.y;
+ },
+ // If point lies outside rectangle `r`, return the nearest point on the boundary of rect `r`,
+ // otherwise return point itself.
+ // (see Squeak Smalltalk, Point>>adhereTo:)
+ adhereToRect: function(r) {
+ if (r.containsPoint(this)){
+ return this;
+ }
+ this.x = mmin(mmax(this.x, r.x), r.x + r.width);
+ this.y = mmin(mmax(this.y, r.y), r.y + r.height);
+ return this;
+ },
+ // Compute the angle between me and `p` and the x axis.
+ // (cartesian-to-polar coordinates conversion)
+ // Return theta angle in degrees.
+ theta: function(p) {
+ p = point(p);
+ // Invert the y-axis.
+ var y = -(p.y - this.y);
+ var x = p.x - this.x;
+ // Makes sure that the comparison with zero takes rounding errors into account.
+ var PRECISION = 10;
+ // Note that `atan2` is not defined for `x`, `y` both equal zero.
+ var rad = (y.toFixed(PRECISION) == 0 && x.toFixed(PRECISION) == 0) ? 0 : atan2(y, x);
+
+ // Correction for III. and IV. quadrant.
+ if (rad < 0) {
+ rad = 2*PI + rad;
+ }
+ return 180*rad / PI;
+ },
+ // Returns distance between me and point `p`.
+ distance: function(p) {
+ return line(this, p).length();
+ },
+ // Returns a manhattan (taxi-cab) distance between me and point `p`.
+ manhattanDistance: function(p) {
+ return abs(p.x - this.x) + abs(p.y - this.y);
+ },
+ // Offset me by the specified amount.
+ offset: function(dx, dy) {
+ this.x += dx || 0;
+ this.y += dy || 0;
+ return this;
+ },
+ magnitude: function() {
+ return sqrt((this.x*this.x) + (this.y*this.y)) || 0.01;
+ },
+ update: function(x, y) {
+ this.x = x || 0;
+ this.y = y || 0;
+ return this;
+ },
+ round: function(decimals) {
+ this.x = decimals ? this.x.toFixed(decimals) : round(this.x);
+ this.y = decimals ? this.y.toFixed(decimals) : round(this.y);
+ return this;
+ },
+ // Scale the line segment between (0,0) and me to have a length of len.
+ normalize: function(len) {
+ var s = (len || 1) / this.magnitude();
+ this.x = s * this.x;
+ this.y = s * this.y;
+ return this;
+ },
+ difference: function(p) {
+ return point(this.x - p.x, this.y - p.y);
+ },
+ // Converts rectangular to polar coordinates.
+ // An origin can be specified, otherwise it's 0@0.
+ toPolar: function(o) {
+ o = (o && point(o)) || point(0,0);
+ var x = this.x;
+ var y = this.y;
+ this.x = sqrt((x-o.x)*(x-o.x) + (y-o.y)*(y-o.y)); // r
+ this.y = toRad(o.theta(point(x,y)));
+ return this;
+ },
+ // Rotate point by angle around origin o.
+ rotate: function(o, angle) {
+ angle = (angle + 360) % 360;
+ this.toPolar(o);
+ this.y += toRad(angle);
+ var p = point.fromPolar(this.x, this.y, o);
+ this.x = p.x;
+ this.y = p.y;
+ return this;
+ },
+ // Move point on line starting from ref ending at me by
+ // distance distance.
+ move: function(ref, distance) {
+ var theta = toRad(point(ref).theta(this));
+ return this.offset(cos(theta) * distance, -sin(theta) * distance);
+ },
+ // Returns change in angle from my previous position (-dx, -dy) to my new position
+ // relative to ref point.
+ changeInAngle: function(dx, dy, ref) {
+ // Revert the translation and measure the change in angle around x-axis.
+ return point(this).offset(-dx, -dy).theta(ref) - this.theta(ref);
+ },
+ equals: function(p) {
+ return this.x === p.x && this.y === p.y;
+ }
+ };
+ // Alternative constructor, from polar coordinates.
+ // @param {number} r Distance.
+ // @param {number} angle Angle in radians.
+ // @param {point} [optional] o Origin.
+ point.fromPolar = function(r, angle, o) {
+ o = (o && point(o)) || point(0,0);
+ var x = abs(r * cos(angle));
+ var y = abs(r * sin(angle));
+ var deg = normalizeAngle(toDeg(angle));
+
+ if (deg < 90) y = -y;
+ else if (deg < 180) { x = -x; y = -y; }
+ else if (deg < 270) x = -x;
+
+ return point(o.x + x, o.y + y);
+ };
+
+ // Create a point with random coordinates that fall into the range `[x1, x2]` and `[y1, y2]`.
+ point.random = function(x1, x2, y1, y2) {
+ return point(floor(random() * (x2 - x1 + 1) + x1), floor(random() * (y2 - y1 + 1) + y1));
+ };
+
+ // Line.
+ // -----
+ function line(p1, p2) {
+ if (!(this instanceof line))
+ return new line(p1, p2);
+ this.start = point(p1);
+ this.end = point(p2);
+ }
+
+ line.prototype = {
+ toString: function() {
+ return this.start.toString() + ' ' + this.end.toString();
+ },
+ // @return {double} length of the line
+ length: function() {
+ return sqrt(this.squaredLength());
+ },
+ // @return {integer} length without sqrt
+ // @note for applications where the exact length is not necessary (e.g. compare only)
+ squaredLength: function() {
+ var x0 = this.start.x;
+ var y0 = this.start.y;
+ var x1 = this.end.x;
+ var y1 = this.end.y;
+ return (x0 -= x1)*x0 + (y0 -= y1)*y0;
+ },
+ // @return {point} my midpoint
+ midpoint: function() {
+ return point((this.start.x + this.end.x) / 2,
+ (this.start.y + this.end.y) / 2);
+ },
+ // @return {point} Point where I'm intersecting l.
+ // @see Squeak Smalltalk, LineSegment>>intersectionWith:
+ intersection: function(l) {
+ var pt1Dir = point(this.end.x - this.start.x, this.end.y - this.start.y);
+ var pt2Dir = point(l.end.x - l.start.x, l.end.y - l.start.y);
+ var det = (pt1Dir.x * pt2Dir.y) - (pt1Dir.y * pt2Dir.x);
+ var deltaPt = point(l.start.x - this.start.x, l.start.y - this.start.y);
+ var alpha = (deltaPt.x * pt2Dir.y) - (deltaPt.y * pt2Dir.x);
+ var beta = (deltaPt.x * pt1Dir.y) - (deltaPt.y * pt1Dir.x);
+
+ if (det === 0 ||
+ alpha * det < 0 ||
+ beta * det < 0) {
+ // No intersection found.
+ return null;
+ }
+ if (det > 0){
+ if (alpha > det || beta > det){
+ return null;
+ }
+ } else {
+ if (alpha < det || beta < det){
+ return null;
+ }
+ }
+ return point(this.start.x + (alpha * pt1Dir.x / det),
+ this.start.y + (alpha * pt1Dir.y / det));
+ }
+ };
+
+ // Rectangle.
+ // ----------
+ function rect(x, y, w, h) {
+ if (!(this instanceof rect))
+ return new rect(x, y, w, h);
+ if (y === undefined) {
+ y = x.y;
+ w = x.width;
+ h = x.height;
+ x = x.x;
+ }
+ if (w === undefined && h === undefined) {
+ // The rectangle is built from topLeft and bottomRight points
+ var topLeft = x;
+ var bottomRight = y;
+ this.x = topLeft.x;
+ this.y = bottomRight.y;
+ this.width = bottomRight.x - topLeft.x;
+ this.height = topLeft.y - bottomRight.y;
+ }
+ else {
+ this.x = x;
+ this.y = y;
+ this.width = w;
+ this.height = h;
+ }
+ }
+
+ rect.prototype = {
+ toString: function() {
+ return this.origin().toString() + ' ' + this.corner().toString();
+ },
+ origin: function() {
+ return point(this.x, this.y);
+ },
+ corner: function() {
+ return point(this.x + this.width, this.y + this.height);
+ },
+ topRight: function() {
+ return point(this.x + this.width, this.y);
+ },
+ bottomLeft: function() {
+ return point(this.x, this.y + this.height);
+ },
+ center: function() {
+ return point(this.x + this.width/2, this.y + this.height/2);
+ },
+ // @return {boolean} true if rectangles intersect
+ intersect: function(r) {
+ var myOrigin = this.origin();
+ var myCorner = this.corner();
+ var rOrigin = r.origin();
+ var rCorner = r.corner();
+
+ if (rCorner.x <= myOrigin.x ||
+ rCorner.y <= myOrigin.y ||
+ rOrigin.x >= myCorner.x ||
+ rOrigin.y >= myCorner.y) return false;
+ return true;
+ },
+ // @return {string} (left|right|top|bottom) side which is nearest to point
+ // @see Squeak Smalltalk, Rectangle>>sideNearestTo:
+ sideNearestToPoint: function(p) {
+ p = point(p);
+ var distToLeft = p.x - this.x;
+ var distToRight = (this.x + this.width) - p.x;
+ var distToTop = p.y - this.y;
+ var distToBottom = (this.y + this.height) - p.y;
+ var closest = distToLeft;
+ var side = 'left';
+
+ if (distToRight < closest) {
+ closest = distToRight;
+ side = 'right';
+ }
+ if (distToTop < closest) {
+ closest = distToTop;
+ side = 'top';
+ }
+ if (distToBottom < closest) {
+ closest = distToBottom;
+ side = 'bottom';
+ }
+ return side;
+ },
+ // @return {bool} true if point p is insight me
+ containsPoint: function(p) {
+ p = point(p);
+ if (p.x > this.x && p.x < this.x + this.width &&
+ p.y > this.y && p.y < this.y + this.height) {
+ return true;
+ }
+ return false;
+ },
+ // Algorithm copied from java.awt.Rectangle from OpenJDK
+ // @return {bool} true if rectangle r is inside me
+ contains: function(r) {
+ var nr = r.normalized();
+ var W = nr.width;
+ var H = nr.height;
+ var X = nr.x;
+ var Y = nr.y;
+ var w = this.width;
+ var h = this.height;
+ if ((w | h | W | H) < 0) {
+ // At least one of the dimensions is negative...
+ return false;
+ }
+ // Note: if any dimension is zero, tests below must return false...
+ var x = this.x;
+ var y = this.y;
+ if (X < x || Y < y) {
+ return false;
+ }
+ w += x;
+ W += X;
+ if (W <= X) {
+ // X+W overflowed or W was zero, return false if...
+ // either original w or W was zero or
+ // x+w did not overflow or
+ // the overflowed x+w is smaller than the overflowed X+W
+ if (w >= x || W > w) return false;
+ } else {
+ // X+W did not overflow and W was not zero, return false if...
+ // original w was zero or
+ // x+w did not overflow and x+w is smaller than X+W
+ if (w >= x && W > w) return false;
+ }
+ h += y;
+ H += Y;
+ if (H <= Y) {
+ if (h >= y || H > h) return false;
+ } else {
+ if (h >= y && H > h) return false;
+ }
+ return true;
+ },
+ // @return {point} a point on my boundary nearest to p
+ // @see Squeak Smalltalk, Rectangle>>pointNearestTo:
+ pointNearestToPoint: function(p) {
+ p = point(p);
+ if (this.containsPoint(p)) {
+ var side = this.sideNearestToPoint(p);
+ switch (side){
+ case "right": return point(this.x + this.width, p.y);
+ case "left": return point(this.x, p.y);
+ case "bottom": return point(p.x, this.y + this.height);
+ case "top": return point(p.x, this.y);
+ }
+ }
+ return p.adhereToRect(this);
+ },
+ // Find point on my boundary where line starting
+ // from my center ending in point p intersects me.
+ // @param {number} angle If angle is specified, intersection with rotated rectangle is computed.
+ intersectionWithLineFromCenterToPoint: function(p, angle) {
+ p = point(p);
+ var center = point(this.x + this.width/2, this.y + this.height/2);
+ var result;
+ if (angle) p.rotate(center, angle);
+
+ // (clockwise, starting from the top side)
+ var sides = [
+ line(this.origin(), this.topRight()),
+ line(this.topRight(), this.corner()),
+ line(this.corner(), this.bottomLeft()),
+ line(this.bottomLeft(), this.origin())
+ ];
+ var connector = line(center, p);
+
+ for (var i = sides.length - 1; i >= 0; --i){
+ var intersection = sides[i].intersection(connector);
+ if (intersection !== null){
+ result = intersection;
+ break;
+ }
+ }
+ if (result && angle) result.rotate(center, -angle);
+ return result;
+ },
+ // Move and expand me.
+ // @param r {rectangle} representing deltas
+ moveAndExpand: function(r) {
+ this.x += r.x;
+ this.y += r.y;
+ this.width += r.width;
+ this.height += r.height;
+ return this;
+ },
+ round: function(decimals) {
+ this.x = decimals ? this.x.toFixed(decimals) : round(this.x);
+ this.y = decimals ? this.y.toFixed(decimals) : round(this.y);
+ this.width = decimals ? this.width.toFixed(decimals) : round(this.width);
+ this.height = decimals ? this.height.toFixed(decimals) : round(this.height);
+ return this;
+ },
+ // Returns a normalized rectangle; i.e., a rectangle that has a non-negative width and height.
+ // If width < 0 the function swaps the left and right corners,
+ // and it swaps the top and bottom corners if height < 0
+ // like in http://qt-project.org/doc/qt-4.8/qrectf.html#normalized
+ normalized: function() {
+ var newx = this.x;
+ var newy = this.y;
+ var newwidth = this.width;
+ var newheight = this.height;
+ if (this.width < 0) {
+ newx = this.x + this.width;
+ newwidth = - this.width;
+ }
+ if (this.height < 0) {
+ newy = this.y + this.height;
+ newheight = - this.height;
+ }
+ return new rect(newx, newy, newwidth, newheight);
+ }
+ };
+
+ // Ellipse.
+ // --------
+ function ellipse(c, a, b) {
+ if (!(this instanceof ellipse))
+ return new ellipse(c, a, b);
+ c = point(c);
+ this.x = c.x;
+ this.y = c.y;
+ this.a = a;
+ this.b = b;
+ }
+
+ ellipse.prototype = {
+ toString: function() {
+ return point(this.x, this.y).toString() + ' ' + this.a + ' ' + this.b;
+ },
+ bbox: function() {
+ return rect(this.x - this.a, this.y - this.b, 2*this.a, 2*this.b);
+ },
+ // Find point on me where line from my center to
+ // point p intersects my boundary.
+ // @param {number} angle If angle is specified, intersection with rotated ellipse is computed.
+ intersectionWithLineFromCenterToPoint: function(p, angle) {
+ p = point(p);
+ if (angle) p.rotate(point(this.x, this.y), angle);
+ var dx = p.x - this.x;
+ var dy = p.y - this.y;
+ var result;
+ if (dx === 0) {
+ result = this.bbox().pointNearestToPoint(p);
+ if (angle) return result.rotate(point(this.x, this.y), -angle);
+ return result;
+ }
+ var m = dy / dx;
+ var mSquared = m * m;
+ var aSquared = this.a * this.a;
+ var bSquared = this.b * this.b;
+ var x = sqrt(1 / ((1 / aSquared) + (mSquared / bSquared)));
+
+ x = dx < 0 ? -x : x;
+ var y = m * x;
+ result = point(this.x + x, this.y + y);
+ if (angle) return result.rotate(point(this.x, this.y), -angle);
+ return result;
+ }
+ };
+
+ // Bezier curve.
+ // -------------
+ var bezier = {
+ // Cubic Bezier curve path through points.
+ // Ported from C# implementation by Oleg V. Polikarpotchkin and Peter Lee (http://www.codeproject.com/KB/graphics/BezierSpline.aspx).
+ // @param {array} points Array of points through which the smooth line will go.
+ // @return {array} SVG Path commands as an array
+ curveThroughPoints: function(points) {
+ var controlPoints = this.getCurveControlPoints(points);
+ var path = ['M', points[0].x, points[0].y];
+
+ for (var i = 0; i < controlPoints[0].length; i++) {
+ path.push('C', controlPoints[0][i].x, controlPoints[0][i].y, controlPoints[1][i].x, controlPoints[1][i].y, points[i+1].x, points[i+1].y);
+ }
+ return path;
+ },
+
+ // Get open-ended Bezier Spline Control Points.
+ // @param knots Input Knot Bezier spline points (At least two points!).
+ // @param firstControlPoints Output First Control points. Array of knots.length - 1 length.
+ // @param secondControlPoints Output Second Control points. Array of knots.length - 1 length.
+ getCurveControlPoints: function(knots) {
+ var firstControlPoints = [];
+ var secondControlPoints = [];
+ var n = knots.length - 1;
+ var i;
+
+ // Special case: Bezier curve should be a straight line.
+ if (n == 1) {
+ // 3P1 = 2P0 + P3
+ firstControlPoints[0] = point((2 * knots[0].x + knots[1].x) / 3,
+ (2 * knots[0].y + knots[1].y) / 3);
+ // P2 = 2P1 – P0
+ secondControlPoints[0] = point(2 * firstControlPoints[0].x - knots[0].x,
+ 2 * firstControlPoints[0].y - knots[0].y);
+ return [firstControlPoints, secondControlPoints];
+ }
+
+ // Calculate first Bezier control points.
+ // Right hand side vector.
+ var rhs = [];
+
+ // Set right hand side X values.
+ for (i = 1; i < n - 1; i++) {
+ rhs[i] = 4 * knots[i].x + 2 * knots[i + 1].x;
+ }
+ rhs[0] = knots[0].x + 2 * knots[1].x;
+ rhs[n - 1] = (8 * knots[n - 1].x + knots[n].x) / 2.0;
+ // Get first control points X-values.
+ var x = this.getFirstControlPoints(rhs);
+
+ // Set right hand side Y values.
+ for (i = 1; i < n - 1; ++i) {
+ rhs[i] = 4 * knots[i].y + 2 * knots[i + 1].y;
+ }
+ rhs[0] = knots[0].y + 2 * knots[1].y;
+ rhs[n - 1] = (8 * knots[n - 1].y + knots[n].y) / 2.0;
+ // Get first control points Y-values.
+ var y = this.getFirstControlPoints(rhs);
+
+ // Fill output arrays.
+ for (i = 0; i < n; i++) {
+ // First control point.
+ firstControlPoints.push(point(x[i], y[i]));
+ // Second control point.
+ if (i < n - 1) {
+ secondControlPoints.push(point(2 * knots [i + 1].x - x[i + 1],
+ 2 * knots[i + 1].y - y[i + 1]));
+ } else {
+ secondControlPoints.push(point((knots[n].x + x[n - 1]) / 2,
+ (knots[n].y + y[n - 1]) / 2));
+ }
+ }
+ return [firstControlPoints, secondControlPoints];
+ },
+
+ // Solves a tridiagonal system for one of coordinates (x or y) of first Bezier control points.
+ // @param rhs Right hand side vector.
+ // @return Solution vector.
+ getFirstControlPoints: function(rhs) {
+ var n = rhs.length;
+ // `x` is a solution vector.
+ var x = [];
+ var tmp = [];
+ var b = 2.0;
+
+ x[0] = rhs[0] / b;
+ // Decomposition and forward substitution.
+ for (var i = 1; i < n; i++) {
+ tmp[i] = 1 / b;
+ b = (i < n - 1 ? 4.0 : 3.5) - tmp[i];
+ x[i] = (rhs[i] - x[i - 1]) / b;
+ }
+ for (i = 1; i < n; i++) {
+ // Backsubstitution.
+ x[n - i - 1] -= tmp[n - i] * x[n - i];
+ }
+ return x;
+ }
+ };
+
+ return {
+
+ toDeg: toDeg,
+ toRad: toRad,
+ snapToGrid: snapToGrid,
+ normalizeAngle: normalizeAngle,
+ point: point,
+ line: line,
+ rect: rect,
+ ellipse: ellipse,
+ bezier: bezier
+ };
+});