Is there a way to set cursor to Konvajs transformer anchor - konvajs

I want to set cursor to anchor on mouse enter event.
I tried the following code, but without success
const tr = new Konva.Transformer({
nodes: [shape]
})
tr.update = function() {
Konva.Transformer.prototype.update.call(tr);
var rot = this.findOne('.rotater');
rot.on("mouseenter", () => {
stage.container().style.cursor = 'move';
})
rot.on('mouseleave', () => {
stage.container().style.cursor = 'default';
})
}
tr.forceUpdate();

const stage = new Konva.Stage({
container: 'container',
width: window.innerWidth,
height: window.innerHeight
});
const layer = new Konva.Layer();
stage.add(layer);
const shape = new Konva.Circle({
x: stage.width() / 2,
y: stage.height() / 2,
radius: 50,
fill: 'green'
});
layer.add(shape);
const tr = new Konva.Transformer({
nodes: [shape]
})
layer.add(tr);
tr.findOne('.rotater').on('mouseenter', () => {
// "content" property is not documented and private
// but for now you can use it
// it is element where transformer is applying its styles
stage.content.style.cursor = 'move';
});
<script src="https://unpkg.com/konva#^8/konva.min.js"></script>
<div id="container"></div>

Related

Are there any issues when using Konva on iOS mobile devices?

