Hide markers outside the viewport

Open on CodeSandbox

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
      crossorigin="anonymous"
    />
    <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, MMapControls, MMapDefaultFeaturesLayer, MMapMarker} = mappable;

        const {MMapZoomControl} = await mappable.import('@mappable-world/mappable-controls@0.0.1');

        map = new MMap(document.getElementById('map'), {location: LOCATION, zoomRange: ZOOM_RANGE}, [
          new MMapDefaultSchemeLayer(),
          new MMapDefaultFeaturesLayer(),
          new MMapControls({position: 'right'}).addChild(new MMapZoomControl({}))
        ]);

        const points = getRandomPoints(2000);

        const markers = points.map((p, i) => {
          return new MMapMarker(
            {
              id: 'p' + i,
              coordinates: p.coordinates,
              hideOutsideViewport: true
            },
            p.element
          );
        });

        markers.forEach((m) => map.addChild(m));

        document.getElementById('toolbar').addEventListener('change', () => {
          markers.forEach((m, i) =>
            m.update({
              hideOutsideViewport: hideOutsideViewportExtent.checked
                ? {
                    extent: points[i].size
                  }
                : true
            })
          );
        });
      }
    </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 id="toolbar" class="toolbar options">
        <div>
          <div class="form-check">
            <input
              name="hideOutsideViewport"
              class="form-check-input"
              checked
              type="radio"
              id="hideOutsideViewportTrue"
            />
            <label class="form-check-label" for="hideOutsideViewportTrue">hideOutsideViewport: true</label>
          </div>
          <div class="form-check">
            <input name="hideOutsideViewport" class="form-check-input" type="radio" id="hideOutsideViewportExtent" />
            <label class="form-check-label" for="hideOutsideViewportExtent">
              hideOutsideViewport: {extent: size}
            </label>
          </div>
        </div>
      </div>
      <div id="map"></div>
    </div>
  </body>
</html>
:root {
  --point-bg-color: #858585;
  --point-size: 10px;
}

.point {
  width: var(--point-size, 10px);
  height: var(--point-size, 10px);
  border: 1px solid #0e86e6;
  background-color: var(--point-bg-color, #858585);
  border-radius: 50%;
  transform: translate(-50%, -50%);
}

#map {
  width: 100%;
  height: 100%;
}

.options {
  background-color: #e9e9e9d9;
}
const LOCATION = {center: [55.44279, 25.24613], zoom: 9};
const BOUNDS = [
  [53.06975, 25.73865],
  [57.81584, 24.74728]
];
const MAX_POINT_SIZE = 220;

const [lb, rt] = BOUNDS;
const DELTA = 6;

const EXT_BOUNDS = [
  [lb[0] - DELTA, lb[1] + DELTA],
  [rt[0] + DELTA, rt[1] - DELTA]
];

const ZOOM_RANGE = {min: 9, max: 10};

const seed = (s) => () => {
  s = Math.sin(s) * 10000;
  return s - Math.floor(s);
};

const rnd = seed(10000); // () => Math.random()
const rndNum = () => Math.floor(Math.random() * (1000 - 1)) + 1;
rnd.point = (bounds) => [
  bounds[0][0] + (bounds[1][0] - bounds[0][0]) * rnd(),
  bounds[1][1] + (bounds[0][1] - bounds[1][1]) * rnd()
];

const MARKER = document.createElement('div');
MARKER.classList.add('point');

