From ca839ff06db3ff790d0fa328823ef9cd4decb47b Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sat, 7 Jun 2025 00:29:00 -0400 Subject: [PATCH 01/16] First Steps to Convert to SDK. --- .gitignore | 2 + GoogleLinkEnhancer.js | 337 +++++++++++++++++++++++++++++------------- biome.json | 35 +++++ package.json | 7 + 4 files changed, 280 insertions(+), 101 deletions(-) create mode 100644 .gitignore create mode 100644 biome.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ccb2c80 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +package-lock.json \ No newline at end of file diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index 69ddf8f..834d7e8 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -5,6 +5,7 @@ // @description Adds some extra WME functionality related to Google place links. // @author MapOMatic, WazeDev group // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ +// @require https://cdn.jsdelivr.net/npm/@turf/turf@7.2.0/turf.min.js // @license GNU GPLv3 // ==/UserScript== @@ -15,7 +16,10 @@ /* eslint-disable max-classes-per-file */ // eslint-disable-next-line func-names -const GoogleLinkEnhancer = (function() { +// import * as turf from "@turf/turf"; +// import { WmeSDK } from "wme-sdk-typings"; + +const GoogleLinkEnhancer = ((() => { 'use strict'; class GooglePlaceCache { @@ -87,23 +91,83 @@ const GoogleLinkEnhancer = (function() { tooFar: 'The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.' }; + #styleConfig = { + styleContext: { + highNodeColor: (context) => { + return context?.feature?.properties?.style?.strokeColor; + }, + strokeColor: (context) => { + return context?.feature?.properties?.style?.strokeColor; + }, + strokeWidth: (context) => { + return context?.feature?.properties?.style?.strokeWidth; + }, + strokeDashStyle: (context) => { + return context?.feature?.properties?.style?.strokeDashstyle; + }, + }, + styleRules: [ + { + predicate: (properties) => { return properties.styleName === "lineStyle"; }, + style: { + strokeColor: "${strokeColor}", + }, + }, + { + predicate: (properties) => { return properties.styleName === "default"; }, + style: { + strokeColor: '${strokeColor}', + strokeWidth: '${strokeWidth}', + strokeDashstyle: '${strokeDashstyle}', + pointRadius: 15, + fillOpacity: 0 + } + }, + { + predicate: (properties) => { return properties.styleName === "venueStyle"; }, + style: { + strokeColor: '${strokeColor}', + strokeWidth: '${strokeWidth}', + } + }, + { + predicate: (properties) => { return properties.styleName === "placeStyle"; }, + style: { + strokeColor: '${strokeColor}', + strokeWidth: '${strokeWidth}', + strokeDashStyle: "${strokeDashStyle}" + } + } + ], + }; + + /* eslint-enable no-unused-vars */ - constructor() { + constructor(sdk = undefined) { + if(!sdk) { + const msg = "SDK Must be defined to use GLE"; + console.error(msg); + throw new Error(msg); + } + this.sdk = sdk; this.linkCache = new GooglePlaceCache(); this.#initLayer(); // NOTE: Arrow functions are necessary for calling methods on object instances. // This could be made more efficient by only processing the relevant places. W.model.events.register('mergeend', null, () => { this.#processPlaces(); }); - W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); - W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); - W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); + // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); + // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); + // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); + this.sdk.Events.on({eventName:"wme-data-model-objects-added", eventHandler: () => { this.#processPlaces();}}); + this.sdk.Events.on({eventName:"wme-data-model-objects-removed", eventHandler: () => { this.#processPlaces();}}); + this.sdk.Events.on({eventName:"wme-data-model-objects-changed", eventHandler: () => { this.#processPlaces();}}); // This is a special event that will be triggered when DOM elements are destroyed. /* eslint-disable wrap-iife, func-names, object-shorthand */ - (function($) { + (($) => { $.event.special.destroyed = { - remove: function(o) { + remove: (o) => { if (o.handler && o.type !== 'destroyed') { o.handler(); } @@ -113,38 +177,45 @@ const GoogleLinkEnhancer = (function() { /* eslint-enable wrap-iife, func-names, object-shorthand */ // In case a place is already selected on load. - const selObjects = W.selectionManager.getSelectedDataModelObjects(); - if (selObjects.length && selObjects[0].type === 'venue') { + /** + * @type Selection + */ + const currentSelection = this.sdk.Editing.getSelection(); + // const selObjects = W.selectionManager.getSelectedDataModelObjects(); + if (currentSelection?.ids?.length && currentSelection.objectType === 'venue') { this.#formatLinkElements(); } - W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); + this.sdk.Events.on({eventName: "wme-selection-changed", eventHandler: this.#onWmeSelectionChanged.bind(this)}); + // W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); } #initLayer() { - this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', { - uniqueName: '___GoogleLinkEnhancements', - displayInLayerSwitcher: true, - styleMap: new OpenLayers.StyleMap({ - default: { - strokeColor: '${strokeColor}', - strokeWidth: '${strokeWidth}', - strokeDashstyle: '${strokeDashstyle}', - pointRadius: '15', - fillOpacity: '0' - } - }) - }); - - this.#mapLayer.setOpacity(0.8); - W.map.addLayer(this.#mapLayer); + // this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', { + // uniqueName: '___GoogleLinkEnhancements', + // displayInLayerSwitcher: true, + // styleMap: new OpenLayers.StyleMap({ + // default: { + // strokeColor: '${strokeColor}', + // strokeWidth: '${strokeWidth}', + // strokeDashstyle: '${strokeDashstyle}', + // pointRadius: '15', + // fillOpacity: '0' + // } + // }) + // }); + + // this.#mapLayer.setOpacity(0.8); + // W.map.addLayer(this.#mapLayer); + this.sdk.Map.addLayer({layerName: "Google Link Enhancements.", styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules}); } #onWmeSelectionChanged() { if (this.#enabled) { this.#destroyPoint(); - const selected = W.selectionManager.getSelectedDataModelObjects(); - if (selected[0]?.type === 'venue') { + // const selected = W.selectionManager.getSelectedDataModelObjects(); + const selected = this.sdk.Editing.getSelection(); + if (selected.objectType === 'venue') { // The setTimeout is necessary (in beta WME currently, at least) to allow the // panel UI DOM to update after a place is selected. setTimeout(() => this.#formatLinkElements(), 0); @@ -157,7 +228,8 @@ const GoogleLinkEnhancer = (function() { this.#interceptPlacesService(); // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function. $('#map').on('mouseenter', null, this, GLE.#onMapMouseenter); - W.model.venues.on('objectschanged', this.#formatLinkElements, this); + // W.model.venues.on('objectschanged', this.#formatLinkElements, this); + this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); this.#processPlaces(); this.#enabled = true; } @@ -166,7 +238,9 @@ const GoogleLinkEnhancer = (function() { disable() { if (this.#enabled) { $('#map').off('mouseenter', GLE.#onMapMouseenter); - W.model.venues.off('objectschanged', this.#formatLinkElements, this); + // W.model.venues.off('objectschanged', this.#formatLinkElements, this); + this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); + this.#enabled = false; } } @@ -191,24 +265,34 @@ const GoogleLinkEnhancer = (function() { // Borrowed from WazeWrap static #distanceBetweenPoints(point1, point2) { - const line = new OpenLayers.Geometry.LineString([point1, point2]); - const length = line.getGeodesicLength(W.map.getProjectionObject()); + // const line = new OpenLayers.Geometry.LineString([point1, point2]); + // const length = line.getGeodesicLength(W.map.getProjectionObject()); + + + const length = turf.length(turf.geometryCollection([point1, point2])); return length; // multiply by 3.28084 to convert to feet } - + static #isPointVenue(venue) { + return venue.geometry.type === "Point" + } #isLinkTooFar(link, venue) { if (link.loc) { - const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); - linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); + // const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); + const linkPt = turf.point([link.loc.lng, link.loc.lat]); + // linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); let venuePt; let distanceLim = this.distanceLimit; - if (venue.isPoint()) { - venuePt = venue.geometry.getCentroid(); + // if (venue.isPoint()) { + if(venue.geometry.type === "Point") { + venuePt = venue.geometry; } else { - const bounds = venue.geometry.getBounds(); - const center = bounds.getCenterLonLat(); - venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); - const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); + // const bounds = venue.geometry.getBounds(); + // const center = bounds.getCenterLonLat(); + const center = turf.centroid(venue.geometry); + // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); + // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); + venuePt = center; + const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt); } const distance = GLE.#distanceBetweenPoints(linkPt, venuePt); @@ -220,59 +304,84 @@ const GoogleLinkEnhancer = (function() { #processPlaces() { if (this.#enabled) { try { - const that = this; // Get a list of already-linked id's - const existingLinks = GoogleLinkEnhancer.#getExistingLinks(); + const existingLinks = GoogleLinkEnhancer.#getExistingLinks(this.sdk); this.#mapLayer.removeAllFeatures(); const drawnLinks = []; - W.model.venues.getObjectArray().forEach(venue => { + for(const venue of this.sdk.DataModel.Venues.getAll()) { + // W.model.venues.getObjectArray().forEach(venue => { const promises = []; - venue.attributes.externalProviderIDs.forEach(provID => { + // venue.attributes.externalProviderIDs.forEach(provID => { + for(const provID of venue.externalProviderIds) { const id = provID.attributes.uuid; // Check for duplicate links const linkInfo = existingLinks[id]; if (linkInfo.count > 1) { - const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); - const width = venue.isPoint() ? '4' : '12'; + // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + const geometry = venue.geometry; + // const width = venue.isPoint() ? '4' : '12'; + const width = GLE.#isPointVenue(venue) ? 4 : 12; const color = '#fb8d00'; - const features = [new OpenLayers.Feature.Vector(geometry, { - strokeWidth: width, strokeColor: color - })]; + // const features = [new OpenLayers.Feature.Vector(geometry, { + // strokeWidth: width, strokeColor: color + // })]; + const features = [ + GLE.#isPointVenue(venue) ? turf.point(geometry, { + styleName: "venueStyle", + style: { + strokeWidth: width, + strokeColor: color + } + }) : turf.polygon(geometry, { + styleName: "venueStyle", + style: { + strokeColor: color, + strokeWidth: width + } + }) + ] const lineStart = geometry.getCentroid(); - linkInfo.venues.forEach(linkVenue => { + // linkInfo.venues.forEach(linkVenue => { + for(const linkVenue of linkInfo.venues) { if (linkVenue !== venue && !drawnLinks.some(dl => (dl[0] === venue && dl[1] === linkVenue) || (dl[0] === linkVenue && dl[1] === venue))) { + const endPoint = turf.centroid(linkVenue.geometry); features.push( - new OpenLayers.Feature.Vector( - new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), - { - strokeWidth: 4, - strokeColor: color, - strokeDashstyle: '12 12' - } - ) + // new OpenLayers.Feature.Vector( + // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), + // { + // strokeWidth: 4, + // strokeColor: color, + // strokeDashstyle: '12 12' + // } + // ) + turf.lineString([lineStart, endPoint], {styleName: "lineStyle", style: { + strokeWidth: 4, + strokeColor: color, + strokeDashstyle: '12 12' + }}) ); drawnLinks.push([venue, linkVenue]); } - }); - that.#mapLayer.addFeatures(features); + }; + this.#mapLayer.addFeatures(features); } - }); + }; // Process all results of link lookups and add a highlight feature if needed. Promise.all(promises).then(results => { let strokeColor = null; let strokeDashStyle = 'solid'; - if (!that.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) { + if (!this.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) { if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.attributes.name) || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.attributes.name)) { - strokeDashStyle = venue.isPoint() ? '2 6' : '2 16'; + strokeDashStyle = GLE.#isPointVenue(venue) ? '2 6' : '2 16'; } strokeColor = '#F00'; - } else if (results.some(res => that.#isLinkTooFar(res, venue))) { + } else if (results.some(res => this.#isLinkTooFar(res, venue))) { strokeColor = '#0FF'; - } else if (!that.#DISABLE_CLOSED_PLACES && that.#showTempClosedPOIs && results.some(res => res.tempclosed)) { + } else if (!this.#DISABLE_CLOSED_PLACES && this.#showTempClosedPOIs && results.some(res => res.tempclosed)) { if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.attributes.name) || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.attributes.name)) { strokeDashStyle = venue.isPoint() ? '2 6' : '2 16'; @@ -283,15 +392,22 @@ const GoogleLinkEnhancer = (function() { } if (strokeColor) { const style = { - strokeWidth: venue.isPoint() ? '4' : '12', + strokeWidth: GLE.#isPointVenue(venue) ? 4 : 12, strokeColor, strokeDashStyle }; - const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); - that.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); + // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + const feature = GLE.#isPointVenue(venue) ? turf.point(venue.geometry, { + styleName: "placeStyle", + style: style + }) : this.polygon(venue.geometry, { + styleName: "placeStyle", + style: style + }); + // this.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); } }); - }); + }; } catch (ex) { console.error('PIE (Google Link Enhancer) error:', ex); } @@ -306,7 +422,7 @@ const GoogleLinkEnhancer = (function() { async #formatLinkElements() { const $links = $('#edit-panel').find(this.#EXT_PROV_ELEM_QUERY); if ($links.length) { - const existingLinks = GLE.#getExistingLinks(); + const existingLinks = GLE.#getExistingLinks(this.sdk); // fetch all links first const promises = []; @@ -354,14 +470,23 @@ const GoogleLinkEnhancer = (function() { } } - static #getExistingLinks() { + static #getExistingLinks(sdk = undefined) { + if(!sdk) { + const msg = "SDK Is Not Available"; + console.error(msg); + throw new Error(msg); + } const existingLinks = {}; - const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; - W.model.venues.getObjectArray().forEach(venue => { + // const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; + const thisVenue = sdk.Editing.getSelection() + // W.model.venues.getObjectArray().forEach(venue => { + for(const venue of sdk.DataModel.Venues.getAll()) { const isThisVenue = venue === thisVenue; const thisPlaceIDs = []; - venue.attributes.externalProviderIDs.forEach(provID => { - const id = provID.attributes.uuid; + // venue.attributes.externalProviderIDs.forEach(provID => { + for(const provID of venue.externalProviderIds) { + // const id = provID.attributes.uuid; + const id = provID; if (!thisPlaceIDs.includes(id)) { thisPlaceIDs.push(id); let link = existingLinks[id]; @@ -378,8 +503,8 @@ const GoogleLinkEnhancer = (function() { } link.isThisVenue = link.isThisVenue || isThisVenue; } - }); - }); + }; + }; return existingLinks; } @@ -393,13 +518,13 @@ const GoogleLinkEnhancer = (function() { } } - static #getOLMapExtent() { - let extent = W.map.getExtent(); - if (Array.isArray(extent)) { - extent = new OpenLayers.Bounds(extent); - extent.transform('EPSG:4326', 'EPSG:3857'); - } - return extent; + static #getOLMapExtent(sdk) { + // let extent = W.map.getExtent(); + // if (Array.isArray(extent)) { + // extent = new OpenLayers.Bounds(extent); + // extent.transform('EPSG:4326', 'EPSG:3857'); + // } + return sdk.Map.getMapExtent(); } // Add the POI point to the map. @@ -409,17 +534,26 @@ const GoogleLinkEnhancer = (function() { if (link) { if (!link.notFound) { const coord = link.loc; - const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); + // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); + const poiPt = turf.point([coord.lng, coord.lat]); poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); - const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); - const ext = GLE.#getOLMapExtent(); - const lsBounds = new OpenLayers.Geometry.LineString([ - new OpenLayers.Geometry.Point(ext.left, ext.bottom), - new OpenLayers.Geometry.Point(ext.left, ext.top), - new OpenLayers.Geometry.Point(ext.right, ext.top), - new OpenLayers.Geometry.Point(ext.right, ext.bottom), - new OpenLayers.Geometry.Point(ext.left, ext.bottom)]); + // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); + const placePt = turf.point([placeGeom.x, placeGeom.y]); + const ext = GLE.#getOLMapExtent(this.sdk); + // const lsBounds = new OpenLayers.Geometry.LineString([ + // new OpenLayers.Geometry.Point(ext.left, ext.bottom), + // new OpenLayers.Geometry.Point(ext.left, ext.top), + // new OpenLayers.Geometry.Point(ext.right, ext.top), + // new OpenLayers.Geometry.Point(ext.right, ext.bottom), + // new OpenLayers.Geometry.Point(ext.left, ext.bottom)]); + const lsBounds = turf.lineString([ + [ext[0], ext[3]], + [ext[0], ext[1]], + [ext[2], ext[1]], + [ext[2], ext[3]], + [ext[0], ext[3]], + ]) let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]); // If the line extends outside the bounds, split it so we don't draw a line across the world. @@ -427,17 +561,18 @@ const GoogleLinkEnhancer = (function() { let label = ''; if (splits) { let splitPoints; - splits.forEach(split => { - split.components.forEach(component => { + for(const split of splits) { + for(const component of split.components) { if (component.x === placePt.x && component.y === placePt.y) splitPoints = split; - }); - }); + }; + }; lsLine = new OpenLayers.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]); let distance = GLE.#distanceBetweenPoints(poiPt, placePt); let unitConversion; let unit1; let unit2; - if (W.model.isImperial) { + // if (W.model.isImperial) { + if(this.sdk.Settings.getUserSettings().isImperial) { distance *= 3.28084; unitConversion = 5280; unit1 = ' ft'; @@ -517,7 +652,7 @@ const GoogleLinkEnhancer = (function() { google.maps.places.PlacesService.prototype.getDetails = function interceptedGetDetails(request, callback) { console.debug('Intercepted getDetails call:', request); - const customCallback = function(result, status) { + const customCallback = (result, status) => { console.debug('Intercepted getDetails response:', result, status); const link = {}; switch (status) { @@ -550,4 +685,4 @@ const GoogleLinkEnhancer = (function() { } return GLE; -}()); +})()); diff --git a/biome.json b/biome.json new file mode 100644 index 0000000..16998b0 --- /dev/null +++ b/biome.json @@ -0,0 +1,35 @@ +{ + "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "vcs": { + "enabled": false, + "clientKind": "git", + "useIgnoreFile": false + }, + "files": { + "ignoreUnknown": false, + "ignore": [] + }, + "formatter": { + "enabled": true, + "indentStyle": "tab" + }, + "organizeImports": { + "enabled": true + }, + "linter": { + "enabled": true, + "rules": { + "recommended": true, + "style": { + }, + "suspicious": { + "noRedundantUseStrict": "off" + } + } + }, + "javascript": { + "formatter": { + "quoteStyle": "double" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a1228d4 --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "devDependencies": { + "@biomejs/biome": "^1.9.4", + "@turf/turf": "^7.2.0", + "wme-sdk-typings": "https://web-assets.waze.com/wme_sdk_docs/beta/latest/wme-sdk-typings.tgz" + } +} From f7b2ad53cf085d191acaadd345f751ada494a0e7 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sat, 7 Jun 2025 14:02:10 -0400 Subject: [PATCH 02/16] Google Link Enhancer to TS and SDK --- GoogleLinkEnhancer.js | 273 ++++++++--------- GoogleLinkEnhancer.ts | 692 ++++++++++++++++++++++++++++++++++++++++++ package.json | 5 + tsconfig.json | 22 ++ 4 files changed, 843 insertions(+), 149 deletions(-) create mode 100644 GoogleLinkEnhancer.ts create mode 100644 tsconfig.json diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index 834d7e8..936a0e9 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -1,3 +1,4 @@ +"use strict"; // ==UserScript== // @name WME Utils - Google Link Enhancer // @namespace WazeDev @@ -8,38 +9,32 @@ // @require https://cdn.jsdelivr.net/npm/@turf/turf@7.2.0/turf.min.js // @license GNU GPLv3 // ==/UserScript== - /* global OpenLayers */ /* global W */ /* global google */ - /* eslint-disable max-classes-per-file */ - // eslint-disable-next-line func-names // import * as turf from "@turf/turf"; -// import { WmeSDK } from "wme-sdk-typings"; - +// import type { Venue, WmeSDK } from "wme-sdk-typings"; +// import $ from "jquery"; const GoogleLinkEnhancer = ((() => { 'use strict'; - + var _a; class GooglePlaceCache { constructor() { this.cache = new Map(); this.pendingPromises = new Map(); } - async getPlace(placeId) { if (this.cache.has(placeId)) { return this.cache.get(placeId); } - if (!this.pendingPromises.has(placeId)) { let resolveFn; let rejectFn; const promise = new Promise((resolve, reject) => { resolveFn = resolve; rejectFn = reject; - // Set a timeout to reject the promise if not resolved in 3 seconds setTimeout(() => { if (this.pendingPromises.has(placeId)) { @@ -48,16 +43,12 @@ const GoogleLinkEnhancer = ((() => { } }, 3000); }); - this.pendingPromises.set(placeId, { promise, resolve: resolveFn, reject: rejectFn }); } - return this.pendingPromises.get(placeId).promise; } - addPlace(placeId, properties) { this.cache.set(placeId, properties); - if (this.pendingPromises.has(placeId)) { this.pendingPromises.get(placeId).resolve(properties); this.pendingPromises.delete(placeId); @@ -69,10 +60,9 @@ const GoogleLinkEnhancer = ((() => { #EXT_PROV_ELEM_QUERY = 'wz-list-item.external-provider'; #EXT_PROV_ELEM_EDIT_QUERY = 'wz-list-item.external-provider-edit'; #EXT_PROV_ELEM_CONTENT_QUERY = 'div.external-provider-content'; - linkCache; #enabled = false; - #mapLayer = null; + static #mapLayer = "Google Link Enhancements."; #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place. // Area place is calculated as #distanceLimit + #showTempClosedPOIs = true; @@ -90,7 +80,6 @@ const GoogleLinkEnhancer = ((() => { badLink: 'Invalid Google link. Please remove it.', tooFar: 'The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.' }; - #styleConfig = { styleContext: { highNodeColor: (context) => { @@ -140,11 +129,10 @@ const GoogleLinkEnhancer = ((() => { } ], }; - - + sdk; /* eslint-enable no-unused-vars */ constructor(sdk = undefined) { - if(!sdk) { + if (!sdk) { const msg = "SDK Must be defined to use GLE"; console.error(msg); throw new Error(msg); @@ -152,17 +140,15 @@ const GoogleLinkEnhancer = ((() => { this.sdk = sdk; this.linkCache = new GooglePlaceCache(); this.#initLayer(); - // NOTE: Arrow functions are necessary for calling methods on object instances. // This could be made more efficient by only processing the relevant places. W.model.events.register('mergeend', null, () => { this.#processPlaces(); }); // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); - this.sdk.Events.on({eventName:"wme-data-model-objects-added", eventHandler: () => { this.#processPlaces();}}); - this.sdk.Events.on({eventName:"wme-data-model-objects-removed", eventHandler: () => { this.#processPlaces();}}); - this.sdk.Events.on({eventName:"wme-data-model-objects-changed", eventHandler: () => { this.#processPlaces();}}); - + this.sdk.Events.on({ eventName: "wme-data-model-objects-added", eventHandler: () => { this.#processPlaces(); } }); + this.sdk.Events.on({ eventName: "wme-data-model-objects-removed", eventHandler: () => { this.#processPlaces(); } }); + this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: () => { this.#processPlaces(); } }); // This is a special event that will be triggered when DOM elements are destroyed. /* eslint-disable wrap-iife, func-names, object-shorthand */ (($) => { @@ -175,7 +161,6 @@ const GoogleLinkEnhancer = ((() => { }; })(jQuery); /* eslint-enable wrap-iife, func-names, object-shorthand */ - // In case a place is already selected on load. /** * @type Selection @@ -185,11 +170,9 @@ const GoogleLinkEnhancer = ((() => { if (currentSelection?.ids?.length && currentSelection.objectType === 'venue') { this.#formatLinkElements(); } - - this.sdk.Events.on({eventName: "wme-selection-changed", eventHandler: this.#onWmeSelectionChanged.bind(this)}); + this.sdk.Events.on({ eventName: "wme-selection-changed", eventHandler: this.#onWmeSelectionChanged.bind(this) }); // W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); } - #initLayer() { // this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', { // uniqueName: '___GoogleLinkEnhancements', @@ -204,76 +187,64 @@ const GoogleLinkEnhancer = ((() => { // } // }) // }); - // this.#mapLayer.setOpacity(0.8); // W.map.addLayer(this.#mapLayer); - this.sdk.Map.addLayer({layerName: "Google Link Enhancements.", styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules}); + this.sdk.Map.addLayer({ layerName: _a.#mapLayer, styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules }); } - #onWmeSelectionChanged() { if (this.#enabled) { this.#destroyPoint(); // const selected = W.selectionManager.getSelectedDataModelObjects(); const selected = this.sdk.Editing.getSelection(); - if (selected.objectType === 'venue') { + if (selected?.objectType === 'venue') { // The setTimeout is necessary (in beta WME currently, at least) to allow the // panel UI DOM to update after a place is selected. setTimeout(() => this.#formatLinkElements(), 0); } } } - enable() { if (!this.#enabled) { this.#interceptPlacesService(); // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function. - $('#map').on('mouseenter', null, this, GLE.#onMapMouseenter); + $('#map').on('mouseenter', null, this, _a.#onMapMouseenter); // W.model.venues.on('objectschanged', this.#formatLinkElements, this); - this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); + this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { this.#formatLinkElements().bind(this); } }); this.#processPlaces(); this.#enabled = true; } } - disable() { if (this.#enabled) { - $('#map').off('mouseenter', GLE.#onMapMouseenter); + $('#map').off('mouseenter', _a.#onMapMouseenter); // W.model.venues.off('objectschanged', this.#formatLinkElements, this); - this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); - + this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { this.#formatLinkElements().bind(this); } }); this.#enabled = false; } } - // The distance (in meters) before flagging a Waze place that is too far from the linked Google place. // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node. get distanceLimit() { return this.#distanceLimit; } - set distanceLimit(value) { this.#distanceLimit = value; } - get showTempClosedPOIs() { return this.#showTempClosedPOIs; } - set showTempClosedPOIs(value) { this.#showTempClosedPOIs = value; } - // Borrowed from WazeWrap static #distanceBetweenPoints(point1, point2) { // const line = new OpenLayers.Geometry.LineString([point1, point2]); // const length = line.getGeodesicLength(W.map.getProjectionObject()); - - const length = turf.length(turf.geometryCollection([point1, point2])); return length; // multiply by 3.28084 to convert to feet } - static #isPointVenue(venue) { - return venue.geometry.type === "Point" + static isPointVenue(venue) { + return venue.geometry.type === "Point"; } #isLinkTooFar(link, venue) { if (link.loc) { @@ -283,51 +254,50 @@ const GoogleLinkEnhancer = ((() => { let venuePt; let distanceLim = this.distanceLimit; // if (venue.isPoint()) { - if(venue.geometry.type === "Point") { + if (venue.geometry.type === "Point") { venuePt = venue.geometry; - } else { + } + else { // const bounds = venue.geometry.getBounds(); // const center = bounds.getCenterLonLat(); const center = turf.centroid(venue.geometry); // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); - venuePt = center; + venuePt = center.geometry; const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); - distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt); + distanceLim += _a.#distanceBetweenPoints(venuePt, topRightPt); } - const distance = GLE.#distanceBetweenPoints(linkPt, venuePt); + const distance = _a.#distanceBetweenPoints(linkPt, venuePt); return distance > distanceLim; } return false; } - #processPlaces() { if (this.#enabled) { try { // Get a list of already-linked id's const existingLinks = GoogleLinkEnhancer.#getExistingLinks(this.sdk); - this.#mapLayer.removeAllFeatures(); + this.sdk.Map.removeAllFeaturesFromLayer({ layerName: _a.#mapLayer }); const drawnLinks = []; - for(const venue of this.sdk.DataModel.Venues.getAll()) { - // W.model.venues.getObjectArray().forEach(venue => { + for (const venue of this.sdk.DataModel.Venues.getAll()) { + // W.model.venues.getObjectArray().forEach(venue => { const promises = []; // venue.attributes.externalProviderIDs.forEach(provID => { - for(const provID of venue.externalProviderIds) { + for (const provID of venue.externalProviderIds) { const id = provID.attributes.uuid; - // Check for duplicate links const linkInfo = existingLinks[id]; if (linkInfo.count > 1) { // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); const geometry = venue.geometry; // const width = venue.isPoint() ? '4' : '12'; - const width = GLE.#isPointVenue(venue) ? 4 : 12; + const width = _a.isPointVenue(venue) ? 4 : 12; const color = '#fb8d00'; // const features = [new OpenLayers.Feature.Vector(geometry, { // strokeWidth: width, strokeColor: color // })]; const features = [ - GLE.#isPointVenue(venue) ? turf.point(geometry, { + _a.isPointVenue(venue) ? turf.point(geometry.coordinates, { styleName: "venueStyle", style: { strokeWidth: width, @@ -340,107 +310,106 @@ const GoogleLinkEnhancer = ((() => { strokeWidth: width } }) - ] - const lineStart = geometry.getCentroid(); + ]; + const lineStart = turf.centroid(geometry); // linkInfo.venues.forEach(linkVenue => { - for(const linkVenue of linkInfo.venues) { + for (const linkVenue of linkInfo.venues) { if (linkVenue !== venue && !drawnLinks.some(dl => (dl[0] === venue && dl[1] === linkVenue) || (dl[0] === linkVenue && dl[1] === venue))) { const endPoint = turf.centroid(linkVenue.geometry); features.push( - // new OpenLayers.Feature.Vector( - // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), - // { - // strokeWidth: 4, - // strokeColor: color, - // strokeDashstyle: '12 12' - // } - // ) - turf.lineString([lineStart, endPoint], {styleName: "lineStyle", style: { + // new OpenLayers.Feature.Vector( + // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), + // { + // strokeWidth: 4, + // strokeColor: color, + // strokeDashstyle: '12 12' + // } + // ) + turf.lineString([lineStart, endPoint], { styleName: "lineStyle", style: { strokeWidth: 4, strokeColor: color, strokeDashstyle: '12 12' - }}) - ); + } })); drawnLinks.push([venue, linkVenue]); } - }; + } + ; this.#mapLayer.addFeatures(features); } - }; - + } + ; // Process all results of link lookups and add a highlight feature if needed. Promise.all(promises).then(results => { let strokeColor = null; let strokeDashStyle = 'solid'; if (!this.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) { - if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.attributes.name) - || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.attributes.name)) { - strokeDashStyle = GLE.#isPointVenue(venue) ? '2 6' : '2 16'; + if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.name) + || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.name)) { + strokeDashStyle = _a.isPointVenue(venue) ? '2 6' : '2 16'; } strokeColor = '#F00'; - } else if (results.some(res => this.#isLinkTooFar(res, venue))) { + } + else if (results.some(res => this.#isLinkTooFar(res, venue))) { strokeColor = '#0FF'; - } else if (!this.#DISABLE_CLOSED_PLACES && this.#showTempClosedPOIs && results.some(res => res.tempclosed)) { - if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.attributes.name) - || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.attributes.name)) { - strokeDashStyle = venue.isPoint() ? '2 6' : '2 16'; + } + else if (!this.#DISABLE_CLOSED_PLACES && this.#showTempClosedPOIs && results.some(res => res.tempclosed)) { + if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.name) + || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.name)) { + strokeDashStyle = _a.isPointVenue(venue) ? '2 6' : '2 16'; } strokeColor = '#FD3'; - } else if (results.some(res => res.notFound)) { + } + else if (results.some(res => res.notFound)) { strokeColor = '#F0F'; } if (strokeColor) { const style = { - strokeWidth: GLE.#isPointVenue(venue) ? 4 : 12, + strokeWidth: _a.isPointVenue(venue) ? 4 : 12, strokeColor, strokeDashStyle }; // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); - const feature = GLE.#isPointVenue(venue) ? turf.point(venue.geometry, { + const feature = _a.isPointVenue(venue) ? turf.point(venue.geometry.coordinates, { styleName: "placeStyle", style: style - }) : this.polygon(venue.geometry, { + }) : turf.polygon(venue.geometry.coordinates, { styleName: "placeStyle", style: style }); // this.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); } }); - }; - } catch (ex) { + } + ; + } + catch (ex) { console.error('PIE (Google Link Enhancer) error:', ex); } } } - static #onMapMouseenter(event) { // If the point isn't destroyed yet, destroy it when mousing over the map. event.data.#destroyPoint(); } - async #formatLinkElements() { const $links = $('#edit-panel').find(this.#EXT_PROV_ELEM_QUERY); if ($links.length) { - const existingLinks = GLE.#getExistingLinks(this.sdk); - + const existingLinks = _a.#getExistingLinks(this.sdk); // fetch all links first const promises = []; const extProvElements = []; $links.each((ix, linkEl) => { const $linkEl = $(linkEl); extProvElements.push($linkEl); - - const id = GLE.#getIdFromElement($linkEl); + const id = _a.#getIdFromElement($linkEl); promises.push(this.linkCache.getPlace(id)); }); const links = await Promise.all(promises); - extProvElements.forEach(($extProvElem, i) => { - const id = GLE.#getIdFromElement($extProvElem); - - if (!id) return; - + const id = _a.#getIdFromElement($extProvElem); + if (!id) + return; const link = links[i]; if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) { setTimeout(() => { @@ -453,15 +422,20 @@ const GoogleLinkEnhancer = ((() => { if (link) { if (link.permclosed && !this.#DISABLE_CLOSED_PLACES) { $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FAA' }).attr('title', this.strings.permClosedPlace); - } else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) { + } + else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) { $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA' }).attr('title', this.strings.tempClosedPlace); - } else if (link.notFound) { + } + else if (link.notFound) { $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#F0F' }).attr('title', this.strings.badLink); - } else { - const venue = W.selectionManager.getSelectedDataModelObjects()[0]; + } + else { + // const venue = W.selectionManager.getSelectedDataModelObjects()[0]; + const selection = this.sdk.Editing.getSelection(); if (this.#isLinkTooFar(link, venue)) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit)); - } else { // reset in case we just deleted another provider + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); + } + else { // reset in case we just deleted another provider $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); } } @@ -469,22 +443,23 @@ const GoogleLinkEnhancer = ((() => { }); } } - static #getExistingLinks(sdk = undefined) { - if(!sdk) { + if (!sdk) { const msg = "SDK Is Not Available"; console.error(msg); throw new Error(msg); } const existingLinks = {}; // const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; - const thisVenue = sdk.Editing.getSelection() + const thisVenue = sdk.Editing.getSelection(); + if (thisVenue?.objectType !== "venue") + return {}; // W.model.venues.getObjectArray().forEach(venue => { - for(const venue of sdk.DataModel.Venues.getAll()) { - const isThisVenue = venue === thisVenue; + for (const venue of sdk.DataModel.Venues.getAll()) { + const isThisVenue = venue.id === thisVenue.ids[0]; const thisPlaceIDs = []; // venue.attributes.externalProviderIDs.forEach(provID => { - for(const provID of venue.externalProviderIds) { + for (const provID of venue.externalProviderIds) { // const id = provID.attributes.uuid; const id = provID; if (!thisPlaceIDs.includes(id)) { @@ -493,7 +468,8 @@ const GoogleLinkEnhancer = ((() => { if (link) { link.count++; link.venues.push(venue); - } else { + } + else { link = { count: 1, venues: [venue] }; existingLinks[id] = link; if (provID.attributes.url != null) { @@ -503,11 +479,12 @@ const GoogleLinkEnhancer = ((() => { } link.isThisVenue = link.isThisVenue || isThisVenue; } - }; - }; + } + ; + } + ; return existingLinks; } - // Remove the POI point from the map. #destroyPoint() { if (this.#ptFeature) { @@ -517,7 +494,6 @@ const GoogleLinkEnhancer = ((() => { this.#lineFeature = null; } } - static #getOLMapExtent(sdk) { // let extent = W.map.getExtent(); // if (Array.isArray(extent)) { @@ -526,10 +502,10 @@ const GoogleLinkEnhancer = ((() => { // } return sdk.Map.getMapExtent(); } - // Add the POI point to the map. async #addPoint(id) { - if (!id) return; + if (!id) + return; const link = await this.linkCache.getPlace(id); if (link) { if (!link.notFound) { @@ -540,7 +516,7 @@ const GoogleLinkEnhancer = ((() => { const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); const placePt = turf.point([placeGeom.x, placeGeom.y]); - const ext = GLE.#getOLMapExtent(this.sdk); + const ext = _a.#getOLMapExtent(this.sdk); // const lsBounds = new OpenLayers.Geometry.LineString([ // new OpenLayers.Geometry.Point(ext.left, ext.bottom), // new OpenLayers.Geometry.Point(ext.left, ext.top), @@ -553,44 +529,48 @@ const GoogleLinkEnhancer = ((() => { [ext[2], ext[1]], [ext[2], ext[3]], [ext[0], ext[3]], - ]) + ]); let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]); - // If the line extends outside the bounds, split it so we don't draw a line across the world. const splits = lsLine.splitWith(lsBounds); let label = ''; if (splits) { let splitPoints; - for(const split of splits) { - for(const component of split.components) { - if (component.x === placePt.x && component.y === placePt.y) splitPoints = split; - }; - }; + for (const split of splits) { + for (const component of split.components) { + if (component.x === placePt.x && component.y === placePt.y) + splitPoints = split; + } + ; + } + ; lsLine = new OpenLayers.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]); - let distance = GLE.#distanceBetweenPoints(poiPt, placePt); + let distance = _a.#distanceBetweenPoints(poiPt, placePt); let unitConversion; let unit1; let unit2; // if (W.model.isImperial) { - if(this.sdk.Settings.getUserSettings().isImperial) { + if (this.sdk.Settings.getUserSettings().isImperial) { distance *= 3.28084; unitConversion = 5280; unit1 = ' ft'; unit2 = ' mi'; - } else { + } + else { unitConversion = 1000; unit1 = ' m'; unit2 = ' km'; } if (distance > unitConversion * 10) { label = Math.round(distance / unitConversion) + unit2; - } else if (distance > 1000) { + } + else if (distance > 1000) { label = (Math.round(distance / (unitConversion / 10)) / 10) + unit2; - } else { + } + else { label = Math.round(distance) + unit1; } } - this.#destroyPoint(); // Just in case it still exists. this.#ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, { pointRadius: 6, @@ -614,7 +594,8 @@ const GoogleLinkEnhancer = ((() => { W.map.getLayerByUniqueName('venues').addFeatures([this.#ptFeature, this.#lineFeature]); this.#timeoutDestroyPoint(); } - } else { + } + else { // this.#getLinkInfoAsync(id).then(res => { // if (res.error || res.apiDisabled) { // // API was temporarily disabled. Ignore for now. @@ -624,34 +605,29 @@ const GoogleLinkEnhancer = ((() => { // }); } } - // Destroy the point after some time, if it hasn't been destroyed already. #timeoutDestroyPoint() { - if (this.#timeoutID) clearTimeout(this.#timeoutID); + if (this.#timeoutID) + clearTimeout(this.#timeoutID); this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000); } - static #getIdFromElement($el) { const providerIndex = $el.parent().children().toArray().indexOf($el[0]); return W.selectionManager.getSelectedDataModelObjects()[0].getExternalProviderIDs()[providerIndex]?.attributes.uuid; } - #addHoverEvent($el) { - $el.hover(() => this.#addPoint(GLE.#getIdFromElement($el)), () => this.#destroyPoint()); + $el.hover(() => this.#addPoint(_a.#getIdFromElement($el)), () => this.#destroyPoint()); } - #interceptPlacesService() { if (typeof google === 'undefined' || !google.maps || !google.maps.places || !google.maps.places.PlacesService) { console.debug('Google Maps PlacesService not loaded yet.'); setTimeout(this.#interceptPlacesService.bind(this), 500); // Retry until it loads return; } - const originalGetDetails = google.maps.places.PlacesService.prototype.getDetails; const that = this; google.maps.places.PlacesService.prototype.getDetails = function interceptedGetDetails(request, callback) { console.debug('Intercepted getDetails call:', request); - const customCallback = (result, status) => { console.debug('Intercepted getDetails response:', result, status); const link = {}; @@ -661,7 +637,8 @@ const GoogleLinkEnhancer = ((() => { link.loc = { lng: loc.lng(), lat: loc.lat() }; if (result.business_status === google.maps.places.BusinessStatus.CLOSED_PERMANENTLY) { link.permclosed = true; - } else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) { + } + else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) { link.tempclosed = true; } that.linkCache.addPlace(request.placeId, link); @@ -676,13 +653,11 @@ const GoogleLinkEnhancer = ((() => { } callback(result, status); // Pass the result to the original callback }; - return originalGetDetails.call(this, request, customCallback); }; - console.debug('Google Maps PlacesService.getDetails intercepted successfully.'); } } - + _a = GLE; return GLE; })()); diff --git a/GoogleLinkEnhancer.ts b/GoogleLinkEnhancer.ts new file mode 100644 index 0000000..75588fd --- /dev/null +++ b/GoogleLinkEnhancer.ts @@ -0,0 +1,692 @@ +// ==UserScript== +// @name WME Utils - Google Link Enhancer +// @namespace WazeDev +// @version 2025.04.11.002 +// @description Adds some extra WME functionality related to Google place links. +// @author MapOMatic, WazeDev group +// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ +// @require https://cdn.jsdelivr.net/npm/@turf/turf@7.2.0/turf.min.js +// @license GNU GPLv3 +// ==/UserScript== + +/* global OpenLayers */ +/* global W */ +/* global google */ + +/* eslint-disable max-classes-per-file */ + +// eslint-disable-next-line func-names +// import * as turf from "@turf/turf"; +// import type { Venue, WmeSDK } from "wme-sdk-typings"; +// import $ from "jquery"; + +const GoogleLinkEnhancer = ((() => { + 'use strict'; + + class GooglePlaceCache { + constructor() { + this.cache = new Map(); + this.pendingPromises = new Map(); + } + + async getPlace(placeId) { + if (this.cache.has(placeId)) { + return this.cache.get(placeId); + } + + if (!this.pendingPromises.has(placeId)) { + let resolveFn; + let rejectFn; + const promise = new Promise((resolve, reject) => { + resolveFn = resolve; + rejectFn = reject; + + // Set a timeout to reject the promise if not resolved in 3 seconds + setTimeout(() => { + if (this.pendingPromises.has(placeId)) { + this.pendingPromises.delete(placeId); + rejectFn(new Error(`Timeout: Place ${placeId} not found within 3 seconds`)); + } + }, 3000); + }); + + this.pendingPromises.set(placeId, { promise, resolve: resolveFn, reject: rejectFn }); + } + + return this.pendingPromises.get(placeId).promise; + } + + addPlace(placeId, properties) { + this.cache.set(placeId, properties); + + if (this.pendingPromises.has(placeId)) { + this.pendingPromises.get(placeId).resolve(properties); + this.pendingPromises.delete(placeId); + } + } + } + class GLE { + #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic. + #EXT_PROV_ELEM_QUERY = 'wz-list-item.external-provider'; + #EXT_PROV_ELEM_EDIT_QUERY = 'wz-list-item.external-provider-edit'; + #EXT_PROV_ELEM_CONTENT_QUERY = 'div.external-provider-content'; + + linkCache; + #enabled = false; + static #mapLayer = "Google Link Enhancements."; + #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place. + // Area place is calculated as #distanceLimit + + #showTempClosedPOIs = true; + #originalHeadAppendChildMethod; + #ptFeature; + #lineFeature; + #timeoutID; + strings = { + permClosedPlace: 'Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.', + tempClosedPlace: 'Google indicates this place is temporarily closed.', + multiLinked: 'Linked more than once already. Please find and remove multiple links.', + linkedToThisPlace: 'Already linked to this place', + linkedNearby: 'Already linked to a nearby place', + linkedToXPlaces: 'This is linked to {0} places', + badLink: 'Invalid Google link. Please remove it.', + tooFar: 'The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.' + }; + + #styleConfig = { + styleContext: { + highNodeColor: (context) => { + return context?.feature?.properties?.style?.strokeColor; + }, + strokeColor: (context) => { + return context?.feature?.properties?.style?.strokeColor; + }, + strokeWidth: (context) => { + return context?.feature?.properties?.style?.strokeWidth; + }, + strokeDashStyle: (context) => { + return context?.feature?.properties?.style?.strokeDashstyle; + }, + }, + styleRules: [ + { + predicate: (properties) => { return properties.styleName === "lineStyle"; }, + style: { + strokeColor: "${strokeColor}", + }, + }, + { + predicate: (properties) => { return properties.styleName === "default"; }, + style: { + strokeColor: '${strokeColor}', + strokeWidth: '${strokeWidth}', + strokeDashstyle: '${strokeDashstyle}', + pointRadius: 15, + fillOpacity: 0 + } + }, + { + predicate: (properties) => { return properties.styleName === "venueStyle"; }, + style: { + strokeColor: '${strokeColor}', + strokeWidth: '${strokeWidth}', + } + }, + { + predicate: (properties) => { return properties.styleName === "placeStyle"; }, + style: { + strokeColor: '${strokeColor}', + strokeWidth: '${strokeWidth}', + strokeDashStyle: "${strokeDashStyle}" + } + } + ], + }; + sdk: WmeSDK; + + + /* eslint-enable no-unused-vars */ + constructor(sdk = undefined) { + if(!sdk) { + const msg = "SDK Must be defined to use GLE"; + console.error(msg); + throw new Error(msg); + } + this.sdk = sdk; + this.linkCache = new GooglePlaceCache(); + this.#initLayer(); + + // NOTE: Arrow functions are necessary for calling methods on object instances. + // This could be made more efficient by only processing the relevant places. + W.model.events.register('mergeend', null, () => { this.#processPlaces(); }); + // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); + // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); + // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); + this.sdk.Events.on({eventName:"wme-data-model-objects-added", eventHandler: () => { this.#processPlaces();}}); + this.sdk.Events.on({eventName:"wme-data-model-objects-removed", eventHandler: () => { this.#processPlaces();}}); + this.sdk.Events.on({eventName:"wme-data-model-objects-changed", eventHandler: () => { this.#processPlaces();}}); + + // This is a special event that will be triggered when DOM elements are destroyed. + /* eslint-disable wrap-iife, func-names, object-shorthand */ + (($) => { + $.event.special.destroyed = { + remove: (o) => { + if (o.handler && o.type !== 'destroyed') { + o.handler(); + } + } + }; + })(jQuery); + /* eslint-enable wrap-iife, func-names, object-shorthand */ + + // In case a place is already selected on load. + /** + * @type Selection + */ + const currentSelection = this.sdk.Editing.getSelection(); + // const selObjects = W.selectionManager.getSelectedDataModelObjects(); + if (currentSelection?.ids?.length && currentSelection.objectType === 'venue') { + this.#formatLinkElements(); + } + + this.sdk.Events.on({eventName: "wme-selection-changed", eventHandler: this.#onWmeSelectionChanged.bind(this)}); + // W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); + } + + #initLayer() { + // this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', { + // uniqueName: '___GoogleLinkEnhancements', + // displayInLayerSwitcher: true, + // styleMap: new OpenLayers.StyleMap({ + // default: { + // strokeColor: '${strokeColor}', + // strokeWidth: '${strokeWidth}', + // strokeDashstyle: '${strokeDashstyle}', + // pointRadius: '15', + // fillOpacity: '0' + // } + // }) + // }); + + // this.#mapLayer.setOpacity(0.8); + // W.map.addLayer(this.#mapLayer); + this.sdk.Map.addLayer({layerName: GLE.#mapLayer, styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules}); + } + + #onWmeSelectionChanged() { + if (this.#enabled) { + this.#destroyPoint(); + // const selected = W.selectionManager.getSelectedDataModelObjects(); + const selected = this.sdk.Editing.getSelection(); + if (selected?.objectType === 'venue') { + // The setTimeout is necessary (in beta WME currently, at least) to allow the + // panel UI DOM to update after a place is selected. + setTimeout(() => this.#formatLinkElements(), 0); + } + } + } + + enable() { + if (!this.#enabled) { + this.#interceptPlacesService(); + // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function. + $('#map').on('mouseenter', null, this, GLE.#onMapMouseenter); + // W.model.venues.on('objectschanged', this.#formatLinkElements, this); + this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); + this.#processPlaces(); + this.#enabled = true; + } + } + + disable() { + if (this.#enabled) { + $('#map').off('mouseenter', GLE.#onMapMouseenter); + // W.model.venues.off('objectschanged', this.#formatLinkElements, this); + this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); + + this.#enabled = false; + } + } + + // The distance (in meters) before flagging a Waze place that is too far from the linked Google place. + // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node. + get distanceLimit() : number { + return this.#distanceLimit; + } + + set distanceLimit(value) { + this.#distanceLimit = value; + } + + get showTempClosedPOIs() { + return this.#showTempClosedPOIs; + } + + set showTempClosedPOIs(value) { + this.#showTempClosedPOIs = value; + } + + // Borrowed from WazeWrap + static #distanceBetweenPoints(point1, point2) { + // const line = new OpenLayers.Geometry.LineString([point1, point2]); + // const length = line.getGeodesicLength(W.map.getProjectionObject()); + + + const length = turf.length(turf.geometryCollection([point1, point2])); + return length; // multiply by 3.28084 to convert to feet + } + static isPointVenue(venue: Venue) { + return venue.geometry.type === "Point" + } + #isLinkTooFar(link, venue: Venue) { + if (link.loc) { + // const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); + const linkPt = turf.point([link.loc.lng, link.loc.lat]); + // linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); + let venuePt: GeoJSON.Point; + let distanceLim = this.distanceLimit; + // if (venue.isPoint()) { + if(venue.geometry.type === "Point") { + venuePt = venue.geometry; + } else { + // const bounds = venue.geometry.getBounds(); + // const center = bounds.getCenterLonLat(); + const center = turf.centroid(venue.geometry); + // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); + // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); + venuePt = center.geometry; + const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); + distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt); + } + const distance = GLE.#distanceBetweenPoints(linkPt, venuePt); + return distance > distanceLim; + } + return false; + } + + #processPlaces() { + if (this.#enabled) { + try { + // Get a list of already-linked id's + const existingLinks = GoogleLinkEnhancer.#getExistingLinks(this.sdk); + this.sdk.Map.removeAllFeaturesFromLayer({layerName: GLE.#mapLayer}); + const drawnLinks = []; + for(const venue of this.sdk.DataModel.Venues.getAll()) { + // W.model.venues.getObjectArray().forEach(venue => { + const promises = []; + // venue.attributes.externalProviderIDs.forEach(provID => { + for(const provID of venue.externalProviderIds) { + const id = provID.attributes.uuid; + + // Check for duplicate links + const linkInfo = existingLinks[id]; + if (linkInfo.count > 1) { + // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + const geometry: GeoJSON.Point | GeoJSON.Polygon = venue.geometry; + // const width = venue.isPoint() ? '4' : '12'; + const width = GLE.isPointVenue(venue) ? 4 : 12; + const color = '#fb8d00'; + // const features = [new OpenLayers.Feature.Vector(geometry, { + // strokeWidth: width, strokeColor: color + // })]; + const features = [ + GLE.isPointVenue(venue) ? turf.point(geometry.coordinates, { + styleName: "venueStyle", + style: { + strokeWidth: width, + strokeColor: color + } + }) : turf.polygon(geometry, { + styleName: "venueStyle", + style: { + strokeColor: color, + strokeWidth: width + } + }) + ] + const lineStart = turf.centroid(geometry); + // linkInfo.venues.forEach(linkVenue => { + for(const linkVenue of linkInfo.venues) { + if (linkVenue !== venue + && !drawnLinks.some(dl => (dl[0] === venue && dl[1] === linkVenue) || (dl[0] === linkVenue && dl[1] === venue))) { + const endPoint = turf.centroid(linkVenue.geometry); + features.push( + // new OpenLayers.Feature.Vector( + // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), + // { + // strokeWidth: 4, + // strokeColor: color, + // strokeDashstyle: '12 12' + // } + // ) + turf.lineString([lineStart, endPoint], {styleName: "lineStyle", style: { + strokeWidth: 4, + strokeColor: color, + strokeDashstyle: '12 12' + }}) + ); + drawnLinks.push([venue, linkVenue]); + } + }; + this.#mapLayer.addFeatures(features); + } + }; + + // Process all results of link lookups and add a highlight feature if needed. + Promise.all(promises).then(results => { + let strokeColor = null; + let strokeDashStyle = 'solid'; + if (!this.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) { + if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.name) + || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.name)) { + strokeDashStyle = GLE.isPointVenue(venue) ? '2 6' : '2 16'; + } + strokeColor = '#F00'; + } else if (results.some(res => this.#isLinkTooFar(res, venue))) { + strokeColor = '#0FF'; + } else if (!this.#DISABLE_CLOSED_PLACES && this.#showTempClosedPOIs && results.some(res => res.tempclosed)) { + if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.name) + || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.name)) { + strokeDashStyle = GLE.isPointVenue(venue) ? '2 6' : '2 16'; + } + strokeColor = '#FD3'; + } else if (results.some(res => res.notFound)) { + strokeColor = '#F0F'; + } + if (strokeColor) { + const style = { + strokeWidth: GLE.isPointVenue(venue) ? 4 : 12, + strokeColor, + strokeDashStyle + }; + // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + const feature = GLE.isPointVenue(venue) ? turf.point(venue.geometry.coordinates, { + styleName: "placeStyle", + style: style + }) : turf.polygon(venue.geometry.coordinates, { + styleName: "placeStyle", + style: style + }); + // this.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); + } + }); + }; + } catch (ex) { + console.error('PIE (Google Link Enhancer) error:', ex); + } + } + } + + static #onMapMouseenter(event) { + // If the point isn't destroyed yet, destroy it when mousing over the map. + event.data.#destroyPoint(); + } + + async #formatLinkElements() { + const $links = $('#edit-panel').find(this.#EXT_PROV_ELEM_QUERY); + if ($links.length) { + const existingLinks = GLE.#getExistingLinks(this.sdk); + + // fetch all links first + const promises = []; + const extProvElements = []; + $links.each((ix, linkEl) => { + const $linkEl = $(linkEl); + extProvElements.push($linkEl); + + const id = GLE.#getIdFromElement($linkEl); + promises.push(this.linkCache.getPlace(id)); + }); + const links = await Promise.all(promises); + + extProvElements.forEach(($extProvElem, i) => { + const id = GLE.#getIdFromElement($extProvElem); + + if (!id) return; + + const link = links[i]; + if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) { + setTimeout(() => { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA500' }).attr({ + title: this.strings.linkedToXPlaces.replace('{0}', existingLinks[id].count) + }); + }, 50); + } + this.#addHoverEvent($extProvElem); + if (link) { + if (link.permclosed && !this.#DISABLE_CLOSED_PLACES) { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FAA' }).attr('title', this.strings.permClosedPlace); + } else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA' }).attr('title', this.strings.tempClosedPlace); + } else if (link.notFound) { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#F0F' }).attr('title', this.strings.badLink); + } else { + // const venue = W.selectionManager.getSelectedDataModelObjects()[0]; + const selection = this.sdk.Editing.getSelection(); + if (this.#isLinkTooFar(link, venue)) { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); + } else { // reset in case we just deleted another provider + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); + } + } + } + }); + } + } + + static #getExistingLinks(sdk: WmeSDK | undefined = undefined) { + if(!sdk) { + const msg = "SDK Is Not Available"; + console.error(msg); + throw new Error(msg); + } + const existingLinks = {}; + // const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; + const thisVenue = sdk.Editing.getSelection() + if(thisVenue?.objectType !== "venue") return {}; + // W.model.venues.getObjectArray().forEach(venue => { + for(const venue of sdk.DataModel.Venues.getAll()) { + const isThisVenue = venue.id === thisVenue.ids[0]; + const thisPlaceIDs: string[] = []; + // venue.attributes.externalProviderIDs.forEach(provID => { + for(const provID of venue.externalProviderIds) { + // const id = provID.attributes.uuid; + const id = provID; + if (!thisPlaceIDs.includes(id)) { + thisPlaceIDs.push(id); + let link = existingLinks[id]; + if (link) { + link.count++; + link.venues.push(venue); + } else { + link = { count: 1, venues: [venue] }; + existingLinks[id] = link; + if (provID.attributes.url != null) { + const u = provID.attributes.url.replace('https://maps.google.com/?', ''); + link.url = u; + } + } + link.isThisVenue = link.isThisVenue || isThisVenue; + } + }; + }; + return existingLinks; + } + + // Remove the POI point from the map. + #destroyPoint() { + if (this.#ptFeature) { + this.#ptFeature.destroy(); + this.#ptFeature = null; + this.#lineFeature.destroy(); + this.#lineFeature = null; + } + } + + static #getOLMapExtent(sdk: WmeSDK) { + // let extent = W.map.getExtent(); + // if (Array.isArray(extent)) { + // extent = new OpenLayers.Bounds(extent); + // extent.transform('EPSG:4326', 'EPSG:3857'); + // } + return sdk.Map.getMapExtent(); + } + + // Add the POI point to the map. + async #addPoint(id) { + if (!id) return; + const link = await this.linkCache.getPlace(id); + if (link) { + if (!link.notFound) { + const coord = link.loc; + // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); + const poiPt = turf.point([coord.lng, coord.lat]); + poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); + const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); + // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); + const placePt = turf.point([placeGeom.x, placeGeom.y]); + const ext = GLE.#getOLMapExtent(this.sdk); + // const lsBounds = new OpenLayers.Geometry.LineString([ + // new OpenLayers.Geometry.Point(ext.left, ext.bottom), + // new OpenLayers.Geometry.Point(ext.left, ext.top), + // new OpenLayers.Geometry.Point(ext.right, ext.top), + // new OpenLayers.Geometry.Point(ext.right, ext.bottom), + // new OpenLayers.Geometry.Point(ext.left, ext.bottom)]); + const lsBounds = turf.lineString([ + [ext[0], ext[3]], + [ext[0], ext[1]], + [ext[2], ext[1]], + [ext[2], ext[3]], + [ext[0], ext[3]], + ]) + let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]); + + // If the line extends outside the bounds, split it so we don't draw a line across the world. + const splits = lsLine.splitWith(lsBounds); + let label = ''; + if (splits) { + let splitPoints; + for(const split of splits) { + for(const component of split.components) { + if (component.x === placePt.x && component.y === placePt.y) splitPoints = split; + }; + }; + lsLine = new OpenLayers.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]); + let distance = GLE.#distanceBetweenPoints(poiPt, placePt); + let unitConversion; + let unit1; + let unit2; + // if (W.model.isImperial) { + if(this.sdk.Settings.getUserSettings().isImperial) { + distance *= 3.28084; + unitConversion = 5280; + unit1 = ' ft'; + unit2 = ' mi'; + } else { + unitConversion = 1000; + unit1 = ' m'; + unit2 = ' km'; + } + if (distance > unitConversion * 10) { + label = Math.round(distance / unitConversion) + unit2; + } else if (distance > 1000) { + label = (Math.round(distance / (unitConversion / 10)) / 10) + unit2; + } else { + label = Math.round(distance) + unit1; + } + } + + this.#destroyPoint(); // Just in case it still exists. + this.#ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, { + pointRadius: 6, + strokeWidth: 30, + strokeColor: '#FF0', + fillColor: '#FF0', + strokeOpacity: 0.5 + }); + this.#lineFeature = new OpenLayers.Feature.Vector(lsLine, {}, { + strokeWidth: 3, + strokeDashstyle: '12 8', + strokeColor: '#FF0', + label, + labelYOffset: 45, + fontColor: '#FF0', + fontWeight: 'bold', + labelOutlineColor: '#000', + labelOutlineWidth: 4, + fontSize: '18' + }); + W.map.getLayerByUniqueName('venues').addFeatures([this.#ptFeature, this.#lineFeature]); + this.#timeoutDestroyPoint(); + } + } else { + // this.#getLinkInfoAsync(id).then(res => { + // if (res.error || res.apiDisabled) { + // // API was temporarily disabled. Ignore for now. + // } else { + // this.#addPoint(id); + // } + // }); + } + } + + // Destroy the point after some time, if it hasn't been destroyed already. + #timeoutDestroyPoint() { + if (this.#timeoutID) clearTimeout(this.#timeoutID); + this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000); + } + + static #getIdFromElement($el) { + const providerIndex = $el.parent().children().toArray().indexOf($el[0]); + return W.selectionManager.getSelectedDataModelObjects()[0].getExternalProviderIDs()[providerIndex]?.attributes.uuid; + } + + #addHoverEvent($el) { + $el.hover(() => this.#addPoint(GLE.#getIdFromElement($el)), () => this.#destroyPoint()); + } + + #interceptPlacesService() { + if (typeof google === 'undefined' || !google.maps || !google.maps.places || !google.maps.places.PlacesService) { + console.debug('Google Maps PlacesService not loaded yet.'); + setTimeout(this.#interceptPlacesService.bind(this), 500); // Retry until it loads + return; + } + + const originalGetDetails = google.maps.places.PlacesService.prototype.getDetails; + const that = this; + google.maps.places.PlacesService.prototype.getDetails = function interceptedGetDetails(request, callback) { + console.debug('Intercepted getDetails call:', request); + + const customCallback = (result, status) => { + console.debug('Intercepted getDetails response:', result, status); + const link = {}; + switch (status) { + case google.maps.places.PlacesServiceStatus.OK: { + const loc = result.geometry.location; + link.loc = { lng: loc.lng(), lat: loc.lat() }; + if (result.business_status === google.maps.places.BusinessStatus.CLOSED_PERMANENTLY) { + link.permclosed = true; + } else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) { + link.tempclosed = true; + } + that.linkCache.addPlace(request.placeId, link); + break; + } + case google.maps.places.PlacesServiceStatus.NOT_FOUND: + link.notfound = true; + that.linkCache.addPlace(request.placeId, link); + break; + default: + link.error = status; + } + callback(result, status); // Pass the result to the original callback + }; + + return originalGetDetails.call(this, request, customCallback); + }; + + console.debug('Google Maps PlacesService.getDetails intercepted successfully.'); + } + } + + return GLE; +})()); diff --git a/package.json b/package.json index a1228d4..1f7c69b 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,11 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@turf/turf": "^7.2.0", + "@types/jquery": "^3.5.32", + "typescript": "^5.8.3", "wme-sdk-typings": "https://web-assets.waze.com/wme_sdk_docs/beta/latest/wme-sdk-typings.tgz" + }, + "dependencies": { + "jquery": "^3.7.1" } } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..f912511 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "target": "es2022", + "module": "ES2022", + "moduleResolution": "node10", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "allowJs": true, + "outDir": "./", + "allowSyntheticDefaultImports": true, + "rootDirs": ["./"], + "types": [ + "wme-sdk-typings", + "geojson" + ] + }, + "exclude": ["node_modules", "releases"], + "include": ["./*.ts", "src/**/*"] +} From 1f25e48b598e9a357c672e2d9119a68127032d3c Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sat, 7 Jun 2025 14:57:31 -0400 Subject: [PATCH 03/16] Store GLE Conversion. --- GoogleLinkEnhancer.js | 23 ++++++++++++++++------- GoogleLinkEnhancer.ts | 24 ++++++++++++++++-------- 2 files changed, 32 insertions(+), 15 deletions(-) diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index 936a0e9..53374c2 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -432,11 +432,14 @@ const GoogleLinkEnhancer = ((() => { else { // const venue = W.selectionManager.getSelectedDataModelObjects()[0]; const selection = this.sdk.Editing.getSelection(); - if (this.#isLinkTooFar(link, venue)) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); - } - else { // reset in case we just deleted another provider - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); + if (selection?.objectType === "venue") { + const venue = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); + if (venue && this.#isLinkTooFar(link, venue)) { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); + } + else { // reset in case we just deleted another provider + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); + } } } } @@ -513,9 +516,15 @@ const GoogleLinkEnhancer = ((() => { // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); const poiPt = turf.point([coord.lng, coord.lat]); poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); - const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); + // const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); + const selection = this.sdk.Editing.getSelection(); + let placeGeom; + if (selection?.objectType === "venue") { + const v = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); + placeGeom = v?.geometry && turf.centroid(v?.geometry)?.geometry; + } // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); - const placePt = turf.point([placeGeom.x, placeGeom.y]); + const placePt = turf.point(placeGeom); const ext = _a.#getOLMapExtent(this.sdk); // const lsBounds = new OpenLayers.Geometry.LineString([ // new OpenLayers.Geometry.Point(ext.left, ext.bottom), diff --git a/GoogleLinkEnhancer.ts b/GoogleLinkEnhancer.ts index 75588fd..369eab7 100644 --- a/GoogleLinkEnhancer.ts +++ b/GoogleLinkEnhancer.ts @@ -266,11 +266,10 @@ const GoogleLinkEnhancer = ((() => { } // Borrowed from WazeWrap - static #distanceBetweenPoints(point1, point2) { + static #distanceBetweenPoints(point1: GeoJSON.Point | GeoJSON.Polygon, point2: GeoJSON.Point | GeoJSON.Polygon) { // const line = new OpenLayers.Geometry.LineString([point1, point2]); // const length = line.getGeodesicLength(W.map.getProjectionObject()); - const length = turf.length(turf.geometryCollection([point1, point2])); return length; // multiply by 3.28084 to convert to feet } @@ -462,10 +461,13 @@ const GoogleLinkEnhancer = ((() => { } else { // const venue = W.selectionManager.getSelectedDataModelObjects()[0]; const selection = this.sdk.Editing.getSelection(); - if (this.#isLinkTooFar(link, venue)) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); - } else { // reset in case we just deleted another provider - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); + if(selection?.objectType === "venue") { + const venue = this.sdk.DataModel.Venues.getById({venueId: selection.ids[0]}); + if (venue && this.#isLinkTooFar(link, venue)) { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); + } else { // reset in case we just deleted another provider + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); + } } } } @@ -541,9 +543,15 @@ const GoogleLinkEnhancer = ((() => { // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); const poiPt = turf.point([coord.lng, coord.lat]); poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); - const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); + // const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); + const selection = this.sdk.Editing.getSelection(); + let placeGeom: GeoJSON.Geometry | undefined; + if(selection?.objectType === "venue") { + const v = this.sdk.DataModel.Venues.getById({venueId: selection.ids[0]}); + placeGeom = v?.geometry && turf.centroid(v?.geometry)?.geometry; + } // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); - const placePt = turf.point([placeGeom.x, placeGeom.y]); + const placePt = turf.point(placeGeom); const ext = GLE.#getOLMapExtent(this.sdk); // const lsBounds = new OpenLayers.Geometry.LineString([ // new OpenLayers.Geometry.Point(ext.left, ext.bottom), From 3c9b566bd24efccc5563fc309c276ebefc030364 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sun, 8 Jun 2025 00:27:15 -0400 Subject: [PATCH 04/16] Store Updates for GLE. --- GoogleLinkEnhancer.js | 19 +++++++++++-------- GoogleLinkEnhancer.ts | 30 +++++++++++++++++++----------- NavigationPoint.js | 4 ++-- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index 53374c2..aedb125 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -55,6 +55,7 @@ const GoogleLinkEnhancer = ((() => { } } } + ; class GLE { #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic. #EXT_PROV_ELEM_QUERY = 'wz-list-item.external-provider'; @@ -190,6 +191,7 @@ const GoogleLinkEnhancer = ((() => { // this.#mapLayer.setOpacity(0.8); // W.map.addLayer(this.#mapLayer); this.sdk.Map.addLayer({ layerName: _a.#mapLayer, styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules }); + this.sdk.Map.setLayerOpacity({ layerName: _a.#mapLayer, opacity: 0.8 }); } #onWmeSelectionChanged() { if (this.#enabled) { @@ -284,10 +286,10 @@ const GoogleLinkEnhancer = ((() => { const promises = []; // venue.attributes.externalProviderIDs.forEach(provID => { for (const provID of venue.externalProviderIds) { - const id = provID.attributes.uuid; + const id = provID; // Check for duplicate links const linkInfo = existingLinks[id]; - if (linkInfo.count > 1) { + if (linkInfo?.count > 1) { // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); const geometry = venue.geometry; // const width = venue.isPoint() ? '4' : '12'; @@ -326,7 +328,7 @@ const GoogleLinkEnhancer = ((() => { // strokeDashstyle: '12 12' // } // ) - turf.lineString([lineStart, endPoint], { styleName: "lineStyle", style: { + turf.lineString([lineStart.geometry.coordinates, endPoint.geometry.coordinates], { styleName: "lineStyle", style: { strokeWidth: 4, strokeColor: color, strokeDashstyle: '12 12' @@ -335,7 +337,8 @@ const GoogleLinkEnhancer = ((() => { } } ; - this.#mapLayer.addFeatures(features); + this.sdk.Map.addFeaturesToLayer({ features: features, layerName: _a.#mapLayer }); + // this.#mapLayer.addFeatures(features); } } ; @@ -475,10 +478,10 @@ const GoogleLinkEnhancer = ((() => { else { link = { count: 1, venues: [venue] }; existingLinks[id] = link; - if (provID.attributes.url != null) { - const u = provID.attributes.url.replace('https://maps.google.com/?', ''); - link.url = u; - } + // if (provID.attributes.url != null) { + // const u = provID.attributes.url.replace('https://maps.google.com/?', ''); + // link.url = u; + // } } link.isThisVenue = link.isThisVenue || isThisVenue; } diff --git a/GoogleLinkEnhancer.ts b/GoogleLinkEnhancer.ts index 369eab7..def6fc2 100644 --- a/GoogleLinkEnhancer.ts +++ b/GoogleLinkEnhancer.ts @@ -65,7 +65,13 @@ const GoogleLinkEnhancer = ((() => { } } } + + interface LinkInfo { + count: number, + venues: Venue[], + }; class GLE { + #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic. #EXT_PROV_ELEM_QUERY = 'wz-list-item.external-provider'; #EXT_PROV_ELEM_EDIT_QUERY = 'wz-list-item.external-provider-edit'; @@ -210,6 +216,7 @@ const GoogleLinkEnhancer = ((() => { // this.#mapLayer.setOpacity(0.8); // W.map.addLayer(this.#mapLayer); this.sdk.Map.addLayer({layerName: GLE.#mapLayer, styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules}); + this.sdk.Map.setLayerOpacity({layerName: GLE.#mapLayer, opacity: 0.8}); } #onWmeSelectionChanged() { @@ -314,11 +321,11 @@ const GoogleLinkEnhancer = ((() => { const promises = []; // venue.attributes.externalProviderIDs.forEach(provID => { for(const provID of venue.externalProviderIds) { - const id = provID.attributes.uuid; + const id = provID; // Check for duplicate links const linkInfo = existingLinks[id]; - if (linkInfo.count > 1) { + if (linkInfo?.count > 1) { // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); const geometry: GeoJSON.Point | GeoJSON.Polygon = venue.geometry; // const width = venue.isPoint() ? '4' : '12'; @@ -327,7 +334,7 @@ const GoogleLinkEnhancer = ((() => { // const features = [new OpenLayers.Feature.Vector(geometry, { // strokeWidth: width, strokeColor: color // })]; - const features = [ + const features: GeoJSON.Feature[] = [ GLE.isPointVenue(venue) ? turf.point(geometry.coordinates, { styleName: "venueStyle", style: { @@ -357,7 +364,7 @@ const GoogleLinkEnhancer = ((() => { // strokeDashstyle: '12 12' // } // ) - turf.lineString([lineStart, endPoint], {styleName: "lineStyle", style: { + turf.lineString([lineStart.geometry.coordinates, endPoint.geometry.coordinates], {styleName: "lineStyle", style: { strokeWidth: 4, strokeColor: color, strokeDashstyle: '12 12' @@ -366,7 +373,8 @@ const GoogleLinkEnhancer = ((() => { drawnLinks.push([venue, linkVenue]); } }; - this.#mapLayer.addFeatures(features); + this.sdk.Map.addFeaturesToLayer({features: features, layerName: GLE.#mapLayer}) + // this.#mapLayer.addFeatures(features); } }; @@ -481,7 +489,7 @@ const GoogleLinkEnhancer = ((() => { console.error(msg); throw new Error(msg); } - const existingLinks = {}; + const existingLinks: Record = {}; // const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; const thisVenue = sdk.Editing.getSelection() if(thisVenue?.objectType !== "venue") return {}; @@ -495,17 +503,17 @@ const GoogleLinkEnhancer = ((() => { const id = provID; if (!thisPlaceIDs.includes(id)) { thisPlaceIDs.push(id); - let link = existingLinks[id]; + let link: LinkInfo = existingLinks[id]; if (link) { link.count++; link.venues.push(venue); } else { link = { count: 1, venues: [venue] }; existingLinks[id] = link; - if (provID.attributes.url != null) { - const u = provID.attributes.url.replace('https://maps.google.com/?', ''); - link.url = u; - } + // if (provID.attributes.url != null) { + // const u = provID.attributes.url.replace('https://maps.google.com/?', ''); + // link.url = u; + // } } link.isThisVenue = link.isThisVenue || isThisVenue; } diff --git a/NavigationPoint.js b/NavigationPoint.js index a74fa05..76e74ba 100644 --- a/NavigationPoint.js +++ b/NavigationPoint.js @@ -18,8 +18,8 @@ class NavigationPoint } with(){ - var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; - if(e.point == null) + const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; + if(e.point === null) e.point = this.toJSON().point; return new this.constructor((this.toJSON().point, e.point)); } From a2a0cd833c6bf2cf3baeaec25105392db102231b Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sun, 8 Jun 2025 13:14:38 -0400 Subject: [PATCH 05/16] More Conversion. --- GoogleLinkEnhancer.js | 2 +- GoogleLinkEnhancer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index aedb125..a6ffde6 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -269,7 +269,7 @@ const GoogleLinkEnhancer = ((() => { const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); distanceLim += _a.#distanceBetweenPoints(venuePt, topRightPt); } - const distance = _a.#distanceBetweenPoints(linkPt, venuePt); + const distance = _a.#distanceBetweenPoints(linkPt.geometry, venuePt); return distance > distanceLim; } return false; diff --git a/GoogleLinkEnhancer.ts b/GoogleLinkEnhancer.ts index def6fc2..5b06624 100644 --- a/GoogleLinkEnhancer.ts +++ b/GoogleLinkEnhancer.ts @@ -303,7 +303,7 @@ const GoogleLinkEnhancer = ((() => { const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt); } - const distance = GLE.#distanceBetweenPoints(linkPt, venuePt); + const distance = GLE.#distanceBetweenPoints(linkPt.geometry, venuePt); return distance > distanceLim; } return false; From f5be3e2bbff15c23f35dfdc1b8d5c3f5340b24b7 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sun, 8 Jun 2025 14:12:34 -0400 Subject: [PATCH 06/16] Done --- GoogleLinkEnhancer.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/GoogleLinkEnhancer.ts b/GoogleLinkEnhancer.ts index 5b06624..e6582cf 100644 --- a/GoogleLinkEnhancer.ts +++ b/GoogleLinkEnhancer.ts @@ -69,6 +69,7 @@ const GoogleLinkEnhancer = ((() => { interface LinkInfo { count: number, venues: Venue[], + isThisVenue: boolean, }; class GLE { From 738bb79a48073a1b0be1b901d510efb83741166f Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Mon, 9 Jun 2025 06:57:16 -0400 Subject: [PATCH 07/16] Store GLE. --- GoogleLinkEnhancer.js | 2 +- GoogleLinkEnhancer.ts | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index a6ffde6..aedb125 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -269,7 +269,7 @@ const GoogleLinkEnhancer = ((() => { const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); distanceLim += _a.#distanceBetweenPoints(venuePt, topRightPt); } - const distance = _a.#distanceBetweenPoints(linkPt.geometry, venuePt); + const distance = _a.#distanceBetweenPoints(linkPt, venuePt); return distance > distanceLim; } return false; diff --git a/GoogleLinkEnhancer.ts b/GoogleLinkEnhancer.ts index e6582cf..def6fc2 100644 --- a/GoogleLinkEnhancer.ts +++ b/GoogleLinkEnhancer.ts @@ -69,7 +69,6 @@ const GoogleLinkEnhancer = ((() => { interface LinkInfo { count: number, venues: Venue[], - isThisVenue: boolean, }; class GLE { @@ -304,7 +303,7 @@ const GoogleLinkEnhancer = ((() => { const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt); } - const distance = GLE.#distanceBetweenPoints(linkPt.geometry, venuePt); + const distance = GLE.#distanceBetweenPoints(linkPt, venuePt); return distance > distanceLim; } return false; From 7ec58ce418f1056496e45d075c936817900ddef6 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Fri, 13 Jun 2025 15:28:39 -0400 Subject: [PATCH 08/16] Mostly Converted to SDK. --- GoogleLinkEnhancer.js | 10 +++++++--- GoogleLinkEnhancer.ts | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index a6ffde6..52703e8 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -2,7 +2,7 @@ // ==UserScript== // @name WME Utils - Google Link Enhancer // @namespace WazeDev -// @version 2025.04.11.002 +// @version 2025.06.13.001 // @description Adds some extra WME functionality related to Google place links. // @author MapOMatic, WazeDev group // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ @@ -266,8 +266,12 @@ const GoogleLinkEnhancer = ((() => { // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); venuePt = center.geometry; - const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); - distanceLim += _a.#distanceBetweenPoints(venuePt, topRightPt); + let bbox = venue.geometry.bbox; + if (!bbox) { + bbox = turf.bbox(venue.geometry); + } + const topRightPt = turf.point([bbox[0], bbox[1]]); + distanceLim += _a.#distanceBetweenPoints(venuePt, topRightPt.geometry); } const distance = _a.#distanceBetweenPoints(linkPt.geometry, venuePt); return distance > distanceLim; diff --git a/GoogleLinkEnhancer.ts b/GoogleLinkEnhancer.ts index e6582cf..5ad3af3 100644 --- a/GoogleLinkEnhancer.ts +++ b/GoogleLinkEnhancer.ts @@ -1,7 +1,7 @@ // ==UserScript== // @name WME Utils - Google Link Enhancer // @namespace WazeDev -// @version 2025.04.11.002 +// @version 2025.06.13.001 // @description Adds some extra WME functionality related to Google place links. // @author MapOMatic, WazeDev group // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ @@ -301,8 +301,12 @@ const GoogleLinkEnhancer = ((() => { // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); venuePt = center.geometry; - const topRightPt = turf.point(venue.geometry.bbox[0], venue.geometry.bbox[1]); - distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt); + let bbox = venue.geometry.bbox; + if(!bbox) { + bbox = turf.bbox(venue.geometry); + } + const topRightPt = turf.point([bbox[0], bbox[1]]); + distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt.geometry); } const distance = GLE.#distanceBetweenPoints(linkPt.geometry, venuePt); return distance > distanceLim; From d75eaa72172ce388a9ad23a77ac8165b3e3ff7da Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sun, 15 Jun 2025 08:42:36 -0400 Subject: [PATCH 09/16] SDK Google Link Enhancer. --- GoogleLinkEnhancer.js | 524 +++++------- SDKGoogleLinkEnhancer.js | 755 ++++++++++++++++++ ...inkEnhancer.ts => SDKGoogleLinkEnhancer.ts | 528 +++++++----- package.json | 7 +- 4 files changed, 1282 insertions(+), 532 deletions(-) create mode 100644 SDKGoogleLinkEnhancer.js rename GoogleLinkEnhancer.ts => SDKGoogleLinkEnhancer.ts (56%) diff --git a/GoogleLinkEnhancer.js b/GoogleLinkEnhancer.js index 7cf8db2..69ddf8f 100644 --- a/GoogleLinkEnhancer.js +++ b/GoogleLinkEnhancer.js @@ -1,40 +1,41 @@ -"use strict"; // ==UserScript== // @name WME Utils - Google Link Enhancer // @namespace WazeDev -// @version 2025.06.13.001 +// @version 2025.04.11.002 // @description Adds some extra WME functionality related to Google place links. // @author MapOMatic, WazeDev group // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ -// @require https://cdn.jsdelivr.net/npm/@turf/turf@7.2.0/turf.min.js // @license GNU GPLv3 // ==/UserScript== + /* global OpenLayers */ /* global W */ /* global google */ + /* eslint-disable max-classes-per-file */ + // eslint-disable-next-line func-names -// import * as turf from "@turf/turf"; -// import type { Venue, WmeSDK } from "wme-sdk-typings"; -// import $ from "jquery"; -const GoogleLinkEnhancer = ((() => { +const GoogleLinkEnhancer = (function() { 'use strict'; - var _a; + class GooglePlaceCache { constructor() { this.cache = new Map(); this.pendingPromises = new Map(); } + async getPlace(placeId) { if (this.cache.has(placeId)) { return this.cache.get(placeId); } + if (!this.pendingPromises.has(placeId)) { let resolveFn; let rejectFn; const promise = new Promise((resolve, reject) => { resolveFn = resolve; rejectFn = reject; + // Set a timeout to reject the promise if not resolved in 3 seconds setTimeout(() => { if (this.pendingPromises.has(placeId)) { @@ -43,27 +44,31 @@ const GoogleLinkEnhancer = ((() => { } }, 3000); }); + this.pendingPromises.set(placeId, { promise, resolve: resolveFn, reject: rejectFn }); } + return this.pendingPromises.get(placeId).promise; } + addPlace(placeId, properties) { this.cache.set(placeId, properties); + if (this.pendingPromises.has(placeId)) { this.pendingPromises.get(placeId).resolve(properties); this.pendingPromises.delete(placeId); } } } - ; class GLE { #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic. #EXT_PROV_ELEM_QUERY = 'wz-list-item.external-provider'; #EXT_PROV_ELEM_EDIT_QUERY = 'wz-list-item.external-provider-edit'; #EXT_PROV_ELEM_CONTENT_QUERY = 'div.external-provider-content'; + linkCache; #enabled = false; - static #mapLayer = "Google Link Enhancements."; + #mapLayer = null; #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place. // Area place is calculated as #distanceLimit + #showTempClosedPOIs = true; @@ -81,80 +86,24 @@ const GoogleLinkEnhancer = ((() => { badLink: 'Invalid Google link. Please remove it.', tooFar: 'The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.' }; - #styleConfig = { - styleContext: { - highNodeColor: (context) => { - return context?.feature?.properties?.style?.strokeColor; - }, - strokeColor: (context) => { - return context?.feature?.properties?.style?.strokeColor; - }, - strokeWidth: (context) => { - return context?.feature?.properties?.style?.strokeWidth; - }, - strokeDashStyle: (context) => { - return context?.feature?.properties?.style?.strokeDashstyle; - }, - }, - styleRules: [ - { - predicate: (properties) => { return properties.styleName === "lineStyle"; }, - style: { - strokeColor: "${strokeColor}", - }, - }, - { - predicate: (properties) => { return properties.styleName === "default"; }, - style: { - strokeColor: '${strokeColor}', - strokeWidth: '${strokeWidth}', - strokeDashstyle: '${strokeDashstyle}', - pointRadius: 15, - fillOpacity: 0 - } - }, - { - predicate: (properties) => { return properties.styleName === "venueStyle"; }, - style: { - strokeColor: '${strokeColor}', - strokeWidth: '${strokeWidth}', - } - }, - { - predicate: (properties) => { return properties.styleName === "placeStyle"; }, - style: { - strokeColor: '${strokeColor}', - strokeWidth: '${strokeWidth}', - strokeDashStyle: "${strokeDashStyle}" - } - } - ], - }; - sdk; + /* eslint-enable no-unused-vars */ - constructor(sdk = undefined) { - if (!sdk) { - const msg = "SDK Must be defined to use GLE"; - console.error(msg); - throw new Error(msg); - } - this.sdk = sdk; + constructor() { this.linkCache = new GooglePlaceCache(); this.#initLayer(); + // NOTE: Arrow functions are necessary for calling methods on object instances. // This could be made more efficient by only processing the relevant places. W.model.events.register('mergeend', null, () => { this.#processPlaces(); }); - // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); - // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); - // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); - this.sdk.Events.on({ eventName: "wme-data-model-objects-added", eventHandler: () => { this.#processPlaces(); } }); - this.sdk.Events.on({ eventName: "wme-data-model-objects-removed", eventHandler: () => { this.#processPlaces(); } }); - this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: () => { this.#processPlaces(); } }); + W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); + W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); + W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); + // This is a special event that will be triggered when DOM elements are destroyed. /* eslint-disable wrap-iife, func-names, object-shorthand */ - (($) => { + (function($) { $.event.special.destroyed = { - remove: (o) => { + remove: function(o) { if (o.handler && o.type !== 'destroyed') { o.handler(); } @@ -162,261 +111,220 @@ const GoogleLinkEnhancer = ((() => { }; })(jQuery); /* eslint-enable wrap-iife, func-names, object-shorthand */ + // In case a place is already selected on load. - /** - * @type Selection - */ - const currentSelection = this.sdk.Editing.getSelection(); - // const selObjects = W.selectionManager.getSelectedDataModelObjects(); - if (currentSelection?.ids?.length && currentSelection.objectType === 'venue') { + const selObjects = W.selectionManager.getSelectedDataModelObjects(); + if (selObjects.length && selObjects[0].type === 'venue') { this.#formatLinkElements(); } - this.sdk.Events.on({ eventName: "wme-selection-changed", eventHandler: this.#onWmeSelectionChanged.bind(this) }); - // W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); + + W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); } + #initLayer() { - // this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', { - // uniqueName: '___GoogleLinkEnhancements', - // displayInLayerSwitcher: true, - // styleMap: new OpenLayers.StyleMap({ - // default: { - // strokeColor: '${strokeColor}', - // strokeWidth: '${strokeWidth}', - // strokeDashstyle: '${strokeDashstyle}', - // pointRadius: '15', - // fillOpacity: '0' - // } - // }) - // }); - // this.#mapLayer.setOpacity(0.8); - // W.map.addLayer(this.#mapLayer); - this.sdk.Map.addLayer({ layerName: _a.#mapLayer, styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules }); - this.sdk.Map.setLayerOpacity({ layerName: _a.#mapLayer, opacity: 0.8 }); + this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', { + uniqueName: '___GoogleLinkEnhancements', + displayInLayerSwitcher: true, + styleMap: new OpenLayers.StyleMap({ + default: { + strokeColor: '${strokeColor}', + strokeWidth: '${strokeWidth}', + strokeDashstyle: '${strokeDashstyle}', + pointRadius: '15', + fillOpacity: '0' + } + }) + }); + + this.#mapLayer.setOpacity(0.8); + W.map.addLayer(this.#mapLayer); } + #onWmeSelectionChanged() { if (this.#enabled) { this.#destroyPoint(); - // const selected = W.selectionManager.getSelectedDataModelObjects(); - const selected = this.sdk.Editing.getSelection(); - if (selected?.objectType === 'venue') { + const selected = W.selectionManager.getSelectedDataModelObjects(); + if (selected[0]?.type === 'venue') { // The setTimeout is necessary (in beta WME currently, at least) to allow the // panel UI DOM to update after a place is selected. setTimeout(() => this.#formatLinkElements(), 0); } } } + enable() { if (!this.#enabled) { this.#interceptPlacesService(); // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function. - $('#map').on('mouseenter', null, this, _a.#onMapMouseenter); - // W.model.venues.on('objectschanged', this.#formatLinkElements, this); - this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { this.#formatLinkElements().bind(this); } }); + $('#map').on('mouseenter', null, this, GLE.#onMapMouseenter); + W.model.venues.on('objectschanged', this.#formatLinkElements, this); this.#processPlaces(); this.#enabled = true; } } + disable() { if (this.#enabled) { - $('#map').off('mouseenter', _a.#onMapMouseenter); - // W.model.venues.off('objectschanged', this.#formatLinkElements, this); - this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { this.#formatLinkElements().bind(this); } }); + $('#map').off('mouseenter', GLE.#onMapMouseenter); + W.model.venues.off('objectschanged', this.#formatLinkElements, this); this.#enabled = false; } } + // The distance (in meters) before flagging a Waze place that is too far from the linked Google place. // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node. get distanceLimit() { return this.#distanceLimit; } + set distanceLimit(value) { this.#distanceLimit = value; } + get showTempClosedPOIs() { return this.#showTempClosedPOIs; } + set showTempClosedPOIs(value) { this.#showTempClosedPOIs = value; } + // Borrowed from WazeWrap static #distanceBetweenPoints(point1, point2) { - // const line = new OpenLayers.Geometry.LineString([point1, point2]); - // const length = line.getGeodesicLength(W.map.getProjectionObject()); - const length = turf.length(turf.geometryCollection([point1, point2])); + const line = new OpenLayers.Geometry.LineString([point1, point2]); + const length = line.getGeodesicLength(W.map.getProjectionObject()); return length; // multiply by 3.28084 to convert to feet } - static isPointVenue(venue) { - return venue.geometry.type === "Point"; - } + #isLinkTooFar(link, venue) { if (link.loc) { - // const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); - const linkPt = turf.point([link.loc.lng, link.loc.lat]); - // linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); + const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); + linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); let venuePt; let distanceLim = this.distanceLimit; - // if (venue.isPoint()) { - if (venue.geometry.type === "Point") { - venuePt = venue.geometry; - } - else { - // const bounds = venue.geometry.getBounds(); - // const center = bounds.getCenterLonLat(); - const center = turf.centroid(venue.geometry); - // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); - // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); - venuePt = center.geometry; - let bbox = venue.geometry.bbox; - if (!bbox) { - bbox = turf.bbox(venue.geometry); - } - const topRightPt = turf.point([bbox[0], bbox[1]]); - distanceLim += _a.#distanceBetweenPoints(venuePt, topRightPt.geometry); + if (venue.isPoint()) { + venuePt = venue.geometry.getCentroid(); + } else { + const bounds = venue.geometry.getBounds(); + const center = bounds.getCenterLonLat(); + venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); + const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); + distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt); } - const distance = _a.#distanceBetweenPoints(linkPt, venuePt); + const distance = GLE.#distanceBetweenPoints(linkPt, venuePt); return distance > distanceLim; } return false; } + #processPlaces() { if (this.#enabled) { try { + const that = this; // Get a list of already-linked id's - const existingLinks = GoogleLinkEnhancer.#getExistingLinks(this.sdk); - this.sdk.Map.removeAllFeaturesFromLayer({ layerName: _a.#mapLayer }); + const existingLinks = GoogleLinkEnhancer.#getExistingLinks(); + this.#mapLayer.removeAllFeatures(); const drawnLinks = []; - for (const venue of this.sdk.DataModel.Venues.getAll()) { - // W.model.venues.getObjectArray().forEach(venue => { + W.model.venues.getObjectArray().forEach(venue => { const promises = []; - // venue.attributes.externalProviderIDs.forEach(provID => { - for (const provID of venue.externalProviderIds) { - const id = provID; + venue.attributes.externalProviderIDs.forEach(provID => { + const id = provID.attributes.uuid; + // Check for duplicate links const linkInfo = existingLinks[id]; - if (linkInfo?.count > 1) { - // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); - const geometry = venue.geometry; - // const width = venue.isPoint() ? '4' : '12'; - const width = _a.isPointVenue(venue) ? 4 : 12; + if (linkInfo.count > 1) { + const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + const width = venue.isPoint() ? '4' : '12'; const color = '#fb8d00'; - // const features = [new OpenLayers.Feature.Vector(geometry, { - // strokeWidth: width, strokeColor: color - // })]; - const features = [ - _a.isPointVenue(venue) ? turf.point(geometry.coordinates, { - styleName: "venueStyle", - style: { - strokeWidth: width, - strokeColor: color - } - }) : turf.polygon(geometry, { - styleName: "venueStyle", - style: { - strokeColor: color, - strokeWidth: width - } - }) - ]; - const lineStart = turf.centroid(geometry); - // linkInfo.venues.forEach(linkVenue => { - for (const linkVenue of linkInfo.venues) { + const features = [new OpenLayers.Feature.Vector(geometry, { + strokeWidth: width, strokeColor: color + })]; + const lineStart = geometry.getCentroid(); + linkInfo.venues.forEach(linkVenue => { if (linkVenue !== venue && !drawnLinks.some(dl => (dl[0] === venue && dl[1] === linkVenue) || (dl[0] === linkVenue && dl[1] === venue))) { - const endPoint = turf.centroid(linkVenue.geometry); features.push( - // new OpenLayers.Feature.Vector( - // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), - // { - // strokeWidth: 4, - // strokeColor: color, - // strokeDashstyle: '12 12' - // } - // ) - turf.lineString([lineStart.geometry.coordinates, endPoint.geometry.coordinates], { styleName: "lineStyle", style: { - strokeWidth: 4, - strokeColor: color, - strokeDashstyle: '12 12' - } })); + new OpenLayers.Feature.Vector( + new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), + { + strokeWidth: 4, + strokeColor: color, + strokeDashstyle: '12 12' + } + ) + ); drawnLinks.push([venue, linkVenue]); } - } - ; - this.sdk.Map.addFeaturesToLayer({ features: features, layerName: _a.#mapLayer }); - // this.#mapLayer.addFeatures(features); + }); + that.#mapLayer.addFeatures(features); } - } - ; + }); + // Process all results of link lookups and add a highlight feature if needed. Promise.all(promises).then(results => { let strokeColor = null; let strokeDashStyle = 'solid'; - if (!this.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) { - if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.name) - || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.name)) { - strokeDashStyle = _a.isPointVenue(venue) ? '2 6' : '2 16'; + if (!that.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) { + if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.attributes.name) + || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.attributes.name)) { + strokeDashStyle = venue.isPoint() ? '2 6' : '2 16'; } strokeColor = '#F00'; - } - else if (results.some(res => this.#isLinkTooFar(res, venue))) { + } else if (results.some(res => that.#isLinkTooFar(res, venue))) { strokeColor = '#0FF'; - } - else if (!this.#DISABLE_CLOSED_PLACES && this.#showTempClosedPOIs && results.some(res => res.tempclosed)) { - if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.name) - || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.name)) { - strokeDashStyle = _a.isPointVenue(venue) ? '2 6' : '2 16'; + } else if (!that.#DISABLE_CLOSED_PLACES && that.#showTempClosedPOIs && results.some(res => res.tempclosed)) { + if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.attributes.name) + || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.attributes.name)) { + strokeDashStyle = venue.isPoint() ? '2 6' : '2 16'; } strokeColor = '#FD3'; - } - else if (results.some(res => res.notFound)) { + } else if (results.some(res => res.notFound)) { strokeColor = '#F0F'; } if (strokeColor) { const style = { - strokeWidth: _a.isPointVenue(venue) ? 4 : 12, + strokeWidth: venue.isPoint() ? '4' : '12', strokeColor, strokeDashStyle }; - // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); - const feature = _a.isPointVenue(venue) ? turf.point(venue.geometry.coordinates, { - styleName: "placeStyle", - style: style - }) : turf.polygon(venue.geometry.coordinates, { - styleName: "placeStyle", - style: style - }); - // this.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); + const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + that.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); } }); - } - ; - } - catch (ex) { + }); + } catch (ex) { console.error('PIE (Google Link Enhancer) error:', ex); } } } + static #onMapMouseenter(event) { // If the point isn't destroyed yet, destroy it when mousing over the map. event.data.#destroyPoint(); } + async #formatLinkElements() { const $links = $('#edit-panel').find(this.#EXT_PROV_ELEM_QUERY); if ($links.length) { - const existingLinks = _a.#getExistingLinks(this.sdk); + const existingLinks = GLE.#getExistingLinks(); + // fetch all links first const promises = []; const extProvElements = []; $links.each((ix, linkEl) => { const $linkEl = $(linkEl); extProvElements.push($linkEl); - const id = _a.#getIdFromElement($linkEl); + + const id = GLE.#getIdFromElement($linkEl); promises.push(this.linkCache.getPlace(id)); }); const links = await Promise.all(promises); + extProvElements.forEach(($extProvElem, i) => { - const id = _a.#getIdFromElement($extProvElem); - if (!id) - return; + const id = GLE.#getIdFromElement($extProvElem); + + if (!id) return; + const link = links[i]; if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) { setTimeout(() => { @@ -429,72 +337,52 @@ const GoogleLinkEnhancer = ((() => { if (link) { if (link.permclosed && !this.#DISABLE_CLOSED_PLACES) { $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FAA' }).attr('title', this.strings.permClosedPlace); - } - else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) { + } else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) { $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA' }).attr('title', this.strings.tempClosedPlace); - } - else if (link.notFound) { + } else if (link.notFound) { $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#F0F' }).attr('title', this.strings.badLink); - } - else { - // const venue = W.selectionManager.getSelectedDataModelObjects()[0]; - const selection = this.sdk.Editing.getSelection(); - if (selection?.objectType === "venue") { - const venue = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); - if (venue && this.#isLinkTooFar(link, venue)) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); - } - else { // reset in case we just deleted another provider - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); - } + } else { + const venue = W.selectionManager.getSelectedDataModelObjects()[0]; + if (this.#isLinkTooFar(link, venue)) { + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit)); + } else { // reset in case we just deleted another provider + $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); } } } }); } } - static #getExistingLinks(sdk = undefined) { - if (!sdk) { - const msg = "SDK Is Not Available"; - console.error(msg); - throw new Error(msg); - } + + static #getExistingLinks() { const existingLinks = {}; - // const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; - const thisVenue = sdk.Editing.getSelection(); - if (thisVenue?.objectType !== "venue") - return {}; - // W.model.venues.getObjectArray().forEach(venue => { - for (const venue of sdk.DataModel.Venues.getAll()) { - const isThisVenue = venue.id === thisVenue.ids[0]; + const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; + W.model.venues.getObjectArray().forEach(venue => { + const isThisVenue = venue === thisVenue; const thisPlaceIDs = []; - // venue.attributes.externalProviderIDs.forEach(provID => { - for (const provID of venue.externalProviderIds) { - // const id = provID.attributes.uuid; - const id = provID; + venue.attributes.externalProviderIDs.forEach(provID => { + const id = provID.attributes.uuid; if (!thisPlaceIDs.includes(id)) { thisPlaceIDs.push(id); let link = existingLinks[id]; if (link) { link.count++; link.venues.push(venue); - } - else { + } else { link = { count: 1, venues: [venue] }; existingLinks[id] = link; - // if (provID.attributes.url != null) { - // const u = provID.attributes.url.replace('https://maps.google.com/?', ''); - // link.url = u; - // } + if (provID.attributes.url != null) { + const u = provID.attributes.url.replace('https://maps.google.com/?', ''); + link.url = u; + } } link.isThisVenue = link.isThisVenue || isThisVenue; } - } - ; - } - ; + }); + }); return existingLinks; } + // Remove the POI point from the map. #destroyPoint() { if (this.#ptFeature) { @@ -504,89 +392,70 @@ const GoogleLinkEnhancer = ((() => { this.#lineFeature = null; } } - static #getOLMapExtent(sdk) { - // let extent = W.map.getExtent(); - // if (Array.isArray(extent)) { - // extent = new OpenLayers.Bounds(extent); - // extent.transform('EPSG:4326', 'EPSG:3857'); - // } - return sdk.Map.getMapExtent(); + + static #getOLMapExtent() { + let extent = W.map.getExtent(); + if (Array.isArray(extent)) { + extent = new OpenLayers.Bounds(extent); + extent.transform('EPSG:4326', 'EPSG:3857'); + } + return extent; } + // Add the POI point to the map. async #addPoint(id) { - if (!id) - return; + if (!id) return; const link = await this.linkCache.getPlace(id); if (link) { if (!link.notFound) { const coord = link.loc; - // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); - const poiPt = turf.point([coord.lng, coord.lat]); + const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); - // const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); - const selection = this.sdk.Editing.getSelection(); - let placeGeom; - if (selection?.objectType === "venue") { - const v = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); - placeGeom = v?.geometry && turf.centroid(v?.geometry)?.geometry; - } - // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); - const placePt = turf.point(placeGeom); - const ext = _a.#getOLMapExtent(this.sdk); - // const lsBounds = new OpenLayers.Geometry.LineString([ - // new OpenLayers.Geometry.Point(ext.left, ext.bottom), - // new OpenLayers.Geometry.Point(ext.left, ext.top), - // new OpenLayers.Geometry.Point(ext.right, ext.top), - // new OpenLayers.Geometry.Point(ext.right, ext.bottom), - // new OpenLayers.Geometry.Point(ext.left, ext.bottom)]); - const lsBounds = turf.lineString([ - [ext[0], ext[3]], - [ext[0], ext[1]], - [ext[2], ext[1]], - [ext[2], ext[3]], - [ext[0], ext[3]], - ]); + const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); + const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); + const ext = GLE.#getOLMapExtent(); + const lsBounds = new OpenLayers.Geometry.LineString([ + new OpenLayers.Geometry.Point(ext.left, ext.bottom), + new OpenLayers.Geometry.Point(ext.left, ext.top), + new OpenLayers.Geometry.Point(ext.right, ext.top), + new OpenLayers.Geometry.Point(ext.right, ext.bottom), + new OpenLayers.Geometry.Point(ext.left, ext.bottom)]); let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]); + // If the line extends outside the bounds, split it so we don't draw a line across the world. const splits = lsLine.splitWith(lsBounds); let label = ''; if (splits) { let splitPoints; - for (const split of splits) { - for (const component of split.components) { - if (component.x === placePt.x && component.y === placePt.y) - splitPoints = split; - } - ; - } - ; + splits.forEach(split => { + split.components.forEach(component => { + if (component.x === placePt.x && component.y === placePt.y) splitPoints = split; + }); + }); lsLine = new OpenLayers.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]); - let distance = _a.#distanceBetweenPoints(poiPt, placePt); + let distance = GLE.#distanceBetweenPoints(poiPt, placePt); let unitConversion; let unit1; let unit2; - // if (W.model.isImperial) { - if (this.sdk.Settings.getUserSettings().isImperial) { + if (W.model.isImperial) { distance *= 3.28084; unitConversion = 5280; unit1 = ' ft'; unit2 = ' mi'; - } - else { + } else { unitConversion = 1000; unit1 = ' m'; unit2 = ' km'; } if (distance > unitConversion * 10) { label = Math.round(distance / unitConversion) + unit2; - } - else if (distance > 1000) { + } else if (distance > 1000) { label = (Math.round(distance / (unitConversion / 10)) / 10) + unit2; - } - else { + } else { label = Math.round(distance) + unit1; } } + this.#destroyPoint(); // Just in case it still exists. this.#ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, { pointRadius: 6, @@ -610,8 +479,7 @@ const GoogleLinkEnhancer = ((() => { W.map.getLayerByUniqueName('venues').addFeatures([this.#ptFeature, this.#lineFeature]); this.#timeoutDestroyPoint(); } - } - else { + } else { // this.#getLinkInfoAsync(id).then(res => { // if (res.error || res.apiDisabled) { // // API was temporarily disabled. Ignore for now. @@ -621,30 +489,35 @@ const GoogleLinkEnhancer = ((() => { // }); } } + // Destroy the point after some time, if it hasn't been destroyed already. #timeoutDestroyPoint() { - if (this.#timeoutID) - clearTimeout(this.#timeoutID); + if (this.#timeoutID) clearTimeout(this.#timeoutID); this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000); } + static #getIdFromElement($el) { const providerIndex = $el.parent().children().toArray().indexOf($el[0]); return W.selectionManager.getSelectedDataModelObjects()[0].getExternalProviderIDs()[providerIndex]?.attributes.uuid; } + #addHoverEvent($el) { - $el.hover(() => this.#addPoint(_a.#getIdFromElement($el)), () => this.#destroyPoint()); + $el.hover(() => this.#addPoint(GLE.#getIdFromElement($el)), () => this.#destroyPoint()); } + #interceptPlacesService() { if (typeof google === 'undefined' || !google.maps || !google.maps.places || !google.maps.places.PlacesService) { console.debug('Google Maps PlacesService not loaded yet.'); setTimeout(this.#interceptPlacesService.bind(this), 500); // Retry until it loads return; } + const originalGetDetails = google.maps.places.PlacesService.prototype.getDetails; const that = this; google.maps.places.PlacesService.prototype.getDetails = function interceptedGetDetails(request, callback) { console.debug('Intercepted getDetails call:', request); - const customCallback = (result, status) => { + + const customCallback = function(result, status) { console.debug('Intercepted getDetails response:', result, status); const link = {}; switch (status) { @@ -653,8 +526,7 @@ const GoogleLinkEnhancer = ((() => { link.loc = { lng: loc.lng(), lat: loc.lat() }; if (result.business_status === google.maps.places.BusinessStatus.CLOSED_PERMANENTLY) { link.permclosed = true; - } - else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) { + } else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) { link.tempclosed = true; } that.linkCache.addPlace(request.placeId, link); @@ -669,11 +541,13 @@ const GoogleLinkEnhancer = ((() => { } callback(result, status); // Pass the result to the original callback }; + return originalGetDetails.call(this, request, customCallback); }; + console.debug('Google Maps PlacesService.getDetails intercepted successfully.'); } } - _a = GLE; + return GLE; -})()); +}()); diff --git a/SDKGoogleLinkEnhancer.js b/SDKGoogleLinkEnhancer.js new file mode 100644 index 0000000..083b2ab --- /dev/null +++ b/SDKGoogleLinkEnhancer.js @@ -0,0 +1,755 @@ +"use strict"; +// ==UserScript== +// @name WME Utils - Google Link Enhancer +// @namespace WazeDev +// @version 2025.06.13.001 +// @description Adds some extra WME functionality related to Google place links. +// @author MapOMatic, WazeDev group +// @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ +// @license GNU GPLv3 +// ==/UserScript== +/* global OpenLayers */ +/* global W */ +/* global google */ +/* eslint-disable max-classes-per-file */ +// eslint-disable-next-line func-names +// import * as turf from "@turf/turf"; +// import type { Venue, WmeSDK } from "wme-sdk-typings"; +// import $ from "jquery"; +const SDKGoogleLinkEnhancer = (() => { + "use strict"; + var _a; + class GooglePlaceCache { + cache; + pendingPromises; + constructor() { + this.cache = new Map(); + this.pendingPromises = new Map(); + } + async getPlace(placeId) { + if (this.cache.has(placeId)) { + return this.cache.get(placeId); + } + if (!this.pendingPromises.has(placeId)) { + let resolveFn; + let rejectFn; + const promise = new Promise((resolve, reject) => { + resolveFn = resolve; + rejectFn = reject; + // Set a timeout to reject the promise if not resolved in 3 seconds + setTimeout(() => { + if (this.pendingPromises.has(placeId)) { + this.pendingPromises.delete(placeId); + rejectFn(new Error(`Timeout: Place ${placeId} not found within 3 seconds`)); + } + }, 3000); + }); + this.pendingPromises.set(placeId, { promise, resolve: resolveFn, reject: rejectFn }); + } + return this.pendingPromises.get(placeId).promise; + } + addPlace(placeId, properties) { + this.cache.set(placeId, properties); + if (this.pendingPromises.has(placeId)) { + this.pendingPromises.get(placeId).resolve(properties); + this.pendingPromises.delete(placeId); + } + } + } + class GLE { + #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic. + #EXT_PROV_ELEM_QUERY = "wz-list-item.external-provider"; + #EXT_PROV_ELEM_EDIT_QUERY = "wz-list-item.external-provider-edit"; + #EXT_PROV_ELEM_CONTENT_QUERY = "div.external-provider-content"; + linkCache; + #enabled = false; + static #mapLayer = "Google Link Enhancements."; + #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place. + // Area place is calculated as #distanceLimit + + #showTempClosedPOIs = true; + #originalHeadAppendChildMethod; + #ptFeature; + #lineFeature; + #timeoutID; + strings = { + permClosedPlace: "Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.", + tempClosedPlace: "Google indicates this place is temporarily closed.", + multiLinked: "Linked more than once already. Please find and remove multiple links.", + linkedToThisPlace: "Already linked to this place", + linkedNearby: "Already linked to a nearby place", + linkedToXPlaces: "This is linked to {0} places", + badLink: "Invalid Google link. Please remove it.", + tooFar: "The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.", + }; + #styleConfig = { + styleContext: { + highNodeColor: (context) => { + return context?.feature?.properties?.style?.strokeColor; + }, + strokeColor: (context) => { + return context?.feature?.properties?.style?.strokeColor; + }, + strokeWidth: (context) => { + return context?.feature?.properties?.style?.strokeWidth; + }, + strokeDashStyle: (context) => { + return context?.feature?.properties?.style?.strokeDashstyle; + }, + }, + styleRules: [ + { + predicate: (properties) => { + return properties.styleName === "lineStyle"; + }, + style: { + strokeColor: "${strokeColor}", + }, + }, + { + predicate: (properties) => { + return properties.styleName === "default"; + }, + style: { + strokeColor: "${strokeColor}", + strokeWidth: "${strokeWidth}", + strokeDashstyle: "${strokeDashstyle}", + pointRadius: 15, + fillOpacity: 0, + }, + }, + { + predicate: (properties) => { + return properties.styleName === "venueStyle"; + }, + style: { + strokeColor: "${strokeColor}", + strokeWidth: "${strokeWidth}", + }, + }, + { + predicate: (properties) => { + return properties.styleName === "placeStyle"; + }, + style: { + strokeColor: "${strokeColor}", + strokeWidth: "${strokeWidth}", + strokeDashStyle: "${strokeDashStyle}", + }, + }, + ], + }; + sdk; + trf; + /* eslint-enable no-unused-vars */ + constructor(sdk, trf) { + let msg = ""; + if (!sdk) { + msg += "SDK Must be defined to use GLE"; + } + if (!turf) { + msg += "\n"; + msg += "Turf Library Must be made available to GLE to Implement Some of the Functionality"; + } + this.sdk = sdk; + this.trf = trf; + this.linkCache = new GooglePlaceCache(); + this.#initLayer(); + // NOTE: Arrow functions are necessary for calling methods on object instances. + // This could be made more efficient by only processing the relevant places. + W.model.events.register("mergeend", null, () => { + this.#processPlaces(); + }); + // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); + // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); + // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-added", + eventHandler: () => { + this.#processPlaces(); + }, + }); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-removed", + eventHandler: () => { + this.#processPlaces(); + }, + }); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-changed", + eventHandler: () => { + this.#processPlaces(); + }, + }); + // This is a special event that will be triggered when DOM elements are destroyed. + /* eslint-disable wrap-iife, func-names, object-shorthand */ + (($) => { + $.event.special.destroyed = { + remove: (o) => { + if (o.handler && o.type !== "destroyed") { + o.handler(); + } + }, + }; + })(jQuery); + /* eslint-enable wrap-iife, func-names, object-shorthand */ + // In case a place is already selected on load. + /** + * @type Selection + */ + const currentSelection = this.sdk.Editing.getSelection(); + // const selObjects = W.selectionManager.getSelectedDataModelObjects(); + if (currentSelection?.ids?.length && currentSelection.objectType === "venue") { + this.#formatLinkElements(); + } + this.sdk.Events.on({ + eventName: "wme-selection-changed", + eventHandler: this.#onWmeSelectionChanged.bind(this), + }); + // W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); + } + #initLayer() { + // this.#mapLayer = new OpenLayers.Layer.Vector('Google Link Enhancements.', { + // uniqueName: '___GoogleLinkEnhancements', + // displayInLayerSwitcher: true, + // styleMap: new OpenLayers.StyleMap({ + // default: { + // strokeColor: '${strokeColor}', + // strokeWidth: '${strokeWidth}', + // strokeDashstyle: '${strokeDashstyle}', + // pointRadius: '15', + // fillOpacity: '0' + // } + // }) + // }); + // this.#mapLayer.setOpacity(0.8); + // W.map.addLayer(this.#mapLayer); + this.sdk.Map.addLayer({ + layerName: _a.#mapLayer, + styleContext: this.#styleConfig.styleContext, + styleRules: this.#styleConfig.styleRules, + }); + this.sdk.Map.setLayerOpacity({ layerName: _a.#mapLayer, opacity: 0.8 }); + } + #onWmeSelectionChanged() { + if (this.#enabled) { + this.#destroyPoint(); + // const selected = W.selectionManager.getSelectedDataModelObjects(); + const selected = this.sdk.Editing.getSelection(); + if (selected?.objectType === "venue") { + // The setTimeout is necessary (in beta WME currently, at least) to allow the + // panel UI DOM to update after a place is selected. + setTimeout(() => this.#formatLinkElements(), 0); + } + } + } + enable() { + if (!this.#enabled) { + this.#interceptPlacesService(); + // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function. + $("#map").on("mouseenter", null, this, _a.#onMapMouseenter); + // W.model.venues.on('objectschanged', this.#formatLinkElements, this); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-changed", + eventHandler: (change) => { + this.#formatLinkElements().bind(this); + }, + }); + this.#processPlaces(); + this.#enabled = true; + } + } + disable() { + if (this.#enabled) { + $("#map").off("mouseenter", _a.#onMapMouseenter); + // W.model.venues.off('objectschanged', this.#formatLinkElements, this); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-changed", + eventHandler: (change) => { + this.#formatLinkElements().bind(this); + }, + }); + this.#enabled = false; + } + } + // The distance (in meters) before flagging a Waze place that is too far from the linked Google place. + // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node. + get distanceLimit() { + return this.#distanceLimit; + } + set distanceLimit(value) { + this.#distanceLimit = value; + } + get showTempClosedPOIs() { + return this.#showTempClosedPOIs; + } + set showTempClosedPOIs(value) { + this.#showTempClosedPOIs = value; + } + // Borrowed from WazeWrap + #distanceBetweenPoints(point1, point2) { + // const line = new OpenLayers.Geometry.LineString([point1, point2]); + // const length = line.getGeodesicLength(W.map.getProjectionObject()); + const ls = this.trf.lineString([point1.coordinates, point2.coordinates]); + const length = this.trf.length(ls); + return length * 1000; // multiply by 3.28084 to convert to feet + } + static isPointVenue(venue) { + return venue.geometry.type === "Point"; + } + #isLinkTooFar(link, venue) { + if (link.loc) { + // const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); + const linkPt = turf.point([link.loc.lng, link.loc.lat]); + // linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); + let venuePt; + let distanceLim = this.distanceLimit; + // if (venue.isPoint()) { + if (venue.geometry.type === "Point") { + venuePt = venue.geometry; + } + else { + // const bounds = venue.geometry.getBounds(); + // const center = bounds.getCenterLonLat(); + const center = this.trf.centroid(venue.geometry); + // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); + // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); + venuePt = center.geometry; + let bbox = venue.geometry.bbox; + if (!bbox) { + bbox = this.trf.bbox(venue.geometry); + } + const topRightPt = this.trf.point([bbox[0], bbox[1]]); + distanceLim += this.#distanceBetweenPoints(venuePt, topRightPt.geometry); + } + const distance = this.#distanceBetweenPoints(linkPt, venuePt); + return distance > distanceLim; + } + return false; + } + #processPlaces() { + if (this.#enabled) { + try { + // Get a list of already-linked id's + const existingLinks = SDKGoogleLinkEnhancer.#getExistingLinks(this.sdk); + this.sdk.Map.removeAllFeaturesFromLayer({ layerName: _a.#mapLayer }); + const drawnLinks = []; + for (const venue of this.sdk.DataModel.Venues.getAll()) { + // W.model.venues.getObjectArray().forEach(venue => { + const promises = []; + // venue.attributes.externalProviderIDs.forEach(provID => { + for (const provID of venue.externalProviderIds) { + const id = provID; + // Check for duplicate links + const linkInfo = existingLinks[id]; + if (linkInfo?.count > 1) { + // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + const geometry = venue.geometry; + // const width = venue.isPoint() ? '4' : '12'; + const width = _a.isPointVenue(venue) ? 4 : 12; + const color = "#fb8d00"; + // const features = [new OpenLayers.Feature.Vector(geometry, { + // strokeWidth: width, strokeColor: color + // })]; + const features = [ + _a.isPointVenue(venue) + ? this.trf.point(geometry.coordinates, { + styleName: "venueStyle", + style: { + strokeWidth: width, + strokeColor: color, + }, + }) + : this.trf.polygon(geometry, { + styleName: "venueStyle", + style: { + strokeColor: color, + strokeWidth: width, + }, + }), + ]; + const lineStart = this.trf.centroid(geometry); + // linkInfo.venues.forEach(linkVenue => { + for (const linkVenue of linkInfo.venues) { + if (linkVenue !== venue && + !drawnLinks.some((dl) => (dl[0] === venue && dl[1] === linkVenue) || + (dl[0] === linkVenue && dl[1] === venue))) { + const endPoint = this.trf.centroid(linkVenue.geometry); + features.push( + // new OpenLayers.Feature.Vector( + // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), + // { + // strokeWidth: 4, + // strokeColor: color, + // strokeDashstyle: '12 12' + // } + // ) + this.trf.lineString([lineStart.geometry.coordinates, endPoint.geometry.coordinates], { + styleName: "lineStyle", + style: { + strokeWidth: 4, + strokeColor: color, + strokeDashstyle: "12 12", + }, + })); + drawnLinks.push([venue, linkVenue]); + } + } + this.sdk.Map.addFeaturesToLayer({ features: features, layerName: _a.#mapLayer }); + // this.#mapLayer.addFeatures(features); + } + } + // Process all results of link lookups and add a highlight feature if needed. + Promise.all(promises).then((results) => { + let strokeColor = null; + let strokeDashStyle = "solid"; + if (!this.#DISABLE_CLOSED_PLACES && results.some((res) => res.permclosed)) { + if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.name) || + /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.name)) { + strokeDashStyle = _a.isPointVenue(venue) ? "2 6" : "2 16"; + } + strokeColor = "#F00"; + } + else if (results.some((res) => this.#isLinkTooFar(res, venue))) { + strokeColor = "#0FF"; + } + else if (!this.#DISABLE_CLOSED_PLACES && + this.#showTempClosedPOIs && + results.some((res) => res.tempclosed)) { + if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.name) || + /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.name)) { + strokeDashStyle = _a.isPointVenue(venue) ? "2 6" : "2 16"; + } + strokeColor = "#FD3"; + } + else if (results.some((res) => res.notFound)) { + strokeColor = "#F0F"; + } + if (strokeColor) { + const style = { + strokeWidth: _a.isPointVenue(venue) ? 4 : 12, + strokeColor, + strokeDashStyle, + }; + // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); + const feature = _a.isPointVenue(venue) + ? this.trf.point(venue.geometry.coordinates, { + styleName: "placeStyle", + style: style, + }) + : this.trf.polygon(venue.geometry.coordinates, { + styleName: "placeStyle", + style: style, + }); + // this.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); + } + }); + } + } + catch (ex) { + console.error("PIE (Google Link Enhancer) error:", ex); + } + } + } + static #onMapMouseenter(event) { + // If the point isn't destroyed yet, destroy it when mousing over the map. + event.data.#destroyPoint(); + } + async #formatLinkElements() { + const $links = $("#edit-panel").find(this.#EXT_PROV_ELEM_QUERY); + if ($links.length) { + const existingLinks = _a.#getExistingLinks(this.sdk); + // fetch all links first + const promises = []; + const extProvElements = []; + $links.each((ix, linkEl) => { + const $linkEl = $(linkEl); + extProvElements.push($linkEl); + const id = _a.#getIdFromElement($linkEl); + promises.push(this.linkCache.getPlace(id)); + }); + const links = await Promise.all(promises); + extProvElements.forEach(($extProvElem, i) => { + const id = _a.#getIdFromElement($extProvElem); + if (!id) + return; + const link = links[i]; + if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) { + setTimeout(() => { + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#FFA500" }) + .attr({ + title: this.strings.linkedToXPlaces.replace("{0}", existingLinks[id].count), + }); + }, 50); + } + this.#addHoverEvent($extProvElem); + if (link) { + if (link.permclosed && !this.#DISABLE_CLOSED_PLACES) { + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#FAA" }) + .attr("title", this.strings.permClosedPlace); + } + else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) { + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#FFA" }) + .attr("title", this.strings.tempClosedPlace); + } + else if (link.notFound) { + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#F0F" }) + .attr("title", this.strings.badLink); + } + else { + // const venue = W.selectionManager.getSelectedDataModelObjects()[0]; + const selection = this.sdk.Editing.getSelection(); + if (selection?.objectType === "venue") { + const venue = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); + if (venue && this.#isLinkTooFar(link, venue)) { + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#0FF" }) + .attr("title", this.strings.tooFar.replace("{0}", this.distanceLimit.toString())); + } + else { + // reset in case we just deleted another provider + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "" }) + .attr("title", ""); + } + } + } + } + }); + } + } + static #getExistingLinks(sdk = undefined) { + if (!sdk) { + const msg = "SDK Is Not Available"; + console.error(msg); + throw new Error(msg); + } + const existingLinks = {}; + // const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; + const thisVenue = sdk.Editing.getSelection(); + if (thisVenue?.objectType !== "venue") + return {}; + // W.model.venues.getObjectArray().forEach(venue => { + for (const venue of sdk.DataModel.Venues.getAll()) { + const isThisVenue = venue.id === thisVenue.ids[0]; + const thisPlaceIDs = []; + // venue.attributes.externalProviderIDs.forEach(provID => { + for (const provID of venue.externalProviderIds) { + // const id = provID.attributes.uuid; + const id = provID; + if (!thisPlaceIDs.includes(id)) { + thisPlaceIDs.push(id); + let link = existingLinks[id]; + if (link) { + link.count++; + link.venues.push(venue); + } + else { + link = { count: 1, venues: [venue] }; + existingLinks[id] = link; + // if (provID.attributes.url != null) { + // const u = provID.attributes.url.replace('https://maps.google.com/?', ''); + // link.url = u; + // } + } + link.isThisVenue = link.isThisVenue || isThisVenue; + } + } + } + return existingLinks; + } + // Remove the POI point from the map. + #destroyPoint() { + if (this.#ptFeature) { + this.#ptFeature.destroy(); + this.#ptFeature = null; + this.#lineFeature.destroy(); + this.#lineFeature = null; + } + } + #getOLMapExtent() { + // let extent = W.map.getExtent(); + // if (Array.isArray(extent)) { + // extent = new OpenLayers.Bounds(extent); + // extent.transform('EPSG:4326', 'EPSG:3857'); + // } + return this.sdk.Map.getMapExtent(); + } + // Add the POI point to the map. + async #addPoint(id) { + if (!id) + return; + const link = await this.linkCache.getPlace(id); + if (link) { + if (!link.notFound) { + const coord = link.loc; + // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); + const poiPt = this.trf.point([coord.lng, coord.lat]); + poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); + // const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); + const selection = this.sdk.Editing.getSelection(); + let placeGeom; + if (selection?.objectType === "venue") { + const v = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); + placeGeom = v?.geometry && this.trf.centroid(v?.geometry)?.geometry; + } + // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); + const placePt = this.trf.point(placeGeom); + const ext = this.#getOLMapExtent(); + // const lsBounds = new OpenLayers.Geometry.LineString([ + // new OpenLayers.Geometry.Point(ext.left, ext.bottom), + // new OpenLayers.Geometry.Point(ext.left, ext.top), + // new OpenLayers.Geometry.Point(ext.right, ext.top), + // new OpenLayers.Geometry.Point(ext.right, ext.bottom), + // new OpenLayers.Geometry.Point(ext.left, ext.bottom)]); + const lsBounds = this.trf.lineString([ + [ext[0], ext[3]], + [ext[0], ext[1]], + [ext[2], ext[1]], + [ext[2], ext[3]], + [ext[0], ext[3]], + ]); + // let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]); + let lsLine = this.trf.lineString([placePt.geometry.coordinates, poiPt.geometry.coordinates]); + // If the line extends outside the bounds, split it so we don't draw a line across the world. + const splits = this.trf.lineSplit(lsLine, lsBounds); + let label = ""; + if (splits) { + let splitPoints; + for (const split of splits.features) { + for (const component of split.geometry.coordinates) { + if (component[0] === placePt.geometry.coordinates[0] && + component[1] === placePt.geometry.coordinates[1]) + splitPoints = split; + } + } + lsLine = splitPoints; + let distance = this.#distanceBetweenPoints(poiPt, placePt); + let unitConversion; + let unit1; + let unit2; + // if (W.model.isImperial) { + if (this.sdk.Settings.getUserSettings().isImperial) { + distance *= 3.28084; + unitConversion = 5280; + unit1 = " ft"; + unit2 = " mi"; + } + else { + unitConversion = 1000; + unit1 = " m"; + unit2 = " km"; + } + if (distance > unitConversion * 10) { + label = Math.round(distance / unitConversion) + unit2; + } + else if (distance > 1000) { + label = Math.round(distance / (unitConversion / 10)) / 10 + unit2; + } + else { + label = Math.round(distance) + unit1; + } + } + this.#destroyPoint(); // Just in case it still exists. + this.#ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, { + pointRadius: 6, + strokeWidth: 30, + strokeColor: "#FF0", + fillColor: "#FF0", + strokeOpacity: 0.5, + }); + this.#lineFeature = new OpenLayers.Feature.Vector(lsLine, {}, { + strokeWidth: 3, + strokeDashstyle: "12 8", + strokeColor: "#FF0", + label, + labelYOffset: 45, + fontColor: "#FF0", + fontWeight: "bold", + labelOutlineColor: "#000", + labelOutlineWidth: 4, + fontSize: "18", + }); + W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); + this.#timeoutDestroyPoint(); + } + } + else { + // this.#getLinkInfoAsync(id).then(res => { + // if (res.error || res.apiDisabled) { + // // API was temporarily disabled. Ignore for now. + // } else { + // this.#addPoint(id); + // } + // }); + } + } + // Destroy the point after some time, if it hasn't been destroyed already. + #timeoutDestroyPoint() { + if (this.#timeoutID) + clearTimeout(this.#timeoutID); + this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000); + } + static #getIdFromElement($el) { + const providerIndex = $el.parent().children().toArray().indexOf($el[0]); + return W.selectionManager.getSelectedDataModelObjects()[0].getExternalProviderIDs()[providerIndex] + ?.attributes.uuid; + } + #addHoverEvent($el) { + $el.hover(() => this.#addPoint(_a.#getIdFromElement($el)), () => this.#destroyPoint()); + } + #interceptPlacesService() { + if (typeof google === "undefined" || + !google.maps || + !google.maps.places || + !google.maps.places.PlacesService) { + console.debug("Google Maps PlacesService not loaded yet."); + setTimeout(this.#interceptPlacesService.bind(this), 500); // Retry until it loads + return; + } + const originalGetDetails = google.maps.places.PlacesService.prototype.getDetails; + const that = this; + google.maps.places.PlacesService.prototype.getDetails = function interceptedGetDetails(request, callback) { + console.debug("Intercepted getDetails call:", request); + const customCallback = (result, status) => { + console.debug("Intercepted getDetails response:", result, status); + const link = {}; + switch (status) { + case google.maps.places.PlacesServiceStatus.OK: { + const loc = result.geometry.location; + link.loc = { lng: loc.lng(), lat: loc.lat() }; + if (result.business_status === google.maps.places.BusinessStatus.CLOSED_PERMANENTLY) { + link.permclosed = true; + } + else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) { + link.tempclosed = true; + } + that.linkCache.addPlace(request.placeId, link); + break; + } + case google.maps.places.PlacesServiceStatus.NOT_FOUND: + link.notfound = true; + that.linkCache.addPlace(request.placeId, link); + break; + default: + link.error = status; + } + callback(result, status); // Pass the result to the original callback + }; + return originalGetDetails.call(this, request, customCallback); + }; + console.debug("Google Maps PlacesService.getDetails intercepted successfully."); + } + } + _a = GLE; + return GLE; +})(); diff --git a/GoogleLinkEnhancer.ts b/SDKGoogleLinkEnhancer.ts similarity index 56% rename from GoogleLinkEnhancer.ts rename to SDKGoogleLinkEnhancer.ts index ef222f2..3547cca 100644 --- a/GoogleLinkEnhancer.ts +++ b/SDKGoogleLinkEnhancer.ts @@ -5,7 +5,6 @@ // @description Adds some extra WME functionality related to Google place links. // @author MapOMatic, WazeDev group // @include /^https:\/\/(www|beta)\.waze\.com\/(?!user\/)(.{2,6}\/)?editor\/?.*$/ -// @require https://cdn.jsdelivr.net/npm/@turf/turf@7.2.0/turf.min.js // @license GNU GPLv3 // ==/UserScript== @@ -20,10 +19,13 @@ // import type { Venue, WmeSDK } from "wme-sdk-typings"; // import $ from "jquery"; -const GoogleLinkEnhancer = ((() => { - 'use strict'; + +const SDKGoogleLinkEnhancer = (() => { + "use strict"; class GooglePlaceCache { + cache: Map; + pendingPromises: Map; constructor() { this.cache = new Map(); this.pendingPromises = new Map(); @@ -67,15 +69,16 @@ const GoogleLinkEnhancer = ((() => { } interface LinkInfo { - count: number, - venues: Venue[], - }; - class GLE { + isThisVenue: boolean; + count: number; + venues: Venue[]; + } + class GLE { #DISABLE_CLOSED_PLACES = false; // Set to TRUE if the feature needs to be temporarily disabled, e.g. during the COVID-19 pandemic. - #EXT_PROV_ELEM_QUERY = 'wz-list-item.external-provider'; - #EXT_PROV_ELEM_EDIT_QUERY = 'wz-list-item.external-provider-edit'; - #EXT_PROV_ELEM_CONTENT_QUERY = 'div.external-provider-content'; + #EXT_PROV_ELEM_QUERY = "wz-list-item.external-provider"; + #EXT_PROV_ELEM_EDIT_QUERY = "wz-list-item.external-provider-edit"; + #EXT_PROV_ELEM_CONTENT_QUERY = "div.external-provider-content"; linkCache; #enabled = false; @@ -88,14 +91,15 @@ const GoogleLinkEnhancer = ((() => { #lineFeature; #timeoutID; strings = { - permClosedPlace: 'Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.', - tempClosedPlace: 'Google indicates this place is temporarily closed.', - multiLinked: 'Linked more than once already. Please find and remove multiple links.', - linkedToThisPlace: 'Already linked to this place', - linkedNearby: 'Already linked to a nearby place', - linkedToXPlaces: 'This is linked to {0} places', - badLink: 'Invalid Google link. Please remove it.', - tooFar: 'The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.' + permClosedPlace: + "Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.", + tempClosedPlace: "Google indicates this place is temporarily closed.", + multiLinked: "Linked more than once already. Please find and remove multiple links.", + linkedToThisPlace: "Already linked to this place", + linkedNearby: "Already linked to a nearby place", + linkedToXPlaces: "This is linked to {0} places", + badLink: "Invalid Google link. Please remove it.", + tooFar: "The Google linked place is more than {0} meters from the Waze place. Please verify the link is correct.", }; #styleConfig = { @@ -115,71 +119,100 @@ const GoogleLinkEnhancer = ((() => { }, styleRules: [ { - predicate: (properties) => { return properties.styleName === "lineStyle"; }, + predicate: (properties) => { + return properties.styleName === "lineStyle"; + }, style: { strokeColor: "${strokeColor}", }, }, { - predicate: (properties) => { return properties.styleName === "default"; }, + predicate: (properties) => { + return properties.styleName === "default"; + }, style: { - strokeColor: '${strokeColor}', - strokeWidth: '${strokeWidth}', - strokeDashstyle: '${strokeDashstyle}', + strokeColor: "${strokeColor}", + strokeWidth: "${strokeWidth}", + strokeDashstyle: "${strokeDashstyle}", pointRadius: 15, - fillOpacity: 0 - } + fillOpacity: 0, + }, }, { - predicate: (properties) => { return properties.styleName === "venueStyle"; }, + predicate: (properties) => { + return properties.styleName === "venueStyle"; + }, style: { - strokeColor: '${strokeColor}', - strokeWidth: '${strokeWidth}', - } + strokeColor: "${strokeColor}", + strokeWidth: "${strokeWidth}", + }, }, { - predicate: (properties) => { return properties.styleName === "placeStyle"; }, + predicate: (properties) => { + return properties.styleName === "placeStyle"; + }, style: { - strokeColor: '${strokeColor}', - strokeWidth: '${strokeWidth}', - strokeDashStyle: "${strokeDashStyle}" - } - } + strokeColor: "${strokeColor}", + strokeWidth: "${strokeWidth}", + strokeDashStyle: "${strokeDashStyle}", + }, + }, ], }; sdk: WmeSDK; - + trf: typeof turf; /* eslint-enable no-unused-vars */ - constructor(sdk = undefined) { - if(!sdk) { - const msg = "SDK Must be defined to use GLE"; - console.error(msg); - throw new Error(msg); + constructor(sdk: WmeSDK, trf: typeof turf) { + let msg = ""; + if (!sdk) { + msg += "SDK Must be defined to use GLE"; + } + if (!turf) { + msg += "\n"; + msg += "Turf Library Must be made available to GLE to Implement Some of the Functionality"; } this.sdk = sdk; + this.trf = trf; this.linkCache = new GooglePlaceCache(); this.#initLayer(); // NOTE: Arrow functions are necessary for calling methods on object instances. // This could be made more efficient by only processing the relevant places. - W.model.events.register('mergeend', null, () => { this.#processPlaces(); }); + W.model.events.register("mergeend", null, () => { + this.#processPlaces(); + }); // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); - this.sdk.Events.on({eventName:"wme-data-model-objects-added", eventHandler: () => { this.#processPlaces();}}); - this.sdk.Events.on({eventName:"wme-data-model-objects-removed", eventHandler: () => { this.#processPlaces();}}); - this.sdk.Events.on({eventName:"wme-data-model-objects-changed", eventHandler: () => { this.#processPlaces();}}); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-added", + eventHandler: () => { + this.#processPlaces(); + }, + }); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-removed", + eventHandler: () => { + this.#processPlaces(); + }, + }); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-changed", + eventHandler: () => { + this.#processPlaces(); + }, + }); // This is a special event that will be triggered when DOM elements are destroyed. /* eslint-disable wrap-iife, func-names, object-shorthand */ - (($) => { + (($: JQueryStatic) => { $.event.special.destroyed = { remove: (o) => { - if (o.handler && o.type !== 'destroyed') { + if (o.handler && o.type !== "destroyed") { o.handler(); } - } + }, }; })(jQuery); /* eslint-enable wrap-iife, func-names, object-shorthand */ @@ -190,11 +223,14 @@ const GoogleLinkEnhancer = ((() => { */ const currentSelection = this.sdk.Editing.getSelection(); // const selObjects = W.selectionManager.getSelectedDataModelObjects(); - if (currentSelection?.ids?.length && currentSelection.objectType === 'venue') { + if (currentSelection?.ids?.length && currentSelection.objectType === "venue") { this.#formatLinkElements(); } - this.sdk.Events.on({eventName: "wme-selection-changed", eventHandler: this.#onWmeSelectionChanged.bind(this)}); + this.sdk.Events.on({ + eventName: "wme-selection-changed", + eventHandler: this.#onWmeSelectionChanged.bind(this), + }); // W.selectionManager.events.register('selectionchanged', null, this.#onWmeSelectionChanged.bind(this)); } @@ -215,8 +251,12 @@ const GoogleLinkEnhancer = ((() => { // this.#mapLayer.setOpacity(0.8); // W.map.addLayer(this.#mapLayer); - this.sdk.Map.addLayer({layerName: GLE.#mapLayer, styleContext: this.#styleConfig.styleContext, styleRules: this.#styleConfig.styleRules}); - this.sdk.Map.setLayerOpacity({layerName: GLE.#mapLayer, opacity: 0.8}); + this.sdk.Map.addLayer({ + layerName: GLE.#mapLayer, + styleContext: this.#styleConfig.styleContext, + styleRules: this.#styleConfig.styleRules, + }); + this.sdk.Map.setLayerOpacity({ layerName: GLE.#mapLayer, opacity: 0.8 }); } #onWmeSelectionChanged() { @@ -224,7 +264,7 @@ const GoogleLinkEnhancer = ((() => { this.#destroyPoint(); // const selected = W.selectionManager.getSelectedDataModelObjects(); const selected = this.sdk.Editing.getSelection(); - if (selected?.objectType === 'venue') { + if (selected?.objectType === "venue") { // The setTimeout is necessary (in beta WME currently, at least) to allow the // panel UI DOM to update after a place is selected. setTimeout(() => this.#formatLinkElements(), 0); @@ -236,9 +276,14 @@ const GoogleLinkEnhancer = ((() => { if (!this.#enabled) { this.#interceptPlacesService(); // Note: Using on() allows passing "this" as a variable, so it can be used in the handler function. - $('#map').on('mouseenter', null, this, GLE.#onMapMouseenter); + $("#map").on("mouseenter", null, this, GLE.#onMapMouseenter); // W.model.venues.on('objectschanged', this.#formatLinkElements, this); - this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-changed", + eventHandler: (change) => { + this.#formatLinkElements().bind(this); + }, + }); this.#processPlaces(); this.#enabled = true; } @@ -246,9 +291,14 @@ const GoogleLinkEnhancer = ((() => { disable() { if (this.#enabled) { - $('#map').off('mouseenter', GLE.#onMapMouseenter); + $("#map").off("mouseenter", GLE.#onMapMouseenter); // W.model.venues.off('objectschanged', this.#formatLinkElements, this); - this.sdk.Events.on({eventName: "wme-data-model-objects-changed", eventHandler: (change) => {this.#formatLinkElements().bind(this)}}); + this.sdk.Events.on({ + eventName: "wme-data-model-objects-changed", + eventHandler: (change) => { + this.#formatLinkElements().bind(this); + }, + }); this.#enabled = false; } @@ -256,7 +306,7 @@ const GoogleLinkEnhancer = ((() => { // The distance (in meters) before flagging a Waze place that is too far from the linked Google place. // Area places use distanceLimit, plus the distance from the centroid of the AP to its furthest node. - get distanceLimit() : number { + get distanceLimit(): number { return this.#distanceLimit; } @@ -273,15 +323,15 @@ const GoogleLinkEnhancer = ((() => { } // Borrowed from WazeWrap - static #distanceBetweenPoints(point1: GeoJSON.Point | GeoJSON.Polygon, point2: GeoJSON.Point | GeoJSON.Polygon) { + #distanceBetweenPoints(point1: GeoJSON.Point, point2: GeoJSON.Point) { // const line = new OpenLayers.Geometry.LineString([point1, point2]); // const length = line.getGeodesicLength(W.map.getProjectionObject()); - - const length = turf.length(turf.geometryCollection([point1, point2])); - return length; // multiply by 3.28084 to convert to feet + const ls = this.trf.lineString([point1.coordinates, point2.coordinates]) + const length = this.trf.length(ls); + return length * 1000; // multiply by 3.28084 to convert to feet } static isPointVenue(venue: Venue) { - return venue.geometry.type === "Point" + return venue.geometry.type === "Point"; } #isLinkTooFar(link, venue: Venue) { if (link.loc) { @@ -291,23 +341,23 @@ const GoogleLinkEnhancer = ((() => { let venuePt: GeoJSON.Point; let distanceLim = this.distanceLimit; // if (venue.isPoint()) { - if(venue.geometry.type === "Point") { + if (venue.geometry.type === "Point") { venuePt = venue.geometry; } else { // const bounds = venue.geometry.getBounds(); // const center = bounds.getCenterLonLat(); - const center = turf.centroid(venue.geometry); + const center = this.trf.centroid(venue.geometry); // venuePt = new OpenLayers.Geometry.Point(center.lon, center.lat); // const topRightPt = new OpenLayers.Geometry.Point(bounds.right, bounds.top); venuePt = center.geometry; let bbox = venue.geometry.bbox; - if(!bbox) { - bbox = turf.bbox(venue.geometry); + if (!bbox) { + bbox = this.trf.bbox(venue.geometry); } - const topRightPt = turf.point([bbox[0], bbox[1]]); - distanceLim += GLE.#distanceBetweenPoints(venuePt, topRightPt.geometry); + const topRightPt = this.trf.point([bbox[0], bbox[1]]); + distanceLim += this.#distanceBetweenPoints(venuePt, topRightPt.geometry); } - const distance = GLE.#distanceBetweenPoints(linkPt, venuePt); + const distance = this.#distanceBetweenPoints(linkPt, venuePt); return distance > distanceLim; } return false; @@ -317,14 +367,14 @@ const GoogleLinkEnhancer = ((() => { if (this.#enabled) { try { // Get a list of already-linked id's - const existingLinks = GoogleLinkEnhancer.#getExistingLinks(this.sdk); - this.sdk.Map.removeAllFeaturesFromLayer({layerName: GLE.#mapLayer}); + const existingLinks = SDKGoogleLinkEnhancer.#getExistingLinks(this.sdk); + this.sdk.Map.removeAllFeaturesFromLayer({ layerName: GLE.#mapLayer }); const drawnLinks = []; - for(const venue of this.sdk.DataModel.Venues.getAll()) { - // W.model.venues.getObjectArray().forEach(venue => { + for (const venue of this.sdk.DataModel.Venues.getAll()) { + // W.model.venues.getObjectArray().forEach(venue => { const promises = []; // venue.attributes.externalProviderIDs.forEach(provID => { - for(const provID of venue.externalProviderIds) { + for (const provID of venue.externalProviderIds) { const id = provID; // Check for duplicate links @@ -334,31 +384,39 @@ const GoogleLinkEnhancer = ((() => { const geometry: GeoJSON.Point | GeoJSON.Polygon = venue.geometry; // const width = venue.isPoint() ? '4' : '12'; const width = GLE.isPointVenue(venue) ? 4 : 12; - const color = '#fb8d00'; + const color = "#fb8d00"; // const features = [new OpenLayers.Feature.Vector(geometry, { // strokeWidth: width, strokeColor: color // })]; const features: GeoJSON.Feature[] = [ - GLE.isPointVenue(venue) ? turf.point(geometry.coordinates, { - styleName: "venueStyle", - style: { - strokeWidth: width, - strokeColor: color - } - }) : turf.polygon(geometry, { - styleName: "venueStyle", - style: { - strokeColor: color, - strokeWidth: width - } - }) - ] - const lineStart = turf.centroid(geometry); + GLE.isPointVenue(venue) + ? this.trf.point(geometry.coordinates, { + styleName: "venueStyle", + style: { + strokeWidth: width, + strokeColor: color, + }, + }) + : this.trf.polygon(geometry, { + styleName: "venueStyle", + style: { + strokeColor: color, + strokeWidth: width, + }, + }), + ]; + const lineStart = this.trf.centroid(geometry); // linkInfo.venues.forEach(linkVenue => { - for(const linkVenue of linkInfo.venues) { - if (linkVenue !== venue - && !drawnLinks.some(dl => (dl[0] === venue && dl[1] === linkVenue) || (dl[0] === linkVenue && dl[1] === venue))) { - const endPoint = turf.centroid(linkVenue.geometry); + for (const linkVenue of linkInfo.venues) { + if ( + linkVenue !== venue && + !drawnLinks.some( + (dl) => + (dl[0] === venue && dl[1] === linkVenue) || + (dl[0] === linkVenue && dl[1] === venue) + ) + ) { + const endPoint = this.trf.centroid(linkVenue.geometry); features.push( // new OpenLayers.Feature.Vector( // new OpenLayers.Geometry.LineString([lineStart, linkVenue.geometry.getCentroid()]), @@ -368,61 +426,77 @@ const GoogleLinkEnhancer = ((() => { // strokeDashstyle: '12 12' // } // ) - turf.lineString([lineStart.geometry.coordinates, endPoint.geometry.coordinates], {styleName: "lineStyle", style: { - strokeWidth: 4, - strokeColor: color, - strokeDashstyle: '12 12' - }}) + this.trf.lineString( + [lineStart.geometry.coordinates, endPoint.geometry.coordinates], + { + styleName: "lineStyle", + style: { + strokeWidth: 4, + strokeColor: color, + strokeDashstyle: "12 12", + }, + } + ) ); drawnLinks.push([venue, linkVenue]); } - }; - this.sdk.Map.addFeaturesToLayer({features: features, layerName: GLE.#mapLayer}) + } + this.sdk.Map.addFeaturesToLayer({ features: features, layerName: GLE.#mapLayer }); // this.#mapLayer.addFeatures(features); } - }; + } // Process all results of link lookups and add a highlight feature if needed. - Promise.all(promises).then(results => { + Promise.all(promises).then((results) => { let strokeColor = null; - let strokeDashStyle = 'solid'; - if (!this.#DISABLE_CLOSED_PLACES && results.some(res => res.permclosed)) { - if (/^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.name) - || /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.name)) { - strokeDashStyle = GLE.isPointVenue(venue) ? '2 6' : '2 16'; + let strokeDashStyle = "solid"; + if (!this.#DISABLE_CLOSED_PLACES && results.some((res) => res.permclosed)) { + if ( + /^(\[|\()?(permanently )?closed(\]|\)| -)/i.test(venue.name) || + /(\(|- |\[)(permanently )?closed(\)|\])?$/i.test(venue.name) + ) { + strokeDashStyle = GLE.isPointVenue(venue) ? "2 6" : "2 16"; } - strokeColor = '#F00'; - } else if (results.some(res => this.#isLinkTooFar(res, venue))) { - strokeColor = '#0FF'; - } else if (!this.#DISABLE_CLOSED_PLACES && this.#showTempClosedPOIs && results.some(res => res.tempclosed)) { - if (/^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.name) - || /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.name)) { - strokeDashStyle = GLE.isPointVenue(venue) ? '2 6' : '2 16'; + strokeColor = "#F00"; + } else if (results.some((res) => this.#isLinkTooFar(res, venue))) { + strokeColor = "#0FF"; + } else if ( + !this.#DISABLE_CLOSED_PLACES && + this.#showTempClosedPOIs && + results.some((res) => res.tempclosed) + ) { + if ( + /^(\[|\()?(temporarily )?closed(\]|\)| -)/i.test(venue.name) || + /(\(|- |\[)(temporarily )?closed(\)|\])?$/i.test(venue.name) + ) { + strokeDashStyle = GLE.isPointVenue(venue) ? "2 6" : "2 16"; } - strokeColor = '#FD3'; - } else if (results.some(res => res.notFound)) { - strokeColor = '#F0F'; + strokeColor = "#FD3"; + } else if (results.some((res) => res.notFound)) { + strokeColor = "#F0F"; } if (strokeColor) { const style = { strokeWidth: GLE.isPointVenue(venue) ? 4 : 12, strokeColor, - strokeDashStyle + strokeDashStyle, }; // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); - const feature = GLE.isPointVenue(venue) ? turf.point(venue.geometry.coordinates, { - styleName: "placeStyle", - style: style - }) : turf.polygon(venue.geometry.coordinates, { - styleName: "placeStyle", - style: style - }); + const feature = GLE.isPointVenue(venue) + ? this.trf.point(venue.geometry.coordinates, { + styleName: "placeStyle", + style: style, + }) + : this.trf.polygon(venue.geometry.coordinates, { + styleName: "placeStyle", + style: style, + }); // this.#mapLayer.addFeatures([new OpenLayers.Feature.Vector(geometry, style)]); } }); - }; + } } catch (ex) { - console.error('PIE (Google Link Enhancer) error:', ex); + console.error("PIE (Google Link Enhancer) error:", ex); } } } @@ -433,7 +507,7 @@ const GoogleLinkEnhancer = ((() => { } async #formatLinkElements() { - const $links = $('#edit-panel').find(this.#EXT_PROV_ELEM_QUERY); + const $links = $("#edit-panel").find(this.#EXT_PROV_ELEM_QUERY); if ($links.length) { const existingLinks = GLE.#getExistingLinks(this.sdk); @@ -457,28 +531,50 @@ const GoogleLinkEnhancer = ((() => { const link = links[i]; if (existingLinks[id] && existingLinks[id].count > 1 && existingLinks[id].isThisVenue) { setTimeout(() => { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA500' }).attr({ - title: this.strings.linkedToXPlaces.replace('{0}', existingLinks[id].count) - }); + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#FFA500" }) + .attr({ + title: this.strings.linkedToXPlaces.replace("{0}", existingLinks[id].count), + }); }, 50); } this.#addHoverEvent($extProvElem); if (link) { if (link.permclosed && !this.#DISABLE_CLOSED_PLACES) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FAA' }).attr('title', this.strings.permClosedPlace); + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#FAA" }) + .attr("title", this.strings.permClosedPlace); } else if (link.tempclosed && !this.#DISABLE_CLOSED_PLACES) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#FFA' }).attr('title', this.strings.tempClosedPlace); + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#FFA" }) + .attr("title", this.strings.tempClosedPlace); } else if (link.notFound) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#F0F' }).attr('title', this.strings.badLink); + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#F0F" }) + .attr("title", this.strings.badLink); } else { // const venue = W.selectionManager.getSelectedDataModelObjects()[0]; const selection = this.sdk.Editing.getSelection(); - if(selection?.objectType === "venue") { - const venue = this.sdk.DataModel.Venues.getById({venueId: selection.ids[0]}); + if (selection?.objectType === "venue") { + const venue = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); if (venue && this.#isLinkTooFar(link, venue)) { - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '#0FF' }).attr('title', this.strings.tooFar.replace('{0}', this.distanceLimit.toString())); - } else { // reset in case we just deleted another provider - $extProvElem.find(this.#EXT_PROV_ELEM_CONTENT_QUERY).css({ backgroundColor: '' }).attr('title', ''); + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "#0FF" }) + .attr( + "title", + this.strings.tooFar.replace("{0}", this.distanceLimit.toString()) + ); + } else { + // reset in case we just deleted another provider + $extProvElem + .find(this.#EXT_PROV_ELEM_CONTENT_QUERY) + .css({ backgroundColor: "" }) + .attr("title", ""); } } } @@ -488,21 +584,21 @@ const GoogleLinkEnhancer = ((() => { } static #getExistingLinks(sdk: WmeSDK | undefined = undefined) { - if(!sdk) { + if (!sdk) { const msg = "SDK Is Not Available"; console.error(msg); throw new Error(msg); } const existingLinks: Record = {}; // const thisVenue = W.selectionManager.getSelectedDataModelObjects()[0]; - const thisVenue = sdk.Editing.getSelection() - if(thisVenue?.objectType !== "venue") return {}; + const thisVenue = sdk.Editing.getSelection(); + if (thisVenue?.objectType !== "venue") return {}; // W.model.venues.getObjectArray().forEach(venue => { - for(const venue of sdk.DataModel.Venues.getAll()) { + for (const venue of sdk.DataModel.Venues.getAll()) { const isThisVenue = venue.id === thisVenue.ids[0]; const thisPlaceIDs: string[] = []; // venue.attributes.externalProviderIDs.forEach(provID => { - for(const provID of venue.externalProviderIds) { + for (const provID of venue.externalProviderIds) { // const id = provID.attributes.uuid; const id = provID; if (!thisPlaceIDs.includes(id)) { @@ -521,8 +617,8 @@ const GoogleLinkEnhancer = ((() => { } link.isThisVenue = link.isThisVenue || isThisVenue; } - }; - }; + } + } return existingLinks; } @@ -536,13 +632,13 @@ const GoogleLinkEnhancer = ((() => { } } - static #getOLMapExtent(sdk: WmeSDK) { + #getOLMapExtent() { // let extent = W.map.getExtent(); // if (Array.isArray(extent)) { // extent = new OpenLayers.Bounds(extent); // extent.transform('EPSG:4326', 'EPSG:3857'); // } - return sdk.Map.getMapExtent(); + return this.sdk.Map.getMapExtent(); } // Add the POI point to the map. @@ -553,89 +649,102 @@ const GoogleLinkEnhancer = ((() => { if (!link.notFound) { const coord = link.loc; // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); - const poiPt = turf.point([coord.lng, coord.lat]); + const poiPt = this.trf.point([coord.lng, coord.lat]); poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); // const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); const selection = this.sdk.Editing.getSelection(); let placeGeom: GeoJSON.Geometry | undefined; - if(selection?.objectType === "venue") { - const v = this.sdk.DataModel.Venues.getById({venueId: selection.ids[0]}); - placeGeom = v?.geometry && turf.centroid(v?.geometry)?.geometry; + if (selection?.objectType === "venue") { + const v = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); + placeGeom = v?.geometry && this.trf.centroid(v?.geometry)?.geometry; } // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); - const placePt = turf.point(placeGeom); - const ext = GLE.#getOLMapExtent(this.sdk); + const placePt = this.trf.point(placeGeom); + const ext = this.#getOLMapExtent(); // const lsBounds = new OpenLayers.Geometry.LineString([ // new OpenLayers.Geometry.Point(ext.left, ext.bottom), // new OpenLayers.Geometry.Point(ext.left, ext.top), // new OpenLayers.Geometry.Point(ext.right, ext.top), // new OpenLayers.Geometry.Point(ext.right, ext.bottom), // new OpenLayers.Geometry.Point(ext.left, ext.bottom)]); - const lsBounds = turf.lineString([ + const lsBounds = this.trf.lineString([ [ext[0], ext[3]], [ext[0], ext[1]], [ext[2], ext[1]], [ext[2], ext[3]], [ext[0], ext[3]], - ]) - let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]); + ]); + // let lsLine = new OpenLayers.Geometry.LineString([placePt, poiPt]); + let lsLine = this.trf.lineString([placePt.geometry.coordinates, poiPt.geometry.coordinates]); // If the line extends outside the bounds, split it so we don't draw a line across the world. - const splits = lsLine.splitWith(lsBounds); - let label = ''; + const splits = this.trf.lineSplit(lsLine, lsBounds); + let label = ""; if (splits) { - let splitPoints; - for(const split of splits) { - for(const component of split.components) { - if (component.x === placePt.x && component.y === placePt.y) splitPoints = split; - }; - }; - lsLine = new OpenLayers.Geometry.LineString([splitPoints.components[0], splitPoints.components[1]]); - let distance = GLE.#distanceBetweenPoints(poiPt, placePt); + let splitPoints: GeoJSON.Feature; + for (const split of splits.features) { + for (const component of split.geometry.coordinates) { + if ( + component[0] === placePt.geometry.coordinates[0] && + component[1] === placePt.geometry.coordinates[1] + ) + splitPoints = split; + } + } + lsLine = splitPoints; + let distance = this.#distanceBetweenPoints(poiPt, placePt); let unitConversion; let unit1; let unit2; // if (W.model.isImperial) { - if(this.sdk.Settings.getUserSettings().isImperial) { + if (this.sdk.Settings.getUserSettings().isImperial) { distance *= 3.28084; unitConversion = 5280; - unit1 = ' ft'; - unit2 = ' mi'; + unit1 = " ft"; + unit2 = " mi"; } else { unitConversion = 1000; - unit1 = ' m'; - unit2 = ' km'; + unit1 = " m"; + unit2 = " km"; } if (distance > unitConversion * 10) { label = Math.round(distance / unitConversion) + unit2; } else if (distance > 1000) { - label = (Math.round(distance / (unitConversion / 10)) / 10) + unit2; + label = Math.round(distance / (unitConversion / 10)) / 10 + unit2; } else { label = Math.round(distance) + unit1; } } this.#destroyPoint(); // Just in case it still exists. - this.#ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, { - pointRadius: 6, - strokeWidth: 30, - strokeColor: '#FF0', - fillColor: '#FF0', - strokeOpacity: 0.5 - }); - this.#lineFeature = new OpenLayers.Feature.Vector(lsLine, {}, { - strokeWidth: 3, - strokeDashstyle: '12 8', - strokeColor: '#FF0', - label, - labelYOffset: 45, - fontColor: '#FF0', - fontWeight: 'bold', - labelOutlineColor: '#000', - labelOutlineWidth: 4, - fontSize: '18' - }); - W.map.getLayerByUniqueName('venues').addFeatures([this.#ptFeature, this.#lineFeature]); + this.#ptFeature = new OpenLayers.Feature.Vector( + poiPt, + { poiCoord: true }, + { + pointRadius: 6, + strokeWidth: 30, + strokeColor: "#FF0", + fillColor: "#FF0", + strokeOpacity: 0.5, + } + ); + this.#lineFeature = new OpenLayers.Feature.Vector( + lsLine, + {}, + { + strokeWidth: 3, + strokeDashstyle: "12 8", + strokeColor: "#FF0", + label, + labelYOffset: 45, + fontColor: "#FF0", + fontWeight: "bold", + labelOutlineColor: "#000", + labelOutlineWidth: 4, + fontSize: "18", + } + ); + W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); this.#timeoutDestroyPoint(); } } else { @@ -657,16 +766,25 @@ const GoogleLinkEnhancer = ((() => { static #getIdFromElement($el) { const providerIndex = $el.parent().children().toArray().indexOf($el[0]); - return W.selectionManager.getSelectedDataModelObjects()[0].getExternalProviderIDs()[providerIndex]?.attributes.uuid; + return W.selectionManager.getSelectedDataModelObjects()[0].getExternalProviderIDs()[providerIndex] + ?.attributes.uuid; } #addHoverEvent($el) { - $el.hover(() => this.#addPoint(GLE.#getIdFromElement($el)), () => this.#destroyPoint()); + $el.hover( + () => this.#addPoint(GLE.#getIdFromElement($el)), + () => this.#destroyPoint() + ); } #interceptPlacesService() { - if (typeof google === 'undefined' || !google.maps || !google.maps.places || !google.maps.places.PlacesService) { - console.debug('Google Maps PlacesService not loaded yet.'); + if ( + typeof google === "undefined" || + !google.maps || + !google.maps.places || + !google.maps.places.PlacesService + ) { + console.debug("Google Maps PlacesService not loaded yet."); setTimeout(this.#interceptPlacesService.bind(this), 500); // Retry until it loads return; } @@ -674,10 +792,10 @@ const GoogleLinkEnhancer = ((() => { const originalGetDetails = google.maps.places.PlacesService.prototype.getDetails; const that = this; google.maps.places.PlacesService.prototype.getDetails = function interceptedGetDetails(request, callback) { - console.debug('Intercepted getDetails call:', request); + console.debug("Intercepted getDetails call:", request); const customCallback = (result, status) => { - console.debug('Intercepted getDetails response:', result, status); + console.debug("Intercepted getDetails response:", result, status); const link = {}; switch (status) { case google.maps.places.PlacesServiceStatus.OK: { @@ -685,7 +803,9 @@ const GoogleLinkEnhancer = ((() => { link.loc = { lng: loc.lng(), lat: loc.lat() }; if (result.business_status === google.maps.places.BusinessStatus.CLOSED_PERMANENTLY) { link.permclosed = true; - } else if (result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY) { + } else if ( + result.business_status === google.maps.places.BusinessStatus.CLOSED_TEMPORARILY + ) { link.tempclosed = true; } that.linkCache.addPlace(request.placeId, link); @@ -704,9 +824,9 @@ const GoogleLinkEnhancer = ((() => { return originalGetDetails.call(this, request, customCallback); }; - console.debug('Google Maps PlacesService.getDetails intercepted successfully.'); + console.debug("Google Maps PlacesService.getDetails intercepted successfully."); } } return GLE; -})()); +})(); diff --git a/package.json b/package.json index 1f7c69b..6d1b8a0 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,11 @@ "@biomejs/biome": "^1.9.4", "@turf/turf": "^7.2.0", "@types/jquery": "^3.5.32", - "typescript": "^5.8.3", - "wme-sdk-typings": "https://web-assets.waze.com/wme_sdk_docs/beta/latest/wme-sdk-typings.tgz" + "typescript": "^5.8.3" }, "dependencies": { - "jquery": "^3.7.1" + "@googlemaps/extended-component-library": "^0.6.14", + "jquery": "^3.7.1", + "wme-sdk-typings": "https://web-assets.waze.com/wme_sdk_docs/beta/latest/wme-sdk-typings.tgz" } } From 7400c0039fc98bfca4560e10585e9540bde9ea9f Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sun, 15 Jun 2025 14:10:33 -0400 Subject: [PATCH 10/16] Store Google Link Enhancer. --- SDKGoogleLinkEnhancer.js | 135 ++++++++++++++++++++++++++++------- SDKGoogleLinkEnhancer.ts | 149 ++++++++++++++++++++++++++++++--------- 2 files changed, 228 insertions(+), 56 deletions(-) diff --git a/SDKGoogleLinkEnhancer.js b/SDKGoogleLinkEnhancer.js index 083b2ab..25d96a6 100644 --- a/SDKGoogleLinkEnhancer.js +++ b/SDKGoogleLinkEnhancer.js @@ -95,6 +95,27 @@ const SDKGoogleLinkEnhancer = (() => { strokeDashStyle: (context) => { return context?.feature?.properties?.style?.strokeDashstyle; }, + label: (context) => { + return context?.feature?.properties?.style?.label; + }, + labelYOffset: (context) => { + return context?.feature?.properties?.style?.labelYOffset; + }, + fontColor: (context) => { + return context?.feature?.properties?.style?.fontColor; + }, + fontWeight: (context) => { + return context?.feature?.properties?.style?.fontWeight; + }, + labelOutlineColor: (context) => { + return context?.feature?.properties?.style?.labelOutlineColor; + }, + labelOutlineWidth: (context) => { + return context?.feature?.properties?.style?.labelOutlineWidth; + }, + fontSize: (context) => { + return context?.feature?.properties?.style?.fontSize; + }, }, styleRules: [ { @@ -136,6 +157,35 @@ const SDKGoogleLinkEnhancer = (() => { strokeDashStyle: "${strokeDashStyle}", }, }, + { + predicate: (properties) => { + return properties.styleName === "googlePlacePointStyle"; + }, + style: { + pointRadius: "${pointRadius}", + strokeWidth: "${strokeWidth}", + strokeColor: "${strokeColor}", + fillColor: "${fillColor}", + strokeOpacity: "${strokeOpacity}", + }, + }, + { + predicate: (properties) => { + return properties.styleName === "googlePlaceLineStyle"; + }, + style: { + strokeWidth: "${strokeWidth}", + strokeDashstyle: "${strokeDashStyle}", + strokeColor: "${strokeColor}", + label: "${label}", + labelYOffset: "${labelYOffset}", + fontColor: "${fontColor}", + fontWeight: "${fontWeight}", + labelOutlineColor: "${labelOutlineColor}", + labelOutlineWidth: "${labelOutlineWidth}", + fontSize: "${fontSize}", + } + } ], }; sdk; @@ -570,9 +620,9 @@ const SDKGoogleLinkEnhancer = (() => { // Remove the POI point from the map. #destroyPoint() { if (this.#ptFeature) { - this.#ptFeature.destroy(); + // this.#ptFeature.destroy(); this.#ptFeature = null; - this.#lineFeature.destroy(); + // this.#lineFeature.destroy(); this.#lineFeature = null; } } @@ -594,7 +644,7 @@ const SDKGoogleLinkEnhancer = (() => { const coord = link.loc; // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); const poiPt = this.trf.point([coord.lng, coord.lat]); - poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); + // poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); // const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); const selection = this.sdk.Editing.getSelection(); let placeGeom; @@ -602,8 +652,11 @@ const SDKGoogleLinkEnhancer = (() => { const v = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); placeGeom = v?.geometry && this.trf.centroid(v?.geometry)?.geometry; } + else { + return; + } // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); - const placePt = this.trf.point(placeGeom); + const placePt = this.trf.point(placeGeom.coordinates); const ext = this.#getOLMapExtent(); // const lsBounds = new OpenLayers.Geometry.LineString([ // new OpenLayers.Geometry.Point(ext.left, ext.bottom), @@ -660,26 +713,60 @@ const SDKGoogleLinkEnhancer = (() => { } } this.#destroyPoint(); // Just in case it still exists. - this.#ptFeature = new OpenLayers.Feature.Vector(poiPt, { poiCoord: true }, { - pointRadius: 6, - strokeWidth: 30, - strokeColor: "#FF0", - fillColor: "#FF0", - strokeOpacity: 0.5, - }); - this.#lineFeature = new OpenLayers.Feature.Vector(lsLine, {}, { - strokeWidth: 3, - strokeDashstyle: "12 8", - strokeColor: "#FF0", - label, - labelYOffset: 45, - fontColor: "#FF0", - fontWeight: "bold", - labelOutlineColor: "#000", - labelOutlineWidth: 4, - fontSize: "18", - }); - W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); + // this.#ptFeature = new OpenLayers.Feature.Vector( + // poiPt, + // { poiCoord: true }, + // { + // pointRadius: 6, + // strokeWidth: 30, + // strokeColor: "#FF0", + // fillColor: "#FF0", + // strokeOpacity: 0.5, + // } + // ); + this.#ptFeature = this.trf.point(poiPt.geometry.coordinates, { + styleName: "googlePlacePointStyle", + style: { + pointRadius: 6, + strokeWidth: 30, + strokeColor: "#FF0", + fillColor: "#FF0", + strokeOpacity: 0.5, + }, + }, { id: `PoiPT_${poiPt.toString()}` }); + // this.#lineFeature = new OpenLayers.Feature.Vector( + // lsLine, + // {}, + // { + // strokeWidth: 3, + // strokeDashstyle: "12 8", + // strokeColor: "#FF0", + // label, + // labelYOffset: 45, + // fontColor: "#FF0", + // fontWeight: "bold", + // labelOutlineColor: "#000", + // labelOutlineWidth: 4, + // fontSize: "18", + // } + // ); + this.#lineFeature = this.trf.lineString(lsLine.geometry.coordinates, { + styleName: "googlePlaceLineStyle", + style: { + strokeWidth: 3, + strokeDashstyle: "12 8", + strokeColor: "#FF0", + label, + labelYOffset: 45, + fontColor: "#FF0", + fontWeight: "bold", + labelOutlineColor: "#000", + labelOutlineWidth: 4, + fontSize: "18", + }, + }, { id: `LsLine_${lsLine.toString()}` }); + // W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); + this.sdk.Map.addFeatures({ featues: [this.#ptFeature, this.#lineFeature], layerName: "venues" }); this.#timeoutDestroyPoint(); } } diff --git a/SDKGoogleLinkEnhancer.ts b/SDKGoogleLinkEnhancer.ts index 3547cca..6b6efaf 100644 --- a/SDKGoogleLinkEnhancer.ts +++ b/SDKGoogleLinkEnhancer.ts @@ -19,7 +19,6 @@ // import type { Venue, WmeSDK } from "wme-sdk-typings"; // import $ from "jquery"; - const SDKGoogleLinkEnhancer = (() => { "use strict"; @@ -116,6 +115,27 @@ const SDKGoogleLinkEnhancer = (() => { strokeDashStyle: (context) => { return context?.feature?.properties?.style?.strokeDashstyle; }, + label: (context) => { + return context?.feature?.properties?.style?.label; + }, + labelYOffset: (context) => { + return context?.feature?.properties?.style?.labelYOffset; + }, + fontColor: (context) => { + return context?.feature?.properties?.style?.fontColor; + }, + fontWeight: (context) => { + return context?.feature?.properties?.style?.fontWeight; + }, + labelOutlineColor: (context) => { + return context?.feature?.properties?.style?.labelOutlineColor; + }, + labelOutlineWidth: (context) => { + return context?.feature?.properties?.style?.labelOutlineWidth; + }, + fontSize: (context) => { + return context?.feature?.properties?.style?.fontSize; + }, }, styleRules: [ { @@ -157,6 +177,35 @@ const SDKGoogleLinkEnhancer = (() => { strokeDashStyle: "${strokeDashStyle}", }, }, + { + predicate: (properties) => { + return properties.styleName === "googlePlacePointStyle"; + }, + style: { + pointRadius: "${pointRadius}", + strokeWidth: "${strokeWidth}", + strokeColor: "${strokeColor}", + fillColor: "${fillColor}", + strokeOpacity: "${strokeOpacity}", + }, + }, + { + predicate: (properties) => { + return properties.styleName === "googlePlaceLineStyle"; + }, + style: { + strokeWidth: "${strokeWidth}", + strokeDashstyle: "${strokeDashStyle}", + strokeColor: "${strokeColor}", + label: "${label}", + labelYOffset: "${labelYOffset}", + fontColor: "${fontColor}", + fontWeight: "${fontWeight}", + labelOutlineColor: "${labelOutlineColor}", + labelOutlineWidth: "${labelOutlineWidth}", + fontSize: "${fontSize}", + } + } ], }; sdk: WmeSDK; @@ -326,7 +375,7 @@ const SDKGoogleLinkEnhancer = (() => { #distanceBetweenPoints(point1: GeoJSON.Point, point2: GeoJSON.Point) { // const line = new OpenLayers.Geometry.LineString([point1, point2]); // const length = line.getGeodesicLength(W.map.getProjectionObject()); - const ls = this.trf.lineString([point1.coordinates, point2.coordinates]) + const ls = this.trf.lineString([point1.coordinates, point2.coordinates]); const length = this.trf.length(ls); return length * 1000; // multiply by 3.28084 to convert to feet } @@ -625,9 +674,9 @@ const SDKGoogleLinkEnhancer = (() => { // Remove the POI point from the map. #destroyPoint() { if (this.#ptFeature) { - this.#ptFeature.destroy(); + // this.#ptFeature.destroy(); this.#ptFeature = null; - this.#lineFeature.destroy(); + // this.#lineFeature.destroy(); this.#lineFeature = null; } } @@ -642,7 +691,7 @@ const SDKGoogleLinkEnhancer = (() => { } // Add the POI point to the map. - async #addPoint(id) { + async #addPoint(id: string | null | undefined) { if (!id) return; const link = await this.linkCache.getPlace(id); if (link) { @@ -650,16 +699,18 @@ const SDKGoogleLinkEnhancer = (() => { const coord = link.loc; // const poiPt = new OpenLayers.Geometry.Point(coord.lng, coord.lat); const poiPt = this.trf.point([coord.lng, coord.lat]); - poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); + // poiPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject().projCode); // const placeGeom = W.selectionManager.getSelectedDataModelObjects()[0].geometry.getCentroid(); const selection = this.sdk.Editing.getSelection(); - let placeGeom: GeoJSON.Geometry | undefined; + let placeGeom: GeoJSON.Point; if (selection?.objectType === "venue") { const v = this.sdk.DataModel.Venues.getById({ venueId: selection.ids[0] }); placeGeom = v?.geometry && this.trf.centroid(v?.geometry)?.geometry; + } else { + return; } // const placePt = new OpenLayers.Geometry.Point(placeGeom.x, placeGeom.y); - const placePt = this.trf.point(placeGeom); + const placePt = this.trf.point(placeGeom.coordinates); const ext = this.#getOLMapExtent(); // const lsBounds = new OpenLayers.Geometry.LineString([ // new OpenLayers.Geometry.Point(ext.left, ext.bottom), @@ -717,34 +768,68 @@ const SDKGoogleLinkEnhancer = (() => { } this.#destroyPoint(); // Just in case it still exists. - this.#ptFeature = new OpenLayers.Feature.Vector( - poiPt, - { poiCoord: true }, + // this.#ptFeature = new OpenLayers.Feature.Vector( + // poiPt, + // { poiCoord: true }, + // { + // pointRadius: 6, + // strokeWidth: 30, + // strokeColor: "#FF0", + // fillColor: "#FF0", + // strokeOpacity: 0.5, + // } + // ); + this.#ptFeature = this.trf.point( + poiPt.geometry.coordinates, { - pointRadius: 6, - strokeWidth: 30, - strokeColor: "#FF0", - fillColor: "#FF0", - strokeOpacity: 0.5, - } + styleName: "googlePlacePointStyle", + style: { + pointRadius: 6, + strokeWidth: 30, + strokeColor: "#FF0", + fillColor: "#FF0", + strokeOpacity: 0.5, + }, + }, + { id: `PoiPT_${poiPt.toString()}` } ); - this.#lineFeature = new OpenLayers.Feature.Vector( - lsLine, - {}, + // this.#lineFeature = new OpenLayers.Feature.Vector( + // lsLine, + // {}, + // { + // strokeWidth: 3, + // strokeDashstyle: "12 8", + // strokeColor: "#FF0", + // label, + // labelYOffset: 45, + // fontColor: "#FF0", + // fontWeight: "bold", + // labelOutlineColor: "#000", + // labelOutlineWidth: 4, + // fontSize: "18", + // } + // ); + this.#lineFeature = this.trf.lineString( + lsLine.geometry.coordinates, { - strokeWidth: 3, - strokeDashstyle: "12 8", - strokeColor: "#FF0", - label, - labelYOffset: 45, - fontColor: "#FF0", - fontWeight: "bold", - labelOutlineColor: "#000", - labelOutlineWidth: 4, - fontSize: "18", - } + styleName: "googlePlaceLineStyle", + style: { + strokeWidth: 3, + strokeDashstyle: "12 8", + strokeColor: "#FF0", + label, + labelYOffset: 45, + fontColor: "#FF0", + fontWeight: "bold", + labelOutlineColor: "#000", + labelOutlineWidth: 4, + fontSize: "18", + }, + }, + { id: `LsLine_${lsLine.toString()}` } ); - W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); + // W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); + this.sdk.Map.addFeatures({featues: [this.#ptFeature, this.#lineFeature], layerName: "venues"}); this.#timeoutDestroyPoint(); } } else { From 3d6db61c9742c5ec78edb478c64ac0f5fb896ba9 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Mon, 30 Jun 2025 23:25:35 -0400 Subject: [PATCH 11/16] Store Google Link Enhancer. --- SDKGoogleLinkEnhancer.js | 28 +++++++++++++----------- SDKGoogleLinkEnhancer.ts | 47 +++++++++++++++++++++------------------- package.json | 2 +- 3 files changed, 41 insertions(+), 36 deletions(-) diff --git a/SDKGoogleLinkEnhancer.js b/SDKGoogleLinkEnhancer.js index 25d96a6..da632fb 100644 --- a/SDKGoogleLinkEnhancer.js +++ b/SDKGoogleLinkEnhancer.js @@ -116,6 +116,9 @@ const SDKGoogleLinkEnhancer = (() => { fontSize: (context) => { return context?.feature?.properties?.style?.fontSize; }, + pointRadius: (context) => { + return context?.feature?.properties?.style?.pointRadius; + }, }, styleRules: [ { @@ -339,7 +342,7 @@ const SDKGoogleLinkEnhancer = (() => { #distanceBetweenPoints(point1, point2) { // const line = new OpenLayers.Geometry.LineString([point1, point2]); // const length = line.getGeodesicLength(W.map.getProjectionObject()); - const ls = this.trf.lineString([point1.coordinates, point2.coordinates]); + const ls = this.trf.lineString([point1, point2]); const length = this.trf.length(ls); return length * 1000; // multiply by 3.28084 to convert to feet } @@ -349,7 +352,7 @@ const SDKGoogleLinkEnhancer = (() => { #isLinkTooFar(link, venue) { if (link.loc) { // const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); - const linkPt = turf.point([link.loc.lng, link.loc.lat]); + const linkPt = this.trf.point([link.loc.lng, link.loc.lat]); // linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); let venuePt; let distanceLim = this.distanceLimit; @@ -369,9 +372,9 @@ const SDKGoogleLinkEnhancer = (() => { bbox = this.trf.bbox(venue.geometry); } const topRightPt = this.trf.point([bbox[0], bbox[1]]); - distanceLim += this.#distanceBetweenPoints(venuePt, topRightPt.geometry); + distanceLim += this.#distanceBetweenPoints(venuePt.coordinates, topRightPt.geometry.coordinates); } - const distance = this.#distanceBetweenPoints(linkPt, venuePt); + const distance = this.#distanceBetweenPoints(linkPt.geometry.coordinates, venuePt.coordinates); return distance > distanceLim; } return false; @@ -408,14 +411,14 @@ const SDKGoogleLinkEnhancer = (() => { strokeWidth: width, strokeColor: color, }, - }) - : this.trf.polygon(geometry, { + }, { id: `venue_${geometry.toString()}` }) + : this.trf.polygon(geometry.coordinates, { styleName: "venueStyle", style: { strokeColor: color, strokeWidth: width, }, - }), + }, { id: `polyvenue_${geometry.toString()}` }), ]; const lineStart = this.trf.centroid(geometry); // linkInfo.venues.forEach(linkVenue => { @@ -440,7 +443,7 @@ const SDKGoogleLinkEnhancer = (() => { strokeColor: color, strokeDashstyle: "12 12", }, - })); + }, { id: `ls_${lineStart.geometry.toString()}_${endPoint.geometry.toString()}` })); drawnLinks.push([venue, linkVenue]); } } @@ -620,6 +623,7 @@ const SDKGoogleLinkEnhancer = (() => { // Remove the POI point from the map. #destroyPoint() { if (this.#ptFeature) { + this.sdk.Map.removeFeaturesFromLayer({ featureIds: [this.#ptFeature.id, this.#lineFeature.id], layerName: _a.#mapLayer }); // this.#ptFeature.destroy(); this.#ptFeature = null; // this.#lineFeature.destroy(); @@ -677,16 +681,14 @@ const SDKGoogleLinkEnhancer = (() => { const splits = this.trf.lineSplit(lsLine, lsBounds); let label = ""; if (splits) { - let splitPoints; for (const split of splits.features) { for (const component of split.geometry.coordinates) { if (component[0] === placePt.geometry.coordinates[0] && component[1] === placePt.geometry.coordinates[1]) - splitPoints = split; + lsLine = split; } } - lsLine = splitPoints; - let distance = this.#distanceBetweenPoints(poiPt, placePt); + let distance = this.#distanceBetweenPoints(poiPt.geometry.coordinates, placePt.geometry.coordinates); let unitConversion; let unit1; let unit2; @@ -766,7 +768,7 @@ const SDKGoogleLinkEnhancer = (() => { }, }, { id: `LsLine_${lsLine.toString()}` }); // W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); - this.sdk.Map.addFeatures({ featues: [this.#ptFeature, this.#lineFeature], layerName: "venues" }); + this.sdk.Map.addFeaturesToLayer({ features: [this.#ptFeature, this.#lineFeature], layerName: _a.#mapLayer }); this.#timeoutDestroyPoint(); } } diff --git a/SDKGoogleLinkEnhancer.ts b/SDKGoogleLinkEnhancer.ts index 6b6efaf..7cefb26 100644 --- a/SDKGoogleLinkEnhancer.ts +++ b/SDKGoogleLinkEnhancer.ts @@ -86,8 +86,8 @@ const SDKGoogleLinkEnhancer = (() => { // Area place is calculated as #distanceLimit + #showTempClosedPOIs = true; #originalHeadAppendChildMethod; - #ptFeature; - #lineFeature; + #ptFeature: GeoJSON.Feature | undefined; + #lineFeature: GeoJSON.Feature | undefined; #timeoutID; strings = { permClosedPlace: @@ -136,6 +136,9 @@ const SDKGoogleLinkEnhancer = (() => { fontSize: (context) => { return context?.feature?.properties?.style?.fontSize; }, + pointRadius: (context) => { + return context?.feature?.properties?.style?.pointRadius; + }, }, styleRules: [ { @@ -372,10 +375,10 @@ const SDKGoogleLinkEnhancer = (() => { } // Borrowed from WazeWrap - #distanceBetweenPoints(point1: GeoJSON.Point, point2: GeoJSON.Point) { + #distanceBetweenPoints(point1: GeoJSON.Position, point2: GeoJSON.Position) { // const line = new OpenLayers.Geometry.LineString([point1, point2]); // const length = line.getGeodesicLength(W.map.getProjectionObject()); - const ls = this.trf.lineString([point1.coordinates, point2.coordinates]); + const ls = this.trf.lineString([point1, point2]); const length = this.trf.length(ls); return length * 1000; // multiply by 3.28084 to convert to feet } @@ -385,7 +388,7 @@ const SDKGoogleLinkEnhancer = (() => { #isLinkTooFar(link, venue: Venue) { if (link.loc) { // const linkPt = new OpenLayers.Geometry.Point(link.loc.lng, link.loc.lat); - const linkPt = turf.point([link.loc.lng, link.loc.lat]); + const linkPt = this.trf.point([link.loc.lng, link.loc.lat]); // linkPt.transform(W.Config.map.projection.remote, W.map.getProjectionObject()); let venuePt: GeoJSON.Point; let distanceLim = this.distanceLimit; @@ -404,9 +407,9 @@ const SDKGoogleLinkEnhancer = (() => { bbox = this.trf.bbox(venue.geometry); } const topRightPt = this.trf.point([bbox[0], bbox[1]]); - distanceLim += this.#distanceBetweenPoints(venuePt, topRightPt.geometry); + distanceLim += this.#distanceBetweenPoints(venuePt.coordinates, topRightPt.geometry.coordinates); } - const distance = this.#distanceBetweenPoints(linkPt, venuePt); + const distance = this.#distanceBetweenPoints(linkPt.geometry.coordinates, venuePt.coordinates); return distance > distanceLim; } return false; @@ -418,7 +421,7 @@ const SDKGoogleLinkEnhancer = (() => { // Get a list of already-linked id's const existingLinks = SDKGoogleLinkEnhancer.#getExistingLinks(this.sdk); this.sdk.Map.removeAllFeaturesFromLayer({ layerName: GLE.#mapLayer }); - const drawnLinks = []; + const drawnLinks: Venue[][] = []; for (const venue of this.sdk.DataModel.Venues.getAll()) { // W.model.venues.getObjectArray().forEach(venue => { const promises = []; @@ -437,7 +440,7 @@ const SDKGoogleLinkEnhancer = (() => { // const features = [new OpenLayers.Feature.Vector(geometry, { // strokeWidth: width, strokeColor: color // })]; - const features: GeoJSON.Feature[] = [ + const features: GeoJSON.Feature[] = [ GLE.isPointVenue(venue) ? this.trf.point(geometry.coordinates, { styleName: "venueStyle", @@ -445,14 +448,14 @@ const SDKGoogleLinkEnhancer = (() => { strokeWidth: width, strokeColor: color, }, - }) - : this.trf.polygon(geometry, { + }, {id: `venue_${geometry.toString()}`}) + : this.trf.polygon(geometry.coordinates, { styleName: "venueStyle", style: { strokeColor: color, strokeWidth: width, }, - }), + }, {id: `polyvenue_${geometry.toString()}`}), ]; const lineStart = this.trf.centroid(geometry); // linkInfo.venues.forEach(linkVenue => { @@ -484,7 +487,8 @@ const SDKGoogleLinkEnhancer = (() => { strokeColor: color, strokeDashstyle: "12 12", }, - } + }, + { id: `ls_${lineStart.geometry.toString()}_${endPoint.geometry.toString()}`} ) ); drawnLinks.push([venue, linkVenue]); @@ -531,7 +535,7 @@ const SDKGoogleLinkEnhancer = (() => { strokeDashStyle, }; // const geometry = venue.isPoint() ? venue.geometry.getCentroid() : venue.geometry.clone(); - const feature = GLE.isPointVenue(venue) + const feature: Feature = GLE.isPointVenue(venue) ? this.trf.point(venue.geometry.coordinates, { styleName: "placeStyle", style: style, @@ -674,6 +678,7 @@ const SDKGoogleLinkEnhancer = (() => { // Remove the POI point from the map. #destroyPoint() { if (this.#ptFeature) { + this.sdk.Map.removeFeaturesFromLayer({featureIds: [this.#ptFeature.id, this.#lineFeature.id], layerName: GLE.#mapLayer}) // this.#ptFeature.destroy(); this.#ptFeature = null; // this.#lineFeature.destroy(); @@ -732,21 +737,19 @@ const SDKGoogleLinkEnhancer = (() => { const splits = this.trf.lineSplit(lsLine, lsBounds); let label = ""; if (splits) { - let splitPoints: GeoJSON.Feature; for (const split of splits.features) { for (const component of split.geometry.coordinates) { if ( component[0] === placePt.geometry.coordinates[0] && component[1] === placePt.geometry.coordinates[1] ) - splitPoints = split; + lsLine = split; } } - lsLine = splitPoints; - let distance = this.#distanceBetweenPoints(poiPt, placePt); - let unitConversion; - let unit1; - let unit2; + let distance = this.#distanceBetweenPoints(poiPt.geometry.coordinates, placePt.geometry.coordinates); + let unitConversion: number; + let unit1: string; + let unit2: string; // if (W.model.isImperial) { if (this.sdk.Settings.getUserSettings().isImperial) { distance *= 3.28084; @@ -829,7 +832,7 @@ const SDKGoogleLinkEnhancer = (() => { { id: `LsLine_${lsLine.toString()}` } ); // W.map.getLayerByUniqueName("venues").addFeatures([this.#ptFeature, this.#lineFeature]); - this.sdk.Map.addFeatures({featues: [this.#ptFeature, this.#lineFeature], layerName: "venues"}); + this.sdk.Map.addFeaturesToLayer({features: [this.#ptFeature, this.#lineFeature], layerName: GLE.#mapLayer}); this.#timeoutDestroyPoint(); } } else { diff --git a/package.json b/package.json index 6d1b8a0..2946d03 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "devDependencies": { - "@biomejs/biome": "^1.9.4", + "@biomejs/biome": "^2.0.6", "@turf/turf": "^7.2.0", "@types/jquery": "^3.5.32", "typescript": "^5.8.3" From e4a7fb663a1a866283ffb0ee7f2eb5eae1e7bab6 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Tue, 1 Jul 2025 23:06:07 -0400 Subject: [PATCH 12/16] SDK Update. --- SDKGoogleLinkEnhancer.js | 4 ++-- SDKGoogleLinkEnhancer.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/SDKGoogleLinkEnhancer.js b/SDKGoogleLinkEnhancer.js index da632fb..b97e291 100644 --- a/SDKGoogleLinkEnhancer.js +++ b/SDKGoogleLinkEnhancer.js @@ -304,7 +304,7 @@ const SDKGoogleLinkEnhancer = (() => { this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { - this.#formatLinkElements().bind(this); + this.#formatLinkElements.bind(this); }, }); this.#processPlaces(); @@ -318,7 +318,7 @@ const SDKGoogleLinkEnhancer = (() => { this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { - this.#formatLinkElements().bind(this); + this.#formatLinkElements.bind(this); }, }); this.#enabled = false; diff --git a/SDKGoogleLinkEnhancer.ts b/SDKGoogleLinkEnhancer.ts index 7cefb26..5d1c29f 100644 --- a/SDKGoogleLinkEnhancer.ts +++ b/SDKGoogleLinkEnhancer.ts @@ -333,7 +333,7 @@ const SDKGoogleLinkEnhancer = (() => { this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { - this.#formatLinkElements().bind(this); + this.#formatLinkElements.bind(this); }, }); this.#processPlaces(); @@ -348,7 +348,7 @@ const SDKGoogleLinkEnhancer = (() => { this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", eventHandler: (change) => { - this.#formatLinkElements().bind(this); + this.#formatLinkElements.bind(this); }, }); From c33ee0b262f7e2b8804ada101bdf4611469ae725 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Fri, 4 Jul 2025 10:47:10 -0400 Subject: [PATCH 13/16] Update GLE Package --- SDKGoogleLinkEnhancer.js | 56 +++++++++++++++++++++++++-------------- SDKGoogleLinkEnhancer.ts | 57 ++++++++++++++++++++++++++-------------- package.json | 2 +- 3 files changed, 75 insertions(+), 40 deletions(-) diff --git a/SDKGoogleLinkEnhancer.js b/SDKGoogleLinkEnhancer.js index b97e291..5a609c9 100644 --- a/SDKGoogleLinkEnhancer.js +++ b/SDKGoogleLinkEnhancer.js @@ -14,8 +14,9 @@ /* eslint-disable max-classes-per-file */ // eslint-disable-next-line func-names // import * as turf from "@turf/turf"; -// import type { Venue, WmeSDK } from "wme-sdk-typings"; +// import type { Venue, WmeSDK, DataModelName} from "wme-sdk-typings"; // import $ from "jquery"; +// import * as googlemaps from "@googlemaps/google-maps-services-js" const SDKGoogleLinkEnhancer = (() => { "use strict"; var _a; @@ -67,10 +68,10 @@ const SDKGoogleLinkEnhancer = (() => { #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place. // Area place is calculated as #distanceLimit + #showTempClosedPOIs = true; - #originalHeadAppendChildMethod; + // #originalHeadAppendChildMethod; #ptFeature; #lineFeature; - #timeoutID; + #timeoutID = -1; strings = { permClosedPlace: "Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.", tempClosedPlace: "Google indicates this place is temporarily closed.", @@ -199,7 +200,7 @@ const SDKGoogleLinkEnhancer = (() => { if (!sdk) { msg += "SDK Must be defined to use GLE"; } - if (!turf) { + if (!trf) { msg += "\n"; msg += "Turf Library Must be made available to GLE to Implement Some of the Functionality"; } @@ -217,21 +218,27 @@ const SDKGoogleLinkEnhancer = (() => { // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); this.sdk.Events.on({ eventName: "wme-data-model-objects-added", - eventHandler: () => { - this.#processPlaces(); - }, + eventHandler: (payload) => { + if (payload.dataModelName === "venues") { + this.#processPlaces(payload.objectIds); + } + } }); this.sdk.Events.on({ eventName: "wme-data-model-objects-removed", - eventHandler: () => { - this.#processPlaces(); + eventHandler: (payload) => { + if (payload.dataModelName === "venues") { + this.#processPlaces(payload.objectIds); + } }, }); this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", - eventHandler: () => { - this.#processPlaces(); - }, + eventHandler: (payload) => { + if (payload.dataModelName === "venues") { + this.#processPlaces(payload.objectIds); + } + } }); // This is a special event that will be triggered when DOM elements are destroyed. /* eslint-disable wrap-iife, func-names, object-shorthand */ @@ -303,8 +310,10 @@ const SDKGoogleLinkEnhancer = (() => { // W.model.venues.on('objectschanged', this.#formatLinkElements, this); this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", - eventHandler: (change) => { - this.#formatLinkElements.bind(this); + eventHandler: (payload) => { + if (payload.dataModelName === "venues") { + this.#formatLinkElements.bind(this); + } }, }); this.#processPlaces(); @@ -317,7 +326,7 @@ const SDKGoogleLinkEnhancer = (() => { // W.model.venues.off('objectschanged', this.#formatLinkElements, this); this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", - eventHandler: (change) => { + eventHandler: ({}) => { this.#formatLinkElements.bind(this); }, }); @@ -379,16 +388,25 @@ const SDKGoogleLinkEnhancer = (() => { } return false; } - #processPlaces() { + #processPlaces(objectIds = undefined) { if (this.#enabled) { try { // Get a list of already-linked id's const existingLinks = SDKGoogleLinkEnhancer.#getExistingLinks(this.sdk); this.sdk.Map.removeAllFeaturesFromLayer({ layerName: _a.#mapLayer }); const drawnLinks = []; - for (const venue of this.sdk.DataModel.Venues.getAll()) { + if (!objectIds) { + objectIds = []; + for (const venue of this.sdk.DataModel.Venues.getAll()) { + objectIds.push(venue.id); + } + } + for (const objId of objectIds) { // W.model.venues.getObjectArray().forEach(venue => { const promises = []; + const venue = this.sdk.DataModel.Venues.getById({ venueId: objId.toString() }); + if (venue === null) + continue; // venue.attributes.externalProviderIDs.forEach(provID => { for (const provID of venue.externalProviderIds) { const id = provID; @@ -404,7 +422,7 @@ const SDKGoogleLinkEnhancer = (() => { // strokeWidth: width, strokeColor: color // })]; const features = [ - _a.isPointVenue(venue) + geometry.type === "Point" ? this.trf.point(geometry.coordinates, { styleName: "venueStyle", style: { @@ -784,7 +802,7 @@ const SDKGoogleLinkEnhancer = (() => { } // Destroy the point after some time, if it hasn't been destroyed already. #timeoutDestroyPoint() { - if (this.#timeoutID) + if (this.#timeoutID > 0) clearTimeout(this.#timeoutID); this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000); } diff --git a/SDKGoogleLinkEnhancer.ts b/SDKGoogleLinkEnhancer.ts index 5d1c29f..88d9077 100644 --- a/SDKGoogleLinkEnhancer.ts +++ b/SDKGoogleLinkEnhancer.ts @@ -16,8 +16,9 @@ // eslint-disable-next-line func-names // import * as turf from "@turf/turf"; -// import type { Venue, WmeSDK } from "wme-sdk-typings"; +// import type { Venue, WmeSDK, DataModelName} from "wme-sdk-typings"; // import $ from "jquery"; +// import * as googlemaps from "@googlemaps/google-maps-services-js" const SDKGoogleLinkEnhancer = (() => { "use strict"; @@ -85,10 +86,10 @@ const SDKGoogleLinkEnhancer = (() => { #distanceLimit = 400; // Default distance (meters) when Waze place is flagged for being too far from Google place. // Area place is calculated as #distanceLimit + #showTempClosedPOIs = true; - #originalHeadAppendChildMethod; + // #originalHeadAppendChildMethod; #ptFeature: GeoJSON.Feature | undefined; #lineFeature: GeoJSON.Feature | undefined; - #timeoutID; + #timeoutID: number = -1; strings = { permClosedPlace: "Google indicates this place is permanently closed.\nVerify with other sources or your editor community before deleting.", @@ -220,7 +221,7 @@ const SDKGoogleLinkEnhancer = (() => { if (!sdk) { msg += "SDK Must be defined to use GLE"; } - if (!turf) { + if (!trf) { msg += "\n"; msg += "Turf Library Must be made available to GLE to Implement Some of the Functionality"; } @@ -239,28 +240,34 @@ const SDKGoogleLinkEnhancer = (() => { // W.model.venues.on('objectsadded', () => { this.#processPlaces(); }); this.sdk.Events.on({ eventName: "wme-data-model-objects-added", - eventHandler: () => { - this.#processPlaces(); - }, + eventHandler: (payload: {dataModelName: DataModelName, objectIds: Array}) => { + if(payload.dataModelName === "venues") { + this.#processPlaces(payload.objectIds); + } + } }); this.sdk.Events.on({ eventName: "wme-data-model-objects-removed", - eventHandler: () => { - this.#processPlaces(); + eventHandler: (payload: {dataModelName: DataModelName, objectIds: Array}) => { + if(payload.dataModelName === "venues") { + this.#processPlaces(payload.objectIds); + } }, }); this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", - eventHandler: () => { - this.#processPlaces(); - }, + eventHandler: (payload: {dataModelName: DataModelName, objectIds: Array}) => { + if(payload.dataModelName === "venues") { + this.#processPlaces(payload.objectIds); + } + } }); // This is a special event that will be triggered when DOM elements are destroyed. /* eslint-disable wrap-iife, func-names, object-shorthand */ (($: JQueryStatic) => { $.event.special.destroyed = { - remove: (o) => { + remove: (o ) => { if (o.handler && o.type !== "destroyed") { o.handler(); } @@ -332,8 +339,10 @@ const SDKGoogleLinkEnhancer = (() => { // W.model.venues.on('objectschanged', this.#formatLinkElements, this); this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", - eventHandler: (change) => { - this.#formatLinkElements.bind(this); + eventHandler: (payload: {dataModelName: DataModelName, objectIds: Array}) => { + if(payload.dataModelName === "venues") { + this.#formatLinkElements.bind(this); + } }, }); this.#processPlaces(); @@ -347,7 +356,7 @@ const SDKGoogleLinkEnhancer = (() => { // W.model.venues.off('objectschanged', this.#formatLinkElements, this); this.sdk.Events.on({ eventName: "wme-data-model-objects-changed", - eventHandler: (change) => { + eventHandler: ({}) => { this.#formatLinkElements.bind(this); }, }); @@ -415,16 +424,24 @@ const SDKGoogleLinkEnhancer = (() => { return false; } - #processPlaces() { + #processPlaces(objectIds: Array | undefined = undefined) { if (this.#enabled) { try { // Get a list of already-linked id's const existingLinks = SDKGoogleLinkEnhancer.#getExistingLinks(this.sdk); this.sdk.Map.removeAllFeaturesFromLayer({ layerName: GLE.#mapLayer }); const drawnLinks: Venue[][] = []; - for (const venue of this.sdk.DataModel.Venues.getAll()) { + if(!objectIds) { + objectIds = []; + for(const venue of this.sdk.DataModel.Venues.getAll()) { + objectIds.push(venue.id); + } + } + for (const objId of objectIds) { // W.model.venues.getObjectArray().forEach(venue => { const promises = []; + const venue = this.sdk.DataModel.Venues.getById({venueId: objId.toString()}); + if(venue === null) continue; // venue.attributes.externalProviderIDs.forEach(provID => { for (const provID of venue.externalProviderIds) { const id = provID; @@ -441,7 +458,7 @@ const SDKGoogleLinkEnhancer = (() => { // strokeWidth: width, strokeColor: color // })]; const features: GeoJSON.Feature[] = [ - GLE.isPointVenue(venue) + geometry.type === "Point" ? this.trf.point(geometry.coordinates, { styleName: "venueStyle", style: { @@ -848,7 +865,7 @@ const SDKGoogleLinkEnhancer = (() => { // Destroy the point after some time, if it hasn't been destroyed already. #timeoutDestroyPoint() { - if (this.#timeoutID) clearTimeout(this.#timeoutID); + if (this.#timeoutID > 0) clearTimeout(this.#timeoutID); this.#timeoutID = setTimeout(() => this.#destroyPoint(), 4000); } diff --git a/package.json b/package.json index 2946d03..0dc1293 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,12 @@ { "devDependencies": { "@biomejs/biome": "^2.0.6", + "@googlemaps/google-maps-services-js": "^3.4.1", "@turf/turf": "^7.2.0", "@types/jquery": "^3.5.32", "typescript": "^5.8.3" }, "dependencies": { - "@googlemaps/extended-component-library": "^0.6.14", "jquery": "^3.7.1", "wme-sdk-typings": "https://web-assets.waze.com/wme_sdk_docs/beta/latest/wme-sdk-typings.tgz" } From 61349adb250ffba5b5f4e82c180d257304efcb21 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sun, 6 Jul 2025 17:04:46 -0400 Subject: [PATCH 14/16] Restore. --- NavigationPoint.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/NavigationPoint.js b/NavigationPoint.js index 76e74ba..a74fa05 100644 --- a/NavigationPoint.js +++ b/NavigationPoint.js @@ -18,8 +18,8 @@ class NavigationPoint } with(){ - const e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; - if(e.point === null) + var e = arguments.length > 0 && void 0 !== arguments[0] ? arguments[0] : {}; + if(e.point == null) e.point = this.toJSON().point; return new this.constructor((this.toJSON().point, e.point)); } From 023e6bd3e532aad6482c428cb0a0758197bb8a25 Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Sun, 13 Jul 2025 22:50:45 -0400 Subject: [PATCH 15/16] replace mergeend --- SDKGoogleLinkEnhancer.js | 10 ++++++++-- SDKGoogleLinkEnhancer.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/SDKGoogleLinkEnhancer.js b/SDKGoogleLinkEnhancer.js index 5a609c9..85fdcc5 100644 --- a/SDKGoogleLinkEnhancer.js +++ b/SDKGoogleLinkEnhancer.js @@ -210,8 +210,14 @@ const SDKGoogleLinkEnhancer = (() => { this.#initLayer(); // NOTE: Arrow functions are necessary for calling methods on object instances. // This could be made more efficient by only processing the relevant places. - W.model.events.register("mergeend", null, () => { - this.#processPlaces(); + // W.model.events.register("mergeend", null, () => { + // this.#processPlaces(); + // }); + this.sdk.Events.on({ + eventName: "wme-map-data-loaded", + eventHandler: () => { + this.#processPlaces(); + } }); // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); diff --git a/SDKGoogleLinkEnhancer.ts b/SDKGoogleLinkEnhancer.ts index 88d9077..311e66e 100644 --- a/SDKGoogleLinkEnhancer.ts +++ b/SDKGoogleLinkEnhancer.ts @@ -232,8 +232,14 @@ const SDKGoogleLinkEnhancer = (() => { // NOTE: Arrow functions are necessary for calling methods on object instances. // This could be made more efficient by only processing the relevant places. - W.model.events.register("mergeend", null, () => { - this.#processPlaces(); + // W.model.events.register("mergeend", null, () => { + // this.#processPlaces(); + // }); + this.sdk.Events.on({ + eventName: "wme-map-data-loaded", + eventHandler: () => { + this.#processPlaces(); + } }); // W.model.venues.on('objectschanged', () => { this.#processPlaces(); }); // W.model.venues.on('objectsremoved', () => { this.#processPlaces(); }); From 27ba3c5be6b0c4a6267e1bade342e7feb056d6ba Mon Sep 17 00:00:00 2001 From: Mikhail Veygman Date: Wed, 16 Jul 2025 19:31:05 -0400 Subject: [PATCH 16/16] Update. --- biome.json | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/biome.json b/biome.json index 16998b0..19eac3a 100644 --- a/biome.json +++ b/biome.json @@ -1,21 +1,17 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.1.1/schema.json", "vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false }, "files": { - "ignoreUnknown": false, - "ignore": [] + "ignoreUnknown": false }, "formatter": { "enabled": true, "indentStyle": "tab" }, - "organizeImports": { - "enabled": true - }, "linter": { "enabled": true, "rules": {