Multi-Features Editor

Open in CodeSandbox

An example demonstrates the optimization of multiple geometry, with the possibility of selective editing.

Thanks to the option dynamic
We can prompt the API that we are going to often update data in MMapFeatureDataSource.

The example generates 1000 random triangles. They are added to MMapFeatureDataSource #1 with the dynamic: false option.

The API translates such geometry into an asynchronous display regimen, it is optimized for drawing a large number of objects.

If you change the geometry of one of the triangles, the API asynchronously carries out [https://en.wikipedia.org/wiki/Tessellation_(computer_graphics)) and renews the performance in the video card.

In fact, this will mean a tangible delay for the eye.

Therefore, in the example when clicking on the triangle, it is removed from MMapFeatureDataSource #1 and is added to MMapFeature #2 with the dynamic: true option.

API translates the geometry mode of synchronous update. The drawing of such geometry with a change does not occur with a delay.

When clicking on another triangle for it, the operation is repeated, and the first returns to MMapFeatureDataSource #1.

More information about the dynamic option can be read in documentation.

<!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://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {BBOX, LOCATION} from '../variables';
      import {FEATURE_COUNT, generateRandomTriangles} from './common';

      window.map = null;

      main();
      async function main() {
        // Waiting for all api elements to be loaded
        await mappable.ready;
        const {
          MMap,
          MMapDefaultSchemeLayer,
          MMapFeatureDataSource,
          MMapLayer,
          MMapFeature,
          MMapListener,
          MMapDefaultFeaturesLayer,
          MMapMarker,
          MMapCollection
        } = mappable;
        // Initialize the map
        map = new MMap(
          // Pass the link to the HTMLElement of the container
          document.getElementById('app'),
          // Pass the map initialization parameters
          {location: LOCATION, showScaleInCopyrights: true},
          // Add a map scheme layer
          [
            new MMapDefaultSchemeLayer({}),
            new MMapFeatureDataSource({id: 'features-vector', dynamic: false}),
            new MMapFeatureDataSource({id: 'features-raster', dynamic: true}),
            new MMapLayer({type: 'features', source: 'features-vector', zIndex: 1400}),
            new MMapLayer({type: 'features', source: 'features-raster', zIndex: 1401}),
            new MMapDefaultFeaturesLayer({})
          ]
        );

        const featuresCollection = new MMapCollection({});
        const fakeFeaturesCollection = new MMapCollection({});
        const pointsCollection = new MMapCollection({});

        map.addChild(featuresCollection);
        map.addChild(fakeFeaturesCollection);
        map.addChild(pointsCollection);

        const triangles = generateRandomTriangles(BBOX, FEATURE_COUNT);
        for (const triangle of triangles) {
          const feature = new MMapFeature({
            ...triangle
          });
          featuresCollection.addChild(feature);
        }

        let previousSelectedIndex = -1;
        let selectedIndex = -1;
        const listener = new MMapListener({
          onFastClick: (object) => {
            if (object.type === 'feature') {
              const newSelectedIndex = triangles.findIndex(
                (triangle: {id: string}) => triangle.id === object.entity.id
              );

              if (newSelectedIndex === -1 || selectedIndex === newSelectedIndex) {
                return;
              }

              selectedIndex = newSelectedIndex;
              updateSelected();
            }
          }
        });

        map.addChild(listener);

        function updateSelected() {
          if (!featuresCollection.children[selectedIndex]) {
            return;
          }

          const featureCopy = new MMapFeature({
            ...triangles[selectedIndex],
            id: `selected-${triangles[selectedIndex].id}`,
            source: 'features-raster'
          });
          fakeFeaturesCollection.addChild(featureCopy);
          featuresCollection.children[selectedIndex].update({
            source: 'features-raster'
          });

          requestAnimationFrame(
            ((previousSelectedIndex) => {
              fakeFeaturesCollection.removeChild(featureCopy);
              featuresCollection.children[previousSelectedIndex]?.update({
                source: 'features-vector'
              });
            }).bind(null, previousSelectedIndex)
          );

          previousSelectedIndex = selectedIndex;

          updateSimpleEditor();
        }

        function updateSimpleEditor() {
          if (!triangles[selectedIndex]) {
            return;
          }

          if (!pointsCollection.children.length) {
            for (let i = 0; i < 3; i++) {
              const div = document.createElement('div');
              div.classList.add('point');
              const marker = new MMapMarker(
                {
                  coordinates: triangles[selectedIndex].geometry.coordinates[0][i],
                  onDragMove: (newCoordinates) => {
                    triangles[selectedIndex].geometry.coordinates[0][i] = newCoordinates;
                    featuresCollection.children[selectedIndex]?.update({
                      geometry: {...triangles[selectedIndex].geometry}
                    });
                    updatePointsPositions();
                    return false;
                  },
                  draggable: true
                },
                div
              );
              pointsCollection.addChild(marker);
            }
          }

          updatePointsPositions();
        }

        function updatePointsPositions() {
          const selectedTriangle = triangles[selectedIndex];
          const coordinates = selectedTriangle.geometry.coordinates[0];
          pointsCollection.children.forEach((point, index) => {
            point.update({
              coordinates: coordinates[index]
            });
          });
        }
      }
    </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://cdn.jsdelivr.net/npm/react@17/umd/react.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/react-dom@17/umd/react-dom.production.min.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>
    <!-- To make the map appear, you must add your apikey -->
    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>

    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import {BBOX, LOCATION} from '../variables';
      import {FEATURE_COUNT, type FeatureProps, generateRandomTriangles} from './common';
      import type {DomEvent, DomEventHandlerObject, LngLat} from '@mappable-world/mappable-types';

      window.map = null;

      main();

      async function main() {
          // For each object in the JS API, there is a React counterpart
          // To use the React version of the API, include the module @mappable-world/mappable-reactify
          const [mappableReact] = await Promise.all([mappable.import('@mappable-world/mappable-reactify'), mappable.ready]);
          const reactify = mappableReact.reactify.bindTo(React, ReactDOM);
          const {
              MMap,
              MMapDefaultSchemeLayer,
              MMapFeatureDataSource,
              MMapLayer,
              MMapFeature,
              MMapListener,
              MMapDefaultFeaturesLayer,
              MMapMarker,
              MMapCollection
          } = reactify.module(mappable);
          const {useMemo, useState, useCallback} = React;

          function App() {
              const [location, setLocation] = useState(LOCATION);
              const [triangles, setTriangles] = useState<FeatureProps[]>(() => {
                  return generateRandomTriangles(BBOX, FEATURE_COUNT);
              }, []);
              const [selectedIndex, setSelectedIndex] = useState<number>(-1);

              const onSelectFeature = useCallback(
                  (object: DomEventHandlerObject) => {
                      if (object.type === 'feature') {
                          const newSelectedIndex = triangles.findIndex(
                              (triangle: {id: string}) => triangle.id === object.entity.id
                          );

                          if (newSelectedIndex === -1 || selectedIndex === newSelectedIndex) {
                              return;
                          }

                          const newTriangles = triangles.map((triangle) => ({
                              ...triangle,
                              source: object.entity.id === triangle.id ? 'features-raster' : 'features-vector'
                          }));
                          setTriangles(newTriangles);
                          setSelectedIndex(newSelectedIndex);
                      }
                  },
                  [selectedIndex, triangles]
              );

              return (
                  // Initialize the map and pass initialization parameters
                  <MMap location={reactify.useDefault(location)} showScaleInCopyrights={true} ref={(x) => (map = x)}>
                      <MMapDefaultSchemeLayer />
                      <MMapFeatureDataSource id={'features-vector'} dynamic={false} />
                      <MMapFeatureDataSource id={'features-raster'} dynamic={true} />
                      <MMapLayer type={'features'} source={'features-vector'} zIndex={1400} />
                      <MMapLayer type={'features'} source={'features-raster'} zIndex={1401} />
                      <MMapDefaultFeaturesLayer />
                      {triangles.map((feature: FeatureProps) => (
                          <MMapFeature key={feature.id} {...feature} />
                      ))}
                      {triangles[selectedIndex] && (
                          <SimpleGeometryEditor
                              selectedIndex={selectedIndex}
                              triangles={triangles}
                              setTriangles={setTriangles}
                          />
                      )}

                      <MMapListener onFastClick={onSelectFeature} />
                  </MMap>
              );
          }

          function SimpleGeometryEditor({
              selectedIndex,
              triangles,
              setTriangles
          }: {
              selectedIndex: number;
              triangles: FeatureProps[];
              setTriangles: (newTriangles: FeatureProps[]) => void;
          }) {
              return (
                  <MMapCollection>
                      {triangles[selectedIndex].geometry.coordinates[0].map((_, index) => (
                          <SimplePoint
                              key={`point_${index}`}
                              pointIndex={index}
                              featureIndex={selectedIndex}
                              triangles={triangles}
                              setTriangles={setTriangles}
                          />
                      ))}
                  </MMapCollection>
              );
          }

          function SimplePoint({
              featureIndex,
              pointIndex,
              triangles,
              setTriangles
          }: {
              featureIndex: number;
              pointIndex: number;
              triangles: FeatureProps[];
              setTriangles: (newTriangles: FeatureProps[]) => void;
          }) {
              const onDragMove = useCallback((newCoordinates: LngLat) => {
                  setTriangles(
                      triangles.map((triangle, triangleIndex) => {
                          if (triangleIndex === featureIndex) {
                              return {
                                  ...triangle,
                                  geometry: {
                                      ...triangle.geometry,
                                      coordinates: [
                                          triangle.geometry.coordinates[0].map((p, i) => {
                                              if (i === pointIndex) {
                                                  return newCoordinates;
                                              }
                                              return p;
                                          })
                                      ]
                                  }
                              } as FeatureProps;
                          }

                          return triangle;
                      })
                  );
                  return false;
              }, [featureIndex, pointIndex, triangles]);

              if (!triangles[featureIndex]) {
                  return null;
              }

              return (
                  <MMapMarker
                      id={`point_${pointIndex}`}
                      coordinates={triangles[featureIndex].geometry.coordinates[0][pointIndex]}
                      onDragMove={onDragMove}
                      draggable={true}
                  >
                      <div className={'point'} />
                  </MMapMarker>
              );
          }

          ReactDOM.render(
              <React.StrictMode>
                  <App />
              </React.StrictMode>,
              document.getElementById('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>
<!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://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
    <script crossorigin src="https://cdn.jsdelivr.net/npm/@babel/standalone@7/babel.min.js"></script>

    <script src="https://js.api.mappable.world/v3/?apikey=<YOUR_APIKEY>&lang=en_US" type="text/javascript"></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {BBOX, LOCATION} from '../variables';
      import {FEATURE_COUNT, generateRandomTriangles} from './common';
      import type {DomEventHandlerObject, LngLat, MMapFeature as MMapfeatureI} from '@mappable-world/mappable-types';

      window.map = null;

      async function main() {
          // For each object in the JS API, there is a Vue counterpart
          // To use the Vue version of the API, include the module @mappable-world/mappable-vuefy
          const [mappableVue] = await Promise.all([mappable.import('@mappable-world/mappable-vuefy'), mappable.ready]);
          const vuefy = mappableVue.vuefy.bindTo(Vue);
          const {
              MMap,
              MMapDefaultSchemeLayer,
              MMapFeatureDataSource,
              MMapLayer,
              MMapFeature,
              MMapListener,
              MMapDefaultFeaturesLayer,
              MMapMarker,
              MMapCollection
          } = vuefy.module(mappable);

          class SimplePoint extends mappable.MMapMarker {
              private __index: number = 0;
              constructor({
                  index,
                  coordinates,
                  onDragMoveIndex
              }: {
                  index: number;
                  coordinates: LngLat;
                  onDragMoveIndex: (pointIndex: number, coordinates: LngLat) => void;
              }) {
                  const div = document.createElement('div');
                  super(
                      {
                          coordinates,
                          onDragMove: (newCoorinates: LngLat) => {
                              onDragMoveIndex(index, newCoorinates);
                              return false;
                          },
                          draggable: true
                      },
                      div
                  );
                  div.classList.add('point');
              }
          }

          const app = Vue.createApp({
              components: {
                  MMap,
                  MMapDefaultSchemeLayer,
                  MMapFeatureDataSource,
                  MMapLayer,
                  MMapFeature,
                  MMapListener,
                  MMapDefaultFeaturesLayer,
                  MMapMarker,
                  MMapCollection,
                  SimplePoint: vuefy.entity(SimplePoint, {
                      index: Number,
                      coordinates: Object as Vue.PropType<LngLat>,
                      onDragMoveIndex: Function
                  })
              },
              setup() {
                  const refMap = (ref) => {
                      window.map = ref?.entity;
                  };

                  const triangles = Vue.shallowRef(generateRandomTriangles(BBOX, FEATURE_COUNT));

                  const selected = Vue.shallowRef({
                      index: -1,
                      coordinates: [] as LngLat[]
                  });

                  const onFastClick = (object: DomEventHandlerObject) => {
                      if (object.type === 'feature' && object.entity.geometry.type === 'Polygon') {
                          const newSelectedIndex = triangles.value.findIndex(
                              (triangle: {id: string}) => triangle.id === object.entity.id
                          );

                          if (newSelectedIndex === -1 || selected.value.index === newSelectedIndex) {
                              return;
                          }

                          selected.value = {
                              index: newSelectedIndex,
                              coordinates: [...(object.entity.geometry.coordinates[0] as LngLat[])]
                          };
                      }
                  };

                  const onDragMove = (index: number, coordinates: LngLat) => {
                      const coordinatesArray = [
                          ...(triangles.value[selected.value.index].geometry.coordinates[0] as LngLat[])
                      ];
                      coordinatesArray[index] = coordinates;
                      selected.value = {
                          ...selected.value,
                          coordinates: coordinatesArray
                      };
                      triangles.value = [
                          ...triangles.value.slice(0, selected.value.index),
                          {
                              ...triangles.value[selected.value.index],
                              geometry: {
                                  ...triangles.value[selected.value.index].geometry,
                                  coordinates: [coordinatesArray]
                              }
                          },
                          ...triangles.value.slice(selected.value.index + 1)
                      ];
                  };

                  return {LOCATION, refMap, triangles, onFastClick, selected, onDragMove};
              },
              template: `
                  <MMap :location="LOCATION" :showScaleInCopyrights="true" :ref="refMap">
                      <MMapDefaultSchemeLayer />
                      <MMapFeatureDataSource id='features-vector' :dynamic=false />
                      <MMapFeatureDataSource id='features-raster' :dynamic=true />
                      <MMapLayer type='features' source='features-vector' :zIndex=1400 />
                      <MMapLayer type='features' source='features-raster' :zIndex=1401 />
                      <MMapDefaultFeaturesLayer />
                      <MMapCollection>
                      <MMapFeature
                          v-for="(triangle, index) in triangles"
                          :key="triangle.id"
                          :source="index === selected.index ? 'features-raster' : 'features-vector'"
                          :geometry="triangle.geometry"
                          :style="triangle.style"
                          :id="triangle.id"
                      />
                      </MMapCollection>
                      <MMapListener :onFastClick="onFastClick" />
                      <MMapCollection v-if="selected.index !== -1">
                          <SimplePoint
                              v-for="(coordinates, index) in selected.coordinates"
                              :key="index"
                              :index="index"
                              :coordinates="coordinates"
                              :onDragMoveIndex="onDragMove"
                          />
                      </MMapCollection>
                  </MMap>`
          });
          app.mount('#app');
      }
      main();
    </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>
.point {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background-color: #0066ff;
  border: 1px solid #000;
  position: absolute;
  transform: translate(-50%, -50%);
  cursor: grab;
}
import type {DrawingStyle, LngLat, LngLatBounds, PolygonGeometry} from '@mappable-world/mappable-types';
export const FEATURE_COUNT = /autotest/.test(location.href) ? 50 : 1000;

const seed = (s: number) => () => {
  s = Math.sin(s) * 10000;
  return s - Math.floor(s);
};
const rnd = seed(10000); // () => Math.random()

function rndColor() {
  const rgb = [Math.floor(rnd() * 256), Math.floor(rnd() * 256), Math.floor(rnd() * 256)];
  return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
}

export type FeatureProps = {
  id: string;
  geometry: PolygonGeometry;
  style: DrawingStyle;
  source: 'features-vector' | 'features-raster';
};

export function generateRandomTriangles(bbox: LngLatBounds, count: number): FeatureProps[] {
  const triangles: FeatureProps[] = [];

  for (let i = 0; i < count; i++) {
    const baseLng = rnd() * (bbox[1][0] - bbox[0][0]) + bbox[0][0];
    const baseLat = rnd() * (bbox[1][1] - bbox[0][1]) + bbox[0][1];

    const coordinates = [
      [
        [baseLng, baseLat] as LngLat,
        [baseLng + rnd() * 0.8 - 0.4, baseLat + rnd() * 0.8 - 0.4] as LngLat,
        [baseLng + rnd() * 0.8 - 0.4, baseLat + rnd() * 0.8 - 0.4] as LngLat
      ]
    ];

    triangles.push({
      id: `triangle-${i}`,
      geometry: {type: 'Polygon', coordinates} as PolygonGeometry,
      style: {
        zIndex: i,
        fill: rndColor(),
        fillOpacity: 0.5,
        stroke: [{width: 3, opacity: 1, color: rndColor()}]
      },
      source: 'features-vector'
    });
  }

  return triangles;
}
import type {LngLatBounds, MMapLocationRequest} from '@mappable-world/mappable-types';

export const LOCATION: MMapLocationRequest = {
  center: [55.2744, 25.1972], // starting position [lng, lat]
  zoom: 9 // starting zoom
};

export const BBOX: LngLatBounds = [
  [52.901353125, 26.34483121687613],
  [57.647446875, 24.038650774820344]
];