How to create a legend from geojson data with react-leaflet - legend

I've got some data represented in my map and I want to create a legend that has the same image of my data and name.
My data are Geojson files.
My map looks like this :
Map without Legend
I want to develop a legend like this :
Map with Legend
This is my code that represents data to the Map :
import React from "react";
import L from "leaflet";
import {
MapContainer,
TileLayer,
GeoJSON,
Marker,
Popup,
LayersControl,
} from "react-leaflet";
import noeud from "./../data/Noeud.json";
import section from "./../data/section.json";
import casier from "./../data/casier.json";
function Macarte() {
const noeud_icon = L.icon({
iconUrl: noeud_image,
iconSize: [10, 10],
});
function casier_style() {
return {
fillColor: "transparent",
color: "red",
};
}
function section_style() {
return {
color: "black",
};
}
return (
<MapContainer className="map" center={[45.7133, 5.52826]} zoom={11}>
<LayersControl position="topright" />
<TileLayer
attribution='© OpenStreetMap contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
{noeud.features.map((data_noeud) => (
<Marker
icon={noeud_icon}
position={{
lat: data_noeud.geometry.coordinates[1],
lng: data_noeud.geometry.coordinates[0],
}}
>
<Popup>
<div>
{/*<h1> {"Nom modèle : " + data_noeud.properties.Nom_modele} </h1>*/}
<h1> {"Noeud : " + data_noeud.properties.NOM} </h1>
</div>
</Popup>
</Marker>
))}
<GeoJSON
style={section_style}
data={section.features}
/>
<GeoJSON
style={casier_style}
data={casier.features}
/>
</MapContainer>
);
}
export default Macarte;
Please Help :)

A similar question has been answered here.
Legend component
function Legend({ map }) {
console.log(map);
useEffect(() => {
if (map) {
const legend = L.control({ position: "bottomright" });
legend.onAdd = () => {
const div = L.DomUtil.create("div", "legend");
div.innerHTML =
"<h4>Legend</h4>"
return div;
};
legend.addTo(map);
}
}, [map]); //here add map
return null;
}
in a .css file you can establish rules for the legend then import it into your legend component.
.legend {
padding: 6px 8px;
font: 14px Arial, Helvetica, sans-serif;
background: rgb(255, 255, 255);
line-height: 24px;
color: rgb(0,0,0)
}
.legend h4 {
text-align: center;
font-size: 16px;
margin: 2px 12px 8px;
color: rgb(0,0,0)
}
Then you can simply put the Legend component wherever you want.

Related

Reinitialize matterjs after page changes in nuxtjs

