Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use leaflet-editable with vue-leaflet to edit polygons #380

Open
sstainba opened this issue Mar 26, 2024 · 0 comments
Open

Use leaflet-editable with vue-leaflet to edit polygons #380

sstainba opened this issue Mar 26, 2024 · 0 comments

Comments

@sstainba
Copy link

sstainba commented Mar 26, 2024

I am trying to move a Razor app using leaflet to Vue3 and so far everything works fine. However, I need to be able to edit a polygon. Previously I was using the leaflet-editable package for this and I am at a loss as to how to get this to work with Vue. Note I am also using leaflet-draw and NOT using the leaflet-toolbar instead opting for my own buttons. The leaflet-editable pages show that I should be able to just call .enableEdit() on the shape but I'm specifically having trouble getting that object. I can find the correct geojson object, but then I'm stuck. Nothing I try seems to work. I am sure it's installed/working correctly though as I can using the package to start a new polyline. I just can't figure out how to edit an existing polygon.

<template>
  <v-container :style="divStyle">
    <l-map ref="map"
           v-model:zoom="zoom"
           no-blocking-animations
           :use-global-leaflet="useGlobalLeaflet"
           :center="appStore.mapConfig.mapCenter"
           :options="getMapOptions()"
           @ready="mapReady">
      <l-tile-layer v-for="layer in appStore.mapConfig.layers"
                    :min-zoom="layer.minZoom"
                    :max-zoom="layer.maxZoom"
                    :key="layer.name"
                    :url="layer.url"
                    :name="layer.name"
                    layer-type="base" />
      <l-feature-group ref="features" />
      <l-geo-json v-for="location in locationStore.mapLocations"
                  ref="layers"
                  :name="location.id"
                  :key="location.id"
                  :geojson="location.feature"
                  :style="getFeatureStyle(location)"
                  @add="onLayerAdded(location, $event)"
                  @click="onFeatureClick(location, $event)">
        <l-tooltip>{{location.name}}</l-tooltip>
      </l-geo-json>
      <l-control-layers />
      <l-control position="topleft">
        <v-btn icon rounded="0" density="comfortable"
               @click="zoom++">
          <v-icon>
            mdi-plus-box-outline
          </v-icon>
        </v-btn>
      </l-control>
      <l-control position="topleft">
        <v-btn icon rounded="0" density="comfortable"
               @click="zoom--">
          <v-icon>
            mdi-minus-box-outline
          </v-icon>
        </v-btn>
      </l-control>
      <l-control position="topleft">
        <v-btn icon rounded="0"
               density="comfortable"
               :disabled="!canDrawFeature"
               @click="onDrawPolygonStart">
          <v-icon>
            mdi-vector-polygon
          </v-icon>
        </v-btn>
      </l-control>
      <l-control position="topleft">
        <v-btn icon rounded="0"
               density="comfortable"
               :disabled="!canDrawFeature"
               @click="onDrawMarkerStart">
          <v-icon>
            mdi-map-marker-outline
          </v-icon>
        </v-btn>
      </l-control>
    </l-map>
    <v-overlay :model-value="isBusy"
               :persistent="true"
               :scrim="true"
               class="align-center justify-center">
      <v-progress-circular color="primary"
                           indeterminate
                           size="128" />
    </v-overlay>
  </v-container>
</template>

<script>
  import 'leaflet/dist/leaflet.css';
  import 'leaflet-draw/dist/leaflet.draw.css';

  import 'leaflet';

  import { mapStores } from 'pinia';
  import { useAppStore } from '../stores/app';
  import { useLocationStore } from '../stores/location';
  import { useLayout } from 'vuetify';
  import {
    LMap,
    LTileLayer,
    LGeoJson,
    LTooltip,
    LControl,
    LControlLayers,
    LMarker,
    LPolygon,
    LIcon,
    LFeatureGroup
  } from '@vue-leaflet/vue-leaflet';

  import 'leaflet-draw/dist/leaflet.draw-src.js';
  import 'leaflet-toolbar';
  import 'leaflet-editable';

  export default {
    name: 'Map',
    components: {
      LMap,
      LTileLayer,
      LGeoJson,
      LTooltip,
      LControl,
      LControlLayers,
      LMarker,
      LPolygon,
      LIcon,
      LFeatureGroup,
    },
    data() {
      return {
        useGlobalLeaflet: true,
        layout: useLayout(),
        zoom: 14,
        menuItems: [
          {
            title: 'New Location',
          },
          {
            title: 'Delete Location',
          },
          {
            title: 'New Geometry',
          },
          {
            title: 'Delete Geometry',
          }
        ],
        map: {},
        layers: [],
        polygonDrawer: undefined,
        markerDrawer: undefined,
        isDrawing: false,
        precision: 10,
      };
    },
    watch: {
      selectedLocation(newVal, oldVal) {
        if (oldVal)
          this.resetFeature(oldVal.id);

        if (newVal) {
          this.highlightFeature(newVal.id);
          this.flyToLocation(newVal.id);
        }
      },
    },
    methods: {
      async onFeatureClick(location, e) {
        this.locationStore.selectLocation(location?.id);
      },
      getFeatureStyle(locationId) {
        const style = { ... this.appStore.mapConfig.defaultLayerStyle };
        const target = this.locationStore.locations.find(f => f.id === locationId);
        if (!target) return;
        if (target.feature.properties.Color)
          style.color = target.feature.properties.Color;
        if (target.feature.properties.FillColor)
          style.fillColor = target.feature.properties.FillColor;
        if (target.feature.properties.FillOpacity)
          style.fillOpacity = target.feature.properties.FillOpacity;
        return style;
      },
      computeCentroid(bounds) {
        const lat = ((bounds._southWest.lat - bounds._northEast.lat) / 2) + bounds._northEast.lat;
        const lng = ((bounds._southWest.lng - bounds._northEast.lng) / 2) + bounds._northEast.lng;
        return {
          lat,
          lng
        };
      },
      onLayerAdded(location, e) {
        if (this.selectedLocation?.id !== location.id) return;
        this.highlightFeature(location.id);
        this.flyToLocation(location.id);
      },
      flyToLocation(locationId) {
        if (!locationId) return;
        const target = this.$refs.layers.find(f => f.name === locationId);
        if (!target || !target.leafletObject) return;
        this.$refs.map.leafletObject.flyToBounds(target.leafletObject.getBounds(),
          { padding: [15, 15], maxZoom: this.appStore.mapConfig.maxZoom });
      },
      highlightFeature(locationId) {
        if (!locationId) return;
        const target = this.$refs.layers.find(f => f.name === locationId);
        if (!target || !target?.leafletObject) return;
        target.leafletObject.setStyle(this.highlightedFeatureStyle)
      },
      resetFeature(locationId) {
        if (!locationId) return;
        const target = this.$refs.layers.find(f => f.name === locationId);
        if (!target || !target?.leafletObject) return;
        target.leafletObject.setStyle(this.getFeatureStyle(locationId));
      },
      getMapOptions() {
        return {
          zoomControl: false,
          editable: true,
          attributionControl: false,
        };
      },
      getDrawOptions() {
        return {
          position: 'topleft',
          draw: {
            polygon: {
              allowIntersection: false, // Restricts shapes to simple polygons
              drawError: {
                color: '#e1e100', // Color the shape will turn when intersects
                message: '<strong>Error!<strong> Bounds may not intersect.' // Message that will show when intersect
              },
              shapeOptions: {
                color: '#97009c'
              }
            },
            // disable toolbar item by setting it to false
            polyline: false,
            circle: false,
            circlemarker: false,
            rectangle: false,
            marker: true,
          },
          edit: {
            featureGroup: this.$refs.features,
            remove: false,
          },
        };
      },
      onDrawPolygonStart() {
        this.markerDrawer?.disable();

        if (this.selectedLocation.feature !== null) {
          const layer = this.$refs.layers.find(f => f.name === this.selectedLocation.id);
          console.log('found layer', layer);
          //layer.geojson.geometry.enableEdit();
          this.map.leafletObject.editTools.startPolyline();

        }
        else {
          this.polygonDrawer?.enable();
        }

        this.isDrawing = true;
      },
      onDrawMarkerStart() {
        this.polygonDrawer?.disable();
        this.markerDrawer?.enable();
        this.isDrawing = true;
      },
      mapReady() {
        if (this.isAdmin) {
          this.polygonDrawer = new L.Draw.Polygon(this.map.leafletObject, this.getDrawOptions());
          this.markerDrawer = new L.Draw.Marker(this.map.leafletObject, this.getDrawOptions());
          this.map.leafletObject.on('draw:created', this.onDrawCreated);
        }
      },
      async onDrawCreated(evt) {
        this.isDrawing = false;
        const feature = evt.layer.toGeoJSON(this.precision);
        await this.locationStore.addFeature(this.selectedLocation?.id, feature);
      },
    },

    mounted() {
      this.map = this.$refs.map;
      this.layers = this.$refs.layers;

      this.locationStore.loadLocations();
    },

    computed: {
      ...mapStores(useAppStore, useLocationStore),
      clientHeight() {
        return `${this.$vuetify.display.height - this.layout.mainRect.top - 46
          }px`;
      },
      clientWidth() {
        return `${this.$vuetify.display.width - this.layout.mainRect.left
          }px`;
      },
      divStyle() {
        return `height: ${this.clientHeight}; width: ${this.clientWidth}`;
      },
      isBusy() {
        return this.appStore.loading || this.locationStore.isLoading;
      },
      selectedLocation() {
        return this.locationStore.selectedLocation;
      },
      mapbounds() {
        //return this.$refs.map.leafletObject.getBounds();
        return this.map?.getBounds();
      },
      highlightedFeatureStyle() {
        return this.appStore.mapConfig.selectedLayerStyle;
      },
      isAdmin() {
        return true;
      },
      canDrawFeature() {
        return this.isAdmin
          && !this.isDrawing
          && this.selectedLocation !== undefined;
        //&& this.selectedLocation.feature === null;
      },
    },
  };
</script>
<style>
  g:focus {
    outline: none;
  }

  path:focus {
    outline: none;
  }
</style>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant