Add a marker with a custom icon to the map

Open in CodeSandbox

<!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="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import type {LngLat} from '@mappable-world/mappable-types';
      import {LOCATION, NAMES, BOUNDS, getImageSrc} from '../variables';
      import {COMMON_LOCATION_PARAMS, type ExpandedFeature, getBounds, getRandomPoints, MARGIN} from './common';

      window.map = null;

      main();
      async function main() {
          // Waiting for all api elements to be loaded
          await mappable.ready;
          const {MMap, MMapDefaultSchemeLayer, MMapDefaultFeaturesLayer, MMapMarker} = mappable;
          const {MMapClusterer, clusterByGrid} = await mappable.import('@mappable-world/mappable-clusterer');

          map = new MMap(document.getElementById('app'), {location: LOCATION, showScaleInCopyrights: true, margin: MARGIN}, [
              // Add a map scheme layer
              new MMapDefaultSchemeLayer({}),
              // Add a layer of geo objects to display the markers
              new MMapDefaultFeaturesLayer({})
          ]);

          /* We declare the function for rendering ordinary markers, we will submit it to the clusterer settings.
        Note that the function must return any Entity element. In the example, this is MMapDefaultMarker. */
          const marker = (feature: ExpandedFeature) => {
              const markerContainerElement = document.createElement('div');
              markerContainerElement.classList.add('marker-container');

              const markerText = document.createElement('div');
              markerText.id = feature.id;
              markerText.classList.add('marker-text', 'hidden');
              markerText.innerText = NAMES[feature.id];

              markerContainerElement.onmouseover = () => {
                  markerText.classList.replace('hidden', 'visible');
              };

              markerContainerElement.onmouseout = () => {
                  markerText.classList.replace('visible', 'hidden');
              };

              const markerElement = document.createElement('div');
              markerElement.classList.add('marker');

              const markerImage = document.createElement('img');
              markerImage.src = getImageSrc(feature.id);
              markerImage.classList.add('image');

              markerElement.appendChild(markerImage);

              markerContainerElement.appendChild(markerText);
              markerContainerElement.appendChild(markerElement);

              return new MMapMarker(
                  {
                      coordinates: feature.geometry.coordinates
                  },
                  markerContainerElement
              );
          };

          // As for ordinary markers, we declare a cluster rendering function that also returns an Entity element.
          const cluster = (coordinates: LngLat, features: ExpandedFeature[]) =>
              new MMapMarker(
                  {
                      coordinates,
                      onClick() {
                          const bounds = getBounds(features.map((feature: ExpandedFeature) => feature.geometry.coordinates));
                          map.update({location: {bounds, ...COMMON_LOCATION_PARAMS}});
                      }
                  },
                  circle(features.length).cloneNode(true) as HTMLElement
              );

          function circle(count: number) {
              const circle = document.createElement('div');
              circle.classList.add('circle');
              circle.innerHTML = `
                        <div class="circle-content">
                            <span class="circle-text">${count}</span>
                        </div>
                    `;
              return circle;
          }

          /* We create a clusterer object and add it to the map object.
        As parameters, we pass the clustering method, an array of features, the functions for rendering markers and clusters.
        For the clustering method, we will pass the size of the grid division in pixels. */
          const clusterer = new MMapClusterer({
              method: clusterByGrid({gridSize: 64}),
              features: getRandomPoints(BOUNDS),
              marker,
              cluster
          });
          map.addChild(clusterer);
      }
    </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" />
    <link rel="stylesheet" href="../variables.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="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="react, typescript" type="text/babel">
      import type {LngLat} from '@mappable-world/mappable-types';
      import {BOUNDS, getImageSrc, LOCATION, NAMES} from '../variables';
      import {COMMON_LOCATION_PARAMS, type ExpandedFeature, getBounds, getRandomPoints, MARGIN} from './common';

      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, MMapDefaultFeaturesLayer, MMapMarker} = reactify.module(mappable);
        // Load the package with the cluster, extract the classes for creating clusterer objects and the clustering method
        const {MMapClusterer, clusterByGrid} = reactify.module(
          await mappable.import('@mappable-world/mappable-clusterer')
        );

        const {useState, useCallback, useMemo} = React;

        function App() {
          const [location, setLocation] = useState(LOCATION);
          const [hoverMarkers, setHoverMarkers] = useState({});
          const points = useMemo(() => getRandomPoints(BOUNDS), []);

          // We declare a render function. For the clustering method, we pass and store the size of one grid division in pixels
          const gridSizedMethod = useMemo(() => clusterByGrid({gridSize: 64}), []);

          const markerMouseOver = useCallback((id: string) => {
            setHoverMarkers((state) => ({
              ...state,
              [id]: true
            }));
          }, []);

          const markerMouseOut = useCallback((id: string) => {
            setHoverMarkers((state) => ({
              ...state,
              [id]: false
            }));
          }, []);

          const onClusterClick = useCallback(
            (features: ExpandedFeature[]) => {
              const bounds = getBounds(features.map((feature: ExpandedFeature) => feature.geometry.coordinates));
              setLocation((prevLocation) => ({...prevLocation, bounds, ...COMMON_LOCATION_PARAMS}));
            },
            [location]
          );

          // We declare a function for rendering markers. Note that the function must return any Entity element. In the example, this is MMapDefaultMarker
          const marker = (feature: ExpandedFeature) => (
            <MMapMarker coordinates={feature.geometry.coordinates}>
              <div
                className="marker-container"
                onMouseOver={() => markerMouseOver(feature.id)}
                onMouseOut={() => markerMouseOut(feature.id)}
              >
                <div className={`marker-text ${hoverMarkers[feature.id] ? 'visible' : 'hidden'}`}>
                  {NAMES[feature.id]}
                </div>
                <div className="marker">
                  <img alt="img" className="image" src={getImageSrc(feature.id)} />
                </div>
              </div>
            </MMapMarker>
          );

          // We declare a cluster rendering function that also returns an Entity element. We will transfer the marker and cluster rendering functions to the clusterer settings
          const cluster = (coordinates: LngLat, features: ExpandedFeature[]) => (
            <MMapMarker onClick={() => onClusterClick(features)} coordinates={coordinates}>
              <div className="circle">
                <div className="circle-content">
                  <span className="circle-text">{features.length}</span>
                </div>
              </div>
            </MMapMarker>
          );

          return (
            // Initialize the map and pass initialization parameters
            <MMap margin={MARGIN} location={location} showScaleInCopyrights={true} ref={(x) => (map = x)}>
              {/* Add a map scheme layer */}
              <MMapDefaultSchemeLayer />
              {/* Add clusterer data sources */}
              <MMapDefaultFeaturesLayer />
              {/* In the clusterer props, we pass the previously declared functions for rendering markers and clusters,
                the clustering method, and an array of features */}
              <MMapClusterer marker={marker} cluster={cluster} method={gridSizedMethod} features={points} />
            </MMap>
          );
        }

        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" />
    <link rel="stylesheet" href="../variables.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="react, typescript"
      type="text/babel"
      src="./common.ts"
    ></script>
    <script
      data-plugins="transform-modules-umd"
      data-presets="react, typescript"
      type="text/babel"
      src="../variables.ts"
    ></script>
    <script data-plugins="transform-modules-umd" data-presets="typescript" type="text/babel">
      import {LOCATION, BOUNDS, NAMES, getImageSrc} from '../variables';
      import {COMMON_LOCATION_PARAMS, ExpandedFeature, getBounds, getRandomPoints, MARGIN} from './common';

      window.map = null;

      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, MMapMarker} = vuefy.module(mappable);
        // Load the package with the cluster, extract the classes for creating clusterer objects and the clustering method
        const {MMapClusterer, clusterByGrid} = vuefy.module(
          await mappable.import('@mappable-world/mappable-clusterer')
        );

        const Marker = Vue.defineComponent({
          props: ['feature'],
          components: {
            MMapMarker
          },
          setup(props) {
            const visible = Vue.ref(false);
            const imgPath = getImageSrc(props.feature.id);

            return {
              imgPath,
              visible,
              NAMES
            };
          },
          template: `
      <MMapMarker iconName="landmark" :key="feature.id" :coordinates="feature.geometry.coordinates">
        <div
          class="marker-container"
          @mouseover="() => visible = true"
          @mouseout="() => visible = false"
        >
          <div :class="[visible ? 'visible' : 'hidden', 'marker-text']">{{ NAMES[feature.id] }}</div>
          <div class="marker">
            <img alt="img" class="image" :src="imgPath"/>
          </div>
        </div>
      </MMapMarker>
    `
        });

        const Cluster = Vue.defineComponent({
          props: {
            coordinates: Array,
            features: Array,
            onClick: Function
          },
          components: {
            MMapMarker
          },
          template: `
      <MMapMarker
        :coordinates="coordinates"
        :onClick="onClick"
      >
        <div class="circle">
          <div class="circle-content">
            <span class="circle-text">{{features.length}}</span>
          </div>
        </div>
      </MMapMarker>
    `
        });

        const App = Vue.createApp({
          components: {
            MMap,
            MMapDefaultSchemeLayer,
            MMapDefaultFeaturesLayer,
            MMapClusterer,
            Cluster,
            Marker
          },
          setup() {
            const refMap = (ref) => {
              window.map = ref?.entity;
            };
            const location = Vue.ref(LOCATION);

            const gridSizedMethod = clusterByGrid({gridSize: 64});
            const points = getRandomPoints(BOUNDS);

            const onClusterClick = (features: ExpandedFeature[]) => {
              const bounds = getBounds(features.map((feature: ExpandedFeature) => feature.geometry.coordinates));
              location.value = {bounds, ...COMMON_LOCATION_PARAMS};
            };

            return {
              MARGIN,
              refMap,
              location,
              gridSizedMethod,
              points,
              onClusterClick
            };
          },
          template: `
      <MMap :margin="MARGIN" :location="location" :showScaleInCopyrights="true" :ref="refMap">
        <!-- Add a map scheme layer -->
        <MMapDefaultSchemeLayer />
        <!-- Add default features layer -->
        <MMapDefaultFeaturesLayer />
        <!-- In the clusterer props, we pass the previously declared functions for rendering markers and clusters,
        the clustering method, and an array of features -->
        <MMapClusterer :method="gridSizedMethod" :features="points">
          <template #marker="{feature}">
            <Marker :feature="feature" />
          </template>
          <template #cluster="{coordinates, features}">
            <Cluster :onClick="() => onClusterClick(features)" :coordinates="coordinates" :features="features" />
          </template>
        </MMapClusterer>
      </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" />
    <link rel="stylesheet" href="../variables.css" />
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>
.circle {
  cursor: pointer;
  position: relative;

  width: 40px;
  height: 40px;

  color: var(--interact-action);
  border: 2px solid rgba(255, 255, 255, 0.7);
  border-radius: 50%;
  background-color: rgba(255, 255, 255, 0.7);
  box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
  transform: translate(-50%, -50%);
}

.circle-content {
  position: absolute;
  top: 50%;
  left: 50%;

  display: flex;
  justify-content: center;
  align-items: center;

  width: 90%;
  height: 90%;

  border-radius: 50%;
  background-color: currentColor;

  transform: translate3d(-50%, -50%, 0);
}

.circle-text {
  font-size: 16px;
  font-weight: 500;
  line-height: 20px;
  color: #fff;
}

.image {
  transition: scale 0.3s ease-out;
}

.image:hover {
  scale: 1.1;
}

.marker-container {
  display: flex;
  flex-direction: column;
  align-items: center;
  position: absolute;
  transform: translate(-50%, -50%);
}

.marker {
  overflow: hidden;
  border: 1px solid #ffffff;
  border-radius: 16px;
  width: 52px;
  display: flex;
  justify-content: center;
  height: 52px;
  transition: border-width 0.3s linear;
  box-sizing: content-box;
}

.marker:hover {
  border-width: 2px;
  box-shadow: 0px 2px 4px 0px #5f698333;
}

.marker-text {
  color: #050d33;
  font-weight: 500;
  font-size: 14px;
  top: -27px;
  position: absolute;
  background-color: #ffffff;
  border-radius: 8px;
  transition: all 0.3s ease-out;
}

.marker-text.visible {
  opacity: 1;
  padding: 4px 8px;
  transform: translateY(0);
}

.marker-text.hidden {
  opacity: 0;
  padding: 4px 0;
  transform: translateY(4px);
}
import type {LngLat, LngLatBounds, Margin, MMapLocationRequest} from '@mappable-world/mappable-types';
import type {Feature} from '@mappable-world/mappable-clusterer';

export type ExpandedFeature = Feature & {id: string};

mappable.ready.then(() => {
  mappable.import.registerCdn('https://cdn.jsdelivr.net/npm/{package}', ['@mappable-world/mappable-clusterer@0.0']);
});

// Function for generating a pseudorandom number
const seed = (s: number) => () => {
  s = Math.sin(s) * 10000;
  return s - Math.floor(s);
};

const rnd = seed(10000); // () => Math.random()

// Generating random coordinates of a point [lng, lat] in a given boundary
const getRandomPointCoordinates = (bounds: LngLatBounds): LngLat => [
  bounds[0][0] + (bounds[1][0] - bounds[0][0]) * rnd(),
  bounds[1][1] + (bounds[0][1] - bounds[1][1]) * rnd()
];

export const COMMON_LOCATION_PARAMS: Partial<MMapLocationRequest> = {easing: 'ease-in-out', duration: 2000};

export function getBounds(coordinates: number[][]): LngLatBounds {
  let minLat = Infinity,
    minLng = Infinity;
  let maxLat = -Infinity,
    maxLng = -Infinity;

  for (const coords of coordinates) {
    const lat = coords[1];
    const lng = coords[0];

    if (lat < minLat) minLat = lat;
    if (lat > maxLat) maxLat = lat;
    if (lng < minLng) minLng = lng;
    if (lng > maxLng) maxLng = lng;
  }

  return [
    [minLng, minLat],
    [maxLng, maxLat]
  ] as LngLatBounds;
}

// A function that creates an array with parameters for each clusterer random point
export const getRandomPoints = (bounds: LngLatBounds): ExpandedFeature[] => {
  return Array.from({length: 40}, (_, index) => ({
    type: 'Feature',
    id: index.toString(),
    geometry: {type: 'Point', coordinates: getRandomPointCoordinates(bounds)},
    properties: {
      name: 'marker',
      description: ''
    }
  }));
};

export const MARGIN: Margin = [100, 100, 100, 100];
:root {
  --interact-action: #313133;
}
import type {LngLatBounds, MMapLocationRequest} from '@mappable-world/mappable-types';

export const LOCATION: MMapLocationRequest = {
  center: [55.6755, 24.9451], // starting position [lng, lat]
  zoom: 11.1 // starting zoom
};

/* Rectangle bounded by bottom-left and top-right coordinates
Inside it, we generate the first bundle of clusterer points */
export const BOUNDS: LngLatBounds = [
  [55.4552, 24.8571],
  [55.6392, 25.0134]
];
export const NAMES = [
  'Angela',
  'Phillip',
  'Butterfly',
  'Sebastian',
  'Sunshine',
  'Olaf',
  'Robert',
  'Lucy',
  'Scrat',
  'Jonatan',
  'Marta',
  'Mimi',
  'Luther',
  'Stuart',
  'Paul',
  'Martin',
  'Mathew',
  'Matt',
  'Merlin',
  'Albert',
  'Roberta',
  'Velma',
  'John',
  'Catherine',
  'Mary',
  'Michele',
  'Kelly',
  'Willy',
  'Sam',
  'Pam',
  'Cookie',
  'Candy',
  'Joey',
  'Charity',
  'Monica',
  'Hope',
  'Chester',
  'Louise',
  'Shaggy',
  'Tina'
];

export const getImageSrc = (id: string) => `../cats/image${id}.png`;