/*\ title: $:/plugins/tiddlywiki/geospatial/geomap.js type: application/javascript module-type: widget Leaflet map widget \*/ (function(){ /*jslint node: true, browser: true */ /*global $tw: false */ "use strict"; var Widget = require("$:/core/modules/widgets/widget.js").widget; var GeomapWidget = function(parseTreeNode,options) { this.initialise(parseTreeNode,options); }; /* Inherit from the base widget class */ GeomapWidget.prototype = new Widget(); /* Render this widget into the DOM */ GeomapWidget.prototype.render = function(parent,nextSibling) { // Housekeeping this.parentDomNode = parent; this.computeAttributes(); this.execute(); // Render the children into a hidden DOM node var parser = { tree: [{ type: "widget", attributes: {}, orderedAttributes: [], children: this.parseTreeNode.children || [] }] }; this.contentRoot = this.wiki.makeWidget(parser,{ document: $tw.fakeDocument, parentWidget: this }); this.contentContainer = $tw.fakeDocument.createElement("div"); this.contentRoot.render(this.contentContainer,null); // Render a wrapper for the map this.domNode = this.document.createElement("div"); this.setMapSize(); // Insert it into the DOM parent.insertBefore(this.domNode,nextSibling); this.domNodes.push(this.domNode); // Render the map if($tw.browser && !this.domNode.isTiddlyWikiFakeDom) { this.renderMap(); this.refreshMap(); } }; GeomapWidget.prototype.renderMap = function() { var self = this; // Create the map var options = { }; if(this.geomapMaxZoom) { options.maxZoom = $tw.utils.parseInt(this.geomapMaxZoom); } this.map = $tw.Leaflet.map(this.domNode,options); // No layers rendered this.renderedLayers = []; this.baseLayers = []; // Disable Leaflet attribution this.map.attributionControl.setPrefix(""); // Add scale $tw.Leaflet.control.scale().addTo(this.map); // Listen for pan and zoom events and update the state tiddler this.map.on("moveend zoomend",function(event) { if(self.hasAttribute("state")) { var stateTitle = self.getAttribute("state"), c = self.map.getCenter(), lat = "" + c.lat, long = "" + c.lng, zoom = "" + self.map.getZoom(), tiddler = self.wiki.getTiddler(stateTitle); // Only write the tiddler if the values have changed if(!tiddler || tiddler.fields.lat !== lat || tiddler.fields.long !== long || tiddler.fields.zoom !== zoom) { self.wiki.addTiddler(new $tw.Tiddler({ title: stateTitle, lat: lat, long: long, zoom: zoom })); } } }); }; GeomapWidget.prototype.refreshMap = function() { var self = this; // Remove any previously rendered layers $tw.utils.each(this.renderedLayers,function(layer) { self.map.removeLayer(layer.layer); }); this.renderedLayers = []; // Array of {name:,layer:} $tw.utils.each(this.renderedBaseLayers,function(baseLayer) { self.map.removeLayer(baseLayer.layer); }); this.renderedBaseLayers = []; // Array of {name:,layer:} // Create default icon var iconProportions = 365/560, iconHeight = 50; var myIcon = new $tw.Leaflet.Icon({ iconUrl: $tw.utils.makeDataUri(this.wiki.getTiddlerText("$:/plugins/tiddlywiki/geospatial/images/markers/pin"),"image/svg+xml"), iconSize: [iconHeight * iconProportions, iconHeight], // Size of the icon iconAnchor: [(iconHeight * iconProportions) / 2, iconHeight], // Position of the anchor within the icon popupAnchor: [0, -iconHeight] // Position of the popup anchor relative to the icon anchor }); // Counter for autogenerated names var untitledCount = 1; // Process embedded geobaselayer widgets function loadBaseLayer(layerInfo) { if(layerInfo.title) { var tiddler = self.wiki.getTiddler(layerInfo.title); if(tiddler) { layerInfo.name = layerInfo.name || tiddler.fields["caption"]; layerInfo.tilesUrl = layerInfo.tilesUrl || tiddler.fields["tiles-url"]; layerInfo.maxZoom = layerInfo.maxZoom || tiddler.fields["max-zoom"]; layerInfo.attribution = layerInfo.attribution || tiddler.fields.text; } } var baseLayer = $tw.Leaflet.tileLayer(layerInfo.tilesUrl, { maxZoom: layerInfo.maxZoom, attribution: layerInfo.attribution }); if(self.renderedBaseLayers.length === 0) { baseLayer.addTo(self.map) } var name = layerInfo.name || ("Untitled " + untitledCount++); self.renderedBaseLayers.push({name: name, layer: baseLayer}); } this.findChildrenDataWidgets(this.contentRoot.children,"geobaselayer",function(widget) { loadBaseLayer({ name: widget.getAttribute("name"), title: widget.getAttribute("title"), tilesUrl: widget.getAttribute("tiles-url"), maxZoom: widget.getAttribute("max-zoom"), attribution: widget.getAttribute("attribution"), }); }); // Create the default base map if none was specified if(this.renderedBaseLayers.length === 0) { // Render in reverse order so that the first tagged base layer will be rendered last, and hence take priority var baseLayerTitles = this.wiki.filterTiddlers("[all[tiddlers+shadows]tag[$:/tags/GeoBaseLayer]]"); $tw.utils.each(baseLayerTitles,function(title) { loadBaseLayer({title: title}); }); } if(this.renderedBaseLayers.length === 0) { loadBaseLayer({title: "$:/plugins/tiddlywiki/geospatial/baselayers/openstreetmap"}); } // Make a marker cluster var markers = $tw.Leaflet.markerClusterGroup({ maxClusterRadius: 40 }); this.map.addLayer(markers); // Process embedded geolayer widgets var defaultPopupTemplateTitle = self.getAttribute("popupTemplate"); this.findChildrenDataWidgets(this.contentRoot.children,"geolayer",function(widget) { var jsonText = widget.getAttribute("json"), popupTemplateTitle = widget.getAttribute("popupTemplate",defaultPopupTemplateTitle), geoJson = []; // Build up the geojson of the layer if(jsonText) { // Layer is defined by JSON blob geoJson = $tw.utils.parseJSONSafe(jsonText,[]); } else if(widget.hasAttribute("lat") && widget.hasAttribute("long")) { // Layer is defined by lat long fields var lat = $tw.utils.parseNumber(widget.getAttribute("lat","0")), long = $tw.utils.parseNumber(widget.getAttribute("long","0")), alt = $tw.utils.parseNumber(widget.getAttribute("alt","0")), properties = widget.getAttribute("properties"); geoJson = { "type": "FeatureCollection", "features": [ { "type": "Feature", "geometry": { "type": "Point", "coordinates": [long,lat,alt] } } ] }; if(properties) { geoJson.features[0].properties = $tw.utils.parseJSONSafe(properties); } } // Create and add layer for the geojson var draggable = widget.getAttribute("draggable","no") === "yes", layer = $tw.Leaflet.geoJSON(geoJson,{ style: function(geoJsonFeature) { return { color: widget.getAttribute("color","yellow") } }, pointToLayer: function(geoJsonPoint,latlng) { var marker = $tw.Leaflet.marker(latlng,{icon: myIcon,draggable: draggable}); marker.addTo(markers); marker.on("moveend",function(event) { var latlng = event.sourceTarget.getLatLng(); self.invokeActionString(widget.getAttribute("updateActions"),null,event,{ lat: latlng.lat, long: latlng.lng, alt: latlng.alt }); }); return marker; }, onEachFeature: function(feature,layer) { if(popupTemplateTitle) { layer.bindPopup(function() { var widget = self.wiki.makeTranscludeWidget(popupTemplateTitle, { document: self.document, parentWidget: self, parseAsInline: false, importPageMacros: true, variables: { feature: JSON.stringify(feature) } }); var container = self.document.createElement("div"); widget.render(container,null); self.wiki.addEventListener("change",function(changes) { widget.refresh(changes,container,null); }); return container; }); } // Add event handlers if(widget.hasAttribute("clickActions")) { layer.on("click",function(event) { self.invokeActionString(widget.getAttribute("clickActions"),null,event.originalEvent,{ lat: event.latlng.lat, long: event.latlng.lng, alt: event.latlng.alt, properties: JSON.stringify(feature.properties) }); }); } } }).addTo(self.map); // Create a name for this layer var name = widget.getAttribute("name") || ("Untitled " + untitledCount++); self.renderedLayers.push({name: name, layer: layer}); }); // Setup the layer control if(this.layerControl) { this.map.removeControl(this.layerControl); } var baseLayers = {}; $tw.utils.each(this.renderedBaseLayers,function(layer) { baseLayers[layer.name] = layer.layer; }); var overlayLayers = {}; $tw.utils.each(this.renderedLayers,function(layer) { overlayLayers[layer.name] = layer.layer; }); this.layerControl = $tw.Leaflet.control.layers(baseLayers,overlayLayers,{ collapsed: this.getAttribute("layersPanel") !== "open" }).addTo(this.map); // Restore the saved map position and zoom level if(!this.setMapView()) { // If there was no saved position then look at the startPosition attribute switch(this.getAttribute("startPosition")) { case "bounds": var bounds = null; $tw.utils.each(this.renderedLayers,function(layer) { var featureBounds = layer.layer.getBounds(); if(featureBounds.isValid()) { if(bounds) { bounds.extend(featureBounds); } else { bounds = featureBounds; } } }); if(bounds) { this.map.fitBounds(bounds); } else { this.map.fitWorld(); } break; default: this.map.fitWorld(); break; } } }; /* Set the map center and zoom level from the values in the state tiddler. Returns true if the map view was successfully set */ GeomapWidget.prototype.setMapView = function() { // Set the maximum zoom level if(this.hasAttribute("maxZoom")) { this.map.setMaxZoom($tw.utils.parseInt(this.getAttribute("maxZoom"))); } // Set the view to the content of the state tiddler var stateTiddler = this.getAttribute("state") && this.wiki.getTiddler(this.getAttribute("state")); if(stateTiddler) { this.map.setView([$tw.utils.parseNumber(stateTiddler.fields.lat,0),$tw.utils.parseNumber(stateTiddler.fields.long,0)], $tw.utils.parseNumber(stateTiddler.fields.zoom,0)); return true; } return false; }; GeomapWidget.prototype.setMapSize = function() { this.domNode.style.width = this.getAttribute("width","100%"); this.domNode.style.height = this.getAttribute("height","600px"); }; /* Compute the internal state of the widget */ GeomapWidget.prototype.execute = function() { }; /* Selectively refreshes the widget if needed. Returns true if the widget or any of its children needed re-rendering */ GeomapWidget.prototype.refresh = function(changedTiddlers) { var changedAttributes = this.computeAttributes(); // Refresh child nodes, and rerender map if there have been any changes var result = this.contentRoot.refresh(changedTiddlers); if(result) { this.refreshMap(); } else { // Reset the width and height and max zoom if they have changed if(changedAttributes.width || changedAttributes.height) { this.setMapSize(); } // If we're not doing a full refresh, reset the position if the state tiddler has changed if(changedAttributes.state || (this.hasAttribute("state") && changedTiddlers[this.getAttribute("state")]) || changedAttributes.maxZoom) { this.setMapView(); } } return result; }; exports.geomap = GeomapWidget; })();