I have a matterjs instance in my nuxt app that drops items on the floor. Everything works when I visit the page for the first time or do a page refresh. But when I change the pages (routes) inside my app, so I come back to the page with the matterjs instance, the instance is gone. I always have to do a page refresh...
How can I reinitialize matterjs?
Fallbox
<section class="fallbox">
<div class="fallbox-content">
<nuxt-link to="/"><h1>Index</h1></nuxt-link>
</div>
<div class="fallbox-scene">
<div v-for="item in items" :key="item.className">
<span :class="item.className" class="item"></span>
</div>
</div>
</section>
export default {
data() {
return {
items: [
{
className: "-i1",
},
{
className: "-i2",
},
{
className: "-i3",
},
{
className: "-i4",
},
{
className: "-i5",
},
],
};
},
mounted() {
window.addEventListener("DOMContentLoaded", () => {
this.startFallbox();
});
},
methods: {
startFallbox() {
const Engine = Matter.Engine;
const Render = Matter.Render;
const Runner = Matter.Runner;
const Bodies = Matter.Bodies;
const Body = Matter.Body;
const Composite = Matter.Composite;
const MouseConstraint = Matter.MouseConstraint;
const engine = Engine.create();
const world = engine.world;
engine.gravity.y = 1;
const fallbox = document.querySelector(".fallbox-scene");
const render = Render.create({
element: fallbox,
engine,
options: {
width: fallbox.offsetWidth,
height: fallbox.offsetHeight,
},
});
// Render.run(render);
const runner = Runner.create();
Runner.run(runner, engine);
const itemArray = this.items;
itemArray.forEach((i) => {
const get = document.getElementsByClassName(i.className)[0];
get.style.opacity = 1;
const item = {
w: get.clientWidth,
h: get.clientHeight,
body: Bodies.rectangle(
Math.random() * window.innerWidth,
Math.random() * -1000,
get.clientWidth,
get.clientHeight,
{
restitution: 0.5,
angle: Math.random() * 360,
}
),
elem: get,
render() {
const { x, y } = this.body.position;
this.elem.style.top = `${y - this.h / 2}px`;
this.elem.style.left = `${x - this.w / 2}px`;
this.elem.style.transform = `rotate(${this.body.angle}rad)`;
},
};
Body.rotate(item.body, Math.random() * 360);
Composite.add(world, [item.body]);
(function rerender() {
item.render();
requestAnimationFrame(rerender);
})();
});
const ground = Bodies.rectangle(
fallbox.offsetWidth / 2,
fallbox.offsetHeight,
2000,
1,
{
isStatic: true,
}
);
const left = Bodies.rectangle(
0,
fallbox.offsetHeight / 2,
1,
fallbox.offsetHeight,
{
isStatic: true,
}
);
const right = Bodies.rectangle(
fallbox.offsetWidth,
fallbox.offsetHeight / 2,
1,
fallbox.offsetHeight,
{
isStatic: true,
}
);
Composite.add(world, [ground, left, right]);
const mouseConstraint = MouseConstraint.create(engine, {
element: fallbox,
constraint: {
stiffness: 0.2,
},
});
mouseConstraint.mouse.element.removeEventListener(
"mousewheel",
mouseConstraint.mouse.mousewheel
);
mouseConstraint.mouse.element.removeEventListener(
"DOMMouseScroll",
mouseConstraint.mouse.mousewheel
);
Composite.add(world, mouseConstraint);
Render.lookAt(render, {
min: { x: 0, y: 0 },
max: { x: fallbox.offsetWidth, y: fallbox.offsetHeight },
});
},
},
};
.fallbox {
position: relative;
height: 100vh;
width: 100%;
margin: auto;
background: black;
overflow: hidden;
.fallbox-content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 95%;
max-width: 1050px;
z-index: 2;
// -khtml-user-select: none;
// -moz-user-select: none;
// -ms-user-select: none;
// user-select: none;
// pointer-events: none;
h1 {
font-size: 160px;
font-weight: 500;
line-height: 140px;
margin-bottom: 150px;
text-align: center;
color: white;
}
}
.fallbox-scene {
height: 100%;
width: 100%;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
contain: strict;
.item {
height: 120px;
width: 120px;
background: red;
position: absolute;
opacity: 0;
user-select: none;
will-change: transform;
-khtml-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
}
}
}
activated() {
this.startFallbox()
}
Use the activated hook to restart the animation. The code when mounted() is still needed.
I assume the problem is that when you navigate away and you some back the initial animation is finished, but as Nuxt caches some pages they are served from cache and therefore the mounted() hook is not called. This is where the activated() hooks comes in. It's called when a page/component was keept alive and is reactivated.

Vue 3 cli-service app: "Slot "default" invoked outside of the render function" warning when component with slots is imported from other component

