diff --git a/src/OSFramework/Maps/Enum/ErrorCodes.ts b/src/OSFramework/Maps/Enum/ErrorCodes.ts index 58b772af..905f4227 100644 --- a/src/OSFramework/Maps/Enum/ErrorCodes.ts +++ b/src/OSFramework/Maps/Enum/ErrorCodes.ts @@ -56,6 +56,11 @@ namespace OSFramework.Maps.Enum { API_FailedRemoveMarkerFromCluster = 'MAPS-API-09001', API_FailedContainsLocation = 'MAPS-API-05005', API_FailedGettingCenterCoordinates = 'MAPS-API-01001', + API_FailedCreateMarker = 'MAPS-API-09002', + API_FailedRemoveMarker = 'MAPS-API-09003', + API_FailedRemoveMarkers = 'MAPS-API-09004', + API_FailedSubscribeMarkerEvent = 'MAPS-API-09005', + API_FailedUnsubscribeMarkerEvent = 'MAPS-API-09006', // Error Codes - GENeral error - General or internal Errors of the component. In the situation of simple components without different features/sections inside it, the GEN acronym should be used. GEN_InvalidChangePropertyMap = 'MAPS-GEN-01001', diff --git a/src/OSFramework/Maps/Event/Marker/MarkerEventsManager.ts b/src/OSFramework/Maps/Event/Marker/MarkerEventsManager.ts index c11bef5a..2aca2310 100644 --- a/src/OSFramework/Maps/Event/Marker/MarkerEventsManager.ts +++ b/src/OSFramework/Maps/Event/Marker/MarkerEventsManager.ts @@ -72,9 +72,12 @@ namespace OSFramework.Maps.Event.Marker { // The following events are being deprecated. They should get removed soon. switch (eventType) { case MarkerEventType.Initialized: + case MarkerEventType.OnMouseout: + case MarkerEventType.OnMouseover: handlerEvent.trigger( this._marker.map.widgetId, // Id of Map block that was initialized this._marker.widgetId || this._marker.uniqueId, // Id of Marker block that was initialized + eventType, this._marker.index // Index of Marker block that was initialized ); break; @@ -82,17 +85,10 @@ namespace OSFramework.Maps.Event.Marker { handlerEvent.trigger( this._marker.map.widgetId, // Id of Map block that was clicked this._marker.widgetId || this._marker.uniqueId, // Id of Marker block that was clicked + eventType, ...args // Coordinates retrieved from the marker event that got triggered ); break; - case MarkerEventType.OnMouseout: - case MarkerEventType.OnMouseover: - handlerEvent.trigger( - this._marker.map.widgetId, // Id of Map block that was clicked - this._marker.widgetId || this._marker.uniqueId, // Id of Marker block that was clicked - this._marker.index // Index of Marker block that was clicked - ); - break; case MarkerEventType.OnEventTriggered: handlerEvent.trigger( this._marker.map.widgetId, // Id of Map block that triggered the event diff --git a/src/OSFramework/Maps/OSMap/AbstractMap.ts b/src/OSFramework/Maps/OSMap/AbstractMap.ts index 7da7b262..5cb63cb8 100644 --- a/src/OSFramework/Maps/OSMap/AbstractMap.ts +++ b/src/OSFramework/Maps/OSMap/AbstractMap.ts @@ -10,6 +10,7 @@ namespace OSFramework.Maps.OSMap { private _heatmapLayersSet: Set; private _isReady: boolean; private _mapEvents: Event.OSMap.MapEventsManager; + private _mapRefreshRequest: number; private _mapType: Enum.MapType; private _markers: Map; private _markersSet: Set; @@ -38,6 +39,7 @@ namespace OSFramework.Maps.OSMap { this._isReady = false; this._mapEvents = new Event.OSMap.MapEventsManager(this); this._mapType = mapType; + this._mapRefreshRequest = 0; this._providerType = providerType; this._zoomChanged = false; this._mapZoomChangeCallback = this._mapZoomChangeHandler.bind(this); @@ -156,6 +158,13 @@ namespace OSFramework.Maps.OSMap { this._widgetId = Helper.GetElementByUniqueId(this.uniqueId).closest(this.mapTag).id; } + public cancelScheduledResfresh(): void { + if (this._mapRefreshRequest !== 0) { + clearTimeout(this._mapRefreshRequest); + this._mapRefreshRequest = 0; + } + } + public changeProperty(propertyName: string, propertyValue: unknown): void { //Update Map's config when the property is available if (this.config.hasOwnProperty(propertyName)) { @@ -404,6 +413,14 @@ namespace OSFramework.Maps.OSMap { } } + public scheduleRefresh(): void { + this.cancelScheduledResfresh(); + + this._mapRefreshRequest = setTimeout(() => { + this.refresh(); + }, 0); + } + public updateHeight(): void { // Because only some specific map providers like Leaflet Provider need to update or refresh the map after changing its height, // this function doesn't need any logic diff --git a/src/OSFramework/Maps/OSMap/IMap.ts b/src/OSFramework/Maps/OSMap/IMap.ts index e95f8ed7..0fe05c9b 100644 --- a/src/OSFramework/Maps/OSMap/IMap.ts +++ b/src/OSFramework/Maps/OSMap/IMap.ts @@ -65,6 +65,12 @@ namespace OSFramework.Maps.OSMap { * @returns Shape that has been created */ addShape(shape: OSFramework.Maps.Shape.IShape): OSFramework.Maps.Shape.IShape; + /** + * Stops the current refresh from being refreshed. + * + * @memberof IMap + */ + cancelScheduledResfresh(): void; /** * Change property of a drawingTools from the DrawingTools by specifying the property name and the new value * @param drawingToolsId id of the DrawingTools @@ -194,12 +200,17 @@ namespace OSFramework.Maps.OSMap { * @param shapeId id of the shape */ removeShape(shapeId: string): void; + /** + * Schedules the map to be refreshed so, that the bulk operations are made fast. + * @memberof IMap + */ + scheduleRefresh(): void; /** * Adds a custom render to the Clusters. Applicable to some providers only. * @param {OSFramework.Maps.Feature.IMarkerClusterer} renderer * @memberof IMap */ - setClusterRenderer?(renderer: OSFramework.Maps.Feature.IMarkerClustererRender); + setClusterRenderer?(renderer: OSFramework.Maps.Feature.IMarkerClustererRender): void; /** * Updates the Height of the Map by refreshing/updating the provider */ diff --git a/src/OutSystems/Maps/MapAPI/MapManager.ts b/src/OutSystems/Maps/MapAPI/MapManager.ts index b4d3687b..697eff36 100644 --- a/src/OutSystems/Maps/MapAPI/MapManager.ts +++ b/src/OutSystems/Maps/MapAPI/MapManager.ts @@ -79,16 +79,23 @@ namespace OutSystems.Maps.MapAPI.MapManager { export function GetMapById(mapId: string, raiseError = true): OSFramework.Maps.OSMap.IMap { let map: OSFramework.Maps.OSMap.IMap; - //mapId is the UniqueId if (maps.has(mapId)) { + // mapId = uniqueId map = maps.get(mapId); } else { - //Search for WidgetId - for (const p of maps.values()) { - if (p.equalsToID(mapId)) { - map = p; - break; - } + // map = widgetId + // Search for (all) the map(s) that have this WidgetId + const mapFiltered = Array.from(maps.values()).filter((p) => p.equalsToID(mapId)); + + // There can be situations, for example when changing from a page + // to another page that also has a "Map", in which, we'll end up + // having 2 maps, with different uniqueIds, but same Widget id. + if (mapFiltered.length > 0) { + // So we'll always pick the last map of that was found and it + // will correspond to the last map that was created in the app + // (in these cases, the new map). The other map, is the one + // that will be destroyed afterwards. + map = mapFiltered[mapFiltered.length - 1]; } } @@ -148,6 +155,9 @@ namespace OutSystems.Maps.MapAPI.MapManager { } map.dispose(); + + //Let's remove the empty markers references from the Markers API. + MarkerManager.RemoveAllMarkers(mapId, false); } /** diff --git a/src/OutSystems/Maps/MapAPI/MarkerManager.Events.ts b/src/OutSystems/Maps/MapAPI/MarkerManager.Events.ts index b78c5116..585b736f 100644 --- a/src/OutSystems/Maps/MapAPI/MarkerManager.Events.ts +++ b/src/OutSystems/Maps/MapAPI/MarkerManager.Events.ts @@ -60,16 +60,30 @@ namespace OutSystems.Maps.MapAPI.MarkerManager.Events { * @export * @param {string} markerId Marker where the events will get attached * @param {OSFramework.Maps.Event.Marker.MarkerEventType} eventName name of the event to get attached - * @param {OSFramework.Maps.Callbacks.Marker.ClickEvent} callback to be invoked when the event occurs + * @param {OSFramework.Maps.Callbacks.Marker.Event} callback to be invoked when the event occurs */ export function Subscribe( markerId: string, eventName: OSFramework.Maps.Event.Marker.MarkerEventType, - // eslint-disable-next-line callback: OSFramework.Maps.Callbacks.Marker.Event - ): void { - const marker = GetMarkerById(markerId); - marker.markerEvents.addHandler(eventName, callback, markerId); + ): string { + const responseObj = { + isSuccess: true, + message: 'Success', + code: '200', + }; + try { + const marker = GetMarkerById(markerId); + marker.markerEvents.addHandler(eventName, callback, markerId); + // Let's make sure the events get refreshed on the Marker provider + marker.refreshProviderEvents(); + } catch (error) { + responseObj.isSuccess = false; + responseObj.message = error.message; + responseObj.code = OSFramework.Maps.Enum.ErrorCodes.API_FailedSubscribeMarkerEvent; + } + + return JSON.stringify(responseObj); } /** @@ -78,7 +92,7 @@ namespace OutSystems.Maps.MapAPI.MarkerManager.Events { * @export * @param {string} eventUniqueId Id of the Event to be attached * @param {OSFramework.Maps.Event.Map.MapEventType} eventName name fo the event to be attached - * @param {MapAPI.Callbacks.OSMap.Event} callback callback to be invoked when the event occurs + * @param {OSFramework.Maps.Callbacks.Marker.Event} callback callback to be invoked when the event occurs */ export function SubscribeByUniqueId( eventUniqueId: string, @@ -135,12 +149,11 @@ namespace OutSystems.Maps.MapAPI.MarkerManager.Events { * @export * @param {string} eventUniqueId Map where the event will be removed * @param {OSFramework.Maps.Event.Map.MapEventType} eventName name of the event to be removed - * @param {MapAPI.Callbacks.OSMap.Event} callback callback that will be removed + * @param {OSFramework.Maps.Callbacks.Marker.Event} callback callback that will be removed */ export function Unsubscribe( eventUniqueId: string, eventName: OSFramework.Maps.Event.Marker.MarkerEventType, - // eslint-disable-next-line callback: OSFramework.Maps.Callbacks.Marker.Event ): void { const markerId = GetMarkerIdByEventUniqueId(eventUniqueId); @@ -160,6 +173,32 @@ namespace OutSystems.Maps.MapAPI.MarkerManager.Events { } } } + + export function UnsubscribeByMarkerId( + markerId: string, + eventName: OSFramework.Maps.Event.Marker.MarkerEventType, + callback: OSFramework.Maps.Callbacks.Marker.Event + ): string { + const responseObj = { + isSuccess: true, + message: 'Success', + code: '200', + }; + try { + const marker = GetMarkerById(markerId); + if (marker !== undefined) { + marker.markerEvents.removeHandler(eventName, callback); + // Let's make sure the events get refreshed on the Marker provider + marker.refreshProviderEvents(); + } + } catch (error) { + responseObj.isSuccess = false; + responseObj.message = error.message; + responseObj.code = OSFramework.Maps.Enum.ErrorCodes.API_FailedUnsubscribeMarkerEvent; + } + + return JSON.stringify(responseObj); + } } /// Overrides for the old namespace - calls the new one, lets users know this is no longer in use diff --git a/src/OutSystems/Maps/MapAPI/MarkerManager.ts b/src/OutSystems/Maps/MapAPI/MarkerManager.ts index dbc1a573..ea988b6a 100644 --- a/src/OutSystems/Maps/MapAPI/MarkerManager.ts +++ b/src/OutSystems/Maps/MapAPI/MarkerManager.ts @@ -3,6 +3,44 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { const markerMap = new Map(); //marker.uniqueId -> map.uniqueId const markerArr = new Array(); + /** + * Creates and adds a marker to a map. + * + * @export + * @param {string} mapId Id of the map to which the marker should be added + * @param {string} configs Configurations for the marker + * @return {*} {string} + */ + export function AddMarker(mapId: string, configs: string): string { + const responseObj = { + isSuccess: true, + message: 'Success', + code: '200', + }; + + try { + const map = MapManager.GetMapById(mapId, true); + const markerId = OSFramework.Maps.Helper.GenerateUniqueId(); + const marker = OSFramework.Maps.Marker.MarkerFactory.MakeMarker( + map, + markerId, + OSFramework.Maps.Enum.MarkerType.Marker, + JSON.parse(configs) + ); + markerArr.push(marker); + markerMap.set(markerId, map.uniqueId); + map.addMarker(marker); + + responseObj.message = markerId; + } catch (error) { + responseObj.isSuccess = false; + responseObj.message = error.message; + responseObj.code = OSFramework.Maps.Enum.ErrorCodes.API_FailedCreateMarker; + } + + return JSON.stringify(responseObj); + } + /** * Changes the property value of a given Marker. * @@ -226,18 +264,71 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { * @export * @param {string} markerID id of the Marker that is about to be removed */ - export function RemoveMarker(markerId: string): void { - const marker = GetMarkerById(markerId); - const map = marker.map; + export function RemoveMarker(markerId: string): string { + const responseObj = { + isSuccess: true, + message: 'Success', + code: '200', + }; + try { + const marker = GetMarkerById(markerId); + const map = marker.map; + map && map.removeMarker(markerId); + + markerMap.delete(markerId); + markerArr.splice( + markerArr.findIndex((p) => { + return p && p.equalsToID(markerId); + }), + 1 + ); + } catch (error) { + responseObj.isSuccess = false; + responseObj.message = error.message; + responseObj.code = OSFramework.Maps.Enum.ErrorCodes.API_FailedRemoveMarker; + } - map && map.removeMarker(markerId); - markerMap.delete(markerId); - markerArr.splice( - markerArr.findIndex((p) => { - return p && p.equalsToID(markerId); - }), - 1 - ); + return JSON.stringify(responseObj); + } + + /** + * Removes all the markers of a given map. + * + * @export + * @param {string} mapId + * @return {*} {string} + */ + export function RemoveAllMarkers(mapId: string, removeFromMap = true): string { + const responseObj = { + isSuccess: true, + message: 'Success', + code: '200', + }; + try { + if (removeFromMap) { + // First we try to remove the markers from the map. + MapManager.RemoveMarkers(mapId); + } + + // Second remove the markers to destroy from local variables. + markerMap.forEach((storedMapId, storedMarkerId) => { + if (mapId === storedMapId) { + markerMap.delete(storedMarkerId); + markerArr.splice( + markerArr.findIndex((p) => { + return p && p.equalsToID(storedMarkerId); + }), + 1 + ); + } + }); + } catch (error) { + responseObj.isSuccess = false; + responseObj.message = error.message; + responseObj.code = OSFramework.Maps.Enum.ErrorCodes.API_FailedRemoveMarkers; + } + + return JSON.stringify(responseObj); } } diff --git a/src/Providers/Maps/Google/Marker/Marker.ts b/src/Providers/Maps/Google/Marker/Marker.ts index 4143d390..7a048a26 100644 --- a/src/Providers/Maps/Google/Marker/Marker.ts +++ b/src/Providers/Maps/Google/Marker/Marker.ts @@ -101,6 +101,7 @@ namespace Provider.Maps.Google.Marker { // OnClick Event (OS accelerator) if (this.markerEvents.hasHandlers(OSFramework.Maps.Event.Marker.MarkerEventType.OnClick)) { + this._addedEvents.push('click'); this._provider.addListener('click', (e: google.maps.MapMouseEvent) => { const coordinates = new OSFramework.Maps.OSStructures.OSMap.OSCoordinates( e.latLng.lat(), @@ -167,6 +168,7 @@ namespace Provider.Maps.Google.Marker { const markerPosition = this._buildMarkerPosition(); // If markerPosition is undefined (should be a promise) -> don't create the marker if (markerPosition !== undefined) { + this.map.cancelScheduledResfresh(); markerPosition .then((markerOptions) => { //The marker was destroyed while waiting for the promise. @@ -188,7 +190,7 @@ namespace Provider.Maps.Google.Marker { this.finishBuild(); // Trigger the new center location after creating the marker - this.map.refresh(); + this.map.scheduleRefresh(); }) .catch((error) => { this.map.mapEvents.trigger( diff --git a/src/Providers/Maps/Leaflet/Marker/Marker.ts b/src/Providers/Maps/Leaflet/Marker/Marker.ts index a1cd3c85..4aa05bd8 100644 --- a/src/Providers/Maps/Leaflet/Marker/Marker.ts +++ b/src/Providers/Maps/Leaflet/Marker/Marker.ts @@ -156,6 +156,7 @@ namespace Provider.Maps.Leaflet.Marker { // OnClick Event if (this.markerEvents.hasHandlers(OSFramework.Maps.Event.Marker.MarkerEventType.OnClick)) { + this._addedEvents.push('click'); this._provider.addEventListener('click', (e?: L.LeafletMouseEvent) => { const coordinates = new OSFramework.Maps.OSStructures.OSMap.OSCoordinates( e.latlng.lat, @@ -223,6 +224,7 @@ namespace Provider.Maps.Leaflet.Marker { const provider_configs = this.getProviderConfig() as L.MarkerOptions; // If markerOptions is undefined (should be a promise) -> don't create the marker if (markerLocation !== undefined) { + this.map.cancelScheduledResfresh(); markerLocation .then((location: L.LatLng) => { //The marker was destroyed while waiting for the promise. @@ -240,7 +242,7 @@ namespace Provider.Maps.Leaflet.Marker { this.finishBuild(); // Trigger the new center location after creating the marker - this.map.refresh(); + this.map.scheduleRefresh(); }) .catch(() => { this.map.mapEvents.trigger(