Dragging objects
vanilla.html
common.css
common.js
react.html
vue.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script src="./common.js"></script>
<script>
window.map = null;
main();
async function main() {
await mappable.ready;
const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapFeature} = mappable;
const {MMapDefaultMarker} = await mappable.import('@mappable-world/mappable-markers@0.0.1');
map = new MMap(document.getElementById('app'), {
location: LOCATION
});
let markerProps = {
coordinates: LOCATION.center,
mapFollowsOnDrag: true,
draggable: true
};
const marker = new MMapDefaultMarker(markerProps);
const draggableGraphics = new MMapFeature({
...DRAGGABLE_FEATURE,
onDragEnd: (coordinates) => {
draggableGraphics.update({
geometry: {
...DRAGGABLE_FEATURE.geometry,
coordinates
}
});
}
});
const controllableBound = new MMapFeature(CONTROL_FEATURE);
const controllablePath = new MMapFeature(CONTROL_LINE_FEATURE);
map
.addChild(new MMapDefaultSchemeLayer())
.addChild(new MMapDefaultFeaturesLayer({zIndex: 1800}))
.addChild(marker)
.addChild(draggableGraphics);
draggable.onchange = (e) => {
marker.update({
draggable: e.target.checked
});
draggableGraphics.update({
draggable: e.target.checked
});
};
controllable.onchange = (e) => {
if (e.target.value === 'bound') {
map.addChild(controllableBound);
} else {
map.removeChild(controllableBound);
}
if (e.target.value === 'path') {
map.addChild(controllablePath);
} else {
map.removeChild(controllablePath);
}
marker.update({
onDragMove: RESTRICT_HANDLERS[e.target.value]((coordinates) => {
marker.update({
coordinates
});
})
});
draggableGraphics.update({
onDragMove: e.target.value === 'bound' ? RESTRICT_HANDLERS[e.target.value]() : () => {}
});
};
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div class="toolbar options">
<label><input checked id="draggable" type="checkbox" />Drag and Drop</label>
<label
><select id="controllable">
<option value="">No</option>
<option value="bound">Rectangle</option>
<option value="path">Path</option>
</select>
Restrict</label
>
</div>
<div id="app"></div>
</body>
</html>
.options {
background-color: #fff;
}
const LOCATION = {center: [55.44279, 25.24613], zoom: 15};
const p = LOCATION.center;
/**
* Set the coordinates of the bounding rectangle.
* We will use these coordinates to prohibit moving elements beyond their aisles.
*/
const CONTROL_BOUND = [
[p[0] - 0.01, p[1] - 0.01],
[p[0] + 0.01, p[1] + 0.01]
];
/**
* We will set this handler on the onDragMove of the dragged elements.
* In it, we simply prohibit movement if the cursor goes beyond the CONTROL_BOUND
* In this handler, we do not control the position of the object when dragging,
* but only forbid it to go beyond the boundaries of the CONTROL_BOUND area.
*/
const onDragMoveRestrictBound = () => (coords) => {
const coordinates = typeof coords[0] === 'number' ? [coords] : coords;
return coordinates.every((point) => {
if (point[0] < CONTROL_BOUND[0][0] || point[0] > CONTROL_BOUND[1][0]) {
return false;
}
if (point[1] < CONTROL_BOUND[0][1] || point[1] > CONTROL_BOUND[1][1]) {
return false;
}
return true;
});
};
/**
* Finds the Y coordinate from X on the line [{x1,y1}, {x2,y2}]
*/
const getYFromX = (x, [{x: x1, y: y1}, {x: x2, y: y2}]) => {
const k = (y2 - y1) / (x2 - x1);
return k * (x - x1) + y1;
};
/**
* The handler will track the movement of the marker and manage it completely independently
*/
const onDragMoveRestrictPath = (setCoordinates) => (coords) => {
/**
* For convenience, we translate the coordinates from the format [lng, Lat] into the format {x: Lng, y: Lat}
* Please note that we do not change or convert the coordinates in any way.
* At smaller zoom sizes, these calculations will give large errors.
* Therefore, in your tasks, convert coordinates to world coordinates using a projection (see MMap.projection)
*/
const path = CONTROL_LINE_FEATURE.geometry.coordinates.map((c) => ({x: c[0], y: c[1]}));
const coordsW = {x: coords[0], y: coords[1]};
// Limiting in X to the extreme points of the path
coordsW.x = Math.max(path[0].x, coordsW.x);
coordsW.x = Math.min(path[path.length - 1].x, coordsW.x);
for (let i = 0; i < path.length - 1; i += 1) {
const line = [path[i], path[i + 1]];
/**
* Find the line above which the cursor is now
*/
if (coordsW.x >= line[0].x && coordsW.x <= line[1].x) {
coordsW.y = getYFromX(coordsW.x, line);
break;
}
}
/**
* We have full control over the position of the marker
*/
setCoordinates([coordsW.x, coordsW.y]); // We return the coordinates back to the format [Lng, Lat]
return false;
};
const RESTRICT_HANDLERS = {
bound: onDragMoveRestrictBound,
path: onDragMoveRestrictPath,
'': () => {}
};
/**
* Let's just visually display the border of the bounding box.
* It has nothing to do with the logic of the constraint.
*/
const CONTROL_FEATURE = {
id: 'controllableBound',
geometry: {
type: 'LineString',
coordinates: [
CONTROL_BOUND[0],
[CONTROL_BOUND[1][0], CONTROL_BOUND[0][1]],
CONTROL_BOUND[1],
[CONTROL_BOUND[0][0], CONTROL_BOUND[1][1]],
CONTROL_BOUND[0]
]
},
style: {
stroke: [{width: 12, color: 'rgb(14, 194, 219)'}]
}
};
/**
* Polyline on the map, we will use its coordinates to limit the movement of the marker
*/
const CONTROL_LINE_FEATURE = {
id: 'controllableLine',
geometry: {
type: 'LineString',
coordinates: [
[55.42911, 25.24289],
[55.43299, 25.24739],
[55.44179, 25.24344],
[55.45529, 25.2507],
[55.46599, 25.24413]
]
},
style: {
stroke: [{width: 12, color: 'rgb(219,14,14)'}]
}
};
/**
* A triangle that will also be draggable
*/
const DRAGGABLE_FEATURE = {
id: 'draggableGraphics',
draggable: true,
geometry: {
type: 'LineString',
coordinates: [
[55.44879, 25.24613],
[55.4498, 25.24913],
[55.44479, 25.24813],
[55.44879, 25.24613]
]
},
style: {
stroke: [{width: 8, color: 'rgb(128,149,208)'}]
}
};
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
<script crossorigin src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script src="./common.js"></script>
<script type="text/babel">
window.map = null;
main();
async function main() {
const [mappableReact] = await Promise.all([
mappable.import('@mappable-world/mappable-reactify'),
mappable.ready
]);
const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapFeature} = reactify.module(mappable);
const {MMapDefaultMarker} = reactify.module(await mappable.import('@mappable-world/mappable-markers@0.0.1'));
const {useState} = React;
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('app')
);
function App() {
const markerRef = React.useRef(null);
const [triangleCoordinates, setTriangleCoordinates] = useState(DRAGGABLE_FEATURE.geometry.coordinates);
const [markerCoordinates, setMarkerCoordinates] = useState(LOCATION.center);
const [draggable, setDraggable] = useState(true);
const [controlMode, setControlMode] = useState('');
const onDraggableChange = React.useCallback(() => {
setDraggable(!draggable);
}, [draggable]);
const onControllableChange = React.useCallback(
(e) => {
setControlMode(e.target.value);
},
[setControlMode]
);
const onDragMoveMarker = React.useMemo(
() =>
RESTRICT_HANDLERS[controlMode]((coordinates) => {
setMarkerCoordinates(coordinates);
}),
[setMarkerCoordinates, controlMode]
);
const onDragMoveTriangle = React.useMemo(
() => (controlMode === 'bound' ? RESTRICT_HANDLERS[controlMode]() : () => {}),
[controlMode]
);
const onDragEndMarker = React.useCallback(() => {
setMarkerCoordinates(markerRef.current.coordinates);
}, []);
return (
<React.Fragment>
<div className="toolbar options">
<label>
<input checked={draggable} onChange={onDraggableChange} type="checkbox" />
Drag and Drop
</label>
<label>
<select value={controlMode} onChange={onControllableChange}>
<option value="">No</option>
<option value="bound">Rectangle</option>
<option value="path">Path</option>
</select>{' '}
Restrict
</label>
</div>
<MMap location={LOCATION} ref={(x) => (map = x)}>
<MMapDefaultSchemeLayer />
<MMapDefaultFeaturesLayer zIndex={1800} />
{controlMode === 'bound' && <MMapFeature {...CONTROL_FEATURE} />}
{controlMode === 'path' && <MMapFeature {...CONTROL_LINE_FEATURE} />}
<MMapDefaultMarker
ref={markerRef}
draggable={draggable}
mapFollowsOnDrag={true}
coordinates={markerCoordinates}
onDragEnd={onDragEndMarker}
onDragMove={onDragMoveMarker}
/>
<MMapFeature
id={DRAGGABLE_FEATURE.id}
style={DRAGGABLE_FEATURE.style}
geometry={{...DRAGGABLE_FEATURE.geometry, coordinates: triangleCoordinates}}
draggable={draggable}
onDragMove={onDragMoveTriangle}
onDragEnd={setTriangleCoordinates}
/>
</MMap>
</React.Fragment>
);
}
}
</script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<script crossorigin src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
<script src="./common.js"></script>
<script></script>
<!-- prettier-ignore -->
<style> html, body, #app { width: 100%; height: 100%; margin: 0; padding: 0; font-family: Arial, Helvetica, sans-serif; } .toolbar { position: absolute; z-index: 1000; top: 0; left: 0; display: flex; align-items: center; padding: 16px; } .toolbar a { padding: 16px; } </style>
<link rel="stylesheet" href="./common.css" />
</head>
<body>
<div id="app"></div>
</body>
</html>