MCVE
I have a Tabpane component that takes slots as input. When imported from the template it works as expected.
<Tabpane>
<div caption="I am div 1">Div 1</div>
<div caption="I am div 2">Div 2</div>
</Tabpane>
However when imported from an other component ( Composite in the example ), then it triggers the following warning:
Slot "default" invoked outside of the render function:
this will not track dependencies used in the slot. Invoke the slot function inside the render function instead.
// src/components/Composite.js
import { defineComponent, h } from "vue";
import Tabpane from "./Tabpane.vue";
export default defineComponent({
name: "Composite",
setup() {
const slots = [
h("div", { caption: "I am div 1" }, ["Div 1"]),
h("div", { caption: "I am div 2" }, ["Div 2"])
];
return () => h(Tabpane, {}, () => slots);
}
});
Solved.
The problem was that I called slots.default() from within setup, but not within the returned render function.
Also this component reflected a very beginner approach to reactivity. By now I know better. The old problematic solution is still there in src/components/Tabpane.vue.
The right solution that triggers no warning is:
// src/components/Tabpane2.vue
<script>
import { defineComponent, h, reactive } from "vue";
export default defineComponent({
name: "Tabpane2",
props: {
width: {
type: Number,
default: 400,
},
height: {
type: Number,
default: 200,
},
},
setup(props, { slots }) {
const react = reactive({
selectedTab: 0,
});
return () =>
h("div", { class: ["vertcont"] }, [
h(
"div",
{
class: ["tabs"],
},
slots.default().map((tab, i) =>
h(
"div",
{
class: {
tab: true,
selected: i === react.selectedTab,
},
onClick: () => {
react.selectedTab = i;
},
},
[tab.props.caption]
)
)
),
h(
"div",
{
class: ["slotscont"],
style: {
width: `${props.width}px`,
height: `${props.height}px`,
},
},
slots.default().map((slot, i) =>
h(
"div",
{
class: {
slot: true,
active: react.selectedTab === i,
},
},
[slot]
)
)
),
]);
},
});
</script>
<style>
.tab.selected {
background-color: #efe;
border: solid 2px #afa !important;
border-bottom: transparent !important;
}
.tab {
background-color: #eee;
}
.tabs .tab {
padding: 5px;
margin: 2px;
border: solid 2px #aaa;
border-radius: 8px;
border-bottom: transparent;
cursor: pointer;
user-select: none;
transition: all 0.5s;
color: #007;
}
.tabs {
display: flex;
align-items: center;
margin-left: 5px;
}
.vertcont {
display: flex;
flex-direction: column;
margin: 3px;
}
.slotscont {
position: relative;
overflow: scroll;
padding: 5px;
border: solid 1px #777;
}
.slot {
visibility: hidden;
position: absolute;
}
.slot.active {
visibility: visible;
}
</style>
Slots need to be invoked within the render function and or the <template> box to ensure they keep their reactivity.
A full explanation can be found in this post: https://zelig880.com/how-to-fix-slot-invoked-outside-of-the-render-function-in-vue-3

Flickity Slider Animation and Gsap