I am using a Konva stage to hover over floor areas on browser (see running Next app here https://www.planpoint.io/themes/modern-1) and it works great on any device, except on iPhones. After some touches on the floors areas, they suddenly dissapear as if the canvas element would no longer be there. Has tested it with Safari and Chrome on iOS16.2 . The only difference for mobile devices is the use of touchstart, touchend events, instead of mouseover, mouseout events.
Here is the code for rendering the canvas component and background image
import { useEffect, useState, useRef, useLayoutEffect } from 'react'
import Image from 'next/image'
import Konva from 'konva';
import i18nService from '../../../helpers/i18nService'
import useWindowSize from '../../../hooks/useWindowSize'
export default function InteractiveCanvas(props) {
const [showCanvas, setShowCanvas] = useState(false);
const [placeholderWidth, setPlaceholderWidth] = useState(1);
const [placeholderHeight, setPlaceholderHeight] = useState(1);
const [width, height] = useWindowSize();
const canvasAreaRef = useRef(null)
const canvasPlaceholderRef = useRef()
let stage, shapesLayer, tooltipLayer;
useEffect(() => {
if (showCanvas) {
props.loaded() // Canvas has loaded
}
},[showCanvas])
useLayoutEffect(() => { initializeCanvas() });
useEffect(() => { initializeCanvas() }, [width, height]);
function initializeCanvas() {
if (!canvasPlaceholderRef.current || !canvasPlaceholderRef.current.clientWidth || !canvasPlaceholderRef.current.clientHeight || !canvasPlaceholderRef.current.clientWidth) {
// wait until the placeholder is available
setTimeout(function() { initializeCanvas() }, 1000)
} else if (canvasPlaceholderRef.current) {
// initialize placeholder
setPlaceholderWidth(canvasAreaRef.current.clientWidth)
setPlaceholderHeight(canvasPlaceholderRef.current.clientHeight)
// initialize konva
let width = canvasPlaceholderRef.current.clientWidth;
let height = canvasPlaceholderRef.current.clientHeight;
stage = new Konva.Stage({
container: props.konvaContainer,
width: width,
height: height,
name: 'stage'
});
shapesLayer = new Konva.Layer({ name: 'shapes' });
tooltipLayer = new Konva.Layer({ name: 'tooltips' });
let tooltip = new Konva.Label({
opacity: 1,
visible: false,
listening: false,
name: 'label'
});
tooltip.add(
new Konva.Tag({
fill: '#313131',
pointerDirection: 'down',
pointerWidth: 20,
pointerHeight: 10,
cornerRadius: 4,
lineJoin: 'round',
shadowColor: 'black',
shadowBlur: 10,
shadowOffsetX: 10,
shadowOffsetY: 10,
shadowOpacity: 0.25,
name: 'tag'
})
);
tooltip.add(
new Konva.Text({
text: '',
align: 'center',
lineHeight: 2,
fontFamily: 'Inter',
fontSize: 13,
padding: 5,
fill: 'white',
name: 'tag',
width: props.wideTooltip ? 160 : 80
})
);
tooltipLayer.add(tooltip);
let areas = getData();
// draw areas
for (let key in areas) {
let area = areas[key];
let shape = new Konva.Line({
stroke: 'white',
strokeWidth: 2,
points: area.points,
fill: area.color,
opacity: area.disabled || area.visible ? 1 : key === props.selected?.name ? 1 : 0, // Keep the selected floor/unit highlight shape with opacity 1 after render
disabled: area.disabled,
closed: true,
target: area.target,
key: key,
subline: area.subline,
perfectDrawEnabled: false,
name: 'line',
cursor: 'pointer'
});
let group = new Konva.Group({name: 'group'})
shapesLayer.add(group);
group.add(shape);
}
stage.add(shapesLayer);
stage.add(tooltipLayer);
stage.on('mouseover touchstart', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if (shape && !shape.attrs.editable) {
shape.opacity(1);
shapesLayer.draw();
if (evt.type === 'touchstart' && shape.attrs.name === 'line') props.onClick(shape.attrs.target)
}
}
});
stage.on('mouseout touchend', function (evt) {
if (evt && evt.target) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && !shape.attrs.editable) {
shape.opacity(shape.attrs.disabled || shape.attrs.visible ? 1 : 0);
shapesLayer.draw();
tooltip.hide();
tooltipLayer.draw();
}
}
});
stage.on('mousemove', function(evt) {
let shape = evt.target;
if (shape) {
let mousePos = stage.getPointerPosition();
let x = mousePos.x;
let y = mousePos.y - 5;
updateTooltip(tooltip, x, y, shape.attrs.key, shape.attrs.subline);
tooltipLayer.draw();
}
});
stage.on('click tap', function(evt) {
let shape = evt.target;
if(shape.attrs.name === 'stage' || shape.attrs.target === props.selected?._id) return // Do nothing unless it is a different line shape
if (shape && shape.attrs.target) props.onClick(shape.attrs.target)
})
setTimeout(() => {
setShowCanvas(true)
shapesLayer.draw();
tooltipLayer.draw();
}, 100)
}
}
function getData() {
let areas = {}
function transformPoints(points) {
const c = JSON.parse(points || '[]')
return c.map((e, i) => (i % 2) ? (placeholderWidth * e) : placeholderHeight * e)
}
// add inactive paths
for (let p of props.inactivePaths) {
let newData = {
target: p.target,
color: p.disabled ? (props.disabledColor || '#E1171799') : props.accentColor || 'rgba(15, 33, 49, 0.65)',
points: transformPoints(p.path),
visible: p.visible,
disabled: p.disabled
}
if (props.project && props.project.showFloorOverview) {
let availableUnits = p.units.filter(u => u.availability.toLowerCase() === 'available').length
newData.subline = props.project.showFloorOverview ? `${availableUnits} ${i18nService.i18n(props.locale, "canvas.unitsavailable")}` : ''
}
areas[p.title] = newData
}
return areas;
}
function updateTooltip(tooltip, x, y, text, subline) {
const conditionalSubline = subline ? `\n${subline}` : ''
tooltip.children[1].text(text+conditionalSubline);
// tooltip.getText().text(text);
tooltip.position({ x: x, y: y });
if (text) tooltip.show();
};
return (
<div className={props.styles.canvasContainer}>
<img
className={props.styles.canvasImgPlaceholder}
ref={canvasPlaceholderRef}
alt='Canvas Placeholder'
src={props.background}
/>
<div className={props.styles.canvasPlaceholderBox} data-hide={showCanvas}>
<Image src="/images/planpoint_icon.svg" width={200} height={200} alt="" />
</div>
<div
className={props.styles.canvasKonvaContainer}
id={props.konvaContainer}
ref={canvasAreaRef}
data-hide={!showCanvas}
style={{backgroundImage: `url('${props.background}')`}}
>
</div>
</div>
)
}

Highcharts react X-range: how to add a custom component?

I have an x-range chart, and I need to add custom components (SVG icons) at the start of each data point ("x" prop value). Something like this (red circles are where custom components should be):
My idea is to place a custom component with position absolute, but I can't figure out how to transform timestamp values of the X axis to pixels. Or maybe there is a better solution?
Codesandbox demo
There are multiple ways to achieve what you want:
A. You can get a chart reference and use Axis.toPixels method to calculate the required coordinates:
export default function Chart() {
const chartComponentRef = useRef(null);
const [x1, setX1] = useState(0);
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const xAxis = chart?.xAxis[0];
const x1 = xAxis?.toPixels(Date.UTC(2022, 7, 10));
setX1(x1);
}
}, [chartComponentRef]);
return (
<div style={{ position: "relative" }}>
<HighchartsReact
ref={chartComponentRef}
highcharts={Highcharts}
options={staticOptions}
/>
<div style={{ position: "absolute", bottom: 70, left: x1 }}>ICON</div>
</div>
);
}
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-ysjfh8?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.Axis#toPixels
B. You can also get coordinates from a point:
useEffect(() => {
if (chartComponentRef) {
const chart = chartComponentRef.current.chart;
const point = chart.series[0].points[2];
setX1(chart.plotLeft + point.plotX);
}
}, [chartComponentRef]);
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f7nveg?file=/src/Chart.js
C. Use Highcharts API to render and update the custom icons:
const staticOptions = {
chart: {
type: "xrange",
height: 200,
events: {
render: function () {
const chart = this;
const r = 10;
const distance = 25;
chart.series[0].points.forEach((point, index) => {
if (!point.customIcon) {
point.customIcon = chart.renderer
.circle()
.attr({
fill: "transparent",
stroke: "red",
"stroke-width": 2
})
.add();
}
point.customIcon.attr({
x: point.plotX + chart.plotLeft + r,
y:
point.plotY +
chart.plotTop +
(index % 2 ? distance : -distance),
r
});
});
}
}
},
...
};
Live demo: https://codesandbox.io/s/highcharts-react-custom-componentt-f-87eof0?file=/src/Chart.js
API Reference: https://api.highcharts.com/class-reference/Highcharts.SVGRenderer#circle

How to make shape only show in react and partially hide on stage

I created a responsive rect as an artboard in the stage, and when I add shapes and move them, I want the shapes to only show on the rect, just like opening a window on the stage, only show the shapes inside the window
You should use clipping for that. Tutorial: https://konvajs.org/docs/clipping/Clipping_Regions.html
import React from "react";
import { createRoot } from "react-dom/client";
import { Stage, Layer, Star } from "react-konva";
function generateShapes() {
return [...Array(50)].map((_, i) => ({
id: i.toString(),
x: Math.random() * window.innerWidth,
y: Math.random() * window.innerHeight,
rotation: Math.random() * 180,
isDragging: false
}));
}
const INITIAL_STATE = generateShapes();
const STAR_PROPS = {
numPoints: 5,
innerRadius: 20,
outerRadius: 40,
fill: "#89b717",
opacity: 0.8,
shadowColorL: "black",
shadowBlur: 10,
shadowOpacity: 0.6,
shadowOffsetX: 5,
shadowOffsetY: 5,
scaleX: 1,
scaleY: 1,
draggable: true
};
const App = () => {
const [stars] = React.useState(INITIAL_STATE);
return (
<Stage width={window.innerWidth} height={window.innerHeight}>
<Layer
clipX={50}
clipY={50}
clipWidth={window.innerWidth - 100}
clipHeight={window.innerWidth - 100}
>
{stars.map((star) => (
<Star
{...STAR_PROPS}
key={star.id}
id={star.id}
x={star.x}
y={star.y}
rotation={star.rotation}
/>
))}
</Layer>
</Stage>
);
};
const container = document.getElementById("root");
const root = createRoot(container);
root.render(<App />);
https://codesandbox.io/s/react-konva-clipping-sg2fpd

React Konva - undo free draw lines

I was following this tutorial on how to build a whiteboard with react and konva and it provides an undo function for shapes but does not work for lines because lines are not added to the layer in the same way. How can I implement undo for free draw line?
EDIT:
To expand on my question, here is the relevant code:
I have a public repo that you can check out (and make a PR if that's easier).
https://github.com/ChristopherHButler/Sandbox-react-whiteboard
I have also have a demo you can try out here:
https://whiteboard-rho.now.sh/
Here is the relevant code
line component:
import Konva from "konva";
export const addLine = (stage, layer, mode = "brush") => {
let isPaint = false;
let lastLine;
stage.on("mousedown touchstart", function(e) {
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({
stroke: mode == "brush" ? "red" : "white",
strokeWidth: mode == "brush" ? 5 : 20,
globalCompositeOperation:
mode === "brush" ? "source-over" : "destination-out",
points: [pos.x, pos.y],
draggable: mode == "brush",
});
layer.add(lastLine);
});
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
};
HomePage component:
import React, { useState, createRef } from "react";
import { v1 as uuidv1 } from 'uuid';
import ButtonGroup from "react-bootstrap/ButtonGroup";
import Button from "react-bootstrap/Button";
import { Stage, Layer } from "react-konva";
import Rectangle from "../Shapes/Rectangle";
import Circle from "../Shapes/Circle";
import { addLine } from "../Shapes/Line";
import { addTextNode } from "../Shapes/Text";
import Image from "../Shapes/Image";
const HomePage = () => {
const [rectangles, setRectangles] = useState([]);
const [circles, setCircles] = useState([]);
const [images, setImages] = useState([]);
const [selectedId, selectShape] = useState(null);
const [shapes, setShapes] = useState([]);
const [, updateState] = useState();
const stageEl = createRef();
const layerEl = createRef();
const fileUploadEl = createRef();
const getRandomInt = max => {
return Math.floor(Math.random() * Math.floor(max));
};
const addRectangle = () => {
const rect = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `rect${rectangles.length + 1}`,
};
const rects = rectangles.concat([rect]);
setRectangles(rects);
const shs = shapes.concat([`rect${rectangles.length + 1}`]);
setShapes(shs);
};
const addCircle = () => {
const circ = {
x: getRandomInt(100),
y: getRandomInt(100),
width: 100,
height: 100,
fill: "red",
id: `circ${circles.length + 1}`,
};
const circs = circles.concat([circ]);
setCircles(circs);
const shs = shapes.concat([`circ${circles.length + 1}`]);
setShapes(shs);
};
const drawLine = () => {
addLine(stageEl.current.getStage(), layerEl.current);
};
const eraseLine = () => {
addLine(stageEl.current.getStage(), layerEl.current, "erase");
};
const drawText = () => {
const id = addTextNode(stageEl.current.getStage(), layerEl.current);
const shs = shapes.concat([id]);
setShapes(shs);
};
const drawImage = () => {
fileUploadEl.current.click();
};
const forceUpdate = React.useCallback(() => updateState({}), []);
const fileChange = ev => {
let file = ev.target.files[0];
let reader = new FileReader();
reader.addEventListener(
"load",
() => {
const id = uuidv1();
images.push({
content: reader.result,
id,
});
setImages(images);
fileUploadEl.current.value = null;
shapes.push(id);
setShapes(shapes);
forceUpdate();
},
false
);
if (file) {
reader.readAsDataURL(file);
}
};
const undo = () => {
const lastId = shapes[shapes.length - 1];
let index = circles.findIndex(c => c.id == lastId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == lastId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == lastId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
shapes.pop();
setShapes(shapes);
forceUpdate();
};
document.addEventListener("keydown", ev => {
if (ev.code == "Delete") {
let index = circles.findIndex(c => c.id == selectedId);
if (index != -1) {
circles.splice(index, 1);
setCircles(circles);
}
index = rectangles.findIndex(r => r.id == selectedId);
if (index != -1) {
rectangles.splice(index, 1);
setRectangles(rectangles);
}
index = images.findIndex(r => r.id == selectedId);
if (index != -1) {
images.splice(index, 1);
setImages(images);
}
forceUpdate();
}
});
return (
<div className="home-page">
<ButtonGroup style={{ marginTop: '1em', marginLeft: '1em' }}>
<Button variant="secondary" onClick={addRectangle}>
Rectangle
</Button>
<Button variant="secondary" onClick={addCircle}>
Circle
</Button>
<Button variant="secondary" onClick={drawLine}>
Line
</Button>
<Button variant="secondary" onClick={eraseLine}>
Erase
</Button>
<Button variant="secondary" onClick={drawText}>
Text
</Button>
<Button variant="secondary" onClick={drawImage}>
Image
</Button>
<Button variant="secondary" onClick={undo}>
Undo
</Button>
</ButtonGroup>
<input
style={{ display: "none" }}
type="file"
ref={fileUploadEl}
onChange={fileChange}
/>
<Stage
style={{ margin: '1em', border: '2px solid grey' }}
width={window.innerWidth * 0.9}
height={window.innerHeight - 150}
ref={stageEl}
onMouseDown={e => {
// deselect when clicked on empty area
const clickedOnEmpty = e.target === e.target.getStage();
if (clickedOnEmpty) {
selectShape(null);
}
}}
>
<Layer ref={layerEl}>
{rectangles.map((rect, i) => {
return (
<Rectangle
key={i}
shapeProps={rect}
isSelected={rect.id === selectedId}
onSelect={() => {
selectShape(rect.id);
}}
onChange={newAttrs => {
const rects = rectangles.slice();
rects[i] = newAttrs;
setRectangles(rects);
}}
/>
);
})}
{circles.map((circle, i) => {
return (
<Circle
key={i}
shapeProps={circle}
isSelected={circle.id === selectedId}
onSelect={() => {
selectShape(circle.id);
}}
onChange={newAttrs => {
const circs = circles.slice();
circs[i] = newAttrs;
setCircles(circs);
}}
/>
);
})}
{images.map((image, i) => {
return (
<Image
key={i}
imageUrl={image.content}
isSelected={image.id === selectedId}
onSelect={() => {
selectShape(image.id);
}}
onChange={newAttrs => {
const imgs = images.slice();
imgs[i] = newAttrs;
}}
/>
);
})}
</Layer>
</Stage>
</div>
);
}
export default HomePage;
As a solution, you should just use the same react modal for lines. It is not recommended to create shape instances manually (like new Konva.Line) when you work with react-konva.
Just define your state and make a correct render() from it, as you do in HomePage component.
You may store all shapes in one array. Or use a separate for lines. So to draw lines in react-konva way you can do this:
const App = () => {
const [lines, setLines] = React.useState([]);
const isDrawing = React.useRef(false);
const handleMouseDown = (e) => {
isDrawing.current = true;
const pos = e.target.getStage().getPointerPosition();
setLines([...lines, [pos.x, pos.y]]);
};
const handleMouseMove = (e) => {
// no drawing - skipping
if (!isDrawing.current) {
return;
}
const stage = e.target.getStage();
const point = stage.getPointerPosition();
let lastLine = lines[lines.length - 1];
// add point
lastLine = lastLine.concat([point.x, point.y]);
// replace last
lines.splice(lines.length - 1, 1, lastLine);
setLines(lines.concat());
};
const handleMouseUp = () => {
isDrawing.current = false;
};
return (
<Stage
width={window.innerWidth}
height={window.innerHeight}
onMouseDown={handleMouseDown}
onMousemove={handleMouseMove}
onMouseup={handleMouseUp}
>
<Layer>
<Text text="Just start drawing" />
{lines.map((line, i) => (
<Line key={i} points={line} stroke="red" />
))}
</Layer>
</Stage>
);
};
Demo: https://codesandbox.io/s/hungry-architecture-v380jlvwrl?file=/index.js
Then the next step is how to implement undo/redo. You just need to keep a history of state changes. Take a look here for demo: https://konvajs.org/docs/react/Undo-Redo.html
If I understand this right you saying that for shapes which are added individually there is an easy 'undo' process, but for lines which use an array of points for their segments, there is no simple undo - and no code in the tutorial you are following?
I can't give you a react code sample but I can explain some of the concepts you need to code up.
The 'freehand line' in your whiteboard is created as a sequence of points. You mousedown and the first point is noted, then you move the mouse and on each movemove event that fires the current mouse position is added to the end of the array. By the time you complete the line and mouseup fires, you have thrown multiple points into the line array.
In the Konvajs line tutorial it states:
To define the path of the line you should use points property. If you
have three points with x and y coordinates you should define points
property as: [x1, y1, x2, y2, x3, y3].
[Because...] Flat array of numbers should work faster and use less memory than
array of objects.
So - your line points are added as separate values into the line.points array.
Now lets think about undo - you are probably there already but I'll write it out anyway - to undo a single segment of the line you need to erase the last 2 entries in the array. To erase the entire line - well you can use the standard shape.remove() or shape.destroy() methods.
In the following snippet the two buttons link to code to 'undo' lines. The 'Undo by segment' button shows how to pop the last two entries in the line.points array to remove a segment of the line, and the 'Undo by line' button removes entire lines. This is not a react example specifically, but you will in the end create something very close to this in your react case.
// Code to erase line one segment at a time.
$('#undosegment').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
let pointsArray = lastLine.points(); // get current points in line
if (pointsArray.length === 0){ // no more points so destroy this line object.
lastLine.destroy();
layer.batchDraw();
lines.pop(); // remove from our lines-tracking array.
return;
}
// remove last x & y entrie, pop appears to be fastest way to achieve AND adjust array length
pointsArray.pop(); // remove the last Y pos
pointsArray.pop(); // remove the last X pos
lastLine.points(pointsArray); // give the points back into the line
layer.batchDraw();
})
// Code to erase entire lines.
$('#undoline').on('click', function(){
// get the last line we added to the canvas - tracked via lines array in this demo
if (lines.length === 0){
return;
}
lastLine = lines[lines.length - 1];
lastLine.destroy(); // remove from our lines-tracking array.
lines.pop();
layer.batchDraw();
})
// code from here on is all about drawing the lines.
let
stage = new Konva.Stage({
container: 'container',
width: $('#container').width(),
height: $('#container').height()
}),
// add a layer to draw on
layer = new Konva.Layer();
stage.add(layer);
stage.draw();
let isPaint = false;
let lastLine;
let lines = [];
stage.on('mousedown', function(){
isPaint = true;
let pos = stage.getPointerPosition();
lastLine = new Konva.Line({ stroke: 'magenta', strokeWidth: 4, points: [pos.x, pos.y]});
layer.add(lastLine);
lines.push(lastLine);
})
stage.on("mouseup touchend", function() {
isPaint = false;
});
stage.on("mousemove touchmove", function() {
if (!isPaint) {
return;
}
const pos = stage.getPointerPosition();
let newPoints = lastLine.points().concat([pos.x, pos.y]);
lastLine.points(newPoints);
layer.batchDraw();
});
body {
margin: 10;
padding: 10;
overflow: hidden;
background-color: #f0f0f0;
}
#container {
border: 1px solid silver;
width: 500px;
height: 300px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/konva#^3/konva.min.js"></script>
<p>Click and drag to draw a line </p>
<p>
<button id='undosegment'>Undo by segment</button> <button id='undoline'>Undo by line</button>
</p>
<div id="container"></div>

