源代码|实时预览

为什么我需要它

有很多方法可以在您的网站或应用程序中包含地图:Google 地图、Mapbox、Leaflet 等。这很简单。某些服务允许您只需单击几下即可完成。

但是当您需要自定义设计、显示一些数据集或做任何您想做的事情时,情况就会变得很糟糕。此外,在 Vue 或 React 中,您不能使用 JSX,而必须使用命令式抽象 JavaScript API(但我使用 Vue,因为我对模板和反应性非常感兴趣)。

还有一些图书馆对私人项目来说不是免费的。

所以我不得不再次在我决定的地图上显示一些数据:我想完全控制我的代码,我将创建自己的带有二十一点和妓女的地图。

第一步:创建静态地图。

让我们从带有 Babel 和 sass 的简单 vue-cli 3 应用开始。

我们需要 D3 和d3-tile(它不包含在 d3 npm 包中)来渲染地图图块。

yarn add d3 d3-tile

实际上我们不需要完整的 d3 代码。对于一个简单的地图,我们只需要 d3-geo 进行地图投影和 d3-tile 生成瓦片,因此我们将只包含这些包。

接下来我们应该定义一些设置,如比例、宽度、高度和初始坐标。通常我通过计算安装时元素的大小来使我的所有图表都响应它的容器。

<script>
const d3 = {
  ...require('d3-geo'),
  ...require('d3-tile'),
};

export default {
  props: {
    center: {
      type: Array,
      default: () => [33.561041, -7.584838],
    },
    scale: {
      type: [Number, String],
      default: 1 << 20,
    },
  },
  data () {
    return {
      width: 0,
      height: 0,
    };
  },
  mounted () {
    const rect = this.$el.getBoundingClientRect();

    this.width = rect.width;
    this.height = rect.height;
  },
  render () {
    if (this.width <= 0 || this.height <= 0) {
      // the dummy for calculating element size
      return <div class="map" />;
    }

    return (
      <div class="map">our map will be here</div>
    );
  },
};
</script>

<style lang="scss" scoped>
.map {
  width: 100%;
  height: 100%;
}
</style>

现在定义投影和切片生成器。

export default {
  // ... 
  computed: {
    projection () {
      return d3.geoMercator()
        .scale(+this.scale / (2 * Math.PI))
        .translate([this.width / 2, this.height / 2])
        .center(this.center)
      ;
    },
    tiles () {
      return d3.tile()
        .size([this.width, this.height])
        .scale(+this.scale)
        .translate(this.projection([0, 0]))()
      ;
    },
  },
  // ...
};

我总是将 d3 辅助函数定义为计算属性,因此当某些参数发生变化时,Vue 会重新计算它们并更新我们的组件。

现在我们拥有了显示地图所需的一切,我们只需渲染生成的图块:

export default {
  render () {
    if (this.width <= 0 || this.height <= 0) {
      return <div class="map" />;
    }

    return (
      <div class="map">
        <svg viewBox={`0 0 ${this.width} ${this.height}`}>
          <g>
            {this.tiles.map(t => (
              <image
                key={`${t.x}_${t.y}_${t.z}`}
                class="map__tile"
                xlinkHref={`https://a.tile.openstreetmap.org/${t.z}/${t.x}/${t.y}.png `}
                x={(t.x + this.tiles.translate[0]) * this.tiles.scale}
                y={(t.y + this.tiles.translate[1]) * this.tiles.scale}
                width={this.tiles.scale}
                height={this.tiles.scale}
              />
            ))}
          </g>
        </svg>
      </div>
    );
  },
};

在这里,我们通过 d3-tile 生成的瓦片并从瓦片服务器请求图像。

您可以在此处找到其他服务器,或者您甚至可以使用自定义样式托管您自己的平铺服务器。

不要忘记添加版权。

<div class="map__copyright">
  ©&nbsp;
  <a
    href="https://www.openstreetmap.org/copyright"
    target="_blank"
  >OpenStreetMap&nbsp;</a>
  contributors