The slider I am building have the active slider bigger than the others. I managed to make it work without the animation with flkty.reposition(). However, I am trying now to add the animation where the next slide grows in and the active decrease out. For The animation I am using GSAP.
The issue I am facing is to overwrite the left property with gsap so that it continuous animate. As of now, the left property (controlled by Flickity) does not take into account the final size (controlled by GSAP) of the selected slide.
https://codepen.io/stefanomonteiro/pen/VwzwjLw?editors=0010
As the left property of each slide is controlled by Flickity, we could use margin-left with a minus value as an alternative property to pull the selected slide to the left. I know margin is not a good property to animate but it works in this case without digging too deep into the Flickity core.
Here is the GSAP code:
gsap.to(slides, {
duration: 1,
width: "220px",
height: "336px"
});
gsap.to(selectedSlide, {
duration: 1,
marginLeft: "-248px", // the empty space calculated by newWidth - oldWidth
width: "468px",
height: "630px",
onComplete: () => {
// once all animations have been settled, we reset the margin
gsap.set(selectedSlide, { marginLeft: "" });
// and tell Flickity to update
flkty.resize();
flkty.reposition();
}
});
And the snippets
const animate = () => {
const flkty = Flickity.data(".carousel");
const selectedSlide = flkty.selectedElement;
const slides = flkty.getCellElements();
// remove the selected slides
slides.splice(flkty.selectedIndex, 1);
gsap.to(slides, {
duration: 1,
width: "220px",
height: "336px"
});
gsap.to(selectedSlide, {
duration: 1,
marginLeft: "-248px", // the empty space calculated by newWidth - oldWidth
width: "468px",
height: "630px",
onComplete: () => {
// once all animations have been settled, we reset the margin
gsap.set(selectedSlide, {
marginLeft: ""
});
// and tell Flickity to update
flkty.resize();
flkty.reposition();
}
});
};
new Flickity(".carousel", {
cellAlign: "right",
wrapAround: true,
percentPosition: false,
on: {
ready: () => animate()
}
});
const nextButton = document.querySelector(".flickity-button.next");
nextButton.addEventListener("click", () => animate());
/* external css: flickity.css */
* {
box-sizing: border-box;
}
body {
font-family: sans-serif;
}
.carousel {
background: #EEE;
}
.carousel-cell {
width: 220px;
height: 336px;
margin-right: 20px;
background: #8C8;
border-radius: 5px;
counter-increment: carousel-cell;
}
.carousel-cell.is-selected {
width: 468px;
height: 630px;
z-index: 1;
}
/* cell number */
.carousel-cell:before {
display: block;
text-align: center;
content: counter(carousel-cell);
line-height: 200px;
font-size: 80px;
color: white;
}
<link href="https://npmcdn.com/flickity#2/dist/flickity.css" rel="stylesheet" />
<script src="https://unpkg.co/gsap#3/dist/gsap.min.js"></script>
<script src="https://npmcdn.com/flickity#2/dist/flickity.pkgd.js"></script>
<h1>Flickity - wrapAround</h1>
<!-- Flickity HTML init -->
<div class="carousel">
<div class="carousel-cell"></div>
<div class="carousel-cell"></div>
<div class="carousel-cell"></div>
<div class="carousel-cell"></div>
<div class="carousel-cell"></div>
</div>
And the Codepen
You can also notice that we have to wait until the animation is finished until we perform the next click, otherwise, it would mess up the whole process. This is predictable. Hence, I personally will try not to manipulate this Flickity slider for this kind of animation. Just want to give you a solution, anyway.

Vue 3 Composition API component doesn't work reactivate class

I have a button and I just want it animate on a click - make as it been pressed. It works if I make optional component as below:
<template>
<button class="button" :class="{'shadow__click': classButton}" #click="buttonClass">
Tell me already!
</button>
</template>
<script>
export default {
data(){
return {
classButton : false
}
},
methods: {
buttonClass(){
this.classButton = true;
setTimeout(() => {
this.classButton = false
}, 1000)
}
}
}
</script>
<style lang="less">
.button{
cursor: pointer;
padding: 12px 24px;
position: relative;
border-radius: 5px;
background-color: #38b2ac;
border: none;
color: #fff8e1;
}
.shadow__click{
animation: click 1s
}
#keyframes click {
0% {
top: 0;
left: 0;
}
25% {
top: 5px;
left: 1px;
}
50% {
top: 10px;
left: 3px;
}
75% {
top: 5px;
left: 1px;
}
100% {
top: 0;
left: 0;
}
}
</style>
but it doesn't want to work when I do Composition way and I don't see a problem but it simply doesn't work (( I console.loged function and it goes to function changes the value of a variable, but class is not applying. Is it a Vue 3 bug?
<script>
import { ref } from "vue"
setup(){
let classButton = ref(false);
function buttonClass(){
classButton = true;
setTimeout(() => {
classButton = false
}, 1000)
}
return { classButton, buttonClass}
}
</script>
You should mutate the ref using the value field :
let classButton = ref(false);
function buttonClass(){
classButton.value = true;
setTimeout(() => {
classButton.value = false
}, 1000)
}

Is it possible to call a component method in nuxt from a page?

