Created
November 11, 2021 22:41
-
-
Save glennsl/93c272ba42f954fcf5eec99b1311ac05 to your computer and use it in GitHub Desktop.
Web component-based Leaflet bindings for Seed
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const LEAFLET_CSS_URL = "https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"; | |
const STYLESHEET = ` | |
#map { | |
width: 100%; | |
height: 100%; | |
overflow: hidden; | |
} | |
.leaflet-top .leaflet-control { | |
margin-top: 2em; | |
} | |
.leaflet-left .leaflet-control { | |
margin-left: 2em; | |
} | |
.leaflet-bottom .leaflet-control { | |
margin-bottom: 2em; | |
} | |
.leaflet-right .leaflet-control { | |
margin-right: 2em; | |
} | |
.leaflet-control.leaflet-control-attribution { | |
margin: 0; | |
margin-top: -1.5em; /* make this "transparent" to other controls margins */ | |
} | |
.leaflet-control-zoom, | |
.leaflet-touch .leaflet-control-zoom { | |
box-shadow: none; | |
border: none; | |
} | |
.leaflet-control-zoom > a.leaflet-control-zoom-in, | |
.leaflet-control-zoom > a.leaflet-control-zoom-out, | |
.leaflet-touch .leaflet-control-zoom > a.leaflet-control-zoom-in, | |
.leaflet-touch .leaflet-control-zoom > a.leaflet-control-zoom-out { | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
width: 1.5em; | |
height: 1.5em; | |
font-size: 26px; | |
font-weight: normal; | |
color: #E16246; | |
border-radius: 50%; | |
box-shadow: 0 1px 5px rgb(0 0 0 / 10%); | |
} | |
.leaflet-control-zoom > a:first-child { | |
margin-bottom: .25em; | |
} | |
`; | |
// leafy-map | |
class Map extends HTMLElement { | |
static get observedAttribtues() { | |
return ["lat", "lng", "zoom"]; | |
} | |
get lat() { | |
return this.getAttribute("lat") || 51; | |
} | |
get lng() { | |
return this.getAttribute("lng") || 0; | |
} | |
get zoom() { | |
return this.getAttribute("zoom") || -1; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
super.attributeChangedCallback(name, oldValue, newValue); | |
switch (name) { | |
case "lat": | |
case "lng": | |
this.map && this.map.setView([this.lat, this.lng]); | |
break; | |
case "zoom": | |
this.map && this.map.setZoom(this.zoom); | |
break; | |
} | |
} | |
constructor() { | |
super(); | |
this.render(); | |
// ensure we've rendered before initializing | |
setTimeout(() => { | |
this.initMap(); | |
this.initView(); | |
this.initChildren(); | |
}); | |
} | |
render() { | |
this.attachShadow({ mode: "open" }); | |
const styleEl = document.createElement("style"); | |
styleEl.textContent = STYLESHEET; | |
const leafletStyleEl = document.createElement("link"); | |
leafletStyleEl.setAttribute("rel", "stylesheet"); | |
leafletStyleEl.setAttribute("href", LEAFLET_CSS_URL); | |
this.mapEl = document.createElement("div"); | |
this.mapEl.setAttribute("id", "map"); | |
this.shadowRoot.append(leafletStyleEl, styleEl, this.mapEl); | |
} | |
initMap() { | |
if (this.map) this.map.remove(); | |
this.map = L.map(this.mapEl, { | |
zoomControl: false, | |
}); | |
L.control | |
.zoom({ | |
position: "bottomright", | |
}) | |
.addTo(this.map); | |
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", { | |
attribution: | |
'Map data © <a href="http://openstreetmap.org.>OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>', | |
maxZoom: 18, | |
}).addTo(this.map); | |
} | |
initView() { | |
this.map.setView([this.lat, this.lng], this.zoom); | |
if (this.zoom == -1) { | |
this.map.fitWorld({ animate: false }); | |
} | |
} | |
initChildren() { | |
for (let child of this.children) { | |
child.container = this.map; | |
} | |
this.mutationObserver = new MutationObserver((mutations) => | |
mutations.forEach((mutation) => { | |
for (let child of mutation.addedNodes) { | |
child.container = this.map; | |
} | |
for (let child of mutation.removedNodes) { | |
child.container = null; | |
} | |
}) | |
); | |
this.mutationObserver.observe(this, { childList: true }); | |
} | |
} | |
customElements.define("leafy-map", Map); | |
// Feature | |
class Feature extends HTMLElement { | |
static get observedAttributes() { | |
return ["data", "tooltip", "tooltip-open"]; | |
} | |
get tooltip() { | |
return this.getAttribute("tooltip"); | |
} | |
get tooltipOpen() { | |
return this.getAttribute("tooltip-open"); | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
switch (name) { | |
case "tooltip": | |
case "tooltip-open": | |
this._setPopup(); | |
break; | |
} | |
} | |
get feature() { | |
return this._feature; | |
} | |
set feature(new_feature) { | |
if (this.container && this._feature) { | |
this.container.removeLayer(this._feature); | |
} | |
this._feature = new_feature; | |
if (this.container && this._feature) { | |
this._feature.addTo(this.container); | |
this._setPopup(); | |
} | |
} | |
get container() { | |
return this._container; | |
} | |
set container(new_container) { | |
if (this._container && this.feature) { | |
this._container.removeLayer(this.feature); | |
} | |
this._container = new_container; | |
if (this._container && this.feature) { | |
this.feature.addTo(this._container); | |
this._setPopup(); | |
} | |
} | |
_setPopup() { | |
if (this.feature) { | |
this.feature.unbindTooltip(); | |
if (this.tooltip) { | |
this.feature.bindTooltip(this.tooltip, { | |
direction: "top", | |
}); | |
if (this.tooltipOpen) { | |
this.feature.openTooltip(); | |
} | |
} | |
} | |
} | |
} | |
// InteractiveFeature | |
class InteractiveFeature extends Feature { | |
connectedCallback() {} | |
get feature() { | |
return this._feature; | |
} | |
set feature(new_feature) { | |
if (this.container && this._feature) { | |
this.container.removeLayer(this._feature); | |
} | |
this._feature = new_feature; | |
if (this._feature) { | |
[ | |
"click", | |
"dblclick", | |
"mousedown", | |
"mouseup", | |
"mouseover", | |
"mouseout", | |
"contextmenu", | |
].forEach((eventName) => | |
this._feature.on(eventName, (event) => | |
this.dispatchEvent(new MouseEvent(eventName, event)) | |
) | |
); | |
} | |
if (this.container && this._feature) { | |
this._feature.addTo(this.container); | |
this._setPopup(); | |
} | |
} | |
} | |
// FeatureGroup | |
class FeatureGroup extends Feature { | |
static get observedAttributes() { | |
return ["zoom-to-fit", ...Feature.observedAttributes]; | |
} | |
get zoomToFit() { | |
return this.getAttribute("zoom-to-fit") || false; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
super.attributeChangedCallback(name, oldValue, newValue); | |
switch (name) { | |
case "zoom-to-fit": | |
if (this.zoomToFit) { | |
this.fitBounds(); | |
} | |
break; | |
} | |
} | |
constructor() { | |
super(); | |
this.feature = L.featureGroup(); | |
for (let child of this.children) { | |
child.container = this.feature; | |
} | |
this.mutationObserver = new MutationObserver((mutations) => | |
mutations.forEach((mutation) => { | |
for (let child of mutation.addedNodes) { | |
child.container = this.feature; | |
} | |
for (let child of mutation.removedNodes) { | |
child.container = null; | |
} | |
this.fitBounds(); | |
}) | |
); | |
this.mutationObserver.observe(this, { childList: true }); | |
this.fitBounds(); | |
} | |
get container() { | |
return super.container; | |
} | |
set container(new_container) { | |
super.container = new_container; | |
if (this.zoomToFit) { | |
this.fitBounds(); | |
} | |
} | |
fitBounds() { | |
if (this.container) { | |
let bounds = this.feature.getBounds(); | |
if (bounds.isValid()) { | |
this.container.fitBounds(bounds, { | |
paddingBottomRight: L.point(0, 32), | |
animate: false, | |
}); | |
} | |
} | |
} | |
} | |
customElements.define("leafy-feature-group", FeatureGroup); | |
// Marker | |
class Marker extends InteractiveFeature { | |
static get observedAttributes() { | |
return ["lat", "lng", ...Feature.observedAttributes]; | |
} | |
get lat() { | |
return this.getAttribute("lat") || 0; | |
} | |
get lng() { | |
return this.getAttribute("lng") || 0; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
super.attributeChangedCallback(name, oldValue, newValue); | |
switch (name) { | |
case "lat": | |
case "lng": | |
this.feature.setLatLng(L.latLng(this.lat, this.lng)); | |
break; | |
} | |
} | |
constructor() { | |
super(); | |
this.feature = L.marker([this.lat, this.lng]); | |
} | |
} | |
customElements.define("leafy-marker", Marker); | |
// Path | |
class Path extends InteractiveFeature { | |
static get observedAttributes() { | |
return [ | |
"stroke", | |
"stroke-width", | |
"stroke-dasharray", | |
"fill", | |
"fill-opacity", | |
"class", | |
...Feature.observedAttributes, | |
]; | |
} | |
get pathOptions() { | |
let fill = this.getAttribute("fill"); | |
return { | |
color: this.getAttribute("stroke") || "red", | |
weight: this.getAttribute("stroke-width") || 3, | |
dashArray: this.getAttribute("stroke-dasharray"), | |
fill: fill == null ? true : !!fill, | |
fillColor: fill || "red", | |
fillOpacity: this.getAttribute("fill-opacity") || 0.2, | |
className: this.getAttribute("class"), | |
}; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
super.attributeChangedCallback(name, oldValue, newValue); | |
switch (name) { | |
case "stroke": | |
case "stroke-width": | |
case "stroke-dasharray": | |
case "fill": | |
case "fill-opacity": | |
case "class": | |
this.feature && this.feature.setStyle(this.pathOptions); | |
break; | |
} | |
} | |
} | |
// Polyline | |
class Polyline extends Path { | |
static get observedAttributes() { | |
return ["zoom-to-fit", ...Path.observedAttributes]; | |
} | |
get zoomToFit() { | |
return this.getAttribute("zoom-to-fit") || false; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
super.attributeChangedCallback(name, oldValue, newValue); | |
switch (name) { | |
case "zoom-to-fit": | |
if (this.zoomToFit) { | |
this.fitBounds(); | |
} | |
break; | |
} | |
} | |
get feature() { | |
return super.feature; | |
} | |
set feature(new_feature) { | |
super.feature = new_feature; | |
if (this.zoomToFit) { | |
this.fitBounds(); | |
} | |
} | |
get container() { | |
return super.container; | |
} | |
set container(new_container) { | |
super.container = new_container; | |
if (this.zoomToFit) { | |
this.fitBounds(); | |
} | |
} | |
fitBounds() { | |
if (this.container && this.feature) { | |
let bounds = this.feature.getBounds(); | |
if (bounds.isValid()) { | |
this.container.fitBounds(bounds, { | |
paddingBottomRight: L.point(0, 32), | |
animate: false, | |
}); | |
} | |
} | |
} | |
} | |
// leafy-circle | |
class Circle extends Polyline { | |
static get observedAttributes() { | |
return ["lat", "lng", "radius", ...Polyline.observedAttributes]; | |
} | |
get lat() { | |
return this.getAttribute("lat") || 0; | |
} | |
get lng() { | |
return this.getAttribute("lng") || 0; | |
} | |
get radius() { | |
return this.getAttribute("radius") || 0; | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
super.attributeChangedCallback(name, oldValue, newValue); | |
switch (name) { | |
case "lat": | |
case "lng": | |
this.feature.setLatLng(L.latLng(this.lat, this.lng)); | |
break; | |
case "radius": | |
this.feature.setRadius(this.radius); | |
break; | |
} | |
} | |
constructor() { | |
super(); | |
this.feature = L.circle( | |
[this.lat, this.lng], | |
this.radius, | |
this.pathOptions | |
); | |
} | |
} | |
customElements.define("leafy-circle", Circle); | |
// leafy-geojson | |
class Geojson extends Polyline { | |
static get observedAttributes() { | |
return ["data", ...Polyline.observedAttributes]; | |
} | |
get data() { | |
try { | |
return JSON.parse(this.getAttribute("data")); | |
} catch (e) { | |
return null; | |
} | |
} | |
attributeChangedCallback(name, oldValue, newValue) { | |
super.attributeChangedCallback(name, oldValue, newValue); | |
switch (name) { | |
case "data": | |
this.feature = L.geoJSON(this.data, this.pathOptions); | |
break; | |
} | |
} | |
constructor() { | |
super(); | |
this.feature = L.geoJSON(this.data, this.pathOptions); | |
} | |
} | |
customElements.define("leafy-geojson", Geojson); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#![allow(unused)] | |
use crate::style::{color::Gradient, Color}; | |
use seed::{prelude::*, *}; | |
use std::fmt; | |
use std::rc::Rc; | |
// Map | |
pub struct Map<'a, Ms> { | |
key: Option<&'a str>, | |
lat: Option<f64>, | |
lng: Option<f64>, | |
zoom: Option<i32>, | |
features: Vec<Box<dyn Feature<Ms>>>, | |
} | |
pub fn map<'a, Ms>() -> Map<'a, Ms> { | |
Map { | |
key: None, | |
lat: None, | |
lng: None, | |
zoom: None, | |
features: vec![], | |
} | |
} | |
impl<'a, Ms> Map<'a, Ms> { | |
pub fn with(mut self, feature: impl Feature<Ms> + 'static) -> Self { | |
self.features.push(Box::new(feature)); | |
self | |
} | |
pub fn key(mut self, key: &'a str) -> Self { | |
self.key = Some(key); | |
self | |
} | |
pub fn center_on(mut self, lat: f64, lng: f64) -> Self { | |
self.lat = Some(lat); | |
self.lng = Some(lng); | |
self | |
} | |
pub fn zoom(mut self, zoom: i32) -> Self { | |
self.zoom = Some(zoom); | |
self | |
} | |
fn into_node(&'a self) -> Node<Ms> { | |
custom![ | |
Tag::from("leafy-map"), | |
self.key.as_ref().map(el_key), | |
attrs! { | |
At::from("lat") => self.lat.as_at_value(), | |
At::from("lng") => self.lng.as_at_value(), | |
At::from("zoom") => self.zoom.as_at_value(), | |
}, | |
self.features.iter().map(|f| f.into_node()), | |
] | |
} | |
fn into_nodes(&self) -> Vec<Node<Ms>> { | |
nodes![self.into_node()] | |
} | |
} | |
impl<'a, Ms> UpdateEl<Ms> for Map<'a, Ms> { | |
fn update_el(self, el: &mut El<Ms>) { | |
el.children.append(&mut self.into_nodes()) | |
} | |
} | |
// Feature | |
pub trait Feature<Ms> { | |
fn into_node(&self) -> Node<Ms>; | |
} | |
enum Fill { | |
Solid(Color), | |
Gradient(Gradient), | |
} | |
impl fmt::Display for Fill { | |
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
match self { | |
Fill::Solid(color) => write!(f, "{}", color), | |
Fill::Gradient(gradient) => write!(f, "{}", gradient), | |
} | |
} | |
} | |
// FeatureGroup | |
pub struct FeatureGroup<Ms> { | |
zoom_to_fit: Option<bool>, | |
features: Vec<Box<dyn Feature<Ms>>>, | |
} | |
pub fn feature_group<Ms>() -> FeatureGroup<Ms> { | |
FeatureGroup { | |
zoom_to_fit: None, | |
features: vec![], | |
} | |
} | |
impl<Ms> FeatureGroup<Ms> { | |
pub fn with(mut self, feature: impl Feature<Ms> + 'static) -> Self { | |
self.features.push(Box::new(feature)); | |
self | |
} | |
pub fn zoom_to_fit(mut self) -> Self { | |
self.zoom_to_fit = Some(true); | |
self | |
} | |
} | |
impl<Ms> Feature<Ms> for FeatureGroup<Ms> { | |
fn into_node(&self) -> Node<Ms> { | |
custom![ | |
Tag::from("leafy-feature-group"), | |
attrs! { | |
At::from("zoom-to-fit") => self.zoom_to_fit.as_at_value(), | |
}, | |
self.features.iter().map(|f| f.into_node()), | |
] | |
} | |
} | |
// Marker | |
pub struct Marker<Ms> { | |
lat: f64, | |
lng: f64, | |
tooltip: Option<String>, | |
tooltip_open: Option<bool>, | |
on_click: Option<Rc<dyn Fn() -> Ms>>, | |
on_mouseover: Option<Rc<dyn Fn() -> Ms>>, | |
on_mouseout: Option<Rc<dyn Fn() -> Ms>>, | |
} | |
pub fn marker<Ms>(lat: f64, lng: f64) -> Marker<Ms> { | |
Marker { | |
lat, | |
lng, | |
tooltip: None, | |
tooltip_open: None, | |
on_click: None, | |
on_mouseover: None, | |
on_mouseout: None, | |
} | |
} | |
impl<Ms> Marker<Ms> { | |
pub fn tooltip<S: Into<String>>(mut self, value: S) -> Self { | |
self.tooltip = Some(value.into()); | |
self | |
} | |
pub fn tooltip_open(mut self, value: bool) -> Self { | |
self.tooltip_open = Some(value); | |
self | |
} | |
pub fn on_click<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_click = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
pub fn on_mouseover<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_mouseover = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
pub fn on_mouseout<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_mouseout = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
} | |
impl<Ms> Feature<Ms> for Marker<Ms> | |
where | |
Ms: 'static, | |
{ | |
fn into_node(&self) -> Node<Ms> { | |
custom![ | |
Tag::from("leafy-marker"), | |
attrs! { | |
At::from("lat") => self.lat, | |
At::from("lng") => self.lng, | |
At::from("tooltip") => self.tooltip.as_ref().as_at_value(), | |
At::from("tooltip-open") => self.tooltip_open.as_ref().as_at_value(), | |
}, | |
self.on_click | |
.clone() | |
.map(|handler| ev(Ev::Click, move |_| handler())), | |
self.on_mouseover | |
.clone() | |
.map(|handler| ev(Ev::MouseOver, move |_| handler())), | |
self.on_mouseout | |
.clone() | |
.map(|handler| ev(Ev::MouseOut, move |_| handler())), | |
] | |
} | |
} | |
// Circle | |
pub struct Circle<Ms> { | |
lat: f64, | |
lng: f64, | |
radius: u32, | |
stroke: Option<Color>, | |
stroke_width: Option<i32>, | |
stroke_dasharray: Option<String>, | |
fill: Option<Fill>, | |
fill_opacity: Option<f64>, | |
zoom_to_fit: Option<bool>, | |
tooltip: Option<String>, | |
tooltip_open: Option<bool>, | |
on_click: Option<Rc<dyn Fn() -> Ms>>, | |
on_mouseover: Option<Rc<dyn Fn() -> Ms>>, | |
on_mouseout: Option<Rc<dyn Fn() -> Ms>>, | |
} | |
pub fn circle<Ms>(lat: f64, lng: f64, radius: u32) -> Circle<Ms> { | |
Circle { | |
lat, | |
lng, | |
radius, | |
stroke: None, | |
stroke_width: None, | |
stroke_dasharray: None, | |
fill: None, | |
fill_opacity: None, | |
zoom_to_fit: None, | |
tooltip: None, | |
tooltip_open: None, | |
on_click: None, | |
on_mouseover: None, | |
on_mouseout: None, | |
} | |
} | |
impl<Ms> Circle<Ms> { | |
pub fn stroke(mut self, color: Color) -> Self { | |
self.stroke = Some(color); | |
self | |
} | |
pub fn stroke_width(mut self, size: i32) -> Self { | |
self.stroke_width = Some(size); | |
self | |
} | |
pub fn stroke_dasharray<S: Into<String>>(mut self, dasharray: S) -> Self { | |
self.stroke_dasharray = Some(dasharray.into()); | |
self | |
} | |
pub fn fill(mut self, color: Color) -> Self { | |
self.fill = Some(Fill::Solid(color)); | |
self | |
} | |
pub fn fill_gradient(mut self, angle: i32, colors: Vec<Color>) -> Self { | |
self.fill = Some(Fill::Gradient(Gradient { angle, colors })); | |
self | |
} | |
pub fn fill_opacity(mut self, opacity: f64) -> Self { | |
self.fill_opacity = Some(opacity); | |
self | |
} | |
pub fn zoom_to_fit(mut self) -> Self { | |
self.zoom_to_fit = Some(true); | |
self | |
} | |
pub fn tooltip<S: Into<String>>(mut self, value: S) -> Self { | |
self.tooltip = Some(value.into()); | |
self | |
} | |
pub fn tooltip_open(mut self, value: bool) -> Self { | |
self.tooltip_open = Some(value); | |
self | |
} | |
pub fn on_click<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_click = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
pub fn on_mouseover<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_mouseover = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
pub fn on_mouseout<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_mouseout = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
} | |
impl<Ms> Feature<Ms> for Circle<Ms> | |
where | |
Ms: 'static, | |
{ | |
fn into_node(&self) -> Node<Ms> { | |
custom![ | |
Tag::from("leafy-circle"), | |
attrs! { | |
At::from("lat") => self.lat, | |
At::from("lng") => self.lng, | |
At::from("radius") => self.radius, | |
At::from("stroke") => self.stroke.as_at_value(), | |
At::from("stroke-width") => self.stroke_width.as_at_value(), | |
At::from("stroke-dasharray") => self.stroke_dasharray.as_at_value(), | |
At::from("fill") => self.fill.as_ref().as_at_value(), | |
At::from("fill-opacity") => self.fill_opacity.as_at_value(), | |
At::from("zoom-to-fit") => self.zoom_to_fit.as_at_value(), | |
At::from("tooltip") => self.tooltip.as_ref().as_at_value(), | |
At::from("tooltip-open") => self.tooltip_open.as_ref().as_at_value(), | |
}, | |
self.on_click | |
.clone() | |
.map(|handler| ev(Ev::Click, move |_| handler())), | |
self.on_mouseover | |
.clone() | |
.map(|handler| ev(Ev::MouseOver, move |_| handler())), | |
self.on_mouseout | |
.clone() | |
.map(|handler| ev(Ev::MouseOut, move |_| handler())), | |
] | |
} | |
} | |
// Geojson | |
pub struct Geojson<Ms> { | |
data: String, | |
key: Option<String>, | |
stroke: Option<Color>, | |
stroke_width: Option<i32>, | |
stroke_dasharray: Option<String>, | |
fill: Option<Fill>, | |
fill_opacity: Option<f64>, | |
zoom_to_fit: Option<bool>, | |
tooltip: Option<String>, | |
tooltip_open: Option<bool>, | |
on_click: Option<Rc<dyn Fn() -> Ms>>, | |
on_mouseover: Option<Rc<dyn Fn() -> Ms>>, | |
on_mouseout: Option<Rc<dyn Fn() -> Ms>>, | |
} | |
pub fn geojson<Ms, S: Into<String>>(data: S) -> Geojson<Ms> { | |
Geojson { | |
data: data.into(), | |
key: None, | |
stroke: None, | |
stroke_width: None, | |
stroke_dasharray: None, | |
fill: None, | |
fill_opacity: None, | |
zoom_to_fit: None, | |
tooltip: None, | |
tooltip_open: None, | |
on_click: None, | |
on_mouseover: None, | |
on_mouseout: None, | |
} | |
} | |
impl<Ms> Geojson<Ms> { | |
pub fn key(mut self, key: impl Into<String>) -> Self { | |
self.key = Some(key.into()); | |
self | |
} | |
pub fn stroke(mut self, color: Color) -> Self { | |
self.stroke = Some(color); | |
self | |
} | |
pub fn stroke_width(mut self, size: i32) -> Self { | |
self.stroke_width = Some(size); | |
self | |
} | |
pub fn stroke_dasharray<S: Into<String>>(mut self, dasharray: S) -> Self { | |
self.stroke_dasharray = Some(dasharray.into()); | |
self | |
} | |
pub fn fill(mut self, color: Color) -> Self { | |
self.fill = Some(Fill::Solid(color)); | |
self | |
} | |
pub fn fill_gradient(mut self, angle: i32, colors: Vec<Color>) -> Self { | |
self.fill = Some(Fill::Gradient(Gradient { angle, colors })); | |
self | |
} | |
pub fn fill_opacity(mut self, opacity: f64) -> Self { | |
self.fill_opacity = Some(opacity); | |
self | |
} | |
pub fn zoom_to_fit(mut self) -> Self { | |
self.zoom_to_fit = Some(true); | |
self | |
} | |
pub fn tooltip<S: Into<String>>(mut self, value: S) -> Self { | |
self.tooltip = Some(value.into()); | |
self | |
} | |
pub fn tooltip_open(mut self, value: bool) -> Self { | |
self.tooltip_open = Some(value); | |
self | |
} | |
pub fn on_click<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_click = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
pub fn on_mouseover<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_mouseover = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
pub fn on_mouseout<F>(mut self, handler: F) -> Self | |
where | |
F: FnOnce() -> Ms + Clone + 'static, | |
{ | |
self.on_mouseout = Some(Rc::new(move || handler.clone()())); | |
self | |
} | |
} | |
impl<Ms> Feature<Ms> for Geojson<Ms> | |
where | |
Ms: 'static, | |
{ | |
fn into_node(&self) -> Node<Ms> { | |
custom![ | |
Tag::from("leafy-geojson"), | |
self.key.as_ref().map(el_key), | |
attrs! { | |
At::from("data") => self.data, | |
At::from("stroke") => self.stroke.as_at_value(), | |
At::from("stroke-width") => self.stroke_width.as_at_value(), | |
At::from("stroke-dasharray") => self.stroke_dasharray.as_at_value(), | |
At::from("fill") => self.fill.as_ref().as_at_value(), | |
At::from("fill-opacity") => self.fill_opacity.as_at_value(), | |
At::from("zoom-to-fit") => self.zoom_to_fit.as_at_value(), | |
At::from("tooltip") => self.tooltip.as_ref().as_at_value(), | |
At::from("tooltip-open") => self.tooltip_open.as_ref().as_at_value(), | |
}, | |
self.on_click | |
.clone() | |
.map(|handler| ev(Ev::Click, move |_| handler())), | |
self.on_mouseover | |
.clone() | |
.map(|handler| ev(Ev::MouseOver, move |_| handler())), | |
self.on_mouseout | |
.clone() | |
.map(|handler| ev(Ev::MouseOut, move |_| handler())), | |
] | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment