1
0
mirror of https://github.com/Jermolene/TiddlyWiki5 synced 2024-09-28 07:08:20 +00:00

Introduce geolayer widget for specifying layers for geomap

This commit is contained in:
jeremy@jermolene.com 2023-05-09 10:35:33 +01:00
parent 29f4d3488b
commit 7f4f48d4ba
12 changed files with 292 additions and 158 deletions

View File

@ -130,8 +130,8 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
DataWidget.prototype.refresh = function(changedTiddlers) {
// Refresh our attributes
var changedAttributes = this.computeAttributes();
// Refresh our children
return this.refreshChildren(changedTiddlers);
// Refresh our children, and indicate that we refreshed if any of our attribute values have changed
return this.refreshChildren(changedTiddlers) || $tw.utils.count(changedAttributes) > 0;
};
exports.data = DataWidget;

View File

@ -100,7 +100,7 @@ TestCaseWidget.prototype.refresh = function(changedTiddlers) {
this.refreshSelf();
return true;
} else {
return false;
return this.contentRoot.refresh(changedTiddlers);
}
};

View File

@ -20,9 +20,14 @@ This demo requires that the API keys needed to access external services be obtai
! Map Showing All Features and Markers
<$geomap
markers="[all[tiddlers+shadows]tag[$:/tags/GeoMarker]]"
features="[all[tiddlers+shadows]tag[$:/tags/GeoFeature]]"
state=<<qualify "$:/state/demo-map">>
/>
>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/GeoMarker]]">
<$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/>
</$list>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/GeoFeature]]">
<$geolayer json={{!!text}} color={{!!color}}/>
</$list>
</$geomap>
<<tabs tabsList:"[all[tiddlers+shadows]tag[$:/tags/GeospatialDemo]]" default:"GeoMarkers">>

View File

@ -10,11 +10,11 @@ title: ui/geofeature
!! Mapped
<$geomap
features="[<currentTiddler>]"
state=<<qualify "$:/state/demo-map">>
/>
>
<$geolayer json={{!!text}} color={{!!color}}/>
</$geomap>
!! Intersect with other features

View File

@ -64,9 +64,10 @@ title: ui/geomarker
!! Mapped
<$geomap
markers="[<currentTiddler>]"
state=<<qualify "$:/state/demo-map">>
/>
>
<$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/>
</$geomap>
!! Distance to other markers

View File

@ -49,8 +49,11 @@ Click to get photos of user
<$geomap
state=<<qualify "$:/state/demo-map">>
markers="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]"
/>
>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]">
<$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/>
</$list>
</$geomap>
"""/>
</$testcase>
@ -74,8 +77,11 @@ Click to get photos from group
<$geomap
state=<<qualify "$:/state/demo-map">>
markers="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]"
/>
>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]">
<$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/>
</$list>
</$geomap>
"""/>
</$testcase>
@ -99,8 +105,11 @@ Click to get photos from album
<$geomap
state=<<qualify "$:/state/demo-map">>
markers="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]"
/>
>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]">
<$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/>
</$list>
</$geomap>
"""/>
</$testcase>
@ -123,7 +132,10 @@ Click to get interesting photos
<$geomap
state=<<qualify "$:/state/demo-map">>
markers="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]"
/>
>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/FlickrPhoto]]">
<$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/>
</$list>
</$geomap>
"""/>
</$testcase>

View File

@ -0,0 +1,20 @@
title: $:/plugins/tiddlywiki/geospatial/docs/geolayer
caption: geolayer widget
tags: $:/tags/GeospatialDocs
!! `<$geolayer>` widget
The `<$geolayer>` widget is used inside the `<$geomap>` widget to indicate the layers and markers to display.
The following attributes are supported:
|!Attribute |!Description |
|''json'' |Optional GeoJSON Feature Collection to be rendered |
|''lat'' |Optional latitude of marker if json attribute missing |
|''long'' |Optional longitude of marker if json attribute missing |
|''alt'' |Optional altitude of marker if json attribute missing |
Note that the `<$geolayer>` widget can be used in one of two modes:
* With the ''json'' attibute specifying the layer to be drawn
* With the ''lat'', ''long'' and optional ''alt'' attributes specifying a marker to be drawn

