I need good examples and best practices on program architecture.
I'm trying to build a JS UI for an app that works with Google.Maps. In the 1st draft, user should be able to draw geometric shapes on the map in a way similar to G.M. Then the shapes are sent through AJAX and the response is displayed.
Problem is that the code got complicated just with polygons editing.
Inspired by Joel's "Duct-tape Programmer", I tried to sketch a straightforward code that makes actions and switches event handlers, to avoid big if-else trees. "New poly" button creates an observer for map.onclick, changes event handlers for other buttons or hides them, and hides itself, etc.
The downside of this approach is that data handling code is mixed with interface. A code that creates a div container to display the data on the new polygon stands next to the code that deals w/ G.M or w/ the shape data. If I want to revise the UI, I'll need to process the WHOLE app.
I could review it later and move this UI-generating code elsewhere, but then came my lead programmer. He instead insisted on "messaging" approach: a simple event system where objects subscribe to events and fire them. Interface code can be perfectly isolated from data handling and talking to G.M, but now each listener has to double-check if this message is to it.
For example, clicking on a poly on a map must highlight it and start editing. But not if another poly is being drawn. So, some are-you-talking-to-ME?-code is necessary everywhere.
I'll appreciate good examples of UI architecture approaches.
The event handling idea suggested to you is a good approach.
Here's some more ideas:
Make the shape drawing thing a component
The shape drawing component sends events to other code, to react to stuff like "editing" or "clicked"
This component could also handle the editing part - It sends "clicked" event to controller, controllers tells the component to go into edit mode
While in edit mode the component would not send normal "clicked" events until the editing was closed, avoiding your problem of having to check
In general, it's a good idea to have a "view" layer which simply deals with displaying the data and sending events about user actions on that data (ie. clicks, etc.) to a "controller" layer, which then decides what to do - for example it could decide to change the view into editing mode.
I don't know if this is beside the point. But I use this as a temple for all my javascript projects.
(function () {
var window = this,
$ = jQuery,
controller,
view,
model;
controller = {
addEventForMenu : function () {
// Add event function for menu
}
};
view = {
content : {
doStuff : function () {
}
},
menu : {
openMenuItem : function () {
}
}
};
model = {
data : {
makeJson : function () {
// make json of string
},
doAjax : function () {
},
handleResponse : function () {
}
}
}
$.extend(true, $.view, view);
})();
The good thing here is that it's only the view object that is extended to the DOM, the rest is kept inside the anonymous scope.
Also in bug project i create on of these files for each part ie, map.js, content.js, editor.js
If you just mind the naming of your methods in the view object you can have as many files as you like during development. When the project is set in to a production environment I just make it one file and minify it.
..fredrik
In short publisher-subscriber paradigm works well to make geometric shapes. First make command-line which primitive is base polygon publisher publishes. Canvas object seems obvious here to paint, usual method repaint() for updating the client view (eventdriven programming normally in C you can review eg opengl or glut eventdriven graphics), combined with the so-so gmap API I too use, publisher-subscriber pattern or factory are good design patterns whatever graphics implementation. tricky gmaps specific thing is latitude and longitude switch places in the array between json and persistence layer, there's no serverside reverse geocoding yet, naming is kinda inconsistent, and for multilingua apps names both change relative user human language and are doubled (Paris in Text, Paris in France...),. Look if you like my going implementation, registers geographic names and spatial coordinated relative user with geoip worldwide
function wAdd(response){
map.clearOverlays();
if(!response||response.Status.code!=200){
}
else{
try{
place=response.Placemark[0];
point=new GLatLng(place.Point.coordinates[1],place.Point.coordinates[0]);
marker=new GMarker(point);
map.addOverlay(marker);
marker.openInfoWindowHtml('<a href="/li?lat='+place.Point.coordinates[1]+'&lon='+place.Point.coordinates[0]+'&cc='+place.AddressDetails.Country.CountryNameCode+'">'+place.AddressDetails.Country.AdministrativeArea.Locality.LocalityName+'<span id="wr2"></span> '+ nads( place.Point.coordinates[1],place.Point.coordinates[0] )+' ' +'<img src="http://geoip.wtanaka.com/flag/'+place.AddressDetails.Country.CountryNameCode.toLowerCase()+'.gif">');
}
catch(e){
try{
place=response.Placemark[0];
point=new GLatLng(place.Point.coordinates[1],place.Point.coordinates[0]);
marker=new GMarker(point);
map.addOverlay(marker);
marker.openInfoWindowHtml('<a href="/li?lat='+place.Point.coordinates[1]+'&lon='+place.Point.coordinates[0]+'&cc='+place.AddressDetails.Country.CountryNameCode+'">'+place.AddressDetails.Country.AdministrativeArea.AdministrativeAreaName+'<span id="wr2"></span> '+ nads( place.Point.coordinates[1],place.Point.coordinates[0] )+' ' +'<img src="http://geoip.wtanaka.com/flag/'+place.AddressDetails.Country.CountryNameCode.toLowerCase()+'.gif">');
}
catch(e){
try {
place=response.Placemark[0];
point=new GLatLng(place.Point.coordinates[1],place.Point.coordinates[0]);
marker=new GMarker(point);
map.addOverlay(marker);
marker.openInfoWindowHtml('<a href="/li?lat='+place.Point.coordinates[1]+'&lon='+place.Point.coordinates[0]+'&cc='+place.AddressDetails.Country.CountryNameCode+'">'+place.AddressDetails.Country.CountryName+'<span id="wr2"></span> '+ nads( place.Point.coordinates[1],place.Point.coordinates[0] )+' ' +'<img src="http://geoip.wtanaka.com/flag/'+place.AddressDetails.Country.CountryNameCode.toLowerCase()+'.gif">');
}
catch(e){
place=response.Placemark[0];
marker=new GMarker(point);
map.addOverlay(marker);
marker.openInfoWindowHtml(''+place.address+'');
}
} }
}map.addOverlay(geoXml);
}
i would recommend having few object variables containing the state (0, drawing, editing, ... any other required) - this would help you deciding whether to allow event handling or just bury it if for example drawing is done and clicking on editable polygone happens.
as for the UI - I am not sure if your question is aimed at you - developing the script or at the user as you are mixing two things here.
keep in mind that for a user everything should be as simple as possible: if he is editing, don't allow him to draw. if he is drawing, don't allow him to edit (overlapping of polygons could occur). however - allow user quickly to switch from editing (e.g. right click?) to drawing - or in other words cancel the current state.
The first thing I would do is create a service that wraps over the google API. This is so that later if you need to change mapping services (windows maps or yahoo maps). Then you can put a facade over the google service. Then you might want to put some wrappers over your service and split it up into a view(output) and model(input) and manage this with controllers/presenters. Check into Model View Controller / Model View Presenter / Presenter First / Humble Dialog on Wikipedia. It should discuss the seperation that your looking for. Also Martin Fowler web page goes into presentation patterns. You should check out my old blog ugly-lisp-code. I have references to event driven/event messaging.
If you have a one-to-one pub/sub just store an event-handler(closure/lambda/first-order-function) in the object that is going to fire the event.
If you have a one-to-many pub/sub then you will need a more complex object to store your closures.
LOL! Right now I've been looking at this same exact issue. I'm going to be writing about using a presenter-first in JavaScript on my blog. A bare bones start on presenter and model.
[edit] you might want to check out this stackoverflow question. One of the answer has a link to separating concerns into MVC. The link is on A List Apart.
Related
Background:
I'm currently integrating HERE maps into our web-based application. I'm trying both - HERE provided Javascript API and Leaflet at the same time to find the best approach for our use-case.
While JavaScript API provided by HERE maps is OK, rendering wise Leaflet performs much better when using raster tiles.
Issue:
It would be fine by me to use raster tiles + leaflet, but our application also needs to display traffic incidents data.
Traffic incident data is provided by HERE in JSON and XML formats (Documentation link, Example JSON). They provide [Z]/[X]/[Y], quadkey, prox, bbox, or corridor filters which can be used to retrieve filtered data set.
I've tried using [Z]/[X]/[Y] addressing with custom L.TileLayer implementation which loads appropriate JSON, converts it to GeoJSON and displays GeoJSON on map. However that approach is very inefficient and significant performance drop is visible.
Question:
Maybe anyone has already solved this issue and could share any insights on how the HERE traffic incidents could be shown on Leaflet map without encountering performance issues?
I created the following script, which works without any performance issues:
var fg = L.featureGroup().addTo(map);
function loadTraffic(data) {
fg.clearLayers();
var d = data.TRAFFICITEMS.TRAFFICITEM.map((r) => {
var latlngs = [];
if (r.LOCATION.GEOLOC) {
if (r.LOCATION.GEOLOC.ORIGIN) {
latlngs.push(L.latLng(r.LOCATION.GEOLOC.ORIGIN.LATITUDE, r.LOCATION.GEOLOC.ORIGIN.LONGITUDE));
}
if (r.LOCATION.GEOLOC.TO) {
if (L.Util.isArray(r.LOCATION.GEOLOC.TO)) {
r.LOCATION.GEOLOC.TO.forEach((latlng) => {
latlngs.push(L.latLng(latlng.LATITUDE, latlng.LONGITUDE));
})
} else {
latlngs.push(L.latLng(r.LOCATION.GEOLOC.TO.LATITUDE, r.LOCATION.GEOLOC.TO.LONGITUDE));
}
}
}
var desc = r.TRAFFICITEMDESCRIPTION.find(x => x.TYPE === "short_desc").content;
return {
latlngs,
desc
}
})
console.log(d);
d.forEach((road)=>{
L.polyline(road.latlngs,{color: 'red'}).addTo(fg).bindPopup(road.desc);
});
map.fitBounds(fg.getBounds())
}
If this script is not working for you, please share your json file.
Ok, so I've found a solution for this task. Apparently I was on a good path, I only needed to optimize my implementation.
What I had to do to achieve appropriate performance is:
Create custom CircleMarker extension which would draw custom icon on canvas
Create JS worker which would fetch the data from a given URL, transform it to GeoJSON and return GeoJSON to it's listener
Create custom GridLayer implementation, which, in fetchTile function, creates worker instance, passes it a link with appropriate [Z]/[X]/[Y] coordinates already set, adds listener, which listens for worker's done event and returns empty tile
On worker's done event, custom GridLayer implementation creates GeoJSON layer, adds it to the dictionary with coordinates as a key and, if zoom level is still the same - adds that layer to the map
Add zoomend observer on a map, which removes any layers that does not match current zoom level from the map
Now the map is definitely usable and works way faster than original HERE JS API.
P.S. Sorry, but I can't share the implementation itself due to our company policies.
function initPano() {
// Set up Street View and initially set it visible. Register the
// custom panorama provider function. Set the StreetView to display
// the custom panorama 'reception' which we check for below.
var panorama = new google.maps.StreetViewPanorama(
document.getElementById('map'), {
pano: 'reception',
visible: true,
panoProvider: getCustomPanorama
});
}
// Return a pano image given the panoID.
function getCustomPanoramaTileUrl(pano, zoom, tileX, tileY) {
// Note: robust custom panorama methods would require tiled pano data.
// Here we're just using a single tile, set to the tile size and equal
// to the pano "world" size.
return 'http://bestofdiscus.gr/portals/0/Discus-Header-WR.jpg';
}
function getCustomPanorama(pano, zoom, tileX, tileY) {
if (pano === 'reception') {
return {
location: {
pano: 'reception',
description: 'Google Sydney - Reception'
},
links: [],
// The text for the copyright control.
copyright: 'Imagery (c) 2010 Google',
// The definition of the tiles for this panorama.
tiles: {
tileSize: new google.maps.Size(1024, 512),
worldSize: new google.maps.Size(1024, 512),
centerHeading: 105,
getTileUrl: getCustomPanoramaTileUrl
}
};
}
}
In this block of code, i don't understand the parameters :pano, zoom, tileX, tileY in the function getCustomPanoramaTileUrl. I understand that, without using these parameter, the function will return an url of image.
my question is:
1/What do these parameters use for and how to use it ?
2/What is a pano ID (i have been searching for it a lot but still can not understand)
What are you talking about?
You may be thinking, "why is my question downvoted?" (PS: I didn't do it!). When asking a question, slapping random code with no context whatsoever will leave anyone trying to help you just as lost as you.
Though the code is useful, your question is missing important information:
What technologies are you using? Any APIs?
What have you tried?
Where is that code from?
Any links to any documentation? What is the context?
Before making a question, always make sure to read the following page https://stackoverflow.com/help/how-to-ask
Your code, where does it come from?
After doing some digging and some research I was able to find that your code is actually a piece of code from the Google Documentation, Custom Street View panoramas.
With this in mind Google has some documentation on the matter that will help you understand what is going on with your code:
https://developers.google.com/maps/documentation/javascript/streetview#TilingPanoramas
I read the documentation, but I still don't get it!
Although Google talks about Custom Panoramas with multiple views, the example provided is too simple to illustrate the full potential of the resources Google provides you.
Now, regarding your specific question...
What are pano, zoom, tileX, tileY used for?
In the code example that you provided, they are used for... nothing. You could literally remove them from getCustomPanoramaTileUrl and the code would still work.
So, what are they used for? Well, according to the References Documentation for StreetView, these parameters have the following objective:
Gets the tile image URL for the specified tile. pano is the panorama
ID of the Street View tile. tileZoom is the zoom level of the tile.
tileX is the x-coordinate of the tile. tileY is the y-coordinate of
the tile. Returns the URL for the tile image.
Now, if this is still, confusing, I will try to explain.
Custom panoramas are sets of images, put together, like in the image bellow:
When using a real panoramic view, you want to pass a set of images, and the StreetView object needs to know which set of images you are referring to (panoId), at which zoom level (zoom) and inside the set, the X and Y positions of the image you are currently seeing (tileX and tileY).
In the example you provided, since it is extremely simple, none of this is used because you always return the same image no matter what. But in a more complex example, that uses a set of images, this information would be crucial to let the StreetView know where you are looking at in order to display the correct image.
Hope it helps!
I am building an app with a MapBox map. I ask to a server a GeoJson file, containing a list of markers, already formatted according to Mongoose schema like this:
var poiSchema = new Schema({
type : { type:String, required:true},
geometry : {
type : { type:String},//point
coordinates : { type: [Number]} //lng, lat
},
properties:
{
"marker-color" : {type:String},
"marker-size" : {type:String},
"marker-symbol" : {type:String},
}
});
In this way I can just take the result and put it on the map with
map.featureLayer.setGeoJSON(result_from_server);
Anyway I have many markers (around 1000) and it takes a while... 5-6 seconds :(
While it's loading, the GUI is almost stuck, the scroll is very very slow like all the rest. Is there a way to perform the setGeoJSON in a WebWorker ?
Thank you
If you actually profiled your application, you would find out that what causes your performance problem. It's the DOM operations that are bogging you down. Not the initialization of the marker is the problem but the attaching of marker and shadow img elements to the DOM tree and the resulting paints the browser has to preform.
Sorry to say, you can't preform DOM operations in a javascript worker. A worker thread doesn't have access to the window object so Mapbox/Leaflet can't operate. It won't even load because the first thing the library wants to do is to attach itself, L, to the window object. Also you can't send Leaflet objects from the mainthread to the workerthread because it only accepts values that can be cloned by the structured clone algorithm. That excludes complex objects that have methods, like Leaflet objects.
If you really want to show a large amount of markers you could try and use pure SVG which preforms better or you could take a look at clustering them.
I've created a multilayer-geoportal based on that cartodb blog post.
I have it up and running and hosted on my GitHub site, but I'm trying to enable infowindows, and can't get it to work. Cartodb support suggested I add in the following line:
cdb.vis.Vis.addInfowindow(map, layer.getSubLayer(0), ['cartodb_id']);
after I create the layer. However, when I do that, the map doesn't load at all.
Any suggestions on code that I may be missing in my github repository to fix this problem (line 77 in the multilayer.js file is currently commented out).
To view this live, go to andrewmartini.github.io/labs-multilayer/multilayer.html?u=andrewmartini&t=multilayer_test&v=0d192f34-2a79-11e5-8e7c-0e4fddd5de28&tt=Title&d=descr
Note:
I'm new to this forum, forgive me if I've broken any posting rules. Also I would add more links but since I'm new, the system won't let me yet. I'm relatively new to using GitHub and Javascript but very familiar with GIS.
Thanks, Andrew
So, this answer from folks at Cartodb was a helpful start, and I wanted to share this for others who are experimenting - the line of code from above needed to go inside the addLayer function:
function addLayer(id, show, map) {
return function (layer) {
if (!show) {
layer.hide();
}
cdb.vis.Vis.addInfowindow(map, layer.getSubLayer(0), ['cartodb_id'])
cartodbLayers[id] = layer;
};
}
And also change this:
cartodb.createLayer(map, layerOptions)
.addTo(map)
.done(addLayer(id, layer.show, map))
.error(function (error) {
console.log("error: " + error);
});
But, this still has issues - for instance if you add other columns to the cbd.vis.Vis.addInfowindow function - only the data layers with that column value will load, and all of the others will fail to load. Can anyone tell me how to fix this so that I can add infowindow for selected columns for multiple datasets from my cartodb account/database?
I have a working application here: http://dola.colorado.gov/gis-cms/sites/default/files/html/census2000v2.html
I'm using the Javascript API with ArcGIS Online. I have a bunch of layers loaded and pre-symbolized in an AGOL 'Web Map'.
I'd like to be able to customize the symbology of each layer dynamically using javascript. I'd ideally like to use a renderer and be able to create a different symbology for each demographic variable.
I've run into a major brick wall. To be able to change the symbology, I need to be able to iterate through graphics in a feature set - yet I have no idea where to get a feature set object from. All the examples I see use 'Feature Layers' loaded through URLs.
I think first you need to get the layer from the webmap:
var featureLayer = mapObject.getLayer(layerName)
Then you can query the featurelayer, which will return a featureSet.
Here is an example:
var query = new esri.tasks.Query();
query.outFields = ["*"];
featureLayer.queryFeatures(query, function(featureSet) {
//do something with the featureSet here!
});