Zpět na hlavní stránku

Mapy.cz filtrování v kruhu

Přidal Filip Kalousek dne

Náhled

Je dostupná i JS verze

Stack na projekt

Použité packages

Použití

Při instalaci @angular/material zvolíme y na BrowserAnimations

$ ng new <nazev_projektu>
$ cd <nazev_projektu>
$ npm i circle-to-polygon
$ ng add @angular/material

Příprava

Package circles-to-polygon mě trošku zlobil musel jsem ho tedy usměrnit v tsconfig.json a v angular.json.

tsconfig.json

"compilerOptions":{
  "allowSyntheticDefaultImports": true
}

angular.json

"build": {
  "options": {
    "allowedCommonJsDependencies":[
      "circle-to-polygon"
    ]
  },
}

Import api.mapy.cz

Do head vložíme:

<script src="https://api.mapy.cz/loader.js"></script>
<script>
  Loader.load(null, {
    suggest: true,
  });
</script>

V komponentě, kde budeme používat api.mapy.cz vložíme pod importy:

import { Component, OnInit } from '@angular/core';
import circleToPolygon from 'circle-to-polygon';
/* ZDE BUDOU DALŠÍ IMPORTY */

declare var SMap: any;
declare var JAK: any;

Modely

Je mi jasný, že pro implemetaci nebudete používat modely přesně 1:1, ale takhle to je na demonstraci.

Vytvoříme soubor IPoint.model.ts a IPolyOption.model.ts

export interface IPointModel {
  title: string;
  lat: number;
  lon: number;
}
export interface IPolyOptionModel {
  x: number;
  y: number;
}

Material Design

V souboru app.module.ts nebo v jakém modulu zrovna budete používát komponentu na filtrování vložíte a naimportujete následující:

imports:[
  /* DALŠÍ IMPORTY */
  MatButtonModule,
  MatSliderModule,
  MatIconModule
]

Nastavíme property v komponentě

@ViewChild('map', { static: false }) mainMap: ElementRef;
@ViewChild('searchInputMaps', { static: true }) placeInput: ElementRef;

points: IPointModel[] = [];
filteredResults: IPointModel[] = [];
polyPoints: IPolyOptionModel[] = [];

sliderKilometers: number = 3;

locationCoords = { lat: 0, lon: 0 };
mapDOM: any;
radiusLayer: any;

Init komponenty

ngOnInit() {
  this.getData().then(() => {
    this.getUserLocation();
    // Default coords, kde se zoomne a default scale mapy
    this.setupMap(
      { coords: { latitude: 49.205939, longitude: 16.5995405 } },8);
    this.sliderChange({ value: this.sliderKilometers });
  });
}

Základní metoda, která nám bude vracet data

async getData() {
    this.points = this._API.getData();
}

Získání polohy uživatele

getUserLocation() {
  if (!navigator.geolocation) {
    alert('Prohlížeč nepodporuje geolokaci.');
  } else {
    navigator.geolocation.getCurrentPosition((e) => {
        this.setupMap(e, 15);
        this.sliderChange({ value: this.sliderKilometers });
    },
      null,
      {
        timeout: 10000,
      }
    );
  }
}

Vytvoříme mapu

setupMap(position: any, scale: number) {
  // Při každém zavolání mapy smažeme všechny elementy, aby se nám nepřekreslovaly
  this.mainMap.nativeElement.innerHTML = '';
  
  const coords = position.coords;
  this.locationCoords.lat = coords.latitude;
  this.locationCoords.lon = coords.longitude;
  
  let center = SMap.Coords.fromWGS84(coords.longitude, coords.latitude);
  this.mapDOM = new SMap(this.mainMap.nativeElement, center, scale);
  this.mapDOM.addDefaultLayer(SMap.DEF_BASE).enable();
  
  // Nastavení controls, kterýma bude moci uživatel používat mapu
  this.mapDOM.addControl(
    new SMap.Control.Mouse(SMap.MOUSE_PAN | SMap.MOUSE_ZOOM | SMap.MOUSE_WHEEL, {
      minDriftSpeed: 1 / 0,
    })
  );
  this.mapDOM.addControl(
    new SMap.Control.Keyboard(SMap.KB_PAN | SMap.KB_ZOOM, {
      focusedOnly: false,
    })
  );
  this.mapDOM.addControl(
    new SMap.Control.Zoom(
      {},
      {
        titles: ['Přiblížit', 'Oddálit'],
        showZoomMenu: false,
      }
    ),
    {
      right: '-1.5rem',
      bottom: '7rem',
    }
  );
  // Zavoláme našeptávač
  this.setSuggest();
}

Nastavíme našeptávač, kam bude moci uživatel moci mít výsledky

