Source: searchers/searcher.js

 /** 
 * @class Base class for all searchers
 * @constructs Septima.Search.Searcher
 * @param {Object} options
 * @param [options.onSelect] {Septima.Search.Searcher~selectCallback} Function to call when a result is selected by the user. 
 * @param [options.searchDelay=0] {int} Delay in ms before executing the search
 * @param [options.matchesPhrase="matcher"] {string} Phrase used to display matches.
 * @param [options.usesGeoFunctions=false] {boolean} Does the implementation need the geo functions 
 * @param [options.iconURI] {boolean} Does the implementation need the geo functions 
 * @param [options.blankBehavior="search"] {string} "none"|"search"
 */

var _septimaSearcherJstsDownloadStarted = false;
var _septimaSearcherJstsWktWriter = null;
var _septimaSearcherJstsWktReader = null;
var _septimaSearcherJstsGeoJSONWriter = null;
var _septimaSearcherJstsGeoJSONReader = null;
Septima.Search.Searcher = Septima.Class (
		/** @lends Septima.Search.Searcher# */
	{
	
	//Private members
	//Dont touch
	_id: null,
	_onSelectCallback: null,
	detailHandlerDefs: null,
	customButtonDefs: null,
	onNewQueryHandlers : null,
	iconURI: null,
	_searchDelay: null,
	_targets: null,
	_matchesPhrase: null,
	allowDetails: false,
	
	
	Searcher: function (options) {
		this.customButtonDefs = {"any": []};
		this.detailHandlerDefs = {"any": []};
		this.onNewQueryHandlers  = [];
		this._searchDelay = 0;
		this._targets = [];
		this._matchesPhrase = "matcher";
		this._onSelectCallback = function(result){};
	    this.blankBehavior = "search"; //none, search
    	this.allowDetails = false;
    	
		if (options !== undefined){
	        if (typeof options.onSelect !== 'undefined'){
		        this._onSelectCallback = options.onSelect;
	        }
	        if (options.matchesPhrase){
	        	this._matchesPhrase = options.matchesPhrase;
	        }
	        if (options.searchDelay){
	        	this._searchDelay = options.searchDelay;
	        }
	        if (options.usesGeoFunctions){
	        	this._prepareJsts();
	        }
	        if (options.iconURI){
	        	this.iconURI = options.iconURI;
	        }
	    	if (options.blankBehavior){
	    		this.blankBehavior = options.blankBehavior;
	    	}
		    if (options.allowDetails){
		    	this.allowDetails = options.allowDetails;
		    }
		}
	    this._id = this.CLASS_NAME+'_'+Math.floor(Math.random()*999999999);
	},
    /**
     * A search result item created by a searcher
     * @typedef {Object} Septima.Search.Searcher.Result
     * @property {string} title The title as displayed in the search result
     * @property {string} description The description as displayed in the search result
     * @property {object} geometry geojson
     * @property {Septima.Search.Searcher} searcher
     * @property {object} data Searcher specific data - See each searcher for details
     */

	/**
	 * A function which is called when a result is selected.
	 * @callback Septima.Search.Searcher~selectCallback
	 * @param {Septima.Search.Searcher.Result} result
	 */

	_prepareJsts: function(){
		if (typeof jsts !== "undefined"){
			var precisionModel = new jsts.geom.PrecisionModel(jsts.geom.PrecisionModel.FLOATING);
			var geometryFactory = new jsts.geom.GeometryFactory(precisionModel);
			 _septimaSearcherJstsWktWriter = new jsts.io.WKTWriter(geometryFactory);
			 _septimaSearcherJstsWktReader = new jsts.io.WKTReader(geometryFactory);
			 _septimaSearcherJstsGeoJSONWriter = new jsts.io.GeoJSONWriter();
			 _septimaSearcherJstsGeoJSONReader = new jsts.io.GeoJSONReader(geometryFactory);
		}else if (!_septimaSearcherJstsDownloadStarted){
			_septimaSearcherJstsDownloadStarted = true;
			//load jsts
			var utilXHR = jQuery.ajax({
		        url: "https://common.cdn.septima.dk/jsts/latest/javascript.util.js",
		        dataType: 'script',
		        cache : true,
		        success: Septima.bind(function(){
					var jstsXHR = jQuery.ajax({
				        url: "https://common.cdn.septima.dk/jsts/latest/jsts.js",
				        dataType: 'script',
				        cache : true,
				        success: Septima.bind(function() {
							var precisionModel = new jsts.geom.PrecisionModel(jsts.geom.PrecisionModel.FLOATING);
							var geometryFactory = new jsts.geom.GeometryFactory(precisionModel);
							 _septimaSearcherJstsWktWriter = new jsts.io.WKTWriter(geometryFactory);
							 _septimaSearcherJstsWktReader = new jsts.io.WKTReader(geometryFactory);
							 _septimaSearcherJstsGeoJSONWriter = new jsts.io.GeoJSONWriter();
							 _septimaSearcherJstsGeoJSONReader = new jsts.io.GeoJSONReader(geometryFactory);
						}, this) 
					});
		        }, this)
			});
		}
	},
	
	/**
	 * @private
	 */
	_fetchData: function(query, caller){
		var realFunction = Septima.bind(function(caller, query){
			if (caller.isActive()){
				this.fetchData(query, caller);
			}
		}, this, caller, query);
		
		setTimeout(realFunction, this._searchDelay);
	},

	/**
	 * @private
	 */
	_onSelect: function(result){
		this.onSelect(result);
	},
	
	completeResult:function(result){
		result.isComplete = true;
		var deferred = jQuery.Deferred();
		deferred.resolve(result);
		return deferred.promise();
	},
	
	/**
	 * This method is called by the controller when a result is selected. <br>
	 * This method MAY be implemented by searchers. <br>
	 * Implementations MUST call this._onSelectCallback(result)
	 * @param {Septima.Search.Searcher.Result} result
	 */
	onSelect: function(result){
		if (typeof result.newquery === 'undefined') {
        	this._onSelectCallback(result);
		}
	},
	
	/**
	 * Register a target.
	 * @param {string} target
	 */
	registerTarget: function (target){
		this._targets.push(target);
	},
	
	/**
	 * @private
	 */
	getId: function (){
		return this._id;
	},
	
	/**
	 * Create a {Septima.Search.QueryResult}.
	 * @returns {Septima.Search.QueryResult}
	 *
	 */
	createQueryResult: function(){
		return new Septima.Search.QueryResult(this);
	},
	
	/**
	 * Convert a wkt string to a geojson object
	 * @param {string} wkt
	 * @returns {GeoJsonObject}
	 */
	translateWktToGeoJsonObject:function(wkt){
		var jtsGeometry =   _septimaSearcherJstsWktReader.read(wkt);
		var GeoJsonObject =  _septimaSearcherJstsGeoJSONWriter.write(jtsGeometry);
		return GeoJsonObject;
	},
	
	translateGeoJsonObjectToWkt: function(geoJsonObject){
		var jtsGeometry =   _septimaSearcherJstsGeoJSONReader.read(geoJsonObject);
		var wkt = _septimaSearcherJstsWktWriter.write(jtsGeometry);
		return wkt;
	},
	
	/**
	 * Find a point guaranteed to lie on the surface
	 * @param {geoJsonObject} geoJsonObject
	 * @returns {GeoJsonObject}
	 */
	getPointOnSurface: function(geoJsonObject){
		var jtsGeometry =   _septimaSearcherJstsGeoJSONReader.read(geoJsonObject);
		var ip = jtsGeometry.getInteriorPoint();
		var ipGeoJsonObject =  _septimaSearcherJstsGeoJSONWriter.write(ip);
		return ipGeoJsonObject;
	},
	
	/**
	 * Find the centroid
	 * @param {geoJsonObject} geoJsonObject
	 * @returns {GeoJsonObject}
	 */
	getCentroid: function(geoJsonObject){
		var jtsGeometry =   _septimaSearcherJstsGeoJSONReader.read(geoJsonObject);
		var cen = jtsGeometry.getCentroid();
		var cenGeoJsonObject =  _septimaSearcherJstsGeoJSONWriter.write(cen);
		return cenGeoJsonObject;
	},

	/**
	 * This method is called is called by the controller when the driver should fetch data. <br>
	 * This method MUST be implemented by all seachers <br>
	 * A call to this method MUST result in either a call to {@link Septima.Search.Controller}.fetchSuccess({@link Septima.Search.QueryResult}) or caller.fetchError()
	 * @param {Septima.Search.Query} query
	 * @param {Septima.Search.Controller} caller
	 * */
	fetchData: function (query, caller) {
		//Return error if fetchData hasn't been implemented in driver
		caller.fetchError(this, "Bad driver implementation for " + this.getId() + ". Method fetchData MUST be implemented");
	},
	
	sq: function(query){
		var deferred = jQuery.Deferred();
		deferred.resolve(this.createQueryResult());
		return deferred.promise(); 
	},
	
	/**
	 * @private
	 */
	getMatchesPhrase: function(){
		return this._matchesPhrase;
	},
	
	//MAY implement *************************************
	
	hasTarget: function (testTarget){
		//Default implementation
		for (var i=0;i<this._targets.length;i++){
			if (testTarget.toLowerCase() == this._targets[i].toLowerCase()){
				return true;
			}
		}
		return false;
	},

	hasTargets: function (){
		//Default implementation
		return (this._targets.length > 1);
	},
	
	getTargets: function (){
		return this._targets;
	},
	
	getSuggestions: function(queryResult, queryString){
		if (this._targets.length > 1){
			var testQueryString = queryString.toLowerCase();
	    	for (var i=0;i<this._targets.length;i++){
	    		var target = this._targets[i];
	    		if (target.toLowerCase().indexOf(testQueryString) === 0){
	                queryResult.addSuggestion(target, target + ':');
	    		}
	    	}
		}
	},
	
	hasdetailHandlerDefs: function(result){
		if (typeof result.detailHandlerDefs === 'undefined'){
			result.detailHandlerDefs = this.getdetailHandlerDefs(result);
		}
		return result.detailHandlerDefs.length > 0;
	},
	
	//This function may be overridden by implementations of search
	//Implementations may want to return this.customButtonDefs.any
    getdetailHandlerDefs: function(result){
        if (typeof result.newquery !== 'undefined'){
            return [];
        }else{
        	var tentativeHandlerDefs = [];
        	if (typeof result.target === 'undefined' || typeof this.detailHandlerDefs[result.target.toLowerCase()] === 'undefined'){
        		tentativeHandlerDefs = this.detailHandlerDefs.any;
        	}else{
        		tentativeHandlerDefs = this.detailHandlerDefs[result.target.toLowerCase()].concat(this.detailHandlerDefs.any);
        	}
        	var handlerDefs = [];
        	for (var i=0;i<tentativeHandlerDefs.length;i++){
        		var tentativeHandlerDef = tentativeHandlerDefs[i];
        		if (typeof tentativeHandlerDef.isApplicable === 'undefined'){
        			handlerDefs.push(tentativeHandlerDef);
        		}else if (tentativeHandlerDef.isApplicable(result)){
        			handlerDefs.push(tentativeHandlerDef);
        		}
        	}
        	return this.getDetailHandlersForResult(result).concat(handlerDefs);
        }
    },
    
	//This function may be overridden by implementations of search
    getDetailHandlersForResult: function(result){
    	return [];
    },
    
	//This function may be overridden by implementations of search
	//Implementations may want to return this.customButtonDefs.any
    getCustomButtonDefs: function(result){
        if (typeof result.newquery !== 'undefined'){
            return [];
        }else{
        	if (typeof result.target === 'undefined' || typeof this.customButtonDefs[result.target.toLowerCase()] === 'undefined'){
                return this.getCustomButtonsForResult(result).concat(this.customButtonDefs.any);
        	}else{
                return this.getCustomButtonsForResult(result).concat(this.customButtonDefs[result.target.toLowerCase()].concat(this.customButtonDefs.any));
        	}
        }
    },
    
	//This function may be overridden by implementations of search
    getCustomButtonsForResult: function(result){
    	return [];
    },
    
	
    //{"buttonText": text, "buttonImage": imageUri, "handler": function(result, cancellableDeferred, detailsContent)[, "target": target][, more: true|false]};
    //cancellableDeferred.resolve(jQuery DOM-able object)

    addDetailHandlerDef: function(detailHandlerDef){
		//fix the callback to make sure the result is completed before the callback is called
		var callback = detailHandlerDef.callBack;
		detailHandlerDef.callBack = Septima.bind(function(callback, result){
				result.searcher.completeResult(result).done(Septima.bind(function(callback, result){
					callback(result);
				}, this, callback));
		}, this, callback);

		if (typeof detailHandlerDef.target === 'undefined'){
			this.detailHandlerDefs.any.push(detailHandlerDef);
		}else{
			var target = detailHandlerDef.target.toLowerCase();
			if (typeof this.detailHandlerDefs[target] === 'undefined'){
				this.detailHandlerDefs[target] = [detailHandlerDef];
			}else{
				this.detailHandlerDefs[target].push(detailHandlerDef);
			}
		}
    },
    
    /**
     * Definition of a button, icon, or link which is presented together with search results
     * @typedef {Object} Septima.Search.Searcher.CustomButtonDef
     * @property {string} buttonText Text or tooltip
     * @property {string} buttonImage URL of image (20*20)
     * @property {function} callBack Called when icon is clicked (callBack(result))
     * @property {string} [target] Name of target
     */
    
    /**
	 * Displays an icon next to each result.
	 * @param {Septima.Search.Searcher.CustomButtonDef} customButtonDef
	 * */
	addCustomButtonDef: function(customButtonDef){
		//fix the callback to make sure the result is completed before the callback is called
		var callback = customButtonDef.callBack;
		customButtonDef.callBack = Septima.bind(function(callback, result){
			if (result.isComplete !== 'undefined' && result.isComplete === false){
				result.searcher.completeResult(result).done(Septima.bind(function(callback, result){
					callback(result);
				}, this, callback));
			}else{
				return callback(result);
			}
		}, this, callback);
		//Put in correct array of buttondefs (any or target)
		if (typeof customButtonDef.target === 'undefined'){
			this.customButtonDefs.any.push(customButtonDef);
		}else{
			var target = customButtonDef.target.toLowerCase();
			if (typeof this.customButtonDefs[target] === 'undefined'){
				this.customButtonDefs[target] = [customButtonDef];
			}else{
				this.customButtonDefs[target].push(customButtonDef);
			}
		}
	},

    getListStrategy: function (query, hitcount){
    	var strategy;
    	if (query.type == 'list.force'){
    		strategy = 'cut';
    	}else{
    		if (query.queryString === ''){
    			strategy = 'newquery';
    		}else{
    			if (hitcount <= query.limit){
    				strategy = 'showall';
    			}else{
    				strategy = 'newquery';
    			}
    		}
    	}
    	var isMixed;
    	if (!query.hasTarget && query.type == 'list'){
    		isMixed = true;
    	}else{
    		isMixed = false;
    	}
    	var count = Math.min(hitcount,query.limit);
    	var show;
    	if (strategy === 'cut' || strategy === 'showall'){
    		show = true;
    	}else{
    		show = false;
    	}
    	var showResultIcon;
    	if (query.type == 'list'){
    		showResultIcon = true;
    	}
    	var showFolderIcon;
    	if (strategy === 'newquery' && query.queryString.length > 0){
    		showFolderIcon = true;
    	}else{
    		showFolderIcon = false;
    	}
    	return {strategy: strategy, isMixed: isMixed, count: count, show: show,showResultIcon: showResultIcon};
    },
	
	CLASS_NAME: 'Septima.Search.Searcher'

});