View File

@ -4,15 +4,12 @@ tags: $:/tags/GeospatialDocs
!! `<$geomap>` widget
The `<$geomap>` widget displays an interactive map using [[Leaflet.js|https://leafletjs.com/]].
The `<$geomap>` widget displays an interactive map using [[Leaflet.js|https://leafletjs.com/]]. `<$geolayer>` widgets inside the `<$geomap>` widget are used to indicate the layers and markers to display.
The following attributes are supported:
|!Attribute |!Description |
|''state'' |The title of a state tiddler used to track the state of the map in the `zoom`, `long` and `lat` fields |
|''features'' |A filter identifying the GeoJSON feature tiddlers to be displayed on the map |
|''markers'' |A filter identifying the GeoJSON marker tiddlers to be displayed on the map |
!! Examples
@ -45,8 +42,11 @@ The following attributes are supported:
text="""This is Oxford!"""/>
<$data title="Output" text="""<$geomap
state=<<qualify "$:/state/demo-map">>
markers="[all[tiddlers+shadows]tag[$:/tags/GeoMarker]]"
/>
>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/GeoMarker]]">
<$geolayer lat={{!!lat}} long={{!!long}} alt={{!!alt}} color={{!!color}}/>
</$list>
</$geomap>
"""/>
<$data $tiddler="$:/plugins/tiddlywiki/geospatial"/>
</$testcase>
@ -87,8 +87,19 @@ The following attributes are supported:
}"""/>
<$data title="Output" text="""<$geomap
state=<<qualify "$:/state/demo-map">>
features="[all[tiddlers+shadows]tag[$:/tags/GeoFeature]]"
/>
>
<$list filter="[all[tiddlers+shadows]tag[$:/tags/GeoFeature]]">
<$geolayer json={{!!text}} color={{!!color}}/>
</$list>
</$geomap>
"""/>
<$data $tiddler="$:/plugins/tiddlywiki/geospatial"/>
</$testcase>
<$testcase>
<$data $compound-tiddler="$:/plugins/tiddlywiki/geospatial/tests/widgets/geomap"/>
</$testcase>
<$testcase>
<$data $compound-tiddler="$:/plugins/tiddlywiki/geospatial/tests/widgets/geomap-refresh"/>
</$testcase>

View File

@ -0,0 +1,58 @@
title: $:/plugins/tiddlywiki/geospatial/tests/widgets/geomap-refresh
description: refreshing of geomap widget
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Description
text: Map using dynamic geolayer to represent features
+
title: Layer
tags: $:/tags/GeoFeature
type: application/json
color: red
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "An example geofeature feature",
"properties": {
"custom": "A custom property of this feature",
"color": "#ff8"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-90,35],
[-90,30],
[-85,30],
[-85,35],
[-90,35]
]
]
}
}
]
}
+
title: Output
<$button>
<$action-setfield $tiddler="$:/state/layer" $value="yes"/>
Click here to enable the layer
</$button>
<$geomap
state=<<qualify "$:/state/demo-map">>
>
<$geolayer
json={{{ [{$:/state/layer}match[yes]then{Layer}else[]] }}}
colorFilter="[<currentTiddler>jsonget[properties],[color]else[red]]"
/>
</$geomap>
+
title: ExpectedResult
<p>Winchester,Oxford,</p>

View File

@ -0,0 +1,53 @@
title: $:/plugins/tiddlywiki/geospatial/tests/widgets/geomap
description: geomap widget
type: text/vnd.tiddlywiki-multiple
tags: [[$:/tags/wiki-test-spec]]
title: Description
text: Map using geolayer to represent features
+
title: Layer
tags: $:/tags/GeoFeature
type: application/json
color: red
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "An example geofeature feature",
"properties": {
"custom": "A custom property of this feature",
"color": "#ff8"
},
"geometry": {
"type": "Polygon",
"coordinates": [
[
[-90,35],
[-90,30],
[-85,30],
[-85,35],
[-90,35]
]
]
}
}
]
}
+
title: Output
<$geomap
state=<<qualify "$:/state/demo-map">>
>
<$geolayer
json={{Layer}}
colorFilter="[<currentTiddler>jsonget[properties],[color]else[red]]"
/>
</$geomap>
+
title: ExpectedResult
<p>Winchester,Oxford,</p>

View File

@ -0,0 +1,17 @@
/*\
title: $:/plugins/tiddlywiki/innerwiki/geolayer.js
type: application/javascript
module-type: widget
geolayer widget to represent a layer for a geomap widget. Clone of the data widget
\*/
(function(){
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict";
exports.geolayer = require("$:/core/modules/widgets/data.js").data;
})();

View File

@ -31,28 +31,40 @@ GeomapWidget.prototype.render = function(parent,nextSibling) {
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
var domNode = this.document.createElement("div");
domNode.style.width = "100%";
domNode.style.height = "600px";
this.domNode = this.document.createElement("div");
this.domNode.style.width = "100%";
this.domNode.style.height = "600px";
// Insert it into the DOM
parent.insertBefore(domNode,nextSibling);
this.domNodes.push(domNode);
parent.insertBefore(this.domNode,nextSibling);
this.domNodes.push(this.domNode);
// Render the map
if($tw.browser) {
this.renderMap(domNode);
this.renderMap();
this.refreshMap();
}
};
GeomapWidget.prototype.renderMap = function(domNode) {
var self = this;
GeomapWidget.prototype.renderMap = function() {
// Create the map
this.map = $tw.Leaflet.map(domNode);
// Set the position
if(!this.setMapView()) {
// Default to showing the whole world
this.map.fitWorld();
}
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,
@ -60,6 +72,22 @@ GeomapWidget.prototype.renderMap = function(domNode) {
}).addTo(this.map);
// 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;
@ -69,8 +97,6 @@ GeomapWidget.prototype.renderMap = function(domNode) {
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
});
// 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.geomapStateTitle) {
@ -90,65 +116,51 @@ GeomapWidget.prototype.renderMap = function(domNode) {
}
}
});
// Track the geofeatures filter
this.trackerGeoFeaturesFilter = new FilterTracker({
wiki: this.wiki,
widget: this,
filter: this.geomapFeaturesFilter,
enter: function(title,tiddler) {
var text = (tiddler && tiddler.fields.text) || "[]",
geoJson = $tw.utils.parseJSONSafe(text,[]),
layer = $tw.Leaflet.geoJSON(geoJson,{
style: function(geoJsonFeature) {
return {
color: (tiddler && tiddler.getFieldString("color")) || "yellow"
}
},
pointToLayer: function(geoJsonPoint,latlng) {
return L.circleMarker(latlng,{
radius: 8
});
},
onEachFeature: function(feature,layer) {
if(feature.properties) {
layer.bindPopup(JSON.stringify(feature.properties,null,4));
}
}
}).addTo(self.map);
return layer;
},
leave: function(title,tiddler,data) {
data.remove();
}
});
// Track the geomarkers filter
// Make a marker cluster
var markers = $tw.Leaflet.markerClusterGroup({
maxClusterRadius: 40
});
this.map.addLayer(markers);
this.trackerGeoMarkersFilter = new FilterTracker({
wiki: this.wiki,
widget: this,
filter: this.geomapMarkerFilter,
enter: function(title,tiddler) {
var lat = $tw.utils.parseNumber((tiddler && tiddler.fields.lat) || "0"),
long = $tw.utils.parseNumber((tiddler && tiddler.fields.long) || "0"),
alt = $tw.utils.parseNumber((tiddler && tiddler.fields.alt) || "0"),
caption = (tiddler && tiddler.fields.caption) || title,
icon = myIcon;
if(tiddler && tiddler.fields["icon-url"]) {
icon = new $tw.Leaflet.Icon({
iconUrl: tiddler && tiddler.fields["icon-url"],
iconSize: [32, 32], // Size of the icon
iconAnchor: [16, 32], // Position of the anchor within the icon
popupAnchor: [16, -32] // Position of the popup anchor relative to the icon anchor
});
}
return $tw.Leaflet.marker([lat,long,alt],{icon: icon,draggable: false}).bindPopup(caption).addTo(markers);
},
leave: function(title,tiddler,data) {
data.remove();
// Process embedded geolayer widgets
this.findChildrenDataWidgets(this.contentRoot.children,"geolayer",function(widget) {
var jsonText = widget.getAttribute("json"),
geoJson = [];
if(jsonText) {
geoJson = $tw.utils.parseJSONSafe(jsonText,[]);
} else if(widget.hasAttribute("lat") && widget.hasAttribute("long")) {
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"));
geoJson = {
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [long,lat,alt]
}
}
]
};
}
var layer = $tw.Leaflet.geoJSON(geoJson,{
style: function(geoJsonFeature) {
return {
color: widget.getAttribute("color","yellow")
}
},
pointToLayer: function(geoJsonPoint,latlng) {
$tw.Leaflet.marker(latlng,{icon: myIcon,draggable: false}).addTo(markers);
return markers;
},
onEachFeature: function(feature,layer) {
if(feature.properties) {
layer.bindPopup(JSON.stringify(feature.properties,null,4));
}
}
}).addTo(self.map);
self.renderedLayers.push(layer);
});
};
@ -169,8 +181,6 @@ Compute the internal state of the widget
*/
GeomapWidget.prototype.execute = function() {
this.geomapStateTitle = this.getAttribute("state");
this.geomapMarkerFilter = this.getAttribute("markers");
this.geomapFeaturesFilter = this.getAttribute("features");
};
/*
@ -178,11 +188,6 @@ Selectively refreshes the widget if needed. Returns true if the widget or any of
*/
GeomapWidget.prototype.refresh = function(changedTiddlers) {
var changedAttributes = this.computeAttributes();
// Refresh entire widget if layers or marker filter changes
if(changedAttributes.features || changedAttributes.markers || changedAttributes.state) {
this.refreshSelf();
return true;
}
// Set zoom and position if the state tiddler has changed
if(changedAttributes.state) {
this.geomapStateTitle = this.getAttribute("state");
@ -190,63 +195,15 @@ GeomapWidget.prototype.refresh = function(changedTiddlers) {
if(changedAttributes.state || changedTiddlers[this.geomapStateTitle]) {
this.setMapView();
}
// Check whether the layers or markers need updating
this.trackerGeoFeaturesFilter.refresh(changedTiddlers);
this.trackerGeoMarkersFilter.refresh(changedTiddlers);
// No children to refresh
return false;
// Refresh child nodes, and rerender map if there have been any changes
var result = this.contentRoot.refresh(changedTiddlers);
if(result) {
this.refreshMap();
}
return result;
};
exports.geomap = GeomapWidget;
function FilterTracker(options) {
var self = this;
// Save parameters
this.filter = options.filter;
this.wiki = options.wiki;
this.widget = options.widget;
this.enter = options.enter;
this.leave = options.leave;
this.update = options.update;
// Calculate initial result set and call enter for each entry
this.items = Object.create(null);
$tw.utils.each(this.wiki.filterTiddlers(this.filter,this.widget),function(title) {
self.items[title] = self.enter(title,self.wiki.getTiddler(title));
});
}
FilterTracker.prototype.refresh = function(changedTiddlers) {
var self = this;
var newItems = this.wiki.filterTiddlers(this.filter,this.widget);
// Go through the new items and call update or enter as appropriate
$tw.utils.each(newItems,function(title) {
// Check if this item is already known
if(title in self.items) {
// Issue an update if the underlying tiddler has changed
if(changedTiddlers[title]) {
// Use the update method if provided
if(self.update) {
self.update(title,self.wiki.getTiddler(title),self.items[title]);
} else {
// Otherwise leave and enter is equivalent to update
self.leave(title,self.wiki.getTiddler(title),self.items[title]);
self.items[title] = self.enter(title,self.wiki.getTiddler(title));
}
}
} else {
// It's a new item, so we need to enter it
self.items[title] = self.enter(title,self.wiki.getTiddler(title));
}
});
// Call leave for any items that are no longer in the list
$tw.utils.each(Object.keys(this.items),function(title) {
if(newItems.indexOf(title) === -1) {
// Remove this item
self.leave(title,self.wiki.getTiddler(title),self.items[title]);
delete self.items[title];
}
});
};
})();