diff --git a/.gitignore b/.gitignore index 36f2b739..3ed5199b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,5 @@ code/*.bat code/package-lock.json package-lock.json -docs/* !docs/Licenses !docs/README.md diff --git a/docs/adr/ADR-0000-Title-of-ADR.md b/docs/adr/ADR-0000-Title-of-ADR.md new file mode 100644 index 00000000..a253dfc8 --- /dev/null +++ b/docs/adr/ADR-0000-Title-of-ADR.md @@ -0,0 +1,46 @@ + + +# ADR-0000: Title of ADR + +## Status + +Proposed/Accepted/Rejected/Superseded by ADR-XXXX + +## Context + +What is the issue that we're seeing that is motivating this decision or change? +What is the current architectural state relevant to this decision? +What are the constraints (technical, business, etc.)? + +## Decision Drivers + +- Driver 1 +- Driver 2 +- ... + +## Considered Options + +- Option 1 + - Pros: + - Cons: +- Option 2 + - Pros: + - Cons: +- ... + +## Decision Outcome + +Chosen option: "Option X", because [justification. e.g., only option that meets k.o. criteria decision driver | satisfies critical requirement | ... | comes out best (see below)]. +Positive consequences: + +- ... + Negative consequences: +- ... + +## Links + +- Link to related issues, discussions, or documents. + +## Date + +YYYY-MM-DD \ No newline at end of file diff --git a/docs/adr/ADR-0001-Google-Markers-Draw-Performance.md b/docs/adr/ADR-0001-Google-Markers-Draw-Performance.md new file mode 100644 index 00000000..77dc37b8 --- /dev/null +++ b/docs/adr/ADR-0001-Google-Markers-Draw-Performance.md @@ -0,0 +1,57 @@ + + +# ADR-0001: Google Markers and Marker Cluster draw phase + +## Status + +Accepted + +## Context + +When a developer places a large number of Markers on a map with clustering enabled, the rendering process becomes overwhelming. This causes the browser to freeze, resulting in significant delays before the Markers and Marker Clusters appear to the user. + +After investigation, it was perceived that adding Markers to the cluster was the root cause of this behavior. Specifically, every time a new Marker is added to a cluster, a redraw is triggered using the existing visual tree. The operation to add a new Marker to the visual tree is computationally expensive and causes the browser to crash or hang. + +Furthermore, in a method called after the addition of the Marker to the cluster, a repaint of the Marker Cluster is triggered. This action is less expensive than the "redraw after add" because it creates a new visual tree rather than modifying the existing one. + +Constraints: + +- Avoid breaking changes for the developer and the runtime experience. +- Keep fixes minimal and localized so they are easier for developers to adopt. + +## Decision Drivers + +- Keep changes minimal and reversible. +- Maintain existing behavior. +- Improve the performance of the overall interaction. + +## Considered Options + +- Option 1: Create new client actions for bulk additions + - Pros: This would optimize the overall draw flow, making it as efficient as possible. It avoids multiple redraws by calling for a repaint only after all new Markers are added. + + - Cons: This would require a higher effort from developers to implement the changes. Additionally, it would necessitate further documentation and significant changes to the codebase to achieve the desired behavior. +- Option 2: Disable draw on addition moment but keep the repaint after addition + - Pros: The difference in performance is significant. It would require no changes for developers to adopt and no need for new documentation. + - Cons: Some discarded repaints will occur, meaning the performance will not be theoretically optimal, though still improved. + +## Decision Outcome + +Chosen option: "Option 2", because it is the only option that allows for performance benefits to reach the majority of users and developers quickly. It is also the best option to ensure that minimal changes are implemented with minimal impact on already developed applications. + +Positive consequences: + +- Increased overall performance of Markers, especially when using Marker Clusters. + +Negative consequences: + +- No negative consequence found. + +## Links + +- ROU-12504 + + +## Date + +2026-02-10 \ No newline at end of file diff --git a/docs/adr/Readme.md b/docs/adr/Readme.md new file mode 100644 index 00000000..4b62fad4 --- /dev/null +++ b/docs/adr/Readme.md @@ -0,0 +1,31 @@ +# Architecture Decision Records (ADRs) + +This directory contains Architecture Decision Records (ADRs) for this project. +ADRs are short documents that capture important architectural decisions, along with their context and consequences. + +## Purpose + +- To document significant architectural decisions. +- To provide context for why decisions were made. +- To help onboard new team members. +- To facilitate future architectural discussions and evolution. +- To provide context to AI-powered development assistants. + +## Format + +Each ADR should follow the template in `ADR-0000-Title-of-ADR.md`. + +## Process + +1. **Propose:** Copy `ADR-0000-Title-of-ADR.md` to a new file named `NNNN-title-of-adr.md`, where `NNNN` is the next sequential number and the rest is a dash-separated, lowercase version of the title. +2. **Discuss:** Fill out the ADR and discuss it with the team. +3. **Decide:** Once a decision is reached, update the status in the ADR (e.g., "Accepted", "Rejected", "Superseded"). +4. **Commit:** Commit the ADR to the repository. + +## ADR Log + +| ADR Number | Title | Status | Date | +| :--------- | :---------------------------------------------------------------- | :------- | :---------------------------------- | +| ADR-0000 | Template for ADRs | Meta | 2026-02-04 | +| ADR-0001 | Use AI for Development Assistance | Accepted | 2026-02-04 | +| ADR-0002 | Highcharts TypeScript typings compatibility fixes v12 Assistance | Accepted | 2026-02-04 | diff --git a/src/OutSystems/Maps/MapAPI/MarkerManager.ts b/src/OutSystems/Maps/MapAPI/MarkerManager.ts index 811fa123..2d629d9d 100644 --- a/src/OutSystems/Maps/MapAPI/MarkerManager.ts +++ b/src/OutSystems/Maps/MapAPI/MarkerManager.ts @@ -1,7 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-unused-vars namespace OutSystems.Maps.MapAPI.MarkerManager { - const markerMap = new Map(); //marker.uniqueId -> map.uniqueId - const markerArr = new Array(); + const markerMap = new Map(); /** * Gets the Map to which the Marker belongs to @@ -10,14 +9,10 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { * @returns {*} {MarkerMapper} this structure has the id of Map, and the reference to the instance of the Map */ function GetMapByMarkerId(markerId: string): OSFramework.Maps.OSMap.IMap { - let map: OSFramework.Maps.OSMap.IMap; + let map: OSFramework.Maps.OSMap.IMap = markerMap.get(markerId)?.map; - //markerId is the UniqueId - if (markerMap.has(markerId)) { - map = MapManager.GetMapById(markerMap.get(markerId), false); - } //UniqueID not found - else { + if (map === undefined) { // Try to find its reference on DOM const elem = OSFramework.Maps.Helper.GetElementByUniqueId(markerId, false); @@ -38,11 +33,7 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { * @param {string} markerId Id of the Marker to be removed */ function CleanMarkerArrays(markerId: string): void { - markerMap.has(markerId) && markerMap.delete(markerId); - const idx = markerArr.findIndex((marker) => marker?.equalsToID(markerId)); - if (idx !== -1) { - markerArr.splice(idx, 1); - } + markerMap.delete(markerId); } /** @@ -69,8 +60,7 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { OSFramework.Maps.Enum.MarkerType.Marker, JSON.parse(configs) ); - markerArr.push(marker); - markerMap.set(markerId, map.uniqueId); + markerMap.set(markerId, marker); map.addMarker(marker); responseObj.message = markerId; @@ -189,8 +179,7 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { OSFramework.Maps.Enum.MarkerType.Marker, JSON.parse(configs) ); - markerArr.push(_marker); - markerMap.set(markerId, map.uniqueId); + markerMap.set(markerId, _marker); map.addMarker(_marker); return _marker; @@ -223,8 +212,7 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { markerType, JSON.parse(configs) ); - markerArr.push(_marker); - markerMap.set(markerId, map.uniqueId); + markerMap.set(markerId, _marker); map.addMarker(_marker); Events.CheckPendingEvents(_marker); @@ -239,7 +227,8 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { * @param markerId Id of the Marker */ export function GetMarkerById(markerId: string, raiseError = true): OSFramework.Maps.Marker.IMarker { - let marker: OSFramework.Maps.Marker.IMarker = markerArr.find((p) => p && p.equalsToID(markerId)); + let marker: OSFramework.Maps.Marker.IMarker = + markerMap.get(markerId) ?? Array.from(markerMap.values()).find((value) => value.equalsToID(markerId)); // if didn't found marker, check if it was draw by the DrawingTools if (marker === undefined) { @@ -262,6 +251,7 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { throw new Error(`Marker id:${markerId} not found`); } } + return marker; } @@ -318,8 +308,8 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { MapManager.RemoveMarkers(mapId); } - for (const [storedMarkerId, storedMapId] of markerMap) { - if (storedMapId === mapId) { + for (const [storedMarkerId, marker] of markerMap) { + if (marker.map !== undefined && marker.map.equalsToID(mapId)) { const marker = GetMarkerById(storedMarkerId, false); if (marker && marker.widgetId === undefined) { // If the marker does not have a widgetId, it means it was created by @@ -356,8 +346,8 @@ namespace OutSystems.Maps.MapAPI.MarkerManager { } // Second remove the markers to destroy from local variables. - markerMap.forEach((storedMapId, storedMarkerId) => { - if (mapId === storedMapId) { + markerMap.forEach((marker, storedMarkerId) => { + if (marker.map !== undefined && marker.map.equalsToID(mapId)) { CleanMarkerArrays(storedMarkerId); } }); diff --git a/src/Providers/Maps/Google/Features/GoogleMarkerClusterer.ts b/src/Providers/Maps/Google/Features/GoogleMarkerClusterer.ts index 09e10dbd..d8f01b02 100644 --- a/src/Providers/Maps/Google/Features/GoogleMarkerClusterer.ts +++ b/src/Providers/Maps/Google/Features/GoogleMarkerClusterer.ts @@ -94,8 +94,9 @@ namespace Provider.Maps.Google.Feature { public addMarker(marker: OSFramework.Maps.Marker.IMarker): void { if (this.isEnabled && marker.isReady) { - // We need to make sure that a redraw is triggered whenever a new marker is added to the clusters - this._markerClusterer.addMarker(marker.provider as GoogleMapsMarker, false); + // The noDraw was set to true to avoid a redraw of the map everytime when a new marker is added to the clusters + // This will not impact the draw of the marker since we are calling the repaint function after the addMarker function + this._markerClusterer.addMarker(marker.provider as GoogleMapsMarker, true); } } @@ -151,8 +152,9 @@ namespace Provider.Maps.Google.Feature { public removeMarker(marker: OSFramework.Maps.Marker.IMarker): void { if (this.isEnabled && marker.isReady) { - // We need to make sure that a redraw is triggered whenever a new marker is removed from the clusters - this._markerClusterer?.removeMarker(marker.provider as GoogleMapsMarker, false); + // The noDraw was set to true to avoid a redraw of the map everytime when a new marker is added to the clusters + // This will not impact the draw of the marker since we are calling the repaint function after the addMarker function + this._markerClusterer?.removeMarker(marker.provider as GoogleMapsMarker, true); } }