Openlayers 3. How to make tootlip for feature

Now I'm moving my project from openlayers 2 to openlayers 3. Unfortunately I can't find how to show title (tooltip) for feature. In OL2 there was a style named graphicTitle.
Could you give me advice how to implement tooltip on OL3?
This is example from ol3 developers.
jsfiddle.net/uarf1888/
var tooltip = document.getElementById('tooltip');
var overlay = new ol.Overlay({
element: tooltip,
offset: [10, 0],
positioning: 'bottom-left'
});
map.addOverlay(overlay);
function displayTooltip(evt) {
var pixel = evt.pixel;
var feature = map.forEachFeatureAtPixel(pixel, function(feature) {
return feature;
});
tooltip.style.display = feature ? '' : 'none';
if (feature) {
overlay.setPosition(evt.coordinate);
tooltip.innerHTML = feature.get('name');
}
};
map.on('pointermove', displayTooltip);
Here's the Icon Symobolizer example from the openlayers website. It shows how to have a popup when you click on an icon feature. The same principle applies to any kind of feature. This is what I used as an example when I did mine.
This is a basic example using the ol library. The most important is to define the overlay object. We will need an element to append the text we want to display in the tooltip, a position to show the tooltip and the offset (x and y) where the tooltip will start.
const tooltip = document.getElementById('tooltip');
const overlay = new ol.Overlay({
element: tooltip,
offset: [10, 0],
positioning: 'bottom-left'
});
map.addOverlay(overlay);
Now, we need to dynamically update the innerHTML of the tooltip.
function displayTooltip(evt) {
const pixel = evt.pixel;
const feature = map.forEachFeatureAtPixel(pixel, function(feature) {
return feature;
});
tooltip.style.display = feature ? '' : 'none';
if (feature) {
overlay.setPosition(evt.coordinate);
tooltip.innerHTML = feature.get('name');
}
};
map.on('pointermove', displayTooltip);
let styleCache = {};
const styleFunction = function(feature, resolution) {
// 2012_Earthquakes_Mag5.kml stores the magnitude of each earthquake in a
// standards-violating <magnitude> tag in each Placemark. We extract it from
// the Placemark's name instead.
const name = feature.get('name');
const magnitude = parseFloat(name.substr(2));
const radius = 5 + 20 * (magnitude - 5);
let style = styleCache[radius];
if (!style) {
style = [new ol.style.Style({
image: new ol.style.Circle({
radius: radius,
fill: new ol.style.Fill({
color: 'rgba(255, 153, 0, 0.4)'
}),
stroke: new ol.style.Stroke({
color: 'rgba(255, 204, 0, 0.2)',
width: 1
})
})
})];
styleCache[radius] = style;
}
return style;
};
const vector = new ol.layer.Vector({
source: new ol.source.Vector({
url: 'https://gist.githubusercontent.com/anonymous/5f4202f2d49d8574fd3c/raw/2c7ee40e3f4ad9dd4c8d9fb31ec53aa07e3865a9/earthquakes.kml',
format: new ol.format.KML({
extractStyles: false
})
}),
style: styleFunction
});
const raster = new ol.layer.Tile({
source: new ol.source.Stamen({
layer: 'toner'
})
});
const map = new ol.Map({
layers: [raster, vector],
target: 'map',
view: new ol.View({
center: [0, 0],
zoom: 2
})
});
const tooltip = document.getElementById('tooltip');
const overlay = new ol.Overlay({
element: tooltip,
offset: [10, 0],
positioning: 'bottom-left'
});
map.addOverlay(overlay);
function displayTooltip(evt) {
const pixel = evt.pixel;
const feature = map.forEachFeatureAtPixel(pixel, function(feature) {
return feature;
});
tooltip.style.display = feature ? '' : 'none';
if (feature) {
overlay.setPosition(evt.coordinate);
tooltip.innerHTML = feature.get('name');
}
};
map.on('pointermove', displayTooltip);
#map {
position: relative;
height: 100vh;
width: 100vw;
}
.tooltip {
position: relative;
padding: 3px;
background: rgba(0, 0, 0, .7);
color: white;
opacity: 1;
white-space: nowrap;
font: 10pt sans-serif;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="map" class="map">
<div id="tooltip" class="tooltip"></div>
</div>
<script src="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.8.1/build/ol.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io#master/en/v6.8.1/css/ol.css">

Resources