I'm starting to work with Vue.js and I'm using Nuxt.js.
I've created a component (a snackbar) and inside this component I created a method "showSnackbar" that works passing 2 parameters: color and text.
So when I call showSnackbar(color,text), it appears.
But, I want to call this method from a page. Because I want to use this snackbar in some pages and I don't want to write the same code all the time, so that's the reason why I decided to create a component. But I can't call from a page the method inside this component.
And that's why I wonder if is it that possible to call a component method from a page (where of course I import the component)
There might be a couple of ways to do it, I would create a plugin.
Then you have both <snackbar/> component for placement and a global API to call the invoking method this.$snackbar.open({someOptions: '...'})
For example:
Create a folder in ./plugins/snackbar and place the following in:
./plugins/snackbar/index.js
import Vue from "vue";
import snackbar from "~/plugins/snackbar/snackbar";
Vue.use(snackbar);
This is for nuxt.config.js, to load globally. Which looks like:
...
/*
** Plugins to load before mounting the App
** Doc: https://nuxtjs.org/guide/plugins
*/
plugins: ["~/plugins/snackbar/index.js"],
...
ok, now create
./plugins/snackbar/snackbar.js
This is the plugin which holds state for the component and acts as an event proxy
import snackbar from "~/plugins/snackbar/snackbar.vue";
const Plugin = {
install(Vue, options = {}) {
/**
* Makes sure that plugin can be installed only once
*/
if (this.installed) {
return;
}
this.installed = true;
/**
* Create event bus
*/
this.event = new Vue();
/**
* Plugin methods
*/
Vue.prototype.$snackbar = {
show(options = {}) {
Plugin.event.$emit("show", options, true);
}
};
/**
* Registration of <snackbar/> component
*/
Vue.component("snackbar", snackbar);
}
};
export default Plugin;
and now...
./plugins/snackbar/snackbar.vue
Where the magic happens...
<template>
<div>
<transition name="snackbar">
<div v-if="show" :class="['snackbar', 'box-shadow', type]">
<slot>{{ options.text }}</slot>
</div>
</transition>
<pre>options: {{ options }}</pre>
<pre>show: {{ show }}</pre>
<pre>type: {{ type }}</pre>
</div>
</template>
<script>
import snackbar from "~/plugins/snackbar/snackbar";
export default {
data: () => ({
options: {
text: "",
type: ""
},
show: false,
type: "",
timer: 0
}),
beforeMount() {
snackbar.event.$on("show", options => {
this.options = options;
this.type = options.type;
this.show = true;
this.close(this.options.closeWait || 3000);
});
},
methods: {
close(timeout) {
clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.show = false;
}, timeout);
}
}
};
</script>
<style>
.snackbar {
min-width: 300px;
margin-left: -150px;
background-color: #F48024;
color: #fff;
text-align: center;
border-radius: 5px;
padding: 16px;
position: fixed;
z-index: 1;
left: 50%;
bottom: 30px;
}
.snackbar.success {
background-color: rgb(71, 244, 36);
}
.snackbar.danger {
background-color: rgb(244, 36, 47);
}
.snackbar-enter-active {
animation: snackbar-in 0.8s;
}
.snackbar-leave-active {
animation: snackbar-in 0.8s reverse;
}
#keyframes snackbar-in {
0% {
transform: scale(0);
}
50% {
transform: scale(1.2);
}
100% {
transform: scale(1);
}
}
.box-shadow {
-webkit-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3),
0 0 40px rgba(0, 0, 0, 0.1) inset;
-moz-box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3),
0 0 40px rgba(0, 0, 0, 0.1) inset;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3), 0 0 40px rgba(0, 0, 0, 0.1) inset;
}
</style>
Then within any component/pages which uses it, you can place with <snackbar/>, and call the methods like:
this.$snackbar.show({
text: "Hello, snackbar!",
type: "success"
});
A working example of the above can be found here https://codesandbox.io/s/codesandbox-nuxt-oeo4h