js singleton - how to avoid accessing class members via the namespace - javascript

I'm trying to improve my Javascript coding style and have been reading that it's good to namespace stuff.
However I can't seem to use the "this" keyword everywhere that I would like to - instead I can only access my class properties via the namespace (in this case "oldMap") from within anonymous functions. This means that I can't change the namespace id without changing the code too - this seems like it's probably wrong.
Here's the class I built - it actually appears to work properly though. (sorry it's quite long).
Any suggestions / hints on what I'm doing right / wrong gratefully received. Thanks
var oldMap = {
map : null,
center : {lat:50, lng:20, zoom:3},
drawn : false,
data : {},
divId : "oldMap",
url : "php/getMapData.php",
infowindow : new google.maps.InfoWindow({
size: new google.maps.Size(150,50)
}),
init : function () {
if (!this.drawn){
$.getJSON(
this.url,
function(d){
if(d){
$.extend(oldMap.data,d);
var latlng = new google.maps.LatLng(oldMap.center.lat, oldMap.center.lng);
var myOptions = {
zoom: oldMap.center.zoom,
center: latlng,
mapTypeId: google.maps.MapTypeId.TERRAIN
};
// create the map
map = new google.maps.Map(document.getElementById("oldMap"),myOptions);
// create the legend
var legendDiv = document.createElement('DIV');
legendDiv.innerHTML = '<div id="legend"><img src="images/markers/legend-blur.png"></div>';
map.controls[google.maps.ControlPosition.TOP_LEFT].push(legendDiv);
google.maps.event.addListener(map, 'click', function() {
infowindow.close();
});
// Set the info window html template
var infoWindowTemplate = "<div id='balloon'>{{#url2}}<img src='{{url2}}' />{{/url2}}<h2>{{project_name}}</h2><p><b>Amount</b> € {{cost}}</p><p><b>Country</b> {{country}}</p><p><b>Year</b> {{year}}</p><p><b>Project Type</b> {{project_type}}</p><p>{{description}}</p>{{#url}}<p><a target='_blank' href='{{url}}'>More info</a></p>{{/url}}</div>"
// loop through the projects
for(var m in oldMap.data) {
// if the project has a marker type defined
if (oldMap.data[m].marker) {
// point
var point = new google.maps.LatLng(oldMap.data[m].lat, oldMap.data[m].longtd);
// create HTML for info window
var infoHtml = Mustache.to_html(infoWindowTemplate, oldMap.data[m]);
// icon
var icon = new google.maps.MarkerImage(
"images/markers/33px/" + oldMap.data[m].marker + ".png",
new google.maps.Size(33,33)
);
// create a marker for this project
var marker = oldMap.createMarker(point,infoHtml,icon);
}
}
oldMap.drawn = true;
}
}
)
}
},
createMarker : function (latlng, html, icon) {
// create the marker
var marker = new google.maps.Marker({
position: latlng,
icon: icon,
map: map,
zIndex: Math.round(latlng.lat()*-100000)<<5
});
// open info window when marker clicked
google.maps.event.addListener(marker, 'click', function() {
oldMap.infowindow.setContent(html);
oldMap.infowindow.open(map,marker);
});
}
};

First line of your functions directly on the object should be...
function () {
var that = this;
...
}
Then, in your inner functions, swap references to this with that.
This is because inner functions' this points to window.

If you're using jQuery as it looks like you are, take a look at the proxy() method:
http://api.jquery.com/jQuery.proxy/
This method is specifically designed to force the scope of this to a particular object. I personally prefer the syntax of PrototypeJS bind():
http://api.prototypejs.org/language/Function/prototype/bind/
...but much though I prefer PrototypeJS to jQuery, it seems that battle has been fought and lost already.

Related

Iterate Over Table with jQuery and Create a New Variable from Element Value

I've iterated over a table with javascript so I can use the data as a variable for creating markers on a Google Map. My map and the code that iterates over the table both work, but I cannot figure out how to make the gridmap value available inside my marker variable. I assume I need to do a for loop around my marker variable, which I can probably figure out, I am simple stuck on taking my element variables, and making them available as variables for my markers. Below is my code, if this makes any sense. I've been at this for hours and it is 3:30am; I will refractor the question in the morning if needed. Thank you.
This is the updated and working code:
I iterated over the DOM to grab the <tr> elements from tbody only. Note the tr:gt(0) part - this skips the first header row. Then I create a new array by filtering out the markers against the gdata array. I still need to clean this up a bit add more functionality like clustering, but I am very happy at this point and hopefully this will help someone else.
<script>
function initMap() {
var map;
var table = $("table");
var labels = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
var labelIndex = 0;
var htmlLabel = labels.split(""); // Add Labels to Html Columns for Matching
var gdata = new Array();
$("table tbody tr:gt(0)").each(function (i) {
gdata[i] = new Array();
$(this).children('td').each(function (ii) {
gdata[i][ii] = $(this).text();
});
});
var bounds = new google.maps.LatLngBounds();
var mapOptions = {
mapTypeId: 'satellite',
disableDefaultUI: true,
scrollwheel: false,
draggable: true
};
// Display a map on the page
map = new google.maps.Map(document.getElementById("map_canvas"), mapOptions);
map.setTilt(45);
// Multiple Markers
var markers = [
['a0', 32.840801, -117.244842],
['a10', 32.840801, -117.244842],
['a20', 32.840777, -117.244864],
['a30', 32.840758, -117.244881],
['a40', 32.840732, -117.244899],
['aa0', 32.840828, -117.244794],
['aa10', 32.840828, -117.244794],
['b0', 32.840624, -117.24493],
['b10', 32.840624, -117.24493],
['b20', 32.840594, -117.244928],
['b30', 32.840567, -117.244924],
['b40', 32.840544, -117.244918],
['b60', 32.840544, -117.244918],
['bb0', 32.840495, -117.244897],
['bb10', 32.840495, -117.244897],
['bb20', 32.840468, -117.244885],
['c0', 32.84082, -117.244712],
['c10', 32.84082, -117.244712],
['c20', 32.840815, -117.244729],
['c30', 32.840806, -117.244749],
['c40', 32.840793, -117.244767],
['c50', 32.840779, -117.244789],
['c70', 32.840755, -117.244816],
['cc0', 32.840828, -117.244661],
['cc10', 32.840828, -117.244661],
['d0', 32.840607, -117.244867],
['d10', 32.840607, -117.244867],
['d20', 32.840586, -117.24486],
['d30', 32.840567, -117.244856],
['d40', 32.840543, -117.244841],
['d50', 32.840514, -117.244824],
['dd0', 32.84046, -117.244774],
['dd10', 32.84046, -117.244774],
['e0', 32.840788, -117.244598],
['e10', 32.840788, -117.244598],
['e20', 32.840791, -117.24462],
['e30', 32.840788, -117.244644],
['e40', 32.840787, -117.244665],
['e50', 32.840783, -117.244687],
['e60', 32.84078, -117.244707],
['e70', 32.840769, -117.244729],
['ee0', 32.84078, -117.244539],
['ee10', 32.84078, -117.244539],
['f10', 32.840607, -117.244809],
['f20', 32.840586, -117.244802],
['f30', 32.840564, -117.244785],
['f40', 32.840543, -117.244765],
['f50', 32.840532, -117.244749],
['f60', 32.840519, -117.244731],
['f70', 32.840508, -117.244714],
['ff0', 32.840473, -117.244632],
['ff10', 32.840473, -117.244632],
['g0', 32.840709, -117.244468],
['g10', 32.840709, -117.244468],
['g20', 32.840718, -117.244484],
['g30', 32.840737, -117.244499],
['g40', 32.840739, -117.244515],
['g50', 32.840747, -117.244531],
['h0', 32.840681, -117.244569],
['h10', 32.840681, -117.244569],
['h20', 32.840707, -117.244574],
['i0', 32.840611, -117.24458],
['i10', 32.840611, -117.24458],
['i20', 32.840576, -117.24461]
];
var filteredMarkers = []; // the results array
for (var iii = 0; iii < gdata.length; iii++) // iterate for every marker key
{
filteredMarkers = filteredMarkers.concat(markers.filter(function (item) {
return item[0] == gdata[iii][0];
}));
}
console.log(filteredMarkers);
console.log(gdata);
console.log(markers);
// Info Window Content
var infoWindowContent = [
['<div class="">' +
'<h3>Add Code Here</h3>' +
'<p>Add Code Here</p>' + '</div>']
];
// Display multiple markers on a map
var infoWindow = new google.maps.InfoWindow(), marker, i;
for (i = 0; i < filteredMarkers.length; i++) {
var position = new google.maps.LatLng(filteredMarkers[i][1], filteredMarkers[i][2]);
bounds.extend(position);
marker = new google.maps.Marker({
position: position,
map: map,
title: filteredMarkers[i][0],
label: labels[labelIndex++ % labels.length]
});
// Allow each marker to have an info window
google.maps.event.addListener(marker, 'click', (function (marker, i) {
return function () {
infoWindow.setContent(infoWindowContent[i][0]);
infoWindow.open(map, marker);
}
})(marker, i));
// Automatically center the map fitting all markers on the screen
map.fitBounds(bounds);
}
// Override our map zoom level once our fitBounds function runs (Make sure it only runs once)
var boundsListener = google.maps.event.addListener((map), 'bounds_changed', function (event) {
this.setZoom(24);
google.maps.event.removeListener(boundsListener);
});
}
</script>
It appears you're also using Angular and that the id='gridmap' div is returning the object name and not the content. The nature of the div content looks like the crux of problem and so would suggest looking at this SO question: How to show object property with Angularjs
From here if you can generate HTML that looks something like this below after Angular has done its thing, keeping the property out of view by inserting its elements into data tags:
<tr>
<td> ..... </td>
<td> ..... </td>
<td> ..... </td>
<td class='gridmap' data-lat='23.424' data-lng='-117.233'>a0</td>
</tr>
If you insert the whole object including curly brackets you may have to do additional JSON stringification and parsing which at this point are better avoided.
Note that HTML id labels should only used for unique DOM elements, so if you have more than one table row it should be a class (which also means adjusting your css accordingly)
Now we can use jquery data to interrogate the same HTML cell tag and pass to grLat and grLng variables :
$('tr').each(function (i) {
var $tds = $(this).find('td').eq(8), // moved the eq(8) here
gLabel = htmlLabel[i],
gridmap = $tds.text(),
grLat = Number($tds.data('lat')), // get the lat data
grLng = Number($tds.data('lng')); // get the lng data
console.log(
'Marker:' + gLabel
+ '\nRow:' + (i + 1)
+ '\nGrid Map: ' + gridmap
+ '\nGrid lat: ' + grLat
+ '\nGrid lng: ' + grLng // check components
);
var marker = new google.maps.Marker({
position: {lat: grLat, lng:grLng}, // recomposed object
map: map,
label: labels[labelIndex++ % labels.length],
animation: google.maps.Animation.DROP
});
})
This should put you in a better position to debug

2 Google Maps Conflicting With Each Other

I am creating a web app that has 2 instances of Google Maps API: one which has many points, and one which only has one point.
They seem to be conflicting with each other, because when I view one page before the other, the other map is not centered in the correct spot.
First Page:
Second Page:
Here is a link to my project: http://jakeserver.com/Apps/BostonLandmarks/B12/index.html
Here is the code that is generating the Google Maps:
var detailsMap = new google.maps.Map(document.getElementById("map_" + this.id), {
zoom: 10,
center: new google.maps.LatLng(landmarkList[rowCount].landmarkGPSNorth, landmarkList[rowCount].landmarkGPSWest),
mapTypeId: google.maps.MapTypeId.ROADMAP
});
var detailsInfoWindow = new google.maps.InfoWindow();
var detailsMarker, j;
detailsMarker = new google.maps.Marker({
position: new google.maps.LatLng(landmarksArray[rowCount].landmarkGPSNorth, landmarksArray[rowCount].landmarkGPSWest),
map: detailsMap,
icon: "Icons/red-pin.pdf"
});
detailsInfoWindow.setContent(landmarksArray[rowCount].landmarkName);
detailsInfoWindow.open(detailsMap, detailsMarker);
google.maps.event.addListener(detailsMarker, 'click', (function(detailsMarker, j) {
return function() {
detailsInfoWindow.open(detailsMap, detailsMarker);
}
})(detailsMarker, j));
}
document.getElementById("map_" + this.id).style.height = 300 + "px";
document.getElementById("map_" + this.id).style.width = 300 + "px";
Any ideas?
A long ago I'd a similar problem with a Ajax-Based navigation in a website, each page had a map, the first one working normally, but the next ones had the same problem you're having.
Before displaying the map you should create a new bound object. Just like this:
var bounds = new google.maps.LatLngBounds();
After that. You've to extend the bounds passing your markers positions. Like this:
bounds.extend(marker.position);
And finally, when the map is actually visible and rendered. Run the following lines.
google.maps.event.trigger(secondMapInstance, 'resize');
secondMapInstance.fitBounds(bounds);

How to bind google maps using HotTowel?

I am trying to show a simple map in HotTowel.
In home.html page I have that:
<section>
<div id="map-canvas" data-bind="map: map"></div>
</section>
In home.js I have that:
define(['services/logger'], function (logger) {
var vm = {
activate: activate,
title: 'Home View',
map: map
};
return vm;
function activate() {
google.maps.event.addDomListener(window, 'load', initialize);
logger.log('Home View Activated', null, 'home', true);
return true;
}
var map;
function initialize() {
var mapOptions = {
zoom: 8,
center: new google.maps.LatLng(-34.397, 150.644),
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
}
});
How to bind model with view to show map?
EDIT**
The below answer was for Durandal 1.2. In durandal 2.0 the viewAttached event was renamed to attached. You can read Durandals documentation about it here.
Durandal has a viewAttached event that is called on your viewmodel once the view has been databound and attached to the dom. That would be a good place to call the google maps api.
define(['services/logger'], function (logger) {
var vm = {
viewAttached: initialize
title: 'Home View',
map: map
};
return vm;
var map;
function initialize(view) {
logger.log('Home View Activated', null, 'home', true);
var mapOptions = {
zoom: 8,
center: new google.maps.LatLng(-34.397, 150.644),
mapTypeId: google.maps.MapTypeId.ROADMAP
};
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
}
});
EDIT AGAIN to address peoples comments**
As per Jamie Hammond's comment it is a better practice to scope your DOM transversal to the view that's being attached. If the DOM element is apart of the view.
So, inside your viewAttached (in durandal 1.2) or attached (in durandal 2.0) you would:
var map;
var mapOptions = { /*map options*/ };
var vm = {
attached: function (view) {
var mapCanvas = $('#map-canvas', view).get(0);
map = new google.maps.Map(mapCanvas, mapOptions);
}
}
I haven't messed with Durandal 2.0 at all because I've been pretty busy with work and stuff and when I was messing around with Durandal 1.0 it was just for fun but I do love the framework and hope to one day get to play with 2.0. With that said I did have an issue with generating a map in the viewattached in Durandal 1.0. But, I wasn't using Google maps. I was using Leafletjs. My solution to the problem was creating a delay in the viewAttached that would redraw the map after a short delay. This was because Durandal's transitioning in the view was not working well with leaflets ability to draw the map in the dom element as it was flying and fading in.
So, inside the viewAttached I would draw the map like so:
window.setTimeout(drawMap, 10);
Again, this was a very specific problem I had and not a problem with Durandal. This was more of a problem with Leafletjs not rendering the map correctly when the DOM element was still transitioning in.
Evan,
I'm also trying to get this working but no joy.
I have my html as and viewmodel exactly as you have, and I know the viewAttached composition is being called because I'm getting my logger event - but no map!
The only other thing I can think of is where you call your googlemaps from? I'm doing in in my index.html are you doing the same?
Regards
BrettH,
For me, the problem was that the height was 0px. My module that works looks like this:
define(['plugins/router', 'knockout', 'plugins/utility'], function (router, ko, utility) {
var vm = { };
vm.map = undefined;
vm.compositionComplete = function () {
var myLatlng = new google.maps.LatLng(29.4000, 69.1833);
var mapOptions = {
zoom: 6,
center: myLatlng
}
vm.map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
var georssLayer = new google.maps.KmlLayer({
url: 'http://www.visualtravelguide.com/Pakistan-Islamabad.kmz'
});
georssLayer.setMap(vm.map);
utility.resizeElementHeight(document.getElementById('map-canvas'), 10);
$(window).resize(function () {
utility.resizeElementHeight(document.getElementById('map-canvas'), 10);
});
};
return vm;
});
My utility module looks like this:
define(['jquery','knockout'], function ($,ko) {
return {
//resizes an element so height goes to bottom of screen, got this from a stack overflow
//usage:
// resizeElementHeight(document.getElementById('projectSelectDiv'));
//$(window).resize(function () {
// resizeElementHeight(document.getElementById('projectSelectDiv'));
//});
//adjustpixels is an optional parameter if you want to leave room at the bottom
resizeElementHeight: function (element,adjustPixels) {
var height = 0;
var adjust = 0;
if (adjustPixels != undefined)
adjust = adjustPixels;
var body = window.document.body;
if (window.innerHeight) {
height = window.innerHeight;
} else if (body.parentElement.clientHeight) {
height = body.parentElement.clientHeight;
} else if (body && body.clientHeight) {
height = body.clientHeight;
}
element.style.height = ((height - element.offsetTop-adjust) + "px");
},
//looks up name by id, returns blank string if not found
//pass in a list and an id (they can be observables)
LookupNameById: function (l, wId) {
var list = ko.utils.unwrapObservable(l);
var id = ko.utils.unwrapObservable(wId);
var name = '';
$.each(list, function (key, value) {
if (value.Id() == id)
name = value.Name();
});
return name;
},
//sets the widths of the columns of headertable to those of basetable
setHeaderTableWidth: function (headertableid,basetableid) {
$("#"+headertableid).width($("#"+basetableid).width());
$("#"+headertableid+" tr th").each(function (i) {
$(this).width($($("#"+basetableid+" tr:first td")[i]).width());
});
$("#" + headertableid + " tr td").each(function (i) {
$(this).width($($("#" + basetableid + " tr:first td")[i]).width());
});
}
};
});
Hope this helps you.
first go to your main.js and add 'async': '../Scripts/async',
require.config({
paths: {
'text': '../Scripts/text',
'durandal': '../Scripts/durandal',
'plugins': '../Scripts/durandal/plugins',
'mapping': '../Scripts/knockout.mapping-latest',
'async': '../Scripts/async',
'transitions': '../Scripts/durandal/transitions'
},
shim: { mapping: { deps: ['knockout'] } }
});
notice that we need to add async.js in the scripts folder so go to Download async.js download the file and save it in hottowel script folder as async.js
the in the main.js add this
// convert Google Maps into an AMD module
define('gmaps', ['async!http://maps.google.com/maps/api/js?v=3&sensor=false'],
function(){
// return the gmaps namespace for brevity
return window.google.maps;
});
in any viewmodel you can now use it like this
define(['plugins/router', 'knockout', 'services/logger', 'durandal/app', 'gmaps'], function (router, ko, logger, app, gmaps) {
i hope this will help:

Google Maps V3 JavaScript works only in Chrome?

I wrote a script that first geocodes an address and then displays a map of that address.
The trouble is, it only works in Chrome / Chromium. Other browsers (Firefox 10 & IE9) display a grey box. A problem that could be related, if I add a marker to the map, the marker does not show in Chrome.
I know that :
I connect with the API successfully with my API key.
The address is properly geocoded.
I use jQuery UI dialog to display the map. This, however, does not seem to be a problem. Removing the dialog and using a static div creates the same "grey box" result.
Below is my script, how I invoke it, and the website that I am using this on.
Here's the script:
function Map(properties)
{
var that = this;
// the HTML div
this.element = properties.element;
// address string to geocode
this.address = properties.address;
// title to use on the map and on the jQuery dialog
this.title = properties.title;
this.latlng = null;
this.map = null;
this.markers = [];
// geocode address and callback
new google.maps.Geocoder().geocode({'address': this.address}, function(data)
{
// geocoded latitude / longitude object
that.latlng = data[0].geometry.location;
// map options
var options =
{
zoom: 16,
center: that.latlng,
zoomControl: false,
streetViewControl: false,
mapTypeControl: false,
mapTypeId: google.maps.MapTypeId.ROADMAP
};
// create a map
that.map = new google.maps.Map(that.element, options);
// add a marker
that.markers.push(new google.maps.Marker({map: that.map,
position: that.latlng,
title: that.title + "\n" +
that.address}));
});
this.get_google_map = function()
{
return that.map;
}
// creates a jQuery UI dialog with a map
this.show_in_dialog = function()
{
// because the dialog can resize, we need to inform the map about this
$(that.element).dialog({ width: 400, height: 300, title: that.title,
resizeStop: function(event)
{
google.maps.event.trigger(that.map, 'resize');
},
open: function(event)
{
google.maps.event.trigger(that.map, 'resize');
}});
}
this.center = function()
{
that.map.setCenter(that.latlng);
}
}
Here's how I invoke it :
// grab the address via HTML5 attribute
var address = $("#address").attr("data-address");
// ... and the title
var title = $("#address").attr("data-title") + " Map";
var map_element = document.createElement('div');
// append the newly created element to the body
$("body").append(map_element);
// create my own map object
var map = new Map({ element : map_element,
address : address,
title : title });
// bind a link to show the map
$("#map_link").click(function()
{
map.center();
map.show_in_dialog();
return false;
});
And here's the URL of the problem (click on map):
http://testing.fordservicecoupons.com/dealer/30000/premium_coupon_page
Last but not least, I combine and obfuscate my JavaScripts, so what you see above is not exactly the same as in the source on the website.
This doesn't look good:
resizeStop: function(event) { google.maps.event.trigger(that.element, 'resize'); },
open: function(event) { google.maps.event.trigger(that.element, 'resize'); }
you trigger the resize-event on the element that contains the map(that.element), but you must trigger resize on the google.maps.Map-object (what should be that.map in this case)
Wow... Here was the issue.
The layout that I built was a fluid layout. So, one of the first CSS rules that I have written was:
img, div { max-width: 100%; }
So that divs and images can scale. Well, for whatever reason, Google maps DOES NOT like this rule with the end result being a grey box.
And so I added another rule - an exception for Google maps:
img.no_fluid, div.no_fluid { max-width: none; }
And then, in javascript:
// AFTER DIALOG CREATION
$(dialog).find('*').addClass("no_fluid");
The find('*') will get us all the descendants.
Viola!

Why does the javascript handler need to be attached in a different function?

In the code available here :
https://gist.github.com/1338861
if I would include the code of the function function attachEvent(marker){...}
into the body of the function
function addResults(json) {...}
like this :
function addResults(json) {
if (json.results && json.results.length) {
for (var i = 0, placemark; placemark = json.results[i]; i++) {
var pos = new google.maps.LatLng(parseFloat(placemark.point.latitude, 10),
parseFloat(placemark.point.longitude, 10));
var marker = new google.maps.Marker({
map: map,
title : placemark.name,
position: pos
});
google.maps.event.addListener(marker, 'click', function(){
var text = marker.getTitle();
showInContentWindow(text);
profileMarkers.push(marker);
}
}
}
the result would be that when clicking on any marker from the map, it would be shown in the content window only the information characteristic to the last marker added to profileMarkers array (instead of the information of the marker clicked).
My question is why does the onclick listener for the marker need to be attached in a different function (attachEvent in this context) ?
Because you keep changing marker to the last one by continuing the loop. Try this:
(function(mkr) {
google.maps.event.addListener(marker, 'click', function(){
var text = mkr.getTitle();
showInContentWindow(text);
profileMarkers.push(mkr);
}
})(marker);
In the place of the current google.maps.event.addListener code. This will create a closure and effectively "fix" the value of the variable.

Categories