From e246816acce5e2a7d9d42abf075398965dc09184 Mon Sep 17 00:00:00 2001 From: Stefan Kuethe Date: Thu, 30 May 2024 22:21:14 +0200 Subject: [PATCH] Support deck tooltips for multiple layers --- docs/examples/deckgl_layer/airports_app.py | 5 ++++- maplibre/map.py | 4 +++- maplibre/srcjs/index.js | 8 ++++---- srcjs/utils.js | 9 +++++++-- 4 files changed, 18 insertions(+), 8 deletions(-) diff --git a/docs/examples/deckgl_layer/airports_app.py b/docs/examples/deckgl_layer/airports_app.py index 89e72af0..2bee28c0 100644 --- a/docs/examples/deckgl_layer/airports_app.py +++ b/docs/examples/deckgl_layer/airports_app.py @@ -51,7 +51,10 @@ m.add_deck_layers( [deck_geojson_layer, deck_arc_layer], - tooltip_template="{{ &properties.name }}", + tooltip_template={ + "airports": "{{ &properties.name }}", + "arcs": "gps_code: {{ properties.gps_code }}", + }, ) # Shiny Express diff --git a/maplibre/map.py b/maplibre/map.py index 21194724..b006dd07 100644 --- a/maplibre/map.py +++ b/maplibre/map.py @@ -274,5 +274,7 @@ def to_html(self, title: str = "My Awesome Map", **kwargs) -> str: ) return output - def add_deck_layers(self, layers: list[dict], tooltip_template: str = None) -> None: + def add_deck_layers( + self, layers: list[dict], tooltip_template: str | dict = None + ) -> None: self.add_call("addDeckOverlay", layers, tooltip_template) diff --git a/maplibre/srcjs/index.js b/maplibre/srcjs/index.js index 44f724b2..80e5ca3a 100644 --- a/maplibre/srcjs/index.js +++ b/maplibre/srcjs/index.js @@ -1,7 +1,7 @@ -(()=>{var A=Object.prototype.toString,k=Array.isArray||function(e){return A.call(e)==="[object Array]"};function P(r){return typeof r=="function"}function B(r){return k(r)?"array":typeof r}function L(r){return r.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function U(r,e){return r!=null&&typeof r=="object"&&e in r}function H(r,e){return r!=null&&typeof r!="object"&&r.hasOwnProperty&&r.hasOwnProperty(e)}var J=RegExp.prototype.test;function F(r,e){return J.call(r,e)}var W=/\S/;function q(r){return!F(W,r)}var G={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function K(r){return String(r).replace(/[&<>"'`=\/]/g,function(t){return G[t]})}var V=/\s*/,z=/\s+/,j=/\s*=/,Q=/\s*\}/,X=/#|\^|\/|>|\{|&|=|!/;function Y(r,e){if(!r)return[];var t=!1,n=[],a=[],s=[],o=!1,p=!1,i="",u=0;function f(){if(o&&!p)for(;s.length;)delete a[s.pop()];else s=[];o=!1,p=!1}var v,m,x;function I(y){if(typeof y=="string"&&(y=y.split(z,2)),!k(y)||y.length!==2)throw new Error("Invalid tags: "+y);v=new RegExp(L(y[0])+"\\s*"),m=new RegExp("\\s*"+L(y[1])),x=new RegExp("\\s*"+L("}"+y[1]))}I(e||g.tags);for(var l=new T(r),w,h,d,_,M,b;!l.eos();){if(w=l.pos,d=l.scanUntil(v),d)for(var E=0,$=d.length;E<$;++E)_=d.charAt(E),q(_)?(s.push(a.length),i+=_):(p=!0,t=!0,i+=" "),a.push(["text",_,w,w+1]),w+=1,_===` -`&&(f(),i="",u=0,t=!1);if(!l.scan(v))break;if(o=!0,h=l.scan(X)||"name",l.scan(V),h==="="?(d=l.scanUntil(j),l.scan(j),l.scanUntil(m)):h==="{"?(d=l.scanUntil(x),l.scan(Q),l.scanUntil(m),h="&"):d=l.scanUntil(m),!l.scan(m))throw new Error("Unclosed tag at "+l.pos);if(h==">"?M=[h,d,w,l.pos,i,u,t]:M=[h,d,w,l.pos],u++,a.push(M),h==="#"||h==="^")n.push(M);else if(h==="/"){if(b=n.pop(),!b)throw new Error('Unopened section "'+d+'" at '+w);if(b[1]!==d)throw new Error('Unclosed section "'+b[1]+'" at '+w)}else h==="name"||h==="{"||h==="&"?p=!0:h==="="&&I(d)}if(f(),b=n.pop(),b)throw new Error('Unclosed section "'+b[1]+'" at '+l.pos);return ee(Z(a))}function Z(r){for(var e=[],t,n,a=0,s=r.length;a0?n[n.length-1][4]:e;break;default:t.push(a)}return e}function T(r){this.string=r,this.tail=r,this.pos=0}T.prototype.eos=function(){return this.tail===""};T.prototype.scan=function(e){var t=this.tail.match(e);if(!t||t.index!==0)return"";var n=t[0];return this.tail=this.tail.substring(n.length),this.pos+=n.length,n};T.prototype.scanUntil=function(e){var t=this.tail.search(e),n;switch(t){case-1:n=this.tail,this.tail="";break;case 0:n="";break;default:n=this.tail.substring(0,t),this.tail=this.tail.substring(t)}return this.pos+=n.length,n};function C(r,e){this.view=r,this.cache={".":this.view},this.parent=e}C.prototype.push=function(e){return new C(e,this)};C.prototype.lookup=function(e){var t=this.cache,n;if(t.hasOwnProperty(e))n=t[e];else{for(var a=this,s,o,p,i=!1;a;){if(e.indexOf(".")>0)for(s=a.view,o=e.split("."),p=0;s!=null&&p"?u=this.renderPartial(p,t,n,s):i==="&"?u=this.unescapedValue(p,t):i==="name"?u=this.escapedValue(p,t,s):i==="text"&&(u=this.rawValue(p)),u!==void 0&&(o+=u);return o};c.prototype.renderSection=function(e,t,n,a,s){var o=this,p="",i=t.lookup(e[1]);function u(m){return o.render(m,t,n,s)}if(i){if(k(i))for(var f=0,v=i.length;f0||!n)&&(s[o]=a+s[o]);return s.join(` -`)};c.prototype.renderPartial=function(e,t,n,a){if(n){var s=this.getConfigTags(a),o=P(n)?n(e[1]):n[e[1]];if(o!=null){var p=e[6],i=e[5],u=e[4],f=o;i==0&&u&&(f=this.indentPartial(o,u,p));var v=this.parse(f,s);return this.renderTokens(v,t,n,f,a)}}};c.prototype.unescapedValue=function(e,t){var n=t.lookup(e[1]);if(n!=null)return n};c.prototype.escapedValue=function(e,t,n){var a=this.getConfigEscape(n)||g.escape,s=t.lookup(e[1]);if(s!=null)return typeof s=="number"&&a===g.escape?String(s):a(s)};c.prototype.rawValue=function(e){return e[1]};c.prototype.getConfigTags=function(e){return k(e)?e:e&&typeof e=="object"?e.tags:void 0};c.prototype.getConfigEscape=function(e){if(e&&typeof e=="object"&&!k(e))return e.escape};var g={name:"mustache.js",version:"4.2.0",tags:["{{","}}"],clearCache:void 0,escape:void 0,parse:void 0,render:void 0,Scanner:void 0,Context:void 0,Writer:void 0,set templateCache(r){O.templateCache=r},get templateCache(){return O.templateCache}},O=new c;g.clearCache=function(){return O.clearCache()};g.parse=function(e,t){return O.parse(e,t)};g.render=function(e,t,n,a){if(typeof e!="string")throw new TypeError('Invalid template! Template should be a "string" but "'+B(e)+'" was given as the first argument for mustache#render(template, view, partials)');return O.render(e,t,n,a)};g.escape=K;g.Scanner=T;g.Context=C;g.Writer=c;var R=g;function N(r,e,t){return t!==null?R.render(t,r.properties):e===null?Object.keys(r.properties).map(a=>`${a}: ${r.properties[a]}`).join("
"):r.properties[e]}function te(r,e){return R.render(r,e)}function D(r){return({layer:e,object:t})=>t&&te(r,t)}function ne(){if(typeof deck>"u")return;let r=new deck.JSONConfiguration({classes:deck});return new deck.JSONConverter({configuration:r})}var S=class{constructor(e){this._id=e.container,this._map=new maplibregl.Map(e),this._map.on("mouseover",()=>{this._map.getCanvas().style.cursor="pointer"}),this._map.on("mouseout",()=>{this._map.getCanvas().style.cursor=""}),this._map.addControl(new maplibregl.NavigationControl),this._JSONConverter=ne()}getMap(){return this._map}applyMapMethod(e,t){this._map[e](...t)}addControl(e,t,n){this._map.addControl(new maplibregl[e](t),n)}addMarker({lngLat:e,popup:t,options:n}){let a=new maplibregl.Marker(n).setLngLat(e);if(t){let s=new maplibregl.Popup(t.options).setHTML(t.text);a.setPopup(s)}a.addTo(this._map)}addLayer(e,t){this._map.addLayer(e,t),typeof Shiny<"u"&&this._map.on("click",e.id,n=>{console.log(n,n.features[0]);let a=e.id.replaceAll("-","_"),s=`${this._id}_layer_${a}`,o={props:n.features[0].properties,layer_id:e.id};console.log(s,o),Shiny.onInputChange(s,o)})}addPopup(e,t=null,n=null){let a={closeButton:!1},s=new maplibregl.Popup(a);this._map.on("click",e,o=>{let p=o.features[0],i=N(p,t,n);s.setLngLat(o.lngLat).setHTML(i).addTo(this._map)})}addTooltip(e,t=null,n=null){let a={closeButton:!1,closeOnClick:!1},s=new maplibregl.Popup(a);this._map.on("mousemove",e,o=>{let p=o.features[0],i=N(p,t,n);s.setLngLat(o.lngLat).setHTML(i).addTo(this._map)}),this._map.on("mouseleave",e,()=>{s.remove()})}setSourceData(e,t){this._map.getSource(e).setData(t)}addDeckOverlay(e,t=null){if(typeof this._JSONConverter>"u"){console.log("deck or JSONConverter is undefined");return}let n=e.map(s=>this._JSONConverter.convert(Object.assign(s,{onHover:({layer:o,coordinate:p,object:i})=>{if(console.log(o.id,p,i),typeof Shiny<"u"){let u=`${this._id}_layer_${s.id}`;console.log("deckInputName",u),Shiny.onInputChange(u,i)}}}))),a=new deck.MapboxOverlay({interleaved:!0,layers:n,getTooltip:t?D(t):null});this._map.addControl(a)}render(e){e.forEach(([t,n])=>{if(["addLayer","addPopup","addTooltip","addMarker","addPopup","addControl","setSourceData","addDeckOverlay"].includes(t)){console.log("Custom method",t,n),this[t](...n);return}console.log("Map method",t),this.applyMapMethod(t,n)})}};var re="0.1.0";console.log("pymaplibregl",re);typeof Shiny>"u"&&(window.pymaplibregl=function({mapOptions:r,calls:e}){let t="pymaplibregl",n=document.getElementById(t),a=new S(Object.assign({container:n.id},r));a.getMap().on("load",()=>{a.render(e)})});if(typeof Shiny<"u"){class r extends Shiny.OutputBinding{find(t){return t.find(".shiny-maplibregl-output")}renderValue(t,n){console.log("id:",t.id,"payload:",n);let a=window._maplibreWidget=new S(Object.assign({container:t.id},n.mapData.mapOptions)),s=a.getMap();s.on("load",()=>{a.render(n.mapData.calls)}),s.on("click",p=>{console.log(p);let i=`${t.id}`,u={coords:p.lngLat,point:p.point};console.log(i,u),Shiny.onInputChange(i,u)});let o=`pymaplibregl-${t.id}`;console.log(o),Shiny.addCustomMessageHandler(o,({id:p,calls:i})=>{console.log(p,i),a.render(i)})}}Shiny.outputBindings.register(new r,"shiny-maplibregl-output")}})(); +(()=>{var A=Object.prototype.toString,k=Array.isArray||function(e){return A.call(e)==="[object Array]"};function R(r){return typeof r=="function"}function B(r){return k(r)?"array":typeof r}function P(r){return r.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function I(r,e){return r!=null&&typeof r=="object"&&e in r}function H(r,e){return r!=null&&typeof r!="object"&&r.hasOwnProperty&&r.hasOwnProperty(e)}var J=RegExp.prototype.test;function F(r,e){return J.call(r,e)}var W=/\S/;function q(r){return!F(W,r)}var G={"&":"&","<":"<",">":">",'"':""","'":"'","/":"/","`":"`","=":"="};function K(r){return String(r).replace(/[&<>"'`=\/]/g,function(t){return G[t]})}var V=/\s*/,z=/\s+/,j=/\s*=/,Q=/\s*\}/,X=/#|\^|\/|>|\{|&|=|!/;function Y(r,e){if(!r)return[];var t=!1,n=[],s=[],a=[],o=!1,p=!1,i="",u=0;function f(){if(o&&!p)for(;a.length;)delete s[a.pop()];else a=[];o=!1,p=!1}var v,m,x;function U(y){if(typeof y=="string"&&(y=y.split(z,2)),!k(y)||y.length!==2)throw new Error("Invalid tags: "+y);v=new RegExp(P(y[0])+"\\s*"),m=new RegExp("\\s*"+P(y[1])),x=new RegExp("\\s*"+P("}"+y[1]))}U(e||g.tags);for(var l=new T(r),w,h,d,_,M,b;!l.eos();){if(w=l.pos,d=l.scanUntil(v),d)for(var L=0,$=d.length;L<$;++L)_=d.charAt(L),q(_)?(a.push(s.length),i+=_):(p=!0,t=!0,i+=" "),s.push(["text",_,w,w+1]),w+=1,_===` +`&&(f(),i="",u=0,t=!1);if(!l.scan(v))break;if(o=!0,h=l.scan(X)||"name",l.scan(V),h==="="?(d=l.scanUntil(j),l.scan(j),l.scanUntil(m)):h==="{"?(d=l.scanUntil(x),l.scan(Q),l.scanUntil(m),h="&"):d=l.scanUntil(m),!l.scan(m))throw new Error("Unclosed tag at "+l.pos);if(h==">"?M=[h,d,w,l.pos,i,u,t]:M=[h,d,w,l.pos],u++,s.push(M),h==="#"||h==="^")n.push(M);else if(h==="/"){if(b=n.pop(),!b)throw new Error('Unopened section "'+d+'" at '+w);if(b[1]!==d)throw new Error('Unclosed section "'+b[1]+'" at '+w)}else h==="name"||h==="{"||h==="&"?p=!0:h==="="&&U(d)}if(f(),b=n.pop(),b)throw new Error('Unclosed section "'+b[1]+'" at '+l.pos);return ee(Z(s))}function Z(r){for(var e=[],t,n,s=0,a=r.length;s0?n[n.length-1][4]:e;break;default:t.push(s)}return e}function T(r){this.string=r,this.tail=r,this.pos=0}T.prototype.eos=function(){return this.tail===""};T.prototype.scan=function(e){var t=this.tail.match(e);if(!t||t.index!==0)return"";var n=t[0];return this.tail=this.tail.substring(n.length),this.pos+=n.length,n};T.prototype.scanUntil=function(e){var t=this.tail.search(e),n;switch(t){case-1:n=this.tail,this.tail="";break;case 0:n="";break;default:n=this.tail.substring(0,t),this.tail=this.tail.substring(t)}return this.pos+=n.length,n};function C(r,e){this.view=r,this.cache={".":this.view},this.parent=e}C.prototype.push=function(e){return new C(e,this)};C.prototype.lookup=function(e){var t=this.cache,n;if(t.hasOwnProperty(e))n=t[e];else{for(var s=this,a,o,p,i=!1;s;){if(e.indexOf(".")>0)for(a=s.view,o=e.split("."),p=0;a!=null&&p"?u=this.renderPartial(p,t,n,a):i==="&"?u=this.unescapedValue(p,t):i==="name"?u=this.escapedValue(p,t,a):i==="text"&&(u=this.rawValue(p)),u!==void 0&&(o+=u);return o};c.prototype.renderSection=function(e,t,n,s,a){var o=this,p="",i=t.lookup(e[1]);function u(m){return o.render(m,t,n,a)}if(i){if(k(i))for(var f=0,v=i.length;f0||!n)&&(a[o]=s+a[o]);return a.join(` +`)};c.prototype.renderPartial=function(e,t,n,s){if(n){var a=this.getConfigTags(s),o=R(n)?n(e[1]):n[e[1]];if(o!=null){var p=e[6],i=e[5],u=e[4],f=o;i==0&&u&&(f=this.indentPartial(o,u,p));var v=this.parse(f,a);return this.renderTokens(v,t,n,f,s)}}};c.prototype.unescapedValue=function(e,t){var n=t.lookup(e[1]);if(n!=null)return n};c.prototype.escapedValue=function(e,t,n){var s=this.getConfigEscape(n)||g.escape,a=t.lookup(e[1]);if(a!=null)return typeof a=="number"&&s===g.escape?String(a):s(a)};c.prototype.rawValue=function(e){return e[1]};c.prototype.getConfigTags=function(e){return k(e)?e:e&&typeof e=="object"?e.tags:void 0};c.prototype.getConfigEscape=function(e){if(e&&typeof e=="object"&&!k(e))return e.escape};var g={name:"mustache.js",version:"4.2.0",tags:["{{","}}"],clearCache:void 0,escape:void 0,parse:void 0,render:void 0,Scanner:void 0,Context:void 0,Writer:void 0,set templateCache(r){O.templateCache=r},get templateCache(){return O.templateCache}},O=new c;g.clearCache=function(){return O.clearCache()};g.parse=function(e,t){return O.parse(e,t)};g.render=function(e,t,n,s){if(typeof e!="string")throw new TypeError('Invalid template! Template should be a "string" but "'+B(e)+'" was given as the first argument for mustache#render(template, view, partials)');return O.render(e,t,n,s)};g.escape=K;g.Scanner=T;g.Context=C;g.Writer=c;var E=g;function N(r,e,t){return t!==null?E.render(t,r.properties):e===null?Object.keys(r.properties).map(s=>`${s}: ${r.properties[s]}`).join("
"):r.properties[e]}function te(r,e,t){return console.log("Trying to get tooltip for layerId = "+t),typeof r=="object"?r[t]&&E.render(r[t],e):E.render(r,e)}function D(r){return({layer:e,object:t})=>t&&te(r,t,e.id)}function ne(){if(typeof deck>"u")return;let r=new deck.JSONConfiguration({classes:deck});return new deck.JSONConverter({configuration:r})}var S=class{constructor(e){this._id=e.container,this._map=new maplibregl.Map(e),this._map.on("mouseover",()=>{this._map.getCanvas().style.cursor="pointer"}),this._map.on("mouseout",()=>{this._map.getCanvas().style.cursor=""}),this._map.addControl(new maplibregl.NavigationControl),this._JSONConverter=ne()}getMap(){return this._map}applyMapMethod(e,t){this._map[e](...t)}addControl(e,t,n){this._map.addControl(new maplibregl[e](t),n)}addMarker({lngLat:e,popup:t,options:n}){let s=new maplibregl.Marker(n).setLngLat(e);if(t){let a=new maplibregl.Popup(t.options).setHTML(t.text);s.setPopup(a)}s.addTo(this._map)}addLayer(e,t){this._map.addLayer(e,t),typeof Shiny<"u"&&this._map.on("click",e.id,n=>{console.log(n,n.features[0]);let s=e.id.replaceAll("-","_"),a=`${this._id}_layer_${s}`,o={props:n.features[0].properties,layer_id:e.id};console.log(a,o),Shiny.onInputChange(a,o)})}addPopup(e,t=null,n=null){let s={closeButton:!1},a=new maplibregl.Popup(s);this._map.on("click",e,o=>{let p=o.features[0],i=N(p,t,n);a.setLngLat(o.lngLat).setHTML(i).addTo(this._map)})}addTooltip(e,t=null,n=null){let s={closeButton:!1,closeOnClick:!1},a=new maplibregl.Popup(s);this._map.on("mousemove",e,o=>{let p=o.features[0],i=N(p,t,n);a.setLngLat(o.lngLat).setHTML(i).addTo(this._map)}),this._map.on("mouseleave",e,()=>{a.remove()})}setSourceData(e,t){this._map.getSource(e).setData(t)}addDeckOverlay(e,t=null){if(typeof this._JSONConverter>"u"){console.log("deck or JSONConverter is undefined");return}let n=e.map(a=>this._JSONConverter.convert(Object.assign(a,{onHover:({layer:o,coordinate:p,object:i})=>{if(console.log(o.id,p,i),typeof Shiny<"u"){let u=`${this._id}_layer_${a.id}`;console.log("deckInputName",u),Shiny.onInputChange(u,i)}}}))),s=new deck.MapboxOverlay({interleaved:!0,layers:n,getTooltip:t?D(t):null});this._map.addControl(s)}render(e){e.forEach(([t,n])=>{if(["addLayer","addPopup","addTooltip","addMarker","addPopup","addControl","setSourceData","addDeckOverlay"].includes(t)){console.log("Custom method",t,n),this[t](...n);return}console.log("Map method",t),this.applyMapMethod(t,n)})}};var re="0.1.0";console.log("pymaplibregl",re);typeof Shiny>"u"&&(window.pymaplibregl=function({mapOptions:r,calls:e}){let t="pymaplibregl",n=document.getElementById(t),s=new S(Object.assign({container:n.id},r));s.getMap().on("load",()=>{s.render(e)})});if(typeof Shiny<"u"){class r extends Shiny.OutputBinding{find(t){return t.find(".shiny-maplibregl-output")}renderValue(t,n){console.log("id:",t.id,"payload:",n);let s=window._maplibreWidget=new S(Object.assign({container:t.id},n.mapData.mapOptions)),a=s.getMap();a.on("load",()=>{s.render(n.mapData.calls)}),a.on("click",p=>{console.log(p);let i=`${t.id}`,u={coords:p.lngLat,point:p.point};console.log(i,u),Shiny.onInputChange(i,u)});let o=`pymaplibregl-${t.id}`;console.log(o),Shiny.addCustomMessageHandler(o,({id:p,calls:i})=>{console.log(p,i),s.render(i)})}}Shiny.outputBindings.register(new r,"shiny-maplibregl-output")}})(); /*! Bundled license information: mustache/mustache.mjs: diff --git a/srcjs/utils.js b/srcjs/utils.js index 4d117c11..73036c28 100644 --- a/srcjs/utils.js +++ b/srcjs/utils.js @@ -15,14 +15,19 @@ function getTextFromFeature(feature, property, template) { return feature.properties[property]; } -function renderPickingObject(template, object) { +function renderPickingObject(template, object, layerId) { + console.log("Trying to get tooltip for layerId = " + layerId); + if (typeof template === "object") { + return template[layerId] && mustache.render(template[layerId], object); + } + return mustache.render(template, object); } // Just as a POC, maybe set tooltip via onHover using Popups from maplibregl function getDeckTooltip(template) { return ({ layer, object }) => { - return object && renderPickingObject(template, object); + return object && renderPickingObject(template, object, layer.id); }; } export { getTextFromFeature, getDeckTooltip };