Skip to content

Commit

Permalink
Add connector & collection for drawing on map
Browse files Browse the repository at this point in the history
- Add the GeoPoints collection, with methods for serializing to GeoJson
- Add a model that listens to a GeoPoints collection and updates a CesiumVectorData model with new geometry
- Use these in the DrawToolView
- Enable clearing a polygon
- Show on first click, line on second click, polygon on subsequent clicks

Issue #2180
  • Loading branch information
robyngit committed Sep 21, 2023
1 parent ef72df5 commit 0fc87fb
Show file tree
Hide file tree
Showing 6 changed files with 656 additions and 184 deletions.
215 changes: 215 additions & 0 deletions src/js/collections/maps/GeoPoints.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
"use strict";

define(["backbone", "models/maps/GeoPoint"], function (Backbone, GeoPoint) {
/**
* @class GeoPoints
* @classdesc A group of ordered geographic points.
* @class GeoPoints
* @classcategory Collections/Maps
* @extends Backbone.Collection
* @since x.x.x
* @constructor
*/
var GeoPoints = Backbone.Collection.extend(
/** @lends GeoPoints.prototype */ {
/**
* The class/model that this collection contains.
* @type {Backbone.Model}
*/
model: GeoPoint,

/**
* Given a point in various formats, format it such that it can be used to
* add to this collection.
* @param {Array|Object|GeoPoint} point - Accepted formats are:
* - An array of the form [longitude, latitude], with an optional third
* element for height
* - An object with a "longitude" and "latitude" property, and
* optionally a "height" property
* - A GeoPoint model
* @returns {Object|GeoPoint} Returns an object with "longitude" and
* "latitude" properties, and optionally a "height" property, or a
* GeoPoint model.
*/
formatPoint: function (point) {
let attributes = {};
if (Array.isArray(point) && point.length > 1) {
attributes.longitude = point[0];
attributes.latitude = point[1];
if (point[2]) {
attributes.height = point[2];
}
} else if (
point instanceof GeoPoint ||
(point.latitude && point.longitude)
) {
attributes = point;
}
return attributes;
},

/**
* Add a point to the collection. Use this rather than the Backbone add
* method to allow for different formats of points to be added.
* @param {Array|Object|GeoPoint} point - See {@link formatPoint} for
* accepted formats.
* @returns {GeoPoint} Returns the GeoPoint model that was added.
*/
addPoint: function (point) {
point = this.formatPoint(point);
return this.add(point);
},

/**
* Remove a specific point from the collection. Use this rather than the
* Backbone remove method to allow for different formats of points to be
* removed.
* @param {Array|Object|GeoPoint|Number} indexOrPoint - The index of the
* point to remove, or the point itself. See {@link formatPoint} for
* accepted formats.
* @returns {GeoPoint} Returns the GeoPoint model that was removed.
*/
removePoint(indexOrPoint) {
if (typeof indexOrPoint === "number") {
this.removePointByIndex(indexOrPoint);
} else if (Array.isArray(indexOrPoint)) {
this.removePointByAttr(indexOrPoint);
}
},

/**
* Remove a point from the collection based on its attributes.
* @param {Array|Object|GeoPoint} point - Any format supported by
* {@link formatPoint} is accepted.
* @returns {GeoPoint} Returns the GeoPoint model that was removed.
*/
removePointByAttr: function (point) {
point = this.formatPoint(point);
const model = this.findWhere(point);
return this.remove(model);
},

/**
* Remove a point from the collection based on its index.
* @param {Number} index - The index of the point to remove.
* @returns {GeoPoint} Returns the GeoPoint model that was removed.
*/
removePointByIndex: function (index) {
if (index < 0 || index >= this.length) {
console.warn("Index out of bounds, GeoPoint not removed.");
return;
}
const model = this.at(index);
return this.remove(model);
},

/**
* Convert the collection to a GeoJSON object. The output can be the
* series of points as Point features, the points connected as a
* LineString feature, or the points connected and closed as a Polygon.
*
* Note: For a "Polygon" geometry type, when there's only one point in the
* collection, the output will be a "Point". If there are only two points,
* the output will be a "LineString", unless `forceAsPolygon` is set to
* true.
*
* @param {String} geometryType - The type of geometry to create. Can be
* "Point", "LineString", or "Polygon".
* @param {Boolean} [forceAsPolygon=false] - Set to true to enforce the
* output as a polygon for the "Polygon" geometry type, regardless of the
* number of points in the collection.
* @returns {Object} Returns a GeoJSON object of type "Point",
* "LineString", or "Polygon".
*/
toGeoJson: function (geometryType, forceAsPolygon = false) {
if (!forceAsPolygon && geometryType === "Polygon" && this.length < 3) {
geometryType = this.length === 1 ? "Point" : "LineString";
}
return {
type: "FeatureCollection",
features: this.toGeoJsonFeatures(geometryType),
};
},

/**
* Convert the collection to a GeoJSON object. The output can be the
* series of points as Point features, the points connected as a
* LineString feature, or the points connected and closed as a Polygon.
* @param {"Point"|"LineString"|"Polygon"} geometryType - The type of
* geometry to create.
* @returns {Object[]} Returns an array of GeoJSON features.
*/
toGeoJsonFeatures: function (geometryType) {
switch (geometryType) {
case "Point":
return this.toGeoJsonPointFeatures();
case "LineString":
return [this.toGeoJsonLineStringFeature()];
case "Polygon":
return [this.toGeoJsonPolygonFeature()];
default:
return [];
}
},

/**
* Convert the collection to an array of GeoJSON point features.
* @returns {Object[]} Returns an array of GeoJSON point features.
*/
toGeoJsonPointFeatures: function () {
return this.models.map((model) => {
return model.toGeoJsonFeature();
});
},

/**
* Convert the collection to a GeoJSON LineString feature.
* @returns {Object} Returns a GeoJSON LineString feature.
*/
toGeoJsonLineStringFeature: function () {
return {
type: "Feature",
geometry: {
type: "LineString",
coordinates: this.to2DArray(),
},
properties: {},
};
},

/**
* Convert the collection to a GeoJSON Polygon feature. The polygon will
* be closed if it isn't already.
* @returns {Object} Returns a GeoJSON Polygon feature.
*/
toGeoJsonPolygonFeature: function () {
const coordinates = this.to2DArray();
// Make sure the polygon is closed
if (coordinates[0] != coordinates[coordinates.length - 1]) {
coordinates.push(coordinates[0]);
}
return {
type: "Feature",
geometry: {
type: "Polygon",
coordinates: [coordinates],
},
properties: {},
};
},

/**
* Convert the collection to an array of arrays, where each sub-array
* contains the longitude and latitude of a point.
* @returns {Array[]} Returns an array of arrays.
*/
to2DArray: function () {
return this.models.map((model) => {
return model.to2DArray();
});
},
}
);

return GeoPoints;
});
169 changes: 169 additions & 0 deletions src/js/models/connectors/GeoPoints-VectorData.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/*global define */
define([
"backbone",
"collections/maps/GeoPoints",
"models/maps/assets/CesiumVectorData",
], function (Backbone, GeoPoints, CesiumVectorData) {
"use strict";

/**
* @class PointsVectorDataConnector
* @classdesc This connector keeps a CesiumVectorData model in sync with the
* points in a GeoPoints collection. This connector will listen for changes to
* the GeoPoints collection and update the cesiumModel with the features
* created from the points in the collection.
* @name PointsVectorDataConnector
* @extends Backbone.Model
* @constructor
* @classcategory Models/Connectors
* @since x.x.x
*
* TODO: Extend to allow for a collection of GeoPoints collections, where each
* GeoPoints collection can be represented as a different polygon in the
* CesiumVectorData model.
*/
return Backbone.Model.extend(
/** @lends PointsVectorDataConnector.prototype */ {
/**
* The type of Backbone.Model this is.
* @type {string}
* @default "PointsVectorDataConnector"
*/
type: "PointsVectorDataConnector",

/**
* Extends the default Backbone.Model.defaults() function to specify
* default attributes for the PointsVectorDataConnector model.
*/
defaults: function () {
return {
points: null,
vectorLayer: null,
isConnected: false,
};
},

/**
* Initialize the model.
* @param {Object} attrs - The attributes for this model.
* @param {GeoPoints | Object} [attributes.points] - The GeoPoints
* collection to use for this connector or a JSON object with options to
* create a new GeoPoints collection. If not provided, a new GeoPoints
* collection will be created.
* @param {CesiumVectorData | Object} [attributes.vectorLayer] - The
* CesiumVectorData model to use for this connector or a JSON object with
* options to create a new CesiumVectorData model. If not provided, a new
* CesiumVectorData model will be created.
*/
initialize: function (attrs) {
try {
attrs = attrs || {};
this.setPoints(attrs.points);
this.setVectorLayer(attrs.vectorLayer);
if (attrs.isConnected) {
this.connect();
}
} catch (e) {
console.log("Error initializing a PointsVectorDataConnector", e);
}
},

/**
* Set or create and set the GeoPoints collection for this connector.
* @param {GeoPoints | Object} [points] - The GeoPoints collection to use
* for this connector or a JSON object with options to create a new
* GeoPoints collection. If not provided, a new GeoPoints collection will
* be created.
* @returns {GeoPoints} The GeoPoints collection for this connector.
*/
setPoints: function (points) {
if (points instanceof GeoPoints) {
this.set("points", points);
} else {
this.set("points", new GeoPoints(points));
}
return this.get("points");
},

/**
* Set or create and set the CesiumVectorData model for this connector.
* @param {CesiumVectorData | Object} [vectorLayer] - The CesiumVectorData
* model to use for this connector or a JSON object with options to create
* a new CesiumVectorData model. If not provided, a new CesiumVectorData
* model will be created.
* @returns {CesiumVectorData} The CesiumVectorData model for this
* connector.
*/
setVectorLayer: function (vectorLayer) {
if (vectorLayer instanceof CesiumVectorData) {
this.set("vectorLayer", vectorLayer);
} else {
this.set("vectorLayer", new CesiumVectorData(vectorLayer));
}
return this.get("vectorLayer");
},

/**
* Listen for changes to the Points collection and update the
* CesiumVectorData model with the features created from the points in
* the collection.
*/
connect: function () {
try {
const connector = this;
this.disconnect();

const handler = (this.eventHandler = new Backbone.Model());
const points = this.get("points") || this.setPoints();

// Update the vectorLayer when the points collection is updated.
handler.listenTo(points, "update reset", () => {
connector.updateVectorLayer();
});

// Restart listeners the points collection or the vectorLayer is
// replaced with a new collection or model.
handler.listenToOnce(this, "change:points change:vectorLayer", () => {
if (this.get("isConnected")) {
connector.connect();
}
});

this.set("isConnected", true);
} catch (e) {
console.warn(
"Error connecting a PointsVectorDataConnector, disconnecting.",
e
);
connector.disconnect();
}
},

/**
* Stop listening for changes to the Points collection.
*/
disconnect: function () {
const handler = this.eventHandler;
if (handler) {
handler.stopListening();
handler.clear();
handler = null;
}
this.set("isConnected", false);
},

/**
* Update the CesiumVectorData model with the features created from the
* points in the collection.
*/
updateVectorLayer: function () {
const points = this.get("points") || this.setPoints();
const layer = this.get("vectorLayer") || this.setVectorLayer();
const geoJson = points.toGeoJson("Polygon");
const opts = layer.getCesiumOptions() || {};
opts.data = geoJson;
layer.set("cesiumOptions", opts);
},
}
);
});
Loading

0 comments on commit 0fc87fb

Please sign in to comment.