const getRandomPoints = (count) =>
  Array.from({length: count}, (_, i) => {
    const size = 10 + rnd() * MAX_POINT_SIZE;
    const element = MARKER.cloneNode(true);
    element.style.setProperty('--point-size', size + 'px');
    element.style.setProperty('--point-bg-color', `hsl(${i % 365}deg 50% 50%)`);

    return {
      coordinates: rnd.point(EXT_BOUNDS),
      element,
      size
    };
  });
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
      crossorigin="anonymous"
    />
    <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, MMapControls, MMapDefaultFeaturesLayer, MMapMarker} =
          reactify.module(mappable);
        const {useState, useRef, useMemo, useCallback} = React;

        const {MMapZoomControl} = reactify.module(await mappable.import('@mappable-world/mappable-controls@0.0.1'));

        ReactDOM.render(
          <React.StrictMode>
            <App />
          </React.StrictMode>,
          document.getElementById('app')
        );

        function App() {
          const [location, setLocation] = useState(LOCATION);
          const [mode, setMode] = useState('true');
          const points = useMemo(() => getRandomPoints(2000), []);

          const onChangeMode = useCallback((e) => {
            setMode(hideOutsideViewportExtent.checked ? 'extent' : 'true');
          }, []);

          return (
            <React.Fragment>
              <div id="toolbar" className="toolbar options" onChange={onChangeMode}>
                <div>
                  <div className="form-check">
                    <input
                      name="hideOutsideViewport"
                      className="form-check-input"
                      checked={mode === 'true'}
                      type="radio"
                      id="hideOutsideViewportTrue"
                    />
                    <label className="form-check-label" htmlFor="hideOutsideViewportTrue">
                      hideOutsideViewport: true
                    </label>
                  </div>
                  <div className="form-check">
                    <input
                      name="hideOutsideViewport"
                      className="form-check-input"
                      checked={mode === 'extent'}
                      type="radio"
                      id="hideOutsideViewportExtent"
                    />
                    <label className="form-check-label" htmlFor="hideOutsideViewportExtent">
                      {'hideOutsideViewport: {extent: size}'}
                    </label>
                  </div>
                </div>
              </div>
              <MMap location={location} zoomRange={ZOOM_RANGE} ref={(x) => (map = x)}>
                <MMapDefaultSchemeLayer />
                <MMapDefaultFeaturesLayer />
                <MMapControls position="right">
                  <MMapZoomControl />
                </MMapControls>
                {points.map((p, i) => (
                  <MMapMarker
                    coordinates={p.coordinates}
                    markerElement={p.element}
                    hideOutsideViewport={
                      mode === 'true'
                        ? true
                        : {
                            extent: points[i].size
                          }
                    }
                  />
                ))}
              </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" />
    <link
      href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.1/dist/css/bootstrap.min.css"
      rel="stylesheet"
      integrity="sha384-4bw+/aepP/YC94hEpVNVgiZdgIC5+VKNBQNGCHeKRQN+PtmoHDEXuppvnDJzQIu9"
      crossorigin="anonymous"
    />
    <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, MMapControls, MMapDefaultFeaturesLayer, MMapMarker} =
          vuefy.module(mappable);
        const {MMapZoomControl} = vuefy.module(await mappable.import('@mappable-world/mappable-controls@0.0.1'));

        const app = Vue.createApp({
          components: {
            MMap,
            MMapDefaultSchemeLayer,
            MMapControls,
            MMapDefaultFeaturesLayer,
            MMapMarker,
            MMapZoomControl
          },
          setup() {
            const points = getRandomPoints(2000);
            const mode = Vue.ref('true');
            const refMap = (ref) => {
              window.map = ref?.entity;
            };
            return {LOCATION, ZOOM_RANGE, refMap, points, mode};
          },
          template: `
                        <div id="toolbar" class="toolbar options">
                            <div>
                                <div class="form-check">
                                    <input
                                        id="hideOutsideViewportTrue"
                                        name="hideOutsideViewport"
                                        class="form-check-input"
                                        v-model="mode"
                                        value="true"
                                        type="radio" />
                                    <label class="form-check-label" for="hideOutsideViewportTrue">
                                        hideOutsideViewport: true
                                    </label>
                                </div>
                                <div class="form-check">
                                    <input
                                        id="hideOutsideViewportExtent"
                                        name="hideOutsideViewport"
                                        class="form-check-input"
                                        v-model="mode"
                                        value="extent"
                                        type="radio" />
                                    <label class="form-check-label" for="hideOutsideViewportExtent">
                                        hideOutsideViewport: {extent: size}
                                    </label>
                                </div>
                            </div>
                        </div>
                        <MMap :location="LOCATION" :zoomRange="ZOOM_RANGE" :ref="refMap">
                            <MMapDefaultSchemeLayer />
                            <MMapDefaultFeaturesLayer />
                            <MMapControls position="right">
                                <MMapZoomControl></MMapZoomControl>
                            </MMapControls>
                            <MMapMarker
                                v-for="(p, i) in points"
                                :key="i"
                                :coordinates="p.coordinates"
                                :markerElement="p.element"
                                :hideOutsideViewport="mode === 'true' ? true : {extent: points[i].size}" />
                        </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>