Dragging objects

Open on CodeSandbox

<!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>
      window.map = null;

      main();
      async function main() {
        const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
        const vuefy = mappableVue.vuefy.bindTo(Vue);
        const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapFeature} = vuefy.module(mappable);
        const {MMapDefaultMarker} = vuefy.module(await mappable.import('@mappable-world/mappable-markers@0.0.1'));

        const app = Vue.createApp({
          components: {
            MMap,
            MMapDefaultSchemeLayer,
            MMapDefaultFeaturesLayer,
            MMapFeature,
            MMapDefaultMarker
          },
          setup() {
            const markerRef = Vue.shallowRef(null);
            const triangleCoordinates = Vue.shallowRef(DRAGGABLE_FEATURE.geometry.coordinates);
            const triangleGeometry = {
              ...DRAGGABLE_FEATURE.geometry,
              coordinates: triangleCoordinates.value
            };
            const markerCoordinates = Vue.shallowRef(LOCATION.center);
            const draggable = Vue.ref(true);
            const controlMode = Vue.ref('');

            const onDragEndMarker = () => {
              markerCoordinates.value = markerRef.value.coordinates;
            };
            const onDragMoveMarker = Vue.computed(() =>
              RESTRICT_HANDLERS[controlMode.value]((coordinates) => {
                markerCoordinates.value = coordinates;
              })
            );
            const onDragEndTriangle = (coordinates) => {
              triangleCoordinates.value = coordinates;
            };
            const onDragMoveTriangle = Vue.computed(() =>
              controlMode.value === 'bound' ? RESTRICT_HANDLERS[controlMode.value]() : () => {}
            );
            const refMap = (ref) => {
              window.map = ref?.entity;
            };
            const markerRefFn = (ref) => {
              markerRef.value = ref.entity;
            };
            return {
              LOCATION,
              DRAGGABLE_FEATURE,
              CONTROL_FEATURE,
              CONTROL_LINE_FEATURE,
              controlMode,
              markerRefFn,
              refMap,
              draggable,
              markerCoordinates,
              triangleCoordinates,
              triangleGeometry,
              onDragEndMarker,
              onDragMoveMarker,
              onDragEndTriangle,
              onDragMoveTriangle
            };
          },
          template: `
                        <div className="toolbar options">
                            <label><input v-model="draggable" type="checkbox"/>Drag and Drop</label>
                            <label><select v-model="controlMode">
                                <option value="">No</option>
                                <option value="bound">Rectangle</option>
                                <option value="path">Path</option>
                            </select> Restrict</label>
                        </div>
                        <MMap :location="LOCATION" :ref="refMap">
                            <MMapDefaultSchemeLayer />
                            <MMapDefaultFeaturesLayer :zIndex="1800" />
                            <MMapFeature v-if="controlMode === 'bound'" v-bind="CONTROL_FEATURE" />
                            <MMapFeature v-if="controlMode === 'path'" v-bind="CONTROL_LINE_FEATURE" />
                            <MMapDefaultMarker
                                :ref="markerRefFn"
                                :draggable="draggable"
                                :coordinates="markerCoordinates"
                                :onDragEnd="onDragEndMarker"
                                :onDragMove="onDragMoveMarker" />
                            <MMapFeature
                                :id="DRAGGABLE_FEATURE.id"
                                :style="DRAGGABLE_FEATURE.style"
                                :geometry="triangleGeometry"
                                :draggable="draggable"
                                :onDragEnd="onDragEndTriangle"
                                :onDragMove="onDragMoveTriangle" />
                        </MMap>`
        });
        app.mount('#app');
      }
    </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>