diff --git a/core/code/_deprecated.js b/core/code/_deprecated.js index 2304ef78e..218e9c196 100644 --- a/core/code/_deprecated.js +++ b/core/code/_deprecated.js @@ -1,4 +1,4 @@ -/* global L -- eslint */ +/* global IITC, L -- eslint */ /** * @file This file contains functions that are not use by IITC itself @@ -174,3 +174,16 @@ window.findPortalLatLng = function (guid) { window.androidCopy = function () { return true; // i.e. execute other actions }; + +/** + * Given the entity detail data, returns the team the entity belongs to. + * Uses TEAM_* enum values. + * + * @deprecated + * @function getTeam + * @param {Object} details - The details hash of an entity. + * @returns {number} The team ID the entity belongs to. + */ +window.getTeam = function (details) { + return IITC.utils.getTeamId(details.team); +}; diff --git a/core/code/entity_info.js b/core/code/entity_info.js deleted file mode 100644 index 60023798f..000000000 --- a/core/code/entity_info.js +++ /dev/null @@ -1,34 +0,0 @@ -/* exported setup --eslint */ - -/** - * Entity Details Tools - * Functions to extract useful data from entity details, such as portals, links, and fields. - * @module entity_info - */ - -/** - * Given the entity detail data, returns the team the entity belongs to. - * Uses TEAM_* enum values. - * - * @function getTeam - * @param {Object} details - The details hash of an entity. - * @returns {number} The team ID the entity belongs to. - */ -window.getTeam = function (details) { - return window.teamStringToId(details.team); -}; - -/** - * Converts a team string to a team ID. - * - * @function teamStringToId - * @param {string} teamStr - The team string to convert. - * @returns {number} The team ID corresponding to the team string. - */ -window.teamStringToId = function (teamStr) { - var teamIndex = window.TEAM_CODENAMES.indexOf(teamStr); - if (teamIndex >= 0) return teamIndex; - teamIndex = window.TEAM_CODES.indexOf(teamStr); - if (teamIndex >= 0) return teamIndex; - return window.TEAM_NONE; -}; diff --git a/core/code/map.js b/core/code/map.js index a18443d76..c79baef8a 100644 --- a/core/code/map.js +++ b/core/code/map.js @@ -316,6 +316,32 @@ window.setupMap = function () { map.attributionControl.setPrefix(''); + /** + * Override default Google Maps attribution to use Leaflet's native attribution control + * instead of creating separate DOM elements. Extracts text content from Google's + * attribution container and adds it to Leaflet's control. + */ + L.GridLayer.GoogleMutant.prototype._setupAttribution = function (ev) { + if (!this._map?.attributionControl) { + return; + } + // eslint-disable-next-line + const pos = google.maps.ControlPosition; + const container = ev.positions.get(pos.BOTTOM_RIGHT); + const attribution = container?.querySelector('span')?.textContent; + if (attribution) { + this._attributionText = attribution; // Сохраняем текст атрибуции + this._map.attributionControl.addAttribution(attribution); + } + }; + const originalGoogleMutantOnRemove = L.GridLayer.GoogleMutant.prototype.onRemove; + L.GridLayer.GoogleMutant.prototype.onRemove = function (map) { + originalGoogleMutantOnRemove.call(this, map); + if (this._attributionText && map.attributionControl) { + map.attributionControl.removeAttribution(this._attributionText); + } + }; + window.map = map; map.on('moveend', function () { diff --git a/core/code/map_data_render.js b/core/code/map_data_render.js index 62a5f79ba..fc1c423d9 100644 --- a/core/code/map_data_render.js +++ b/core/code/map_data_render.js @@ -46,7 +46,9 @@ window.Render.prototype.clearPortalsOutsideBounds = function (bounds) { var p = window.portals[guid]; // clear portals outside visible bounds - unless it's the selected portal, or it's relevant to artifacts if (!bounds.contains(p.getLatLng()) && guid !== window.selectedPortal && !window.artifact.isInterestingPortal(guid)) { - this.deletePortalEntity(guid); + // remove the marker as a layer first + // deletion will be done at endRenderPass + p.remove(); } } }; @@ -208,10 +210,7 @@ window.Render.prototype.endRenderPass = function () { // reorder portals to be after links/fields this.bringPortalsToFront(); - // re-select the selected portal, to re-render the side-bar. ensures that any data calculated from the map data is up to date - if (window.selectedPortal) { - window.renderPortalDetails(window.selectedPortal); - } + this.isRendering = false; }; /** @@ -305,6 +304,7 @@ window.Render.prototype.deleteFieldEntity = function (guid) { * @param {number} latE6 - The latitude of the portal in E6 format. * @param {number} lngE6 - The longitude of the portal in E6 format. * @param {string} team - The team faction of the portal. + * @param {number} [timestamp=0] - Timestamp of the portal data. Defaults to 0 to allow newer data sources to override * @param {number} [timestamp] - The timestamp of the portal data. */ window.Render.prototype.createPlaceholderPortalEntity = function (guid, latE6, lngE6, team, timestamp) { @@ -319,7 +319,7 @@ window.Render.prototype.createPlaceholderPortalEntity = function (guid, latE6, l var ent = [ guid, // ent[0] = guid - -1, // ent[1] = timestamp - zero will mean any other source of portal data will have a higher timestamp + timestamp, // ent[1] = timestamp // ent[2] = an array with the entity data [ 'p', // 0 - a portal @@ -329,22 +329,7 @@ window.Render.prototype.createPlaceholderPortalEntity = function (guid, latE6, l ], ]; - // placeholder portals don't have a useful timestamp value - so the standard code that checks for updated - // portal details doesn't apply - // so, check that the basic details are valid and delete the existing portal if out of date - var portalMoved = false; - if (guid in window.portals) { - var p = window.portals[guid]; - portalMoved = latE6 !== p.options.data.latE6 || lngE6 !== p.options.data.lngE6; - if (team !== p.options.data.team && p.options.timestamp < timestamp) { - // team - delete existing portal - this.deletePortalEntity(guid); - } - } - - if (!portalMoved) { - this.createPortalEntity(ent, 'core'); // placeholder - } + this.createPortalEntity(ent, 'core'); // placeholder }; /** @@ -362,102 +347,95 @@ window.Render.prototype.createPortalEntity = function (ent, details) { var previousData = undefined; var data = window.decodeArray.portal(ent[2], details); + var guid = ent[0]; + + // add missing fields + data.guid = guid; + if (!data.timestamp) { + data.timestamp = ent[1]; + } + + // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .data instead + data.ent = ent; // check if entity already exists - if (ent[0] in window.portals) { + const oldPortal = guid in window.portals; + + if (oldPortal) { // yes. now check to see if the entity data we have is newer than that in place - var p = window.portals[ent[0]]; + var p = window.portals[guid]; - if (!data.history || p.options.data.history === data.history) - if (p.options.timestamp >= ent[1]) { - return; // this data is identical or older - abort processing - } + if (!p.willUpdate(data)) { + // this data doesn't bring new detail - abort processing + // re-add the portal to the relevant layer (does nothing if already in the correct layer) + // useful for portals outside the view + this.addPortalToMapLayer(p); + return p; + } // the data we have is newer. many data changes require re-rendering of the portal // (e.g. level changed, so size is different, or stats changed so highlighter is different) - // so to keep things simple we'll always re-create the entity in this case // remember the old details, for the callback - - previousData = p.options.data; - - // preserve history - if (!data.history) { - data.history = previousData.history; - } - - this.deletePortalEntity(ent[0]); + previousData = $.extend(true, {}, p.getDetails()); } - var portalLevel = parseInt(data.level) || 0; - var team = window.teamStringToId(data.team); - // the data returns unclaimed portals as level 1 - but IITC wants them treated as level 0 - if (team === window.TEAM_NONE) portalLevel = 0; - var latlng = L.latLng(data.latE6 / 1e6, data.lngE6 / 1e6); - var dataOptions = { - level: portalLevel, - team: team, - ent: ent, // LEGACY - TO BE REMOVED AT SOME POINT! use .guid, .timestamp and .data instead - guid: ent[0], - timestamp: ent[1], - data: data, - }; - - window.pushPortalGuidPositionCache(ent[0], data.latE6, data.lngE6); - - var marker = window.createMarker(latlng, dataOptions); - - function handler_portal_click(e) { - window.renderPortalDetails(e.target.options.guid); - } - function handler_portal_dblclick(e) { - window.renderPortalDetails(e.target.options.guid); - window.map.setView(e.target.getLatLng(), window.DEFAULT_ZOOM); - } - function handler_portal_contextmenu(e) { - window.renderPortalDetails(e.target.options.guid); - if (window.isSmartphone()) { - window.show('info'); - } else if (!$('#scrollwrapper').is(':visible')) { - $('#sidebartoggle').click(); - } - } - - marker.on('click', handler_portal_click); - marker.on('dblclick', handler_portal_dblclick); - marker.on('contextmenu', handler_portal_contextmenu); - - window.runHooks('portalAdded', { portal: marker, previousData: previousData }); - - window.portals[ent[0]] = marker; + window.pushPortalGuidPositionCache(data.guid, data.latE6, data.lngE6); // check for URL links to portal, and select it if this is the one - if (window.urlPortalLL && window.urlPortalLL[0] === marker.getLatLng().lat && window.urlPortalLL[1] === marker.getLatLng().lng) { + if (window.urlPortalLL && window.urlPortalLL[0] === latlng.lat && window.urlPortalLL[1] === latlng.lng) { // URL-passed portal found via pll parameter - set the guid-based parameter - log.log('urlPortalLL ' + window.urlPortalLL[0] + ',' + window.urlPortalLL[1] + ' matches portal GUID ' + ent[0]); + log.log('urlPortalLL ' + window.urlPortalLL[0] + ',' + window.urlPortalLL[1] + ' matches portal GUID ' + data.guid); - window.urlPortal = ent[0]; + window.urlPortal = data.guid; window.urlPortalLL = undefined; // clear the URL parameter so it's not matched again } - if (window.urlPortal === ent[0]) { + if (window.urlPortal === data.guid) { // URL-passed portal found via guid parameter - set it as the selected portal log.log('urlPortal GUID ' + window.urlPortal + ' found - selecting...'); - window.selectedPortal = ent[0]; + window.selectedPortal = data.guid; window.urlPortal = undefined; // clear the URL parameter so it's not matched again } - // (re-)select the portal, to refresh the sidebar on any changes - if (ent[0] === window.selectedPortal) { - log.log('portal guid ' + ent[0] + ' is the selected portal - re-rendering portal details'); - window.renderPortalDetails(window.selectedPortal); + let marker = undefined; + if (oldPortal) { + // update marker style/highlight and layer + marker = window.portals[data.guid]; + + marker.updateDetails(data); + + window.runHooks('portalAdded', { portal: marker, previousData: previousData }); + } else { + marker = window.createMarker(latlng, data); + + // in case of incomplete data while having fresh details in cache, update the portal with those details + if (window.portalDetail.isFresh(guid)) { + var oldDetails = window.portalDetail.get(guid); + if (data.timestamp > oldDetails.timestamp) { + // data is more recent than the cached details so we remove them from the cache + window.portalDetail.remove(guid); + } else if (marker.willUpdate(oldDetails)) { + marker.updateDetails(oldDetails); + } + } + + window.runHooks('portalAdded', { portal: marker }); + + window.portals[data.guid] = marker; + + if (window.selectedPortal === data.guid) { + marker.renderDetails(); + } } window.ornaments.addPortal(marker); // TODO? postpone adding to the map layer this.addPortalToMapLayer(marker); + + return marker; }; /** @@ -482,7 +460,7 @@ window.Render.prototype.createFieldEntity = function (ent) { // create placeholder portals for field corners. we already do links, but there are the odd case where this is useful for (var i = 0; i < 3; i++) { var p = data.points[i]; - this.createPlaceholderPortalEntity(p.guid, p.latE6, p.lngE6, data.team, data.timestamp); + this.createPlaceholderPortalEntity(p.guid, p.latE6, p.lngE6, data.team, 0); } // check if entity already exists diff --git a/core/code/map_data_request.js b/core/code/map_data_request.js index 1402c4235..91a1073c1 100644 --- a/core/code/map_data_request.js +++ b/core/code/map_data_request.js @@ -65,14 +65,6 @@ window.MapDataRequest = function () { // ensure we have some initial map status this.setStatus('startup', undefined, -1); - - // add a portalDetailLoaded hook, so we can use the extended details to update portals on the map - var _this = this; - window.addHook('portalDetailLoaded', function (data) { - if (data.success) { - _this.render.createPortalEntity(data.ent, 'detailed'); - } - }); }; /** diff --git a/core/code/portal_detail.js b/core/code/portal_detail.js index 39f25db6e..aa38386ef 100644 --- a/core/code/portal_detail.js +++ b/core/code/portal_detail.js @@ -54,27 +54,24 @@ window.portalDetail.isFresh = function (guid) { return cache.isFresh(guid); }; +window.portalDetail.remove = function (guid) { + return cache.remove(guid); +}; + var handleResponse = function (deferred, guid, data, success) { if (!data || data.error || !data.result) { success = false; } if (success) { - var dict = window.decodeArray.portal(data.result, 'detailed'); - // entity format, as used in map data - var ent = [guid, dict.timestamp, data.result]; + var ent = [guid, data.result[13], data.result]; + var portal = window.mapDataRequest.render.createPortalEntity(ent, 'detailed'); - cache.store(guid, dict); - - // FIXME..? better way of handling sidebar refreshing... - - if (guid === window.selectedPortal) { - window.renderPortalDetails(guid); - } + cache.store(guid, portal.options.data); - deferred.resolve(dict); - window.runHooks('portalDetailLoaded', { guid: guid, success: success, details: dict, ent: ent }); + deferred.resolve(portal.options.data); + window.runHooks('portalDetailLoaded', { guid: guid, success: success, details: portal.options.data, ent: ent }); } else { if (data && data.error === 'RETRY') { // server asked us to try again diff --git a/core/code/portal_detail_display.js b/core/code/portal_detail_display.js index 7aa44fb6c..ab6231f3c 100644 --- a/core/code/portal_detail_display.js +++ b/core/code/portal_detail_display.js @@ -69,9 +69,12 @@ window.renderPortalUrl = function (lat, lng, title, guid) { * * @function renderPortalDetails * @param {string|null} guid - The globally unique identifier of the portal to display details for. + * @param {boolean} [forceSelect=false] - If true, forces the portal to be selected even if it's already the current portal. */ -window.renderPortalDetails = function (guid) { - window.selectPortal(window.portals[guid] ? guid : null); +window.renderPortalDetails = function (guid, forceSelect) { + if (forceSelect || window.selectedPortal !== guid) { + window.selectPortal(window.portals[guid] ? guid : null, 'renderPortalDetails'); + } if ($('#sidebar').is(':visible')) { window.resetScrollOnNewPortal(); window.renderPortalDetails.lastVisible = guid; @@ -95,34 +98,29 @@ window.renderPortalDetails = function (guid) { } var portal = window.portals[guid]; - var data = portal.options.data; - var details = window.portalDetail.get(guid); - var historyDetails = window.getPortalHistoryDetails(data); - - // details and data can get out of sync. if we have details, construct a matching 'data' - if (details) { - data = window.getPortalSummaryData(details); - } + var details = portal.getDetails(); + var hasFullDetails = portal.hasFullDetails(); + var historyDetails = window.getPortalHistoryDetails(details); - var modDetails = details ? '
' + window.getModDetails(details) + '
' : ''; - var miscDetails = details ? window.getPortalMiscDetails(guid, details) : ''; - var resoDetails = details ? window.getResonatorDetails(details) : ''; + var modDetails = hasFullDetails ? '
' + window.getModDetails(details) + '
' : ''; + var miscDetails = hasFullDetails ? window.getPortalMiscDetails(guid, details) : ''; + var resoDetails = hasFullDetails ? window.getResonatorDetails(details) : ''; // TODO? other status details... - var statusDetails = details ? '' : '
Loading details...
'; + var statusDetails = hasFullDetails ? '' : '
Loading details...
'; - var img = window.fixPortalImageUrl(details ? details.image : data.image); - var title = (details && details.title) || (data && data.title) || 'null'; + var img = window.fixPortalImageUrl(details.image); + var title = details.title || 'null'; - var lat = data.latE6 / 1e6; - var lng = data.lngE6 / 1e6; + var lat = details.latE6 / 1e6; + var lng = details.lngE6 / 1e6; var imgTitle = title + '\n\nClick to show full image.'; // portal level. start with basic data - then extend with fractional info in tooltip if available - var levelInt = window.teamStringToId(data.team) === window.TEAM_NONE ? 0 : data.level; + var levelInt = portal.options.level; var levelDetails = levelInt; - if (details) { + if (hasFullDetails) { levelDetails = window.getPortalLevel(details); if (levelDetails !== 8) { if (levelDetails === Math.ceil(levelDetails)) levelDetails += '\n8'; @@ -136,7 +134,7 @@ window.renderPortalDetails = function (guid) { $('#portaldetails') .html('') // to ensure it's clear - .attr('class', window.TEAM_TO_CSS[window.teamStringToId(data.team)]) + .attr('class', window.TEAM_TO_CSS[window.teamStringToId(details.team)]) .append( $('

', { class: 'title' }) .text(title) @@ -147,7 +145,7 @@ window.renderPortalDetails = function (guid) { style: 'float: left', }) .click(function () { - window.zoomToAndShowPortal(guid, [data.latE6 / 1e6, data.lngE6 / 1e6]); + window.zoomToAndShowPortal(guid, [details.latE6 / 1e6, details.lngE6 / 1e6]); if (window.isSmartphone()) { window.show('map'); } @@ -163,9 +161,6 @@ window.renderPortalDetails = function (guid) { .text('X') .click(function () { window.renderPortalDetails(null); - if (window.isSmartphone()) { - window.show('map'); - } }), // help cursor via ".imgpreview img" @@ -187,9 +182,12 @@ window.renderPortalDetails = function (guid) { window.renderPortalUrl(lat, lng, title, guid); + // compatibility + var data = hasFullDetails ? window.getPortalSummaryData(details) : details; + // only run the hooks when we have a portalDetails object - most plugins rely on the extended data // TODO? another hook to call always, for any plugins that can work with less data? - if (details) { + if (hasFullDetails) { window.runHooks('portalDetailsUpdated', { guid: guid, portal: portal, portalDetails: details, portalData: data }); } }; @@ -337,7 +335,7 @@ window.setPortalIndicators = function (p) { * @param {string} guid - The GUID of the portal to select. * @returns {boolean} True if the same portal is re-selected (just an update), false if a different portal is selected. */ -window.selectPortal = function (guid) { +window.selectPortal = function (guid, event) { var update = window.selectedPortal === guid; var oldPortalGuid = window.selectedPortal; window.selectedPortal = guid; @@ -346,20 +344,18 @@ window.selectPortal = function (guid) { var newPortal = window.portals[guid]; // Restore style of unselected portal - if (!update && oldPortal) window.setMarkerStyle(oldPortal, false); + if (!update && oldPortal) oldPortal.setSelected(false); // Change style of selected portal - if (newPortal) { - window.setMarkerStyle(newPortal, true); - - if (window.map.hasLayer(newPortal)) { - newPortal.bringToFront(); - } - } + if (newPortal) newPortal.setSelected(true); window.setPortalIndicators(newPortal); - window.runHooks('portalSelected', { selectedPortalGuid: guid, unselectedPortalGuid: oldPortalGuid }); + window.runHooks('portalSelected', { + selectedPortalGuid: guid, + unselectedPortalGuid: oldPortalGuid, + event: event, + }); return update; }; diff --git a/core/code/portal_highlighter.js b/core/code/portal_highlighter.js index a55b1b138..0156d2454 100644 --- a/core/code/portal_highlighter.js +++ b/core/code/portal_highlighter.js @@ -61,12 +61,10 @@ window.updatePortalHighlighterControl = function () { if (window._highlighters !== null) { if ($('#portal_highlight_select').length === 0) { - $('body').append(""); + $('.leaflet-top.leaflet-left').first().append(""); $('#portal_highlight_select').change(function () { window.changePortalHighlights($(this).val()); }); - $('.leaflet-top.leaflet-left').css('padding-top', '20px'); - $('.leaflet-control-scale-line').css('margin-top', '25px'); } $('#portal_highlight_select').html(''); $('#portal_highlight_select').append($('