How to dynamically update MGLPointFeature attribute value of MGLShapeSource for Mapbox on iOS? - mapbox

My application stores data, which includes coordinates and other info, in a local database. Due to the number of data points, the application uses clustering to display the data with Mapbox on iOS. Some of the marker styling is based on data, which can change on the run. Map setup is:
// fetch data from DB
let dataArray: [MyData] = fetchData()
// build features from data array
var features = [MGLPointFeature]()
dataArray.forEach({ (data) in
let feature = MGLPointFeature()
feature.identifier = data.id
feature.coordinate = CLLocationCoordinate2D(latitude: data.lat, longitude: data.lng)
// our attributes
feature.attributes = [
"amount": data.amount
"marked": false
]
features.append(feature)
})
// make and add source
let source = MGLShapeSource(identifier: "MySourceId", features: features, options: [
.clustered: true
])
style.addSource(source)
// regular marker layer
let layer = MGLSymbolStyleLayer(identifier: "unclustered", source: source)
layer.iconImageName = NSExpression(forConstantValue: "MyIcon")
layer.text = NSExpression(forKeyPath: "amount")
layer.iconScale = NSExpression(forMGLConditional: NSPredicate(format: "%# == true", NSExpression(forKeyPath: "marked")), trueExpression: NSExpression(forConstantValue: 2.0), falseExpression: NSExpression(forConstantValue: 1.0))
layer.predicate = NSPredicate(format: "cluster != YES")
style.addLayer(layer)
// point_count layers
...
The above code is simplified to help illustrate the concept more clearly. Array of MGLPoint is used because data is stored in DB, so we don't have a GeoJSON file or a URL. MGLShapeSource is used because clustering is required, and that's what I found in the examples. MGLShapeSource constructor taking "features" as a parameter is used because that's the one that matches the data I have. The regular marker layer is setup to show different size icons based on the value of "marked" attribute in iconScale.
During runtime, the value of "marked" attribute can change (for example, when a marker is tapped), and the corresponding icon's size needs to be updated to reflect the change in "marker" value. However, I am unable to figure out how to change the marker attribute value. MGPShapeSource only shows access to shape and URL, neither of which is the features array I initialized the source with. I need to access the features array the source was constructed with, change the marker value, and have the marker icons updated.
I have thought about remaking the source on each data change. But with the number of markers involved, this would perform poorly. Plus I believe I would also need to remake all of the style layers, as they're constructed with the actual source object, which would make this perform even worse.
I need help figuring out how to change the attribute value of MGLPointFeature within MGLShapeSource during runtime and have the map updated.

I did not find a solution close to what I was hoping for, but I did find something that's better than remaking everything every time.
On feature attribute value change, instead of remaking everything, including the source and all the layers, I update the source's shape with the MGLPointFeature array. I do have to remake the MGLPointFeature array, but then I can make a MGLShapeCollectionFeature with this array and set it as existing source's shape. This way, the existing layers don't need to be changed, either. Something like:
// build features from data array
...same as in original question, but with updated feature.attributes values as needed.
// look for existing source and update it if found; otherwise, make a new source
if let existingSource = style.source(withIdentifier: "MySourceId") {
// update data
guard let shapeSource = existingSource as? MGLShapeSource else {
throw <some_error>
}
shapeSource.shape = MGLShapeCollectionFeature(shapes: features)
} else {
// make new (same as original post)
let source = MGLShapeSource(identifier: "MySourceId", features: features, options: [.clustered: true])
style.addSource(source)
}

Related

Migration to change the configuration of CoreData

