官术网_书友最值得收藏!

Application overview

The mapviewer project is a Dart web application with an index.html entry point that kicks off the application on main.dart:

main() async {
  quakeMap = new MapDisplay(querySelector('#mapview'), width, height);
  await quakeMap.loadImage();

  featPlotter = new FeaturePlotter(width, height, quakeMap.mapCtx);

  quakeMap.showPopup = showPopup;
  quakeUpdate();

  querySelector('#zoombtn').onClick.listen(zoomMap);
  querySelector('#locatebtn').onClick.listen(locateUser);
  querySelector('#sortbtn').onClick.listen(sortFeatures);

  new Timer.periodic(new Duration(seconds: 60), quakeUpdate);
  new Timer.periodic(new Duration(milliseconds: 100), animationUpdate);
}

The MapDisplay class sets up the map on the web page (on the div element with the ID mapview) and the image is loaded in. Then, quakeUpdate() is called to ensure the initial display of the map and data on the page.

Once the initial display is handled, the Zoom button is connected to the zoomMap function. Then the two timers have handlers to deal with updating the quake data and the animation display. The FeaturePlotter class handles the data and display updates and is used by quakeUpdate and animationUpdate.

Drawing the map image

The HTML5 canvas will host the live display. The background is a block-like map of the world; feel free to find and use a different version!

Let's have a look at the following screenshot:

Drawing the map image

The red lines represent 0 degrees of latitude and longitude.

Plotting on the map

The geometry point contains three pieces of data in a list called coordinates. They are the longitude, latitude, and also the depth in kilometers of where the earthquake originated:

{type: Point, coordinates: [-117.4505005, 34.2316666, 13.31]}

To map these coordinates to the map, we use the method toPoint in the FeaturePlotter class in the file featureplotter.dart, which performs the conversion as follows:

  Point toPoint(double longitude, double latitude) {
    double x = 0.0;
    double y = 0.0;
    double hdeg = width.toDouble() / 360;
    double vdeg = height.toDouble() / 180;

    x = (width / 2) + (hdeg * longitude);
    y = (height / 2) - (vdeg * latitude);

    return new Point(x.toInt(), y.toInt());
}

The longitude and latitude is based on the spherical nature of the Earth, and the x axis is 360 degrees wide with 0 being the mid-point. The y axis is 180 with 0 being the midpoint at the equator. In terms of the GeoJSON data, the x values are in the range of 180 to +180 and y is from -90 to 90.

Note

Dart, at the time of writing is at version 1.11 and you may be wondering if it will reach version 2 in the near future. Yes, it will!

During the first Dart Developer Summit, version 2 of Dart was mentioned for the first time by Lars Bak. While it was hinted that there may be some language changes, it was strongly emphasized these would only be made with tools to handle updating existing code. In Bak's words, "this is not Python!" This quote is a reference for the somewhat disruptive move from Python 2 to Python 3.

One key change that has already begun is the moving of some packages out of the SDK. This allows them to update on their own schedule. For example, dart:html has to respond more quickly than the SDK update cycle to web browser changes.

Animating the display

The animationUpdate handler set up in the main function in the main.dart file fires the FeaturePlotter method updateDisplay:

void animationUpdate([Timer t = null]) {
  featPlotter.updateDisplay();
}

This method in turn calls an update on each map indicator object instance, as shown in the following code snippet. The userLocation will be covered later in this chapter:

void updateDisplay() {
   mapIndicators.forEach((mapIndicator) => mapIndicator.update());
   userLocation.forEach((mapIndicator) => mapIndicator.update());
}

The MapIndicator class in mapindicator.dart handles the display and animation of the feature. The display will be a growing circle point with the maximum size of the point matching the magnitude of the earthquake event. The constructor and update method handle the task of keeping the plotted shape within range:

  MapIndicator(this.x, this.y, this.ctx, this.magnitude) {
    maxWidth = (width + 1) + magnitude * 2;
  }

  void update() {
    width++;
    if (width == maxWidth) width = 0;
    draw();
  }

This will cause the point width to grow upwards to the limit before being reset back to a value of 0, as shown here:

Animating the display

The circle is drawn using the arc method of the HTML5 canvas:

  void draw() {
    ctx
      ..beginPath()
      ..arc(x, y, width, 0, 2 * PI, false)
      ..fillStyle = colorPrimary
      ..fill()
      ..lineWidth = 1
      ..strokeStyle = colorSecondary
      ..stroke();
  }

The x and y specify the point where the feature will be drawn and w specifies the radius. The next two parameters are the start and end angles, which are measured in radians, and in a circle there are 2 * PI radians. The constant value for PI is taken from the dart:math library, which is amongst the imports of this class. The last parameter of this method is a flag to determine whether the arc should be drawn counterclockwise or not.

Fetching the data

The data is fetched as an HttpRequest in the same manner as in the grid view project, and the fields we are interested in are put into the list quakepoints before being filtered into the geoFeatures list:

  fetchFeatureList() async {
    geoFeatures.clear();

    String data = await HttpRequest.getString(jsonSrc);
    List items;
    List quakePoints = [];

    try {
      items = JSON.decode(data);
    } catch (exception, stacktrace) {
      print(exception);
      print(stacktrace);
    }

    if (items != null) {
      items.forEach((String post) {
        Map feature = JSON.decode(post);

        List quakedata = [
          feature['geometry']['coordinates'][0],
          feature['geometry']['coordinates'][1],
          feature['properties']['mag'],
          feature['properties']['place'],
          feature['properties']['type'],
          feature['geometry']['coordinates'][2]
        ];

        quakePoints.add(quakedata);
      });

      quakePoints.where((qp) => qp[4] == 'earthquake').forEach((qp) {
        geoFeatures.add(qp);
      });
    }
  }

All the items returned are added to the quake points list, but we only want to add the earthquake ones to the geoFeatures list. The list may contain other types such as quarry blast.

Using a where clause, we can filter the list an item at a time. The list item is run through the function, and the list item at index number 4 is compared against the string earthquake.

This is a very convenient method for getting only the required data and best of all it is available on any iterable object. On top of this, there are number of variants such as firstWhere and lastWhere that help to quickly get a reference to the data that matches a criterion.

Updating the map indicators

Once a list of geoFeatures is available, it can be used to construct a list of mapIndicator objects that will appear on the map. The entire list is cleared out each time so that the number of indicators on the map does not build up too much over time. Let's take a look at the following screenshot:

updateData() async {
  await fetchFeatureList();
  mapIndicators.clear();
  geoFeatures.forEach((List feature) {
    Point pos = toPoint(feature[0], feature[1]);
    MapIndicator mi;
    mi = new MapIndicator(pos.x, pos.y, ctx, feature[2].toInt());
    mi.summary = "Magnitude ${feature[2]} - ${feature[3]}.";
    mapIndicators.add(mi);
  });
}

This function is marked async as there is an await being used for the fetchFeatureList method call. Once the features are available, the mapIndicators list is reset and a new one is constructed.

Mouse over popups

To give the user more details about the feature on the map, a popup appears at the top of the screen when the pointer is moved over a map indicator. To get the mouse pointer coordinates, a handler can be added to handle the mouse move events on the canvas element:

MapDisplay(CanvasElement mapcanvas, this.width, this.height) {
  mapctx = mapcanvas.getContext("2d");
  mapcanvas.onMouseMove.listen(mouseMove);
}

This is carried out in the MapDisplay constructor and the method handles the triggering of the implementation function. This function is a field in the class that is set by the page using the map display:

void mouseMove(MouseEvent e) {
  if (showPopup != null)
    showPopup(e.client.x - 50, e.client.y - 90);
}

The implementation of the showPopup function is in main.dart. It displays the initially hidden popup above the map and then populates it with the feature details:

void showPopup(int x, int y) {
  bool inHotspot = false;

  if (zoomed) {
    x = x ~/ 2;
    y = y ~/ 2;
  }

  featPlotter.hotspotInfo.forEach((Rectangle<int> key, String value) {
    if (key.containsPoint(new Point(x, y))) {
      inHotspot = true;
      querySelector('#popup').innerHtml = value;
    }
  });

  if (inHotspot)
    querySelector('#popup').style.visibility = "visible";
}

If the map display is zoomed in, then the coordinates must be updated; this is performed with the ~/ division operator, which returns an integer value. This is faster than the equivalent (x/2).toInt()and gives the required whole numbers for plotting:

Mouse over popups

The Map containing the hotspotInfo is iterated over with a convenient forEach loop, giving both the dictionary key and the respective value. The current point of the mouse relative to the top-left corner of the canvas element is tested against each rectangle that defines the hotspot (the hotspot is the area on the map that will trigger the popup).

The hotspots are stored as a list of Rectangle<int> objects generated when the data list is being updated by the FeaturePlotter class. This class is from the dart:math package:

  void updateHotspots() {
    hotspotInfo.clear();
    mapIndicators.forEach((MapIndicator mi) {
      Rectangle<int> rect = new Rectangle(mi.x - mi.maxWidth,
          mi.y - mi.maxWidth, mi.maxWidth * 2, mi.maxWidth * 2);

      hotspotInfo[rect] = mi.summary;
    });
  }

The hotspot is defined as a square area around the map indicator when it is at its maximum width. The summary property gives the text to be displayed. The screenshot of the quake map is as follows:

Mouse over popups

Hopefully, this can help you in improving your geographical knowledge and learning more about Dart!

When debugging in Dartium using a REST or other similar web service, it is possible to get extra logging in the console window of the browser's developer tools for XMLHTTPrequests.

Bring up the context menu (right-click), and there is an option named Log XMLHttpRequests that displays the HTTP requests made by the web page in the console output window:

Tip

[-121.5371704, 36.8165016, 0.93]
129.91739911111114 177.278328
2.2222222222222223 3.3333333333333335
maxWidth 2
XHR finished loading: GET "http://127.0.0.1:8080/api/quake/v1/latest".

This can be invaluable when debugging a project and saves adding logging to every part of the code that makes an HTTP request.

Zooming into the display

The canvas element has a range of features for 2D drawing, and this includes a scaling feature that we can use to hone in on the most active part of the map, North America. In main.dart, a flag exists called zoomed, defaulting to false, that aids the application in keeping track of the current display. A button is bound to the zoom code implementation:

zoomMap(Event evt) async {
  if (zoomed == true) {
    zoomed = false;
    quakeMap.mapCtx.resetTransform();
    quakeMap.mapCtx.scale(1, 1);
  } else {
    zoomed = true;
    quakeMap.mapCtx.scale(2, 2);
  }
  quakeMap.drawMapGrid();
}

The canvas is toggled between scaling settings, with a critical call to the method resetTransfom. Without this, the scaling would not be back to normal. The entire map view must be redrawn immediately so that the user has a responsive experience. Let's have a look at the following screenshot:

Zooming into the display

If you watch this page for a while, you will find the California and Alaska feature embedded heavily in the data. As the pop-up code handles the change in scale, these are still available. Pressing the Zoom button a second time returns to the full world view.

Tip

The print function is not just for command-line applications but can be used in web applications, too. The output appears in the Developer Tools console log window, which can be displayed by right-clicking on the page to bring up the context menu and then clicking Inspect Element.

主站蜘蛛池模板: 祁连县| 阿克| 吴桥县| 拜城县| 出国| 三河市| 高清| 龙井市| 泽库县| 台中市| 峨山| 吐鲁番市| 竹北市| 房产| 广汉市| 松阳县| 南投县| 仲巴县| 西充县| 讷河市| 乐山市| 郯城县| 苗栗市| 浦城县| 读书| 潼南县| 兴城市| 墨竹工卡县| 上林县| 景东| 景东| 崇阳县| 博乐市| 大庆市| 景洪市| 开鲁县| 信阳市| 哈巴河县| 公主岭市| 招远市| 都匀市|