</div>
.map {
  // ...
  position: relative;
  font-family: Arial, sans, sans-serif;

  &__copyright {
    position: absolute;
    bottom: 8px;
    right: 8px;
    padding: 2px 4px;
    background-color: rgba(#ffffff, .6);
    font-size: 14px;
  }
}

现在我们有了卡萨布兰卡的静态地图。还不是很令人兴奋。

[卡萨布兰卡地图](https://res.cloudinary.com/practicaldev/image/fetch/s--F7-yd3U---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://thepracticaldev .s3.amazonaws.com/i/i5xqgdvy0qobkeyjffob.png)

第 2 步:添加地图控件。

对我来说最令人兴奋的是 Vue 如何让创建交互式地图的方式变得更简单。我们只是更新投影参数和地图更新。第一次就像简单的魔术一样!

我们将通过拖动地图来制作缩放按钮和位置控制。

让我们从拖动开始。我们需要在组件数据中定义投影转换道具,并在 svg 元素上定义一些鼠标事件监听器(或者您可以在瓦片组上监听它们)。

<script>
// ...

export default {
  // ...
  data () {
    return {
      // ...
      translateX: 0,
      translateY: 0,

      touchStarted: false,
      touchLastX: 0,
      touchLastY: 0,
    };
  },
  computed: {
    projection () {
      return d3.geoMercator()
        .scale(+this.scale / (2 * Math.PI))
        .translate([this.translateX, this.translateY])
        .center(this.center)
      ;
    },
    // ...
  },
  mounted () {
    // ...
    this.translateX = this.width / 2;
    this.translateY = this.height / 2;
  },
  methods: {
    onTouchStart (e) {
      this.touchStarted = true;

      this.touchLastX = e.clientX;
      this.touchLastY = e.clientY;
    },
    onTouchEnd () {
      this.touchStarted = false;
    },
    onTouchMove (e) {
      if (this.touchStarted) {
        this.translateX = this.translateX + e.clientX - this.touchLastX;
        this.translateY = this.translateY + e.clientY - this.touchLastY;

        this.touchLastX = e.clientX;
        this.touchLastY = e.clientY;
      }
    },
  },
  render () {
    // ...
    return (
      <div class="map">
        <svg
          viewBox={`0 0 ${this.width} ${this.height}`}
          onMousedown={this.onTouchStart}
          onMousemove={this.onTouchMove}
          onMouseup={this.onTouchEnd}
          onMouseleave={this.onTouchEnd}
        >
          // ...
        </svg>
        // ...
      </div>
    );
  },
};
</script>

<style lang="scss" scoped>
.map {
  // ...

  &__tile {
    // reset pointer events on images to prevent image dragging in Firefox
    pointer-events: none;
  }
  // ...
}
</style>

哇!我们只是更新翻译值并加载新图块,以便我们可以探索世界。但是如果没有缩放控件,它就不是很舒服,所以让我们来实现它。

我们需要在组件的数据中移动scale属性,添加zoom属性并渲染缩放按钮。

根据我的经验,最小和最大磁贴的缩放级别是 10 和 27(老实说,我不太确定这是否适用于所有磁贴提供商)。

<script>
// ...

const MIN_ZOOM = 10;
const MAX_ZOOM = 27;

export default {
  props: {
    center: {
      type: Array,
      default: () => [-7.584838, 33.561041],
    },
    initialZoom: {
      type: [Number, String],
      default: 20,
    },
  },
  data () {
    return {
      // ...
      zoom: +this.initialZoom,
      scale: 1 << +this.initialZoom,
    };
  },
  // ...
  watch: {
    zoom (zoom, prevZoom) {
      const k = zoom - prevZoom > 0 ? 2 : .5;

      this.scale = 1 << zoom;
      this.translateY = this.height / 2 - k * (this.height / 2 - this.translateY);
      this.translateX = this.width / 2 - k * (this.width / 2 - this.translateX);
    },
  },
  // ...
  methods: {
    // ...
    zoomIn () {
      this.zoom = Math.min(this.zoom + 1, MAX_ZOOM);
    },
    zoomOut () {
      this.zoom = Math.max(this.zoom - 1, MIN_ZOOM);
    },
  },
  render () {
    // ...
    return (
      <div class="map">
        <div class="map__controls">
          <button
            class="map__button"
            disabled={this.zoom >= MAX_ZOOM}
            onClick={this.zoomIn}
          >+</button>
          <button
            class="map__button"
            disabled={this.zoom <= MIN_ZOOM}
            onClick={this.zoomOut}
          >-</button>
        </div>
        //...
      </div>
    );
  },
};
</script>

<style lang="scss" scoped>
.map {
  // ...
  &__controls {
    position: absolute;
    left: 16px;
    top: 16px;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    height: 56px;
  }
  &__button {
    border: 0;
    padding: 0;
    width: 24px;
    height: 24px;
    line-height: 24px;
    border-radius: 50%;
    font-size: 18px;
    background-color: #ffffff;
    color: #343434;
    box-shadow: 0 1px 4px rgba(0, 0, 0, .4);

    &:hover,
    &:focus {
      background-color: #eeeeee;
    }

    &:disabled {
      background-color: rgba(#eeeeee, .4);
    }
  }
  // ...
}
</style>

这里是。只需两个步骤,我们就使用 Vue、D3 和 OpenStreetMap 创建了简单的交互式地图。

结论

使用 D3 的强大功能和 Vue 的反应性创建自己的地图视图组件并不难。我认为最重要的事情之一是对 DOM 的完全控制,而不是使用一些抽象地图渲染器的 API,它会用我可爱的元素做一些晦涩的事情。

当然,要制作出强大的地图,我们需要实现更多功能,如平滑缩放、最大边界等。但是所有的东西都是完全可定制的,所以你可以做任何你想做或需要做的事情。

如果您觉得这篇文章有用,我可以写更多关于如何改进此地图并在其上显示数据的信息。

请随时提出您的问题。

Logo

前往低代码交流专区

更多推荐