Skip to content

Commit ac4f076

Browse files
author
Nicolò Maria Mezzopera
authored
Merge pull request #268 from byWulf/master
Added Icon component to easily add dynamic composed marker icons
2 parents cd85b64 + 0ba0035 commit ac4f076

File tree

5 files changed

+364
-4
lines changed

5 files changed

+364
-4
lines changed

examples/src/App.vue

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,14 @@
2727
@click="currentView='custom-url-params'">Custom Url Params</a>
2828
</li>
2929
<li>
30-
<a href="#" @click="currentView='custom-control'">Custom Leaflet Control</a>
30+
<a
31+
href="#"
32+
@click="currentView='custom-control'">Custom Leaflet Control</a>
3133
</li>
3234
<li>
33-
<a href="#" @click="currentView='set-bounds'">Set bounds</a>
35+
<a
36+
href="#"
37+
@click="currentView='set-bounds'">Set bounds</a>
3438
</li>
3539
<li>
3640
<a
@@ -77,6 +81,11 @@
7781
href="#"
7882
@click="currentView='stress'">Load Test</a>
7983
</li>
84+
<li>
85+
<a
86+
href="#"
87+
@click="currentView='icon'">Custom Marker Icons</a>
88+
</li>
8089
</ul>
8190
<component
8291
id="full_div"
@@ -102,6 +111,7 @@ import Simple from './components/Simple';
102111
import WMSLayers from './components/WMSLayers';
103112
import WorldCopyJump from './components/WorldCopyJump';
104113
import LoadTest from './components/LoadTest';
114+
import Icon from './components/Icon';
105115
106116
export default {
107117
name: 'App',
@@ -121,7 +131,8 @@ export default {
121131
'geo-json2': GeoJSON2,
122132
'wms-layers': WMSLayers,
123133
'crs': CRSAndImageOverlay,
124-
stress: LoadTest
134+
stress: LoadTest,
135+
Icon
125136
},
126137
data () {
127138
return {

examples/src/components/Icon.vue

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<template>
2+
<div>
3+
<div style="height: 20%; overflow: auto;">
4+
<h3>Custom Marker Icons</h3>
5+
<label for="iconSize">Icon size:</label>
6+
<input
7+
id="iconSize"
8+
v-model="iconSize"
9+
type="range"
10+
min="1"
11+
max="200"
12+
value="64">
13+
<label for="customTextInput">Custom text: </label>
14+
<input
15+
id="customTextInput"
16+
v-model="customText"
17+
type="text">
18+
</div>
19+
<l-map
20+
:zoom="zoom"
21+
:center="center"
22+
style="height: 80%">
23+
<l-tile-layer
24+
:url="url"
25+
:attribution="attribution"/>
26+
27+
<!-- Use default icon -->
28+
<l-marker :lat-lng="[47.413220, -1.219482]" />
29+
30+
<!-- Use icon given in icon property -->
31+
<l-marker
32+
:lat-lng="[47.413220, -1.209482]"
33+
:icon="icon" />
34+
35+
<!-- Create image icon (L.icon) from l-icon tag -->
36+
<l-marker :lat-lng="[47.413220, -1.199482]">
37+
<l-icon
38+
:icon-size="dynamicSize"
39+
:icon-anchor="dynamicAnchor"
40+
icon-url="static/images/baseball-marker.png" />
41+
</l-marker>
42+
43+
<!-- Create HTML icon (L.divIcon) by providing content inside the l-icon tag -->
44+
<l-marker :lat-lng="[47.413220, -1.189482]">
45+
<l-icon
46+
:icon-anchor="staticAnchor"
47+
class-name="someExtraClass">
48+
<div class="headline">{{ customText }}</div>
49+
<img src="static/images/layers.png">
50+
</l-icon>
51+
</l-marker>
52+
53+
</l-map>
54+
</div>
55+
</template>
56+
57+
<script>
58+
import { LMap, LTileLayer, LMarker, LIcon } from 'vue2-leaflet';
59+
60+
export default {
61+
name: 'Icon',
62+
components: {
63+
LMap,
64+
LTileLayer,
65+
LMarker,
66+
LIcon
67+
},
68+
data () {
69+
return {
70+
zoom: 13,
71+
center: L.latLng(47.413220, -1.219482),
72+
url: 'http://{s}.tile.osm.org/{z}/{x}/{y}.png',
73+
attribution: '&copy; <a href="http://osm.org/copyright">OpenStreetMap</a> contributors',
74+
75+
icon: L.icon({
76+
iconUrl: 'static/images/baseball-marker.png',
77+
iconSize: [32, 37],
78+
iconAnchor: [16, 37]
79+
}),
80+
staticAnchor: [16, 37],
81+
customText: 'Foobar',
82+
iconSize: 64
83+
};
84+
},
85+
computed: {
86+
dynamicSize () {
87+
return [this.iconSize, this.iconSize * 1.15];
88+
},
89+
dynamicAnchor () {
90+
return [this.iconSize / 2, this.iconSize * 1.15];
91+
}
92+
},
93+
methods: {
94+
}
95+
};
96+
</script>
97+
98+
<style>
99+
.someExtraClass {
100+
background-color: aqua;
101+
padding: 10px;
102+
border: 1px solid #333;
103+
border-radius: 0 20px 20px 20px;
104+
box-shadow: 5px 3px 10px rgba(0,0,0,0.2);
105+
text-align: center;
106+
width: auto !important;
107+
height: auto !important;
108+
margin: 0 !important;
109+
}
110+
</style>

src/components/LIcon.vue

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
<template>
2+
<div>
3+
<slot/>
4+
</div>
5+
</template>
6+
7+
<script>
8+
import propsBinder from '../utils/propsBinder.js';
9+
import findRealParent from '../utils/findRealParent.js';
10+
import { optionsMerger } from '../utils/optionsUtils.js';
11+
12+
export default {
13+
name: 'LIcon',
14+
props: {
15+
iconUrl: {
16+
type: String,
17+
custom: true,
18+
default: null
19+
},
20+
iconRetinaUrl: {
21+
type: String,
22+
custom: true,
23+
default: null
24+
},
25+
iconSize: {
26+
type: [Object, Array],
27+
custom: true,
28+
default: null
29+
},
30+
iconAnchor: {
31+
type: [Object, Array],
32+
custom: true,
33+
default: null
34+
},
35+
popupAnchor: {
36+
type: [Object, Array],
37+
custom: true,
38+
default: () => [0, 0]
39+
},
40+
tooltipAnchor: {
41+
type: [Object, Array],
42+
custom: true,
43+
default: () => [0, 0]
44+
},
45+
shadowUrl: {
46+
type: String,
47+
custom: true,
48+
default: null
49+
},
50+
shadowRetinaUrl: {
51+
type: String,
52+
custom: true,
53+
default: null
54+
},
55+
shadowSize: {
56+
type: [Object, Array],
57+
custom: true,
58+
default: null
59+
},
60+
shadowAnchor: {
61+
type: [Object, Array],
62+
custom: true,
63+
default: null
64+
},
65+
bgPos: {
66+
type: [Object, Array],
67+
custom: true,
68+
default: () => [0, 0]
69+
},
70+
className: {
71+
type: String,
72+
custom: true,
73+
default: ''
74+
},
75+
options: {
76+
type: Object,
77+
custom: true,
78+
default: () => ({})
79+
}
80+
},
81+
82+
data () {
83+
return {
84+
parentContainer: null,
85+
observer: null,
86+
recreationNeeded: false,
87+
swapHtmlNeeded: false
88+
};
89+
},
90+
91+
mounted () {
92+
this.parentContainer = findRealParent(this.$parent);
93+
94+
propsBinder(this, null, this.$options.props);
95+
96+
this.observer = new MutationObserver(() => {
97+
this.scheduleHtmlSwap();
98+
});
99+
this.observer.observe(
100+
this.$el,
101+
{ attributes: true, childList: true, characterData: true, subtree: true }
102+
);
103+
104+
this.scheduleCreateIcon();
105+
},
106+
107+
beforeDestroy () {
108+
if (this.parentContainer.mapObject) {
109+
this.parentContainer.mapObject.setIcon(null);
110+
}
111+
112+
this.observer.disconnect();
113+
},
114+
115+
methods: {
116+
scheduleCreateIcon () {
117+
this.recreationNeeded = true;
118+
119+
this.$nextTick(this.createIcon);
120+
},
121+
122+
scheduleHtmlSwap () {
123+
this.htmlSwapNeeded = true;
124+
125+
this.$nextTick(this.createIcon);
126+
},
127+
128+
createIcon () {
129+
// If only html of a divIcon changed, we can just replace the DOM without the need of recreating the whole icon
130+
if (this.htmlSwapNeeded && !this.recreationNeeded && this.iconObject) {
131+
this.parentContainer.mapObject.getElement().innerHTML = this.$el.innerHTML;
132+
133+
this.htmlSwapNeeded = false;
134+
return;
135+
}
136+
137+
if (!this.recreationNeeded) {
138+
return;
139+
}
140+
141+
if (this.iconObject) {
142+
L.DomEvent.off(this.iconObject, this.$listeners);
143+
}
144+
145+
const options = optionsMerger({
146+
iconUrl: this.iconUrl,
147+
iconRetinaUrl: this.iconRetinaUrl,
148+
iconSize: this.iconSize,
149+
iconAnchor: this.iconAnchor,
150+
popupAnchor: this.popupAnchor,
151+
tooltipAnchor: this.tooltipAnchor,
152+
shadowUrl: this.shadowUrl,
153+
shadowRetinaUrl: this.shadowRetinaUrl,
154+
shadowSize: this.shadowSize,
155+
shadowAnchor: this.shadowAnchor,
156+
bgPos: this.bgPos,
157+
className: this.className,
158+
html: this.$el.innerHTML || this.html
159+
}, this);
160+
161+
if (options.html) {
162+
this.iconObject = L.divIcon(options);
163+
} else {
164+
this.iconObject = L.icon(options);
165+
}
166+
167+
L.DomEvent.on(this.iconObject, this.$listeners);
168+
169+
this.parentContainer.mapObject.setIcon(this.iconObject);
170+
171+
this.recreationNeeded = false;
172+
this.htmlSwapNeeded = false;
173+
},
174+
175+
setIconUrl () {
176+
this.scheduleCreateIcon();
177+
},
178+
setIconRetinaUrl () {
179+
this.scheduleCreateIcon();
180+
},
181+
setIconSize () {
182+
this.scheduleCreateIcon();
183+
},
184+
setIconAnchor () {
185+
this.scheduleCreateIcon();
186+
},
187+
setPopupAnchor () {
188+
this.scheduleCreateIcon();
189+
},
190+
setTooltipAnchor () {
191+
this.scheduleCreateIcon();
192+
},
193+
setShadowUrl () {
194+
this.scheduleCreateIcon();
195+
},
196+
setShadowRetinaUrl () {
197+
this.scheduleCreateIcon();
198+
},
199+
setShadowAnchor () {
200+
this.scheduleCreateIcon();
201+
},
202+
setBgPos () {
203+
this.scheduleCreateIcon();
204+
},
205+
setClassName () {
206+
this.scheduleCreateIcon();
207+
},
208+
setHtml () {
209+
this.scheduleCreateIcon();
210+
}
211+
},
212+
213+
render () {
214+
return null;
215+
}
216+
};
217+
</script>

src/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ exports.LControlScale = require('./components/LControlScale').default;
1010
exports.LControlZoom = require('./components/LControlZoom').default;
1111
exports.LFeatureGroup = require('./components/LFeatureGroup').default;
1212
exports.LGeoJson = require('./components/LGeoJson').default;
13+
exports.LIcon = require('./components/LIcon').default;
1314
exports.LIconDefault = require('./components/LIconDefault').default;
1415
exports.LImageOverlay = require('./components/LImageOverlay').default;
1516
exports.LLayerGroup = require('./components/LLayerGroup').default;

0 commit comments

Comments
 (0)