/* global IITC, L -- eslint */
/**
* @memberOf IITC.search.Query
* @typedef {Object} SearchResult
* @property {string} title - The label for this result (HTML-formatted).
* @property {string} [description] - Secondary information for this result (HTML-formatted).
* @property {L.LatLng} [position] - Position of this result.
* @property {L.LatLngBounds} [bounds] - Bounds of this result.
* @property {L.Layer|null} [layer] - Layer to be added to the map on result selection.
* @property {string} [icon] - URL to a 12x12px icon for the result list.
* @property {IITC.search.Query.onSelectedCallback} [onSelected] - Handler called when result is selected.
* May return `true` to prevent the map from being repositioned.
* You may reposition the map yourself or do other work.
* @property {IITC.search.Query.onRemoveCallback} [onRemove] - Handler called when result is removed from map.
* (because another result has been selected or the search was cancelled by the user).
*/
/**
* @memberOf IITC.search.Query
* @callback onSelectedCallback
* @param {IITC.search.Query.SearchResult} result - The selected search result.
* @param {Event} event - The event that triggered the selection.
* @returns {boolean} - Returns true to prevent map repositioning.
*/
/**
* @memberOf IITC.search.Query
* @callback onRemoveCallback
* @param {IITC.search.Query.SearchResult} result - The search result that is being removed.
* @returns {void} - No return value.
*/
/**
* Represents a search query within the IITC search module, managing query state, results, and UI rendering.
*
* This class provides functionality to handle search operations such as displaying and interacting with results,
* including selection, hover actions, and map adjustments. Hooks for custom search actions are triggered when
* a new search query is initialized.
*
* @memberof IITC.search
* @class
*/
class Query {
/**
* Initializes the search query, setting up UI elements and triggering the 'search' hook.
*
* @constructor
* @param {string} term - The search term.
* @param {boolean} confirmed - Indicates if the search is confirmed (e.g., by pressing Enter).
*/
constructor(term, confirmed) {
this.term = term;
this.confirmed = confirmed;
this.results = [];
this.resultsView = new IITC.search.QueryResultsView(term, confirmed);
window.runHooks('search', this);
}
/**
* Displays the search query results in the specified resultsView container.
*
* @memberof IITC.search.Query
* @function show
* @private
*/
show() {
this.resultsView.renderIn('#searchwrapper');
}
/**
* Hides and removes the current search results, clearing selection and hover states.
*
* @memberof IITC.search.Query
* @function show
* @private
*/
hide() {
this.resultsView.remove();
this.removeSelectedResult();
this.removeHoverResult();
}
/**
* Adds a search result to the query and triggers re-rendering of the results list.
*
* @memberof IITC.search.Query
* @function addResult
* @param {IITC.search.Query.SearchResult} result - The search result to add, including title, position, and interactions.
*/
addResult(result) {
this.results.push(result);
this.renderResults();
}
/**
* Adds a search result for a portal to the search query results.
*
* @memberof IITC.search.Query
* @function addPortalResult
* @param {Object} data - The portal data for the search result. This includes information such as title, team, level, health, etc.
* @param {string} guid - GUID if the portal.
*/
addPortalResult(data, guid) {
const team = window.teamStringToId(data.team);
const color = team === window.TEAM_NONE ? '#CCC' : window.COLORS[team];
const latLng = L.latLng(data.latE6 / 1e6, data.lngE6 / 1e6);
this.addResult({
title: data.title,
description: `${window.TEAM_SHORTNAMES[team]}, L${data.level}, ${data.health}%, ${data.resCount} Resonators`,
position: latLng,
icon: `data:image/svg+xml;base64,${btoa('@include_string:images/icon-portal.svg@'.replace(/%COLOR%/g, color))}`,
onSelected(result, event) {
const { position } = result;
if (event.type === 'dblclick') {
window.zoomToAndShowPortal(guid, latLng);
} else if (window.portals[guid]) {
if (!window.map.getBounds().contains(position)) {
window.map.setView(position);
}
window.renderPortalDetails(guid);
} else {
window.selectPortalByLatLng(latLng);
}
return true;
},
});
}
/**
* Handles keyboard interactions for selecting a result with Enter or Space keys.
*
* @memberof IITC.search.Query
* @function handleKeyPress
* @param {Event} ev - The keyboard event.
* @param {Object} result - The result being interacted with.
* @private
*/
handleKeyPress(ev, result) {
if (ev.key === ' ' || ev.key === 'Enter') {
ev.preventDefault();
const type = ev.key === ' ' ? 'click' : 'dblclick';
this.onResultSelected(result, { ...ev, type });
}
}
/**
* Renders all search results through the resultsView class and sets up event handling for each result.
*
* @memberof IITC.search.Query
* @function renderResults
* @private
*/
renderResults() {
this.resultsView.renderResults(this.results, (result, event) => this.handleResultInteraction(result, event));
}
/**
* Manages interactions with search results, such as clicks, hovers, and keyboard events.
*
* @memberof IITC.search.Query
* @function handleResultInteraction
* @param {Object} result - The result being interacted with.
* @param {Event} event - The event associated with the interaction.
* @private
*/
handleResultInteraction(result, event) {
switch (event.type) {
case 'click':
case 'dblclick':
this.onResultSelected(result, event);
break;
case 'mouseover':
this.onResultHoverStart(result);
break;
case 'mouseout':
this.onResultHoverEnd();
break;
case 'keydown':
this.handleKeyPress(event, result);
break;
}
}
/**
* Creates and returns a map layer for the given search result, which could include markers or shapes.
*
* @memberof IITC.search.Query
* @function resultLayer
* @param {Object} result - The search result object.
* @returns {L.Layer} - The generated layer for the result.
* @private
*/
resultLayer(result) {
if (!result.layer) {
result.layer = L.layerGroup();
if (result.position) {
L.marker(result.position, {
icon: L.divIcon.coloredSvg('red'),
title: result.title,
}).addTo(result.layer);
}
if (result.bounds) {
L.rectangle(result.bounds, {
title: result.title,
interactive: false,
color: 'red',
fill: false,
}).addTo(result.layer);
}
}
return result.layer;
}
/**
* Handles the selection of a search result, adjusting the map view and adding its layer to the map.
*
* @memberof IITC.search.Query
* @function onResultSelected
* @param {Object} result - The selected search result object.
* @param {Event} event - The event associated with the selection.
* @private
*/
onResultSelected(result, event) {
this.removeHoverResult();
this.removeSelectedResult();
this.selectedResult = result;
if (result.onSelected && result.onSelected(result, event)) return;
const { position, bounds } = result;
if (event.type === 'dblclick') {
if (position) {
window.map.setView(position, window.DEFAULT_ZOOM);
} else if (bounds) {
window.map.fitBounds(bounds, { maxZoom: window.DEFAULT_ZOOM });
}
} else {
if (bounds) {
window.map.fitBounds(bounds, { maxZoom: window.DEFAULT_ZOOM });
} else if (position) {
window.map.setView(position);
}
}
result.layer = this.resultLayer(result);
if (result.layer) window.map.addLayer(result.layer);
if (window.isSmartphone()) window.show('map');
}
/**
* Removes the currently selected search result from the map and performs necessary cleanup.
*
* @memberof IITC.search.Query
* @function removeSelectedResult
* @private
*/
removeSelectedResult() {
if (this.selectedResult) {
if (this.selectedResult.layer) window.map.removeLayer(this.selectedResult.layer);
if (this.selectedResult.onRemove) this.selectedResult.onRemove(this.selectedResult);
}
}
/**
* Starts a hover interaction on a search result, displaying its layer on the map.
*
* @memberof IITC.search.Query
* @function onResultHoverStart
* @param {Object} result - The result being hovered over.
* @private
*/
onResultHoverStart(result) {
this.removeHoverResult();
this.hoverResult = result;
if (result === this.selectedResult) return;
result.layer = this.resultLayer(result);
if (result.layer) window.map.addLayer(result.layer);
}
/**
* Ends a hover interaction by removing the hover layer from the map if it is not selected.
*
* @memberof IITC.search.Query
* @function removeHoverResult
* @private
*/
removeHoverResult() {
if (this.hoverResult && this.hoverResult.layer && this.hoverResult !== this.selectedResult) {
window.map.removeLayer(this.hoverResult.layer);
}
this.hoverResult = null;
}
/**
* Handles the end of a hover event, removing the hover layer from the map.
*
* @memberof IITC.search.Query
* @function onResultHoverEnd
* @private
*/
onResultHoverEnd() {
this.removeHoverResult();
}
}
IITC.search.Query = Query;