ember-leaflet - initialize map with zoom to fit all pins - ember.js

I am using ember-leaflet with a new project and I've got a component which houses a map with several markers on it called embedded-map. I'm following the tutorial on their page and can set a static zoom value for my map.
I saw there is some sort of fitBounds function available for the leaflet javascript library. Is there a way to access that while using ember-leaflet? My documentation search came up short, but I did find a template helper that seems related, but not quite what I'm looking for: https://miguelcobain.github.io/ember-leaflet/docs/helpers/lat-lng-bounds.
embedded-map.js
import Component from "#ember/component";
import { computed } from "#ember/object";
import { readOnly } from "#ember/object/computed";
export default Component.extend({
tagName: "",
zoom: 10.25, /* I'd like this to initialize to fit all map markers */
lat: readOnly("trip.averageLatitude"), /* `trip` is a model passed into the component */
lng: readOnly("trip.averageLongitude"),
location: computed("trip.averageLatitude", "trip.averageLongitude", function() {
return [this.get("averageLatitude"), this.get("averageLongitude")];
}),
});
embedded-map.hbs
{{#leaflet-map lat=lat lng=lng zoom=zoom as |layers|}}
{{layers.tile url="http://{s}.tile.osm.org/{z}/{x}/{y}.png"}}
{{#each trip.days as |day|}}
{{#each day.dayItems as |item|}}
{{#layers.marker location=item.coordinates as |marker|}}
{{#marker.popup}}
<h3>{{item.name}}</h3>
{{/marker.popup}}
{{/layers.marker}}
{{/each}}
{{/each}}
{{/leaflet-map}}

I'm not intimately familiar with the latest api, but when I used ember-leaflet in a project late 2017/early 2018, I determined at least two options for this use case. Which you choose depends on your use case and what you consider "proper".
You could leverage the onLoad action of the ember-map component
{{#leaflet-map onLoad=(action "showAllPins") }}
and handle the bounds setting logic here in the action
showAllPins(map){
let mapTarget = map.target;
let disableDrag = this.disableDrag;
// you *can* do totally store the map for later use if you want but... the
// addon author is trying to help you avoid accessing leaflet functions directly
this.set('map', mapTarget);
/* determine min/max lat and lng */
//set the bounds
mapTarget.fitBounds([[lowestLatitude, lowestLongitude], [highestLatitude, highestLongitude]]);
}
or you can use inheritance by extending LeafletMap and have your own custom version of the leaflet map that handles the zoom setting in a domain specific way (extending this class allows you to override/enhance existing methods that have access to the underlying leaflet object).

I've found a solution to the problem, thanks to an error message I saw in the console while pursuing #mistahenry's solution above.
The tutorial suggests you pass lat, lng, and zoom parameters into the leaflet-map declaration, but the error message states you can also pass in bounds instead of the above 3, and wouldn't you know it - it does exactly what I'm trying to do!
I calculate the maximum and minimum latitude and longitude using all of the map markers I have on hand, and pass this into the embedded-map component to render the map in a way that will fit all markers on-screen.

Related

Ember.js - Matching model data and has many relationship in controller

I'm trying to build a table component that displays all matching data. I don't know how to get this working.
I have multiple Platforms that have many Markets.
The model is easy:
model() {
return this.store.findAll('platform', {include: 'markets'});
}
I can display check boxes so the user can select what platforms to compare and access the id's in the controller.
How do I go about getting the correct records from the model in the controller? I can't do this in the route because it depends on what platforms are selected.
I can use Ember Data:
this.get('store').findRecord('platform', id, {include: 'markets'})
But I can't figure out how to access the markets.
I tried enumerables also, but the same issue:
this.get('model').filterBy('id', id)
After this, what is a clean way to get the matching markets based on their name?
For your situation, you can access and compare the markets based on any selected platforms within your component. Each platform should have a relationship established to its associated markets within the model file. The relationship will allow you to access the markets off of the platform. For example, platform.get('markets') within a controller, component or {{platform.markets}} within a template. For a bit more detail on the rough process for implementing throughout the app files:
//Within the platform.js model just to set the basic relationship
markets: hasMany(),
//Within your controller.js build an array of selected platforms
selectedPlatforms: null,
actions: {
selectPlatform(platform) {
this.get('selectedPlatforms').pushObject(platform);
},
}
//Within your template.hbs provide your component the array
{{matching-markets platforms=selectedPlatforms}}
//Within your component.js compare the platform markets
platforms: null,
matchingMarkets: computed('platforms', function() {
const platform1Markets = this.get('platforms.firstObject.markets');
const platform2Markets = this.get('platforms.lastObject.markets');
return /* Compare platform1Markets against platform2Markets */;
}),
// Within the component.hbs
{{#each matchingMarkets as |market|}}
{{market.name}}
{{/each}}
Please reference the below link to an EmberTwiddle to see a rough (slightly hacky) example that might provide some better insight:
https://ember-twiddle.com/364d9c04d37593f4a7c40cccf065a8fc?openFiles=routes.application.js%2C

Parent property is not bind to the child (Ember 2.8)

My code:
signup.emblem:
= validating-form onsubmit=(action 'signUp')
= input-field value=username
span {{usernameError}}
validating-form.js:
submit(event) {
console.log(this.get('username') //undefined
this.sendAction('onsubmit')
}
signup.js:
actions: {
signUp() {
console.log(this.get('username')) // value from input
}
}
As you can see the basic idea is some value in input gets validated in validating-form component and then if everything is fine it'll call some controller action or set some properties.
The problem is that apparently this form component isn't bind to properties from controller, even though its child component (input-field) is. Can you tell me what am I doing wrong here?
If I have to bind it explicitely, is there some way to do that with multiple properties at once?
The problem is that the standard input element isn't two-way bound to your username variable. You can bind it quickly using the action and mut helpers.
(example in handlebars, but you should be able to convert to emblem easily enough)
<input value={{username}} onblur={{action (mut username) value='target.value'}}>
This is saying:
on the onblur event
mut(ate) the username
to match the current target.value - which is the value of the input box
You can see evidence of this working in this twiddle
The other option is Input Helpers
I've not used these, as they don't follow the current Ember thinking of Data Down Actions Up, but it should be as simple as:
{{input value=username}}
And this will two-way-bind directly username.

Where to put code from another JS library in Ember: controller, model, adapter, service?

Wrapping another javascript library to use with Ember bindings, etc, seems like an ordinary thing to do, but I haven't found much discussion of it.
I want to filter an ember record array using distance and travel time from the Google Maps Distance Matrix
service. I'm just not sure where in the application to encapsulate Google's javascript. Note: this is not a question about embedding a google map, it's about getting data into ember that doesn't come from a rest/json or fixtures as in all the tutorials and examples I've found.
Would people typically do this in the controller or create new models/adapters to get benefits from store caching? Or is there another way?
update: in case that's too vague, consider this: 20 records (with a google map component etc) listed by an array controller, a text field where the user types in a home address, a couple of other inputs where they set a maximum time or distance, and a search button which filters the listed records by comparing the user requirements with the result of querying the distance matrix for the home address to the 20 records' addresses, only showing the ones close enough to their home.
Use of the service in an application that doesn't display a Google map is prohibited.
So,the question is really about integrating a Google map to an Ember app.
Without any doubt you'll have to add the Google JS like in any other HTML project with:
<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?key=MYSECRETKEY"></script>
So, the API is in global space and you just use it whenever you need it. Mostly all that will happen in your views, so you could wrap everything in a component. (I'm assuming that all relevant data has been passed from the controller to the view, it all depends on the design of your app.)
The following works, but it seems like it should be in the model/store/adapter layer.
App.DistanceController = Ember.Controller.extend
origin: (->
data = #get('data')
data.origin if data
).property('data')
destinationDistances: (->
data = #get('data')
data.distances if data
).property('data')
data: ((key, value)->
if arguments.length > 1
value
else
_this = this
value = null
service = new google.maps.DistanceMatrixService()
service.getDistanceMatrix(
origins: ["London, England"],
destinations: [
"Bristol, England",
"Edinburgh, Scotland",
"Leeds, England",
"Liverpool, England",
"Manchester, England",
"Newcastle, England",
"Nottingham, England",
"York, England"
],
travelMode: google.maps.TravelMode.DRIVING,
avoidHighways: false,
avoidTolls: false
, (response, status) ->
if (status == google.maps.DistanceMatrixStatus.OK)
distances = []
for destination, n in response.destinationAddresses
distances.push {
destination: destination
distance: response.rows[0].elements[n].distance.text
}
_this.set('data', {
origin: response.originAddresses[0]
distances: distances
})
)
value
).property()
kudos #rlivsey https://stackoverflow.com/a/20623551/395180

Force a controller to always act as a proxy to a model in Ember

I'm looping through a content of an ArrayController whose content is set to a RecordArray. Each record is DS.Model, say Client
{{# each item in controller}}
{{item.balance}}
{{/each}}
balance is a property of the Client model and a call to item.balance will fetch the property from the model directly. I want to apply some formatting to balance to display in a money format. The easy way to do this is to add a computed property, balanceMoney, to the Client object and do the formatting there:
App.Client = DS.Model({
balance: DS.attr('balance'),
balanceMoney: function() {
// format the balance property
return Money.format(this.get('balance');
}.property('balance')
});
This serves well the purpose, the right place for balanceMoney computed property though, is the client controller rather than the client model. I was under the impression that Ember lookup properties in the controller first and then tries to retrieve them in the model if nothing has been found. None of this happen here though, a call to item.balanceMoney will just be ignored and will never reach the controller.
Is it possible to configure somehow a controller to act always as a proxy to the model in all circumstances.
UPDATE - Using the latest version from emberjs master repository you can configure the array controller to resolve records' methods through a controller proxy by overriding the lookupItemController method in the ArrayController. The method should return the name of the controller without the 'controller' suffix i.e. client instead of clientController. Merely setting the itemControllerClass property in the array controller doesn't seem to work for the moment.
lookupItemController: function( object ) {
return 'client';
},
This was recently added to master: https://github.com/emberjs/ember.js/commit/2a75cacc30c8d02acc83094b47ae8a6900c0975b
As of this writing it is not in any released versions. It will mostly likely be part of 1.0.0.pre.3.
If you're only after formatting, another possibility is to make a handlebars helper. You could implement your own {{formatMoney item.balance}} helper, for instance.
For something more general, I made this one to wrap an sprintf implementation (pick one of several out there):
Ember.Handlebars.registerHelper('sprintf', function (/*arbitrary number of arguments*/) {
var options = arguments[arguments.length - 1],
fmtStr = arguments[0],
params = Array.prototype.slice.call(arguments, 1, -1);
for (var i = 0; i < params.length; i++) {
params[i] = this.get(params[i]);
}
return vsprintf(fmtStr, params);
});
And then you can do {{sprintf "$%.2f" item.balance}}.
However, the solution #luke-melia gave will be far more flexible--for example letting you calculate a balance in the controller, as opposed to simply formatting a single value.
EDIT:
A caveat I should have mentioned because it's not obvious: the above solution does not create a bound handlebars helper, so changes to the underlying model value won't be reflected. There's supposed to be a registerBoundHelper already committed to Ember.js which would fix this, but that too is not released yet.

Bind Ember.Map to a Ember.TextField value

How can I bind an Ember Map value to a TextField value. Let's say I have this configuration:
App.AppsController = Em.Controller.extend({
selections: null
});
App.AppsRoute = Ember.Route.extend({
setupControllers: function(controller, model) {
controller.set('selections', Ember.Map.create());
}
});
And in my template:
{{view Ember.TextField valueBinding="bindingToMap"}}
I have tried to valueBinding="controller.selections.somekey" where somekey is a key from my map. However, the value is never bound. Note that initially the map is empty. Can this be the root of the problem?
EDIT:
I have also tried to use the binding with an integer value in the controller and it works. So the problem comes when I bind a more complex data structure such as a Map. I couldn't find anything in the docs explaining how to bind a map.
Caveat: I'm pretty new to Ember myself.
Looking at the implementation of Ember.Map, I don't think you can currently (in 1.0.0-pre2) do this. Ember.Map implements create, get, and set, but is not a normal Ember object. So among other things, "properties" in a map aren't really properties, and there's no observables support. The handlebars implementation relies heavily on observable support, so I think what you're doing won't work.
Someone correct me if I'm wrong, but this is what I'm looking at in the 1.0.0-pre2 code:
var Map = Ember.Map = function() {
this.keys = Ember.OrderedSet.create();
this.values = {};
};
/**
#method create
#static
*/
Map.create = function() {
return new Map();
};
Map.prototype = {
/**
Retrieve the value associated with a given key.
#method get
#param {anything} key
#return {anything} the value associated with the key, or undefined
*/
get: function(key) {
var values = this.values,
guid = guidFor(key);
return values[guid];
},
...
Point being, it implements its own get (and create) rather than extending Ember.Object...so no observable support. Though I might have missed it if it reopen's later or something.
EDIT:
Also, not really what you asked, but if you're building an interface that relies on the existence of some key, you should really probably be defining your own model class that has these keys as properties. You can still set them to null if they're "not set". If you also need the ability to set arbitrary keys, make one of your properties an Ember.Map and call it otherProperties or something, and put them in there. But if your view depends on a known key, it should be a defined property.