setSuggest() {
  let suggest = new SMap.Suggest(this.placeInput.nativeElement, {
    provider: new SMap.SuggestProvider({
      updateParams: (params: any) => {
        params.lang = 'cs';
      },
    }),
  });
  suggest.addListener('suggest', (res: any) => {
    setTimeout(() => {
      this.setupMap(
        {
          coords: {
            latitude: res.data.latitude,
            longitude: res.data.longitude,
          },
        },
        13
      );
      this.sliderChange({ value: this.sliderKilometers });
    }, 500);
  });
}

Nastavení eventu když se změní poloha, nebo uživatel použije slider

sliderChange(event: any) {
  if (this.radiusLayer != null || this.radiusLayer != undefined) {
    this.radiusLayer.clear();
    this.radiusLayer.removeAll();
    this.radiusLayer.redraw();
  }
  this.sliderKilometers = event.value;
  this.drawMapRadius(event.value);
}

Kreslení radiusu

drawMapRadius(radius: number) {
  // Vynulování všech vyfiltrovaných hodnot a předešlého radiusu
  this.polyPoints = [];
  this.filteredResults = [];
  // Vytvoření vrstvy, do které budeme vkládat "kruh"
  this.radiusLayer = new SMap.Layer.Geometry();
  
  // Nastavení barev apod.
  const options = {
    color: '#3a6a3a',
    opacity: 0.1,
    outlineColor: '#3a6a3a',
    outlineOpacity: 0.5,
    outlineWidth: 3,
  };

  // Přidání vrstvy a její následné povolední
  this.mapDOM.addLayer(this.radiusLayer);
  this.radiusLayer.enable();
  
  // r = radius z matslideru, nejak random to pomoci "vzorce" funguje
  const r = radius * 1000;
  
  // Stanovení oblasti, kde se bude vyhledávat
  const center = SMap.Coords.fromWGS84(
    this.locationCoords.lon,
    this.locationCoords.lat
  );
  
  // Získání poloměru z kruhu, díky - https://napoveda.seznam.cz/forum/threads/113565/1
  const point = SMap.Coords.fromWGS84(
  this.locationCoords.lon,
  this.locationCoords.lat - ((Math.sign(this.locationCoords.lat) * r) / 10e6) * 90);
  
  
  // Přidání vrstvy
  const circle = new SMap.Geometry(
    SMap.GEOMETRY_CIRCLE,
    null,
    [center, point],
    options
  );
  
  this.radiusLayer.addGeometry(circle);

  // SET MAP ZOOM - VZOREC JESTE POFIXOVAT
  const zoomCalc = 13 - radius / 19;
  this.mapDOM.setCenterZoom(center, parseInt(zoomCalc.toFixed(0)), true);

  // Protože api.mapy.cz fungují přes polygony, musíme si kruh převést do polygonu
  // který bude mít 32 hran
  const polygonCoords = circleToPolygon(
    [circle._coords[0].x, circle._coords[0].y],
    r,
    32
  ).coordinates;
  polygonCoords[0].forEach((e, i) => {
    this.polyPoints.push({ x: e[0], y: e[1] });
  });

  // Vložíme marker, do kterého budeme přidávat vygenerované body z mapy
  const markerLayer = new SMap.Layer.Marker();

  this.points.forEach((e, i) => {
    const editedPoints = SMap.Coords.fromWGS84(e.lon, e.lat);
    if (
      SMap.Util.pointInPolygon(
        [editedPoints.x, editedPoints.y],
        this.polyPoints.map(Object.values)
      )
    ) {
      this.filteredResults.push(e);
    }

    let c = SMap.Coords.fromWGS84(e.lon, e.lat);

    const marker = JAK.mel(
      'p',
      {
        title: e.title,
      },
      {
        width: '0.75rem',
        height: '0.75rem',
        background: '#3a6a3a',
        borderRadius: '50%',
        border: '1px solid #fff',
        cursor: 'pointer',
        marginTop: '1.5rem',
      }
    );
    const pointMarker = new SMap.Marker(c, i, {
      url: marker,
    });
    markerLayer.addMarker(pointMarker);
  });
  this.mapDOM.addLayer(markerLayer);
  markerLayer.enable();
}

Vrácení dat pro slider aby to bylo "heski"

formatLabel(value: number) {
  return value + 'km';
}

Finální html, aby ukazovalo 😙

<div #map class="map"></div>

<!-- Když uživatel omylem odklikne popup, tak bude moci mít možnost povolit svoji polohu --->
<button mat-icon-button (click)="getUserLocation()">
  <mat-icon> my_location </mat-icon>
</button>

<!--- Našeptávač --->
<input #searchInputMaps type="text" placeholder="Zadej místo"/>

<!-- Slider na měnění radiusu--->
<mat-slider 
    thumbLabel[displayWith]="formatLabel"
    min="0.5"
    max="100"
    step="0.2"
    [value]="sliderKilometers"
    (input)="sliderChange($event)">
</mat-slider>

JS verze

Aktualizace přepsání do čistého JS, jediná použítá lib je circle-to-polygon. Ta ale je vložena rovnou v JS, protože se mi s tím nechtělo patlat, ale enjoy!