From dbabdfce53365d5c5f73bb27f27cbb5d9138d037 Mon Sep 17 00:00:00 2001 From: "jeremy@jermolene.com" Date: Sun, 21 May 2023 22:06:32 +0100 Subject: [PATCH] Support for multiple base layers * Make the layers control visible which allows the base layer to be chosen, and individual overlay layers to be hidden * Add tiddlers tagged $:/tags/GeoBaseLayer to define some useful map base layers * Add geobaselayer widget to define base layers --- .../geospatialdemo/tiddlers/HelloThere.tid | 7 +- .../geospatial/baselayers/TagsGeoBaseLayer.md | 2 + .../geospatial/baselayers/openstreetmap.tid | 7 ++ .../geospatial/baselayers/opentopomap.tid | 7 ++ .../baselayers/stamen-watercolor.tid | 7 ++ .../geospatial/docs/geobaselayer.tid | 18 +++ plugins/tiddlywiki/geospatial/docs/geomap.tid | 2 + .../files/leaflet.js/tiddlywiki.files | 15 ++- plugins/tiddlywiki/geospatial/styles.tid | 6 + .../geospatial/widgets/geobaselayer.js | 17 +++ .../tiddlywiki/geospatial/widgets/geomap.js | 117 +++++++++++++----- 11 files changed, 168 insertions(+), 37 deletions(-) create mode 100644 plugins/tiddlywiki/geospatial/baselayers/TagsGeoBaseLayer.md create mode 100644 plugins/tiddlywiki/geospatial/baselayers/openstreetmap.tid create mode 100644 plugins/tiddlywiki/geospatial/baselayers/opentopomap.tid create mode 100644 plugins/tiddlywiki/geospatial/baselayers/stamen-watercolor.tid create mode 100644 plugins/tiddlywiki/geospatial/docs/geobaselayer.tid create mode 100644 plugins/tiddlywiki/geospatial/widgets/geobaselayer.js diff --git a/editions/geospatialdemo/tiddlers/HelloThere.tid b/editions/geospatialdemo/tiddlers/HelloThere.tid index da1393a77..6fd833bfc 100644 --- a/editions/geospatialdemo/tiddlers/HelloThere.tid +++ b/editions/geospatialdemo/tiddlers/HelloThere.tid @@ -22,11 +22,14 @@ This demo requires that the API keys needed to access external services be obtai <$geomap state=<> > + <$list filter="[all[tiddlers+shadows]tag[$:/tags/GeoBaseLayer]]"> + <$geobaselayer title=<>/> + <$list filter="[all[tiddlers+shadows]tag[$:/tags/GeoMarker]]"> - <$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/> + <$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}} name={{!!caption}}/> <$list filter="[all[tiddlers+shadows]tag[$:/tags/GeoFeature]]"> - <$geolayer json={{!!text}} color={{!!color}}/> + <$geolayer json={{!!text}} color={{!!color}} name={{!!caption}}/> diff --git a/plugins/tiddlywiki/geospatial/baselayers/TagsGeoBaseLayer.md b/plugins/tiddlywiki/geospatial/baselayers/TagsGeoBaseLayer.md new file mode 100644 index 000000000..a59cb4f82 --- /dev/null +++ b/plugins/tiddlywiki/geospatial/baselayers/TagsGeoBaseLayer.md @@ -0,0 +1,2 @@ +title: $:/tags/GeoBaseLayer +list: $:/plugins/tiddlywiki/geospatial/baselayers/openstreetmap $:/plugins/tiddlywiki/geospatial/baselayers/opentopomap $:/plugins/tiddlywiki/geospatial/baselayers/stamen-watercolor \ No newline at end of file diff --git a/plugins/tiddlywiki/geospatial/baselayers/openstreetmap.tid b/plugins/tiddlywiki/geospatial/baselayers/openstreetmap.tid new file mode 100644 index 000000000..327598dd2 --- /dev/null +++ b/plugins/tiddlywiki/geospatial/baselayers/openstreetmap.tid @@ -0,0 +1,7 @@ +title: $:/plugins/tiddlywiki/geospatial/baselayers/openstreetmap +caption: OpenStreetMap +tags: $:/tags/GeoBaseLayer +tiles-url: https://tile.openstreetmap.org/{z}/{x}/{y}.png +max-zoom: 19 + +© OpenStreetMap \ No newline at end of file diff --git a/plugins/tiddlywiki/geospatial/baselayers/opentopomap.tid b/plugins/tiddlywiki/geospatial/baselayers/opentopomap.tid new file mode 100644 index 000000000..214ab19d2 --- /dev/null +++ b/plugins/tiddlywiki/geospatial/baselayers/opentopomap.tid @@ -0,0 +1,7 @@ +title: $:/plugins/tiddlywiki/geospatial/baselayers/opentopomap +caption: OpenTopoMap +tags: $:/tags/GeoBaseLayer +tiles-url: https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png +max-zoom: 17 + +Map data: © OpenStreetMap contributors, SRTM | Map style: © OpenTopoMap (CC-BY-SA) \ No newline at end of file diff --git a/plugins/tiddlywiki/geospatial/baselayers/stamen-watercolor.tid b/plugins/tiddlywiki/geospatial/baselayers/stamen-watercolor.tid new file mode 100644 index 000000000..b6a8783ca --- /dev/null +++ b/plugins/tiddlywiki/geospatial/baselayers/stamen-watercolor.tid @@ -0,0 +1,7 @@ +title: $:/plugins/tiddlywiki/geospatial/baselayers/stamen-watercolor +caption: Stamen Watercolor +tags: $:/tags/GeoBaseLayer +tiles-url: https://stamen-tiles.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg +max-zoom: 16 + +© Map tiles by Stamen Design, under CC BY 3.0. Data by OpenStreetMap, under CC BY SA. \ No newline at end of file diff --git a/plugins/tiddlywiki/geospatial/docs/geobaselayer.tid b/plugins/tiddlywiki/geospatial/docs/geobaselayer.tid new file mode 100644 index 000000000..4b1adfc23 --- /dev/null +++ b/plugins/tiddlywiki/geospatial/docs/geobaselayer.tid @@ -0,0 +1,18 @@ +title: $:/plugins/tiddlywiki/geospatial/docs/geobaselayer +caption: geobaselayer widget +tags: $:/tags/GeospatialDocs + +!! `<$geobaselayer>` widget + +The `<$geobaselayer>` widget is used inside the `<$geomap>` widget to define the base layers to display on the map. + +The following attributes are supported: + +|!Attribute |!Description | +|''title'' |Optional title of a tiddler that defines the base layer through the fields ''caption'', ''tiles-url'', ''max-zoom'' and ''text'' (the text field defines the attribution string for the base layer) | +|''name'' |Optional name for the base layer | +|''tiles-url'' |Optional templated tile server URL for the base layer | +|''max-zoom'' |Optional maximum zoom level for the base layer | +|''attribution'' |Optional attribution text for the base layer | + +The base layer will only work if all four of the required items ''name'', ''tiles-url'', ''max-zoom'' and ''attribution'' must be provided, either through the base layer tiddler specified in the title attribute, or explicitly via their own attributes. diff --git a/plugins/tiddlywiki/geospatial/docs/geomap.tid b/plugins/tiddlywiki/geospatial/docs/geomap.tid index 863225f57..26783a984 100644 --- a/plugins/tiddlywiki/geospatial/docs/geomap.tid +++ b/plugins/tiddlywiki/geospatial/docs/geomap.tid @@ -98,8 +98,10 @@ The following attributes are supported: <$testcase> <$data $compound-tiddler="$:/plugins/tiddlywiki/geospatial/tests/widgets/geomap"/> +<$data $tiddler="$:/plugins/tiddlywiki/geospatial"/> <$testcase> <$data $compound-tiddler="$:/plugins/tiddlywiki/geospatial/tests/widgets/geomap-refresh"/> +<$data $tiddler="$:/plugins/tiddlywiki/geospatial"/> diff --git a/plugins/tiddlywiki/geospatial/files/leaflet.js/tiddlywiki.files b/plugins/tiddlywiki/geospatial/files/leaflet.js/tiddlywiki.files index 01825fd9e..9c1379da1 100644 --- a/plugins/tiddlywiki/geospatial/files/leaflet.js/tiddlywiki.files +++ b/plugins/tiddlywiki/geospatial/files/leaflet.js/tiddlywiki.files @@ -6,9 +6,7 @@ "type": "application/javascript", "title": "$:/plugins/tiddlywiki/geospatial/leaflet.js", "module-type": "library" - }, - "prefix": "", - "suffix": "" + } }, { "file": "leaflet.css", @@ -16,9 +14,14 @@ "type": "text/css", "title": "$:/plugins/tiddlywiki/geospatial/leaflet.css", "tags": "[[$:/tags/Stylesheet]]" - }, - "prefix": "", - "suffix": "" + } + }, + { + "file": "images/layers-2x.png", + "fields": { + "type": "image/png", + "title": "$:/plugins/tiddlywiki/geospatial/leaflet/images/layers-2x.png" + } }, { "file": "LICENSE", diff --git a/plugins/tiddlywiki/geospatial/styles.tid b/plugins/tiddlywiki/geospatial/styles.tid index 571837995..b994c3d76 100644 --- a/plugins/tiddlywiki/geospatial/styles.tid +++ b/plugins/tiddlywiki/geospatial/styles.tid @@ -2,3 +2,9 @@ title: $:/plugins/tiddlywiki/geospatial/styles tags: [[$:/tags/Stylesheet]] \rules only filteredtranscludeinline transcludeinline macrodef macrocallinline + +.leaflet-retina .leaflet-control-layers-toggle, +.leaflet-control-layers-toggle { + background-image: url(<>); + +} \ No newline at end of file diff --git a/plugins/tiddlywiki/geospatial/widgets/geobaselayer.js b/plugins/tiddlywiki/geospatial/widgets/geobaselayer.js new file mode 100644 index 000000000..3ce02a541 --- /dev/null +++ b/plugins/tiddlywiki/geospatial/widgets/geobaselayer.js @@ -0,0 +1,17 @@ +/*\ +title: $:/plugins/tiddlywiki/innerwiki/geobaselayer.js +type: application/javascript +module-type: widget + +geobaselayer widget to represent a base layer for a geomap widget. Clone of the data widget + +\*/ +(function(){ + +/*jslint node: true, browser: true */ +/*global $tw: false */ +"use strict"; + +exports.geobaselayer = require("$:/core/modules/widgets/data.js").data; + +})(); diff --git a/plugins/tiddlywiki/geospatial/widgets/geomap.js b/plugins/tiddlywiki/geospatial/widgets/geomap.js index 57a796ef5..326eeea24 100644 --- a/plugins/tiddlywiki/geospatial/widgets/geomap.js +++ b/plugins/tiddlywiki/geospatial/widgets/geomap.js @@ -61,42 +61,16 @@ GeomapWidget.prototype.render = function(parent,nextSibling) { }; GeomapWidget.prototype.renderMap = function() { + var self = this; // Create the map this.map = $tw.Leaflet.map(this.domNode); // No layers rendered this.renderedLayers = []; - // Setup the tile layer - const tiles = $tw.Leaflet.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', { - maxZoom: 19, - attribution: '© OpenStreetMap' - }).addTo(this.map); + this.baseLayers = []; // Disable Leaflet attribution this.map.attributionControl.setPrefix(""); // Add scale $tw.Leaflet.control.scale().addTo(this.map); -}; - -GeomapWidget.prototype.refreshMap = function() { - var self = this; - // Remove any previously rendered layers - $tw.utils.each(this.renderedLayers,function(layer) { - self.map.removeLayer(layer); - }); - this.renderedLayers = []; - // Set the position - if(!this.setMapView()) { - // Default to showing the whole world - this.map.fitWorld(); - } - // Create default icon - const iconProportions = 365/560, - iconHeight = 50; - const 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 - }); // Listen for pan and zoom events and update the state tiddler this.map.on("moveend zoomend",function(event) { if(self.geomapStateTitle) { @@ -116,6 +90,73 @@ GeomapWidget.prototype.refreshMap = function() { } } }); +}; + +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:} + this.renderedBaseLayers = []; // Array of {name:,layer:} + // Restore the saved map position and zoom level + if(!this.setMapView()) { + // If there was no saved position then default to showing the whole world + this.map.fitWorld(); + } + // 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.getTiddlersWithTag("$:/tags/GeoBaseLayer").reverse(); + $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 @@ -126,8 +167,10 @@ GeomapWidget.prototype.refreshMap = function() { var jsonText = widget.getAttribute("json"), geoJson = []; 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")); @@ -160,8 +203,24 @@ GeomapWidget.prototype.refreshMap = function() { } } }).addTo(self.map); - self.renderedLayers.push(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: true + }).addTo(this.map); }; /*