I started a macOS project using Default configuration of CoreData. Application was released and some users started to use it. Now, I need some data to be synced with iCloud and some data to be only stored locally. If I understand correctly, the only way I can achieve this is to create two different configurations (in CoreData data model), add the needed entities in each configuration, and configure the NSPersistentContainer accordingly.
However the above method might lead to some data loss since I wont be using the Default configuration anymore.
Is there any way I can "migrate" the data saved under the Default configuration to another configuration?
After some trial and error I found a solution that seems to do the work (however, it seems dirty).
First, when instantiating the container, I make sure I add my 3 storeDescriptors to persistentStoreDescriptions (each representing an scheme)
let defaultDirectoryURL = NSPersistentContainer.defaultDirectoryURL()
var persistentStoreDescriptions: [NSPersistentStoreDescription] = []
let localStoreLocation = defaultDirectoryURL.appendingPathComponent("Local.sqlite")
let localStoreDescription = NSPersistentStoreDescription(url: localStoreLocation)
localStoreDescription.cloudKitContainerOptions = nil
localStoreDescription.configuration = "Local"
persistentStoreDescriptions.append(localStoreDescription)
let cloudStoreLocation = defaultDirectoryURL.appendingPathComponent("Cloud.sqlite")
let cloudStoreDescription = NSPersistentStoreDescription(url: cloudStoreLocation)
cloudStoreDescription.configuration = "iCloud"
cloudStoreDescription.cloudKitContainerOptions = "iCloud.com.xxx.yyy"
persistentStoreDescriptions.append(cloudStoreDescription)
let defaultStoreLocation = defaultDirectoryURL.appendingPathComponent("Default.sqlite")
let defaultStoreDescription = NSPersistentStoreDescription(url: defaultStoreLocation)
defaultStoreDescription.cloudKitContainerOptions = nil
defaultStoreDescription.configuration = "Default"
persistentStoreDescriptions.append(defaultStoreDescription)
container.persistentStoreDescriptions = persistentStoreDescriptions
Note: One important thing is to make sure that NSPersistentStoreDescription with the Default configuration is added last.
Secondly, I am for-eaching thought all data saved in core data checking if managedObject.objectID.persistentStore?.configurationName is "Default" (or any string containing Default. With my empiric implementation I got to the conclusion that configuration name might be different from case to case). If the above condition is true, create a new managedObject, I copy all properties from the old one to new one, delete the old managed object, and save the context.
for oldManagedObject in managedObjectRepository.getAll() {
guard let configurationName = oldManagedObject.objectID.persistentStore?.configurationName else {
continue
}
if (configurationName == "Default") {
let newManagedObject = managedObjectRepository.newManagedObject()
newManagedObject.uuid = oldManagedObject.uuid
newManagedObject.createDate = oldManagedObject.createDate
......
managedObjectRepository.delete(item: oldManagedObject)
managedObjectRepository.saveContext()
}
}
With this implementation, old data that was previously saved in Default.sqlite is now saved in Local.sqlite or 'Cloud.sqlite' (depending on which configuration contains which entity).

Is is possible to add the data into echart box that appear when mouse-on in bar chart

whitebox
The sun withboard box. May i ask that how to append the data and display after search engine. Thankyou.
The way to insert the additional data into tooltip:
Declare formatter
formatter: function (params) { //params can get the data needed like series name
//params is to get the corresponding data that belong to the pointer
// var index_ds = params[0].dataIndex (Can use this code to get the index of dataset which is the index where the data come from)
//Declare a variable to display the data
let additional = "Additional Data"
//If want add a new line just like
additional += "xxxxx"
}
The output will be like this:
==============================
Additional Data
xxxxx
==============================

How to load basic initial and unique data to Core Data in Swift?

I am working on a project and would like to have some initial data loaded to Core Data. There are two attributes in the Core Data. First belongs to body part and second belongs to its property. Such as Eyes to have one of the four colors and so forth. The data will be like following:
Eyes Blue
Eyes Brown
Eyes Green
Eyes Black
Hair Red
Hair Brunette
Hair Blonde
Clothes Dress
Clothes Skirt
Clothes Shoes
Clothes Hat
Clothes Gloves
I have searched some CSV or pList versions and heard some sqLite shipment alternatives but couldn't figure out how to do them effectively.
I appreciate any clear explanation to load small initial data to Core Data and also removing any duplicate value from Core Data, if exists. Thank you in advance.
Here is some very basic code to show one simple way of doing this.
You need to add your own identifier attribute so you can check if an item already exists. Let's say you call that id.
Then, when you start your app, you check for each of your default values and if they don't already exist, add them, like this:
// create a fetch request to get all items with id=1
let fr = NSFetchRequest<MyItem>(entityName: MyItem.entity().name!)
fr.predicate = NSPredicate(format: "id == %#", "1")
do {
// if that request returns no results
if (try myManagedObjectContext.fetch(fr).isEmpty == true) {
// create the default item
let item = NSEntityDescription.insertNewObject(forEntityName: MyItem.entity().name!, into: myManagedObjectContext) as! MyItem
// set the id
item.id = "1"
// set other attributes here
}
} catch {
// fetching failed, handle the error
}
After adding the data, you have to save it:
// save the context
do {
try myManagedObjectContext.save()
} catch {
// handle the error
}
You could also use Core Datas Unique Constraints, but I think this solution is much simpler. Hope this helps!

Using JSZip to extract multiple KML files for Leaflet VectorGrid

The map uses KML files to generate a single geoJSON object to pass to VectorGrid's slicer function. To improve performance, the files are served as a single KMZ and extracted using the JSZip library. We then loop through each file (KML), parse it and convert to geoJSON. The features are concatenated to a separate array which is used to create a final geoJSON object (a cheap way of merging).
var vectorGrid;
JSZipUtils.getBinaryContent('/pathto/file.kmz', function (error, data) {
JSZip.loadAsync(data).then(function (zip) {
var featureArray = [];
zip.forEach(function (path, file) {
file.async('string').then(function (data) {
// convert to geoJSON, concatenate features array
featureArray = featureArray.concat(geoJSON.features);
}
}
var consolidatedGeoJSON = {
'type': 'FeatureCollection,
'features': featureArray
};
vectorGrid = L.vectorGrid.slicer(consolidatedGeoJSON, options);
}
}
The idea was that once that operation was complete, I could take the final geoJSON and simply pass it to the slicer. However, due to the nature of the promises, it was always constructing the slicer first and then parsing the files after.
To get around this, I was forced to put the slicer function inside the forEach, but inside an if statement checking if the current file is the last in the zip. This allows the vectors to be drawn on the map, but now I can't enable a hover effect on each layer separately (each KML contains a specific layer used as an area outline for interaction).
The vectorGrid slider options allows you to specify a getFeatureId function, but I don't understand how to pass that id to the setFeatureStyle function in the event handlers.
The basic problem is that you try to assign value to vactorGrid before you assigned value to featureArray. I think that you need to use Promise.all(..). Something like that:
var zips=[];
zip.forEach(function(path,file) {
zips.push(file.async('string');
});
Promise.all(zips).then(function(data){
return data.map(function(value){
return value.features;
});
}).then(function(featureArray) {
vectorGrid = L.vectorGrid.slicer(
{type:'FeatureCollection',feature:featureArray}, options);
});

On Google Earth, can I vary the altitude on a lineString/LinearRing programmatically?

I am using the Google Earth plugin on an HTML page. In this context, say you have a line string or polygon like this
// Create the placemark
var lineStringPlacemark = ge.createPlacemark('');
// Create the LineString
var lineString = ge.createLineString('');
lineStringPlacemark.setGeometry(lineString);
// Add LineString points
lineString.getCoordinates().pushLatLngAlt(48.754, -121.835, 0);
lineString.getCoordinates().pushLatLngAlt(48.764, -121.828, 0);
// Add the feature to Earth
ge.getFeatures().appendChild(lineStringPlacemark);
I got the sample from https://developers.google.com/earth/documentation/geometries
Now, say you would like to vary the altitude (height) programmatically, after you append the lineString, how would you do it?
I saw you can retrieve the features through ge.getFeatures(). However, the returned object can not be inspected and I am struggling with the syntax to change the altitude.
I could remove the whole object and redraw it but that is hacky and the user can see the redraw. This is the code to remove
var features = ge.getFeatures();
while (features.getFirstChild())
features.removeChild(features.getFirstChild());
I got the code from https://developers.google.com/earth/documentation/containers
Does someone know the right syntax?
If you have a reference to the LineString (you can hold on to it, or walk the KML DOM and get it again), you can change the altitude of the entire LineString via
lineString.setAltitudeOffset(offsetFromCurrentAltitude);
If you want to change the altitude on a per coordinate basis, you can access them basically as you constructed it above. lineString.getCoordinates() returns the KmlCoordArray, and then you can read values from individual coordinates from there. One kind of awkward thing about KmlCoordArray is that it returns copies of its KmlCoord children, not its children directly. So you can do lineString.getCoordinates().get(0) and then read the lat/lng/alt values from the KmlCoord it returns, but if you set those values on that coordinate, it won't automatically be reflected in the LineString. Instead, you have to readd that KmlCoord to the KmlCoordArray. It's somewhat awkward, but useable.
So you might do something like this, if you're usually only altering one altitude at a time:
function setNewAltitude(lineString, coordIndex, altitude) {
var coords = lineString.getCoordinates();
if (coordIndex >= 0 && coordIndex < coords.getLength()) {
var coord = coords.get(coordIndex);
coord.setAltitude(altitude);
coords.set(coordIndex, coord);
}
}
Check out the KmlCoordArray reference page for its other methods to see if they would be more helpful for the exact use case you have in mind.
I found the answer. My insight was requesting the type as I navigated through the objects. See below
// read the number of features in GE
var length = ge.getFeatures().getChildNodes().getLength();
// get the first feature
var feature = ge.getFeatures().getFirstChild();
// for debugging get type - expecting KmlPlacemark
var featureType = feature.getType();
console.log(featureType);
// get KmlPlacemark geometry
var geometry = feature.getGeometry();
// for debugging get type - expecting KmlLineString
var geometryType = geometry.getType();
console.log(geometryType);
// get KmlLineString coordinates
var coordinates = geometry.getCoordinates();
// for debugging get type - expecting KmlCoordArray
var coordinatesType = coordinates.getType();
console.log(coordinatesType);
var altitude = Math.random()*10000;
var coordinatesLength = coordinates.getLength();
for(var i=0; i< coordinatesLength; i++){
var coordinate = coordinates.get(i);
console.log(coordinate.getType());
coordinate.setAltitude(altitude);
coordinates.set(i,coordinate)
}
for(var i=0; i< coordinatesLength; i++){
var coordinate = coordinates.get(i);
console.log(coordinate.getAltitude());
}