diff --git a/README.rst b/README.rst index ae949d4..a0c995e 100644 --- a/README.rst +++ b/README.rst @@ -18,7 +18,7 @@ Blog: The Minecraft Overviewer is a command-line tool for rendering high-resolution maps of Minecraft worlds. It generates a set of static html and image files and -uses the Google Maps API to display a nice interactive map. +uses Leaflet to display a nice interactive map. The Overviewer has been in active development for several years and has many features, including day and night lighting, cave rendering, mineral overlays, @@ -66,16 +66,9 @@ Viewing the Results ------------------- Within the output directory you will find two things: an index.html file, and a directory hierarchy full of images. To view your world, simply open index.html -in a web browser. Internet access is required to load the Google Maps API -files, but you otherwise don't need anything else. +in a web browser. -You can throw these files up to a web server to let others view your map. To -ensure the map works when viewed through the Internet, you'll need a Google -Maps API key, which you can `get for free from Google`_. Please note that you -are also bound by the `Google Maps API Terms of Service`_. - -.. _get for free from Google: https://developers.google.com/maps/documentation/javascript/get-api-key -.. _Google Maps API Terms of Service: https://developers.google.com/maps/terms +You can throw these files up to a web server to let others view your map. Bugs ==== diff --git a/docs/design/designdoc.rst b/docs/design/designdoc.rst index 8b9a943..2196e63 100644 --- a/docs/design/designdoc.rst +++ b/docs/design/designdoc.rst @@ -25,8 +25,8 @@ Background Info =============== The Overviewer's task is to take Minecraft worlds and render them into a set of -tiles that can be displayed with a Google Maps interface. This section goes over -how Minecraft worlds work and are stored. +tiles that can be displayed with a Leaflet interface. This section goes over how +Minecraft worlds work and are stored. A Minecraft world extends indefinitely along the two horizontal axes, and are exactly 256 units high. Minecraft worlds are made of voxels (volumetric pixels), @@ -455,12 +455,12 @@ quickly become too much data to handle at once. (Early versions of the Overviewer did this, but the large, unwieldy images quickly motivated the development of rendering to individual tiles). -Hence choosing a technology like Google Maps, which draws small tiles together -to make it look like one large image, lets rendering even the largest worlds -possible. The Overviewer can draw each tile separately and not have to load the -entire map into memory at once. The next sections describe how to determine -which chunks to render in which tiles, and how to reason about tile ↔ chunk -mappings. +Hence choosing a technology like Google Maps or Leaflet, which draws small +tiles together to make it look like one large image, lets rendering even the +largest worlds possible. The Overviewer can draw each tile separately and not +have to load the entire map into memory at once. The next sections describe +how to determine which chunks to render in which tiles, and how to reason +about tile ↔ chunk mappings. Tile Layout ----------- diff --git a/docs/faq.rst b/docs/faq.rst index 2fbafc7..1b9dda4 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -19,10 +19,10 @@ supporting mod blocks is not trivial. Can I view Overviewer maps without having an internet connection? ----------------------------------------------------------------- -Not at the moment. The Overviewer relies on the Google maps API to display -maps, which your browser needs to load from Google. However, switching away -from Google Maps is something that will most likely be looked into in the -future. +Yes, absolutely. The Overviewer switched away from the Google Maps API and +now uses Leaflet. All files which Overviewer needs are included in the output, +so even if you have no internet connection, you will still be able to view the +map without any issues. When my map expands, I see remnants of another zoom level --------------------------------------------------------- diff --git a/docs/index.rst b/docs/index.rst index 1769ae2..c16017c 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -19,7 +19,7 @@ Introduction ============ The Minecraft Overviewer is a command-line tool for rendering high-resolution maps of Minecraft worlds. It generates a set of static html and image files and -uses the Google Maps API to display a nice interactive map. +uses Leaflet to display a nice interactive map. The Overviewer has been in active development for several years and has many features, including day and night lighting, cave rendering, mineral overlays, @@ -64,7 +64,7 @@ Features * Choose from four rendering angles. -* Generates a Google Maps powered map! +* Generates a Leaflet powered map! * Runs on Linux, Windows, and Mac platforms! diff --git a/overviewer_core/assetmanager.py b/overviewer_core/assetmanager.py index f58712c..a0a4b82 100644 --- a/overviewer_core/assetmanager.py +++ b/overviewer_core/assetmanager.py @@ -27,6 +27,7 @@ import world import util from files import FileReplacer, mirror_dir, get_fs_caps + class AssetManager(object): """\ These objects provide an interface to metadata and persistent data, and at the @@ -36,9 +37,9 @@ There should only be one instances of these per execution. def __init__(self, outputdir, custom_assets_dir=None): """\ -Initializes the AssetManager with the top-level output directory. -It can read/parse and write/dump the overviewerConfig.js file into this top-level -directory. +Initializes the AssetManager with the top-level output directory. +It can read/parse and write/dump the overviewerConfig.js file into this +top-level directory. """ self.outputdir = outputdir self.custom_assets_dir = custom_assets_dir @@ -47,13 +48,16 @@ directory. self.fs_caps = get_fs_caps(self.outputdir) # look for overviewerConfig in self.outputdir + config_loc = os.path.join(self.outputdir, "overviewerConfig.js") try: - with open(os.path.join(self.outputdir, "overviewerConfig.js")) as c: - overviewerConfig_str = "{" + "\n".join(c.readlines()[1:-1]) + "}" - self.overviewerConfig = json.loads(overviewerConfig_str) + with open(config_loc) as c: + ovconf_str = "{" + "\n".join(c.readlines()[1:-1]) + "}" + self.overviewerConfig = json.loads(ovconf_str) except Exception, e: - if os.path.exists(os.path.join(self.outputdir, "overviewerConfig.js")): - logging.warning("A previous overviewerConfig.js was found, but I couldn't read it for some reason. Continuing with a blank config") + if os.path.exists(config_loc): + logging.warning("A previous overviewerConfig.js was found, " + "but I couldn't read it for some reason." + "Continuing with a blank config") logging.debug(traceback.format_exc()) self.overviewerConfig = dict(tilesets=dict()) @@ -73,7 +77,6 @@ directory. if conf['path'] == name: return conf return dict() - def initialize(self, tilesets): """Similar to finalize() but calls the tilesets' get_initial_data() @@ -92,26 +95,36 @@ directory. def _output_assets(self, tilesets, initial): if not initial: - get_data = lambda tileset: tileset.get_persistent_data() + def get_data(tileset): + return tileset.get_persistent_data() else: - get_data = lambda tileset: tileset.get_initial_data() + def get_data(tileset): + return tileset.get_initial_data() - # dictionary to hold the overviewerConfig.js settings that we will dumps + # dictionary to hold the overviewerConfig.js settings that we will dump + # to JSON using dumps dump = dict() dump['CONST'] = dict(tileSize=384) dump['CONST']['image'] = { - 'defaultMarker': 'signpost.png', - 'signMarker': 'signpost_icon.png', - 'bedMarker': 'bed.png', - 'spawnMarker': 'https://google-maps-icons.googlecode.com/files/home.png', - 'queryMarker': 'https://google-maps-icons.googlecode.com/files/regroup.png' - } + 'defaultMarker': 'signpost.png', + 'signMarker': 'signpost_icon.png', + 'bedMarker': 'bed.png', + 'spawnMarker': 'icons/marker_home.png', + 'spawnMarker2x': 'icons/marker_home_2x.png', + 'queryMarker': 'icons/marker_location.png', + 'queryMarker2x': 'icons/marker_location_2x.png' + } dump['CONST']['mapDivId'] = 'mcmap' - dump['CONST']['regionStrokeWeight'] = 2 # Obselete - dump['CONST']['UPPERLEFT'] = world.UPPER_LEFT; - dump['CONST']['UPPERRIGHT'] = world.UPPER_RIGHT; - dump['CONST']['LOWERLEFT'] = world.LOWER_LEFT; - dump['CONST']['LOWERRIGHT'] = world.LOWER_RIGHT; + dump['CONST']['UPPERLEFT'] = world.UPPER_LEFT + dump['CONST']['UPPERRIGHT'] = world.UPPER_RIGHT + dump['CONST']['LOWERLEFT'] = world.LOWER_LEFT + dump['CONST']['LOWERRIGHT'] = world.LOWER_RIGHT + dump['CONST']['image']['compass'] = { + world.UPPER_LEFT: 'compass_upper-left.png', + world.UPPER_RIGHT: 'compass_upper-right.png', + world.LOWER_LEFT: 'compass_lower-left.png', + world.LOWER_RIGHT: 'compass_lower-right.png' + } # based on the tilesets we have, group them by worlds worlds = [] @@ -124,7 +137,7 @@ directory. dump['map'] = dict() dump['map']['debug'] = True dump['map']['cacheTag'] = str(int(time.time())) - dump['map']['north_direction'] = 'lower-left' # only temporary + dump['map']['north_direction'] = 'lower-left' # only temporary dump['map']['center'] = [-314, 67, 94] dump['map']['controls'] = { 'pan': True, @@ -134,13 +147,10 @@ directory. 'mapType': True, 'overlays': True, 'coordsBox': True, - 'searchBox': True # Lolwat. Obselete } - dump['tilesets'] = [] - for tileset in tilesets: dump['tilesets'].append(get_data(tileset)) @@ -152,38 +162,42 @@ directory. # write out config jsondump = json.dumps(dump, indent=4) - with FileReplacer(os.path.join(self.outputdir, "overviewerConfig.js"), capabilities=self.fs_caps) as tmpfile: + with FileReplacer(os.path.join(self.outputdir, "overviewerConfig.js"), + capabilities=self.fs_caps) as tmpfile: with codecs.open(tmpfile, 'w', encoding='UTF-8') as f: f.write("var overviewerConfig = " + jsondump + ";\n") - #Copy assets, modify index.html - self.output_noconfig() - + # Copy assets, modify index.html + self.output_noconfig() def output_noconfig(self): - # copy web assets into destdir: - global_assets = os.path.join(util.get_program_path(), "overviewer_core", "data", "web_assets") + global_assets = os.path.join(util.get_program_path(), + "overviewer_core", "data", "web_assets") if not os.path.isdir(global_assets): global_assets = os.path.join(util.get_program_path(), "web_assets") mirror_dir(global_assets, self.outputdir, capabilities=self.fs_caps) if self.custom_assets_dir: - # Could have done something fancy here rather than just overwriting - # the global files, but apparently this what we used to do pre-rewrite. - mirror_dir(self.custom_assets_dir, self.outputdir, capabilities=self.fs_caps) - - # write a dummy baseMarkers.js if none exists - if not os.path.exists(os.path.join(self.outputdir, "baseMarkers.js")): - with open(os.path.join(self.outputdir, "baseMarkers.js"), "w") as f: - f.write("// if you wants signs, please see genPOI.py\n"); + # We could have done something fancy here rather than just + # overwriting the global files, but apparently this what we used to + # do pre-rewrite. + mirror_dir(self.custom_assets_dir, self.outputdir, + capabilities=self.fs_caps) + # write a dummy baseMarkers.js if none exists + basemarkers_path = os.path.join(self.outputdir, "baseMarkers.js") + if not os.path.exists(basemarkers_path): + with open(basemarkers_path, "w") as f: + f.write("// if you wants signs, please see genPOI.py\n") # create overviewer.js from the source js files - js_src = os.path.join(util.get_program_path(), "overviewer_core", "data", "js_src") + js_src = os.path.join(util.get_program_path(), + "overviewer_core", "data", "js_src") if not os.path.isdir(js_src): js_src = os.path.join(util.get_program_path(), "js_src") - with FileReplacer(os.path.join(self.outputdir, "overviewer.js"), capabilities=self.fs_caps) as tmpfile: + with FileReplacer(os.path.join(self.outputdir, "overviewer.js"), + capabilities=self.fs_caps) as tmpfile: with open(tmpfile, "w") as fout: # first copy in js_src/overviewer.js with open(os.path.join(js_src, "overviewer.js"), 'r') as f: @@ -191,16 +205,20 @@ directory. # now copy in the rest for js in os.listdir(js_src): if not js.endswith("overviewer.js") and js.endswith(".js"): - with open(os.path.join(js_src,js)) as f: + with open(os.path.join(js_src, js)) as f: fout.write(f.read()) - + # Add time and version in index.html indexpath = os.path.join(self.outputdir, "index.html") index = codecs.open(indexpath, 'r', encoding='UTF-8').read() index = index.replace("{title}", "Minecraft Overviewer") - index = index.replace("{time}", time.strftime("%a, %d %b %Y %H:%M:%S %Z", time.localtime()).decode(self.preferredencoding)) - versionstr = "%s (%s)" % (util.findGitVersion(), util.findGitHash()[:7]) + index = index.replace("{time}", + time.strftime("%a, %d %b %Y %H:%M:%S %Z", + time.localtime()) + .decode(self.preferredencoding)) + versionstr = "%s (%s)" % (util.findGitVersion(), + util.findGitHash()[:7]) index = index.replace("{version}", versionstr) with FileReplacer(indexpath, capabilities=self.fs_caps) as indexpath: diff --git a/overviewer_core/data/js_src/models.js b/overviewer_core/data/js_src/models.js deleted file mode 100644 index 930d36d..0000000 --- a/overviewer_core/data/js_src/models.js +++ /dev/null @@ -1,47 +0,0 @@ -overviewer.models = {}; - -/* WorldModel - * Primarily has a collection of TileSets - */ -overviewer.models.WorldModel = Backbone.Model.extend({ - initialize: function(attrs) { - attrs.tileSets = new overviewer.models.TileSetCollection(); - this.set(attrs); - } -}); - - -/* WorldCollection - * A collection of WorldModels - */ -overviewer.models.WorldCollection = Backbone.Collection.extend({ - model: overviewer.models.WorldModel -}); - - -/* TileSetModel - */ -overviewer.models.TileSetModel = Backbone.Model.extend({ - defaults: { - markers: [] - }, - initialize: function(attrs) { - // this implies that the Worlds collection must be - // initialized before any TIleSetModels are created - attrs.world = overviewer.collections.worlds.get(attrs.world); - this.set(attrs); - } -}); - -overviewer.models.TileSetCollection = Backbone.Collection.extend({ - model: overviewer.models.TileSetModel -}); - - -overviewer.models.GoogleMapModel = Backbone.Model.extend({ - initialize: function(attrs) { - attrs.currentWorldView = overviewer.collections.worldViews[0]; - this.set(attrs); - } -}); - diff --git a/overviewer_core/data/js_src/overviewer.js b/overviewer_core/data/js_src/overviewer.js index f8de2d0..8261d7a 100644 --- a/overviewer_core/data/js_src/overviewer.js +++ b/overviewer_core/data/js_src/overviewer.js @@ -11,8 +11,15 @@ var overviewer = {}; * This holds the map, probably the most important var in this file */ overviewer.map = null; -overviewer.mapView = null; +overviewer.worldCtrl = null; +overviewer.layerCtrl = null; +overviewer.compass = null; +overviewer.coord_box = null; +overviewer.current_world = null; +/// Records the current layer by name (if any) of each world +overviewer.current_layer = {}; + overviewer.collections = { /** @@ -29,6 +36,14 @@ overviewer.collections = { */ 'infoWindow': null, + /** + * When switching regionsets, where should we zoom to? + * Defaults to spawn. Stored as map of world names to [latlng, zoom] + */ + 'centers': {}, + + 'overlays': {}, + 'worldViews': [], 'haveSigns': false, @@ -39,13 +54,14 @@ overviewer.collections = { 'markerInfo': {}, /** - * holds a reference to the spawn marker. + * holds a reference to the spawn marker. */ 'spawnMarker': null, - - /** - * if a user visits a specific URL, this marker will point to the coordinates in the hash - */ + + /** + * if a user visits a specific URL, this marker will point to the + * coordinates in the hash + */ 'locationMarker': null }; @@ -72,39 +88,3 @@ overviewer.classes = { } }; - - -overviewer.gmap = { - - /** - * Generate a function to get the path to a tile at a particular location - * and zoom level. - * - * @param string path - * @param string pathBase - * @param string pathExt - */ - 'getTileUrlGenerator': function(path, pathBase, pathExt) { - return function(tile, zoom) { - var url = path; - var urlBase = ( pathBase ? pathBase : '' ); - if(tile.x < 0 || tile.x >= Math.pow(2, zoom) || - tile.y < 0 || tile.y >= Math.pow(2, zoom)) { - url += '/blank'; - } else if(zoom === 0) { - url += '/base'; - } else { - for(var z = zoom - 1; z >= 0; --z) { - var x = Math.floor(tile.x / Math.pow(2, z)) % 2; - var y = Math.floor(tile.y / Math.pow(2, z)) % 2; - url += '/' + (x + 2 * y); - } - } - url = url + '.' + pathExt; - if(typeof overviewerConfig.map.cacheTag !== 'undefined') { - url += '?c=' + overviewerConfig.map.cacheTag; - } - return(urlBase + url); - }; - } -}; diff --git a/overviewer_core/data/js_src/util.js b/overviewer_core/data/js_src/util.js index 6121cf5..97a9595 100644 --- a/overviewer_core/data/js_src/util.js +++ b/overviewer_core/data/js_src/util.js @@ -32,145 +32,296 @@ overviewer.util = { * feature gets added. */ 'initialize': function() { - overviewer.util.initializeClassPrototypes(); + //overviewer.util.initializeClassPrototypes(); + overviewer.util.initializePolyfills(); + overviewer.util.initializeMarkers(); - overviewer.collections.worlds = new overviewer.models.WorldCollection(); + document.getElementById('NoJSWarning').remove(); - $.each(overviewerConfig.worlds, function(index, el) { - var n = new overviewer.models.WorldModel({name: el, id:el}); - overviewer.collections.worlds.add(n); - }); + overviewer.coordBoxClass = L.Control.extend({ + options: { + position: 'bottomleft', + }, + initialize: function() { + this.coord_box = L.DomUtil.create('div', 'coordbox'); + }, + render: function(latlng) { + var currWorld = overviewer.current_world; + if (currWorld == null) {return;} - $.each(overviewerConfig.tilesets, function(index, el) { - var newTset = new overviewer.models.TileSetModel(el); - overviewer.collections.worlds.get(el.world).get("tileSets").add(newTset); - }); + var currTileset = overviewer.current_layer[currWorld]; + if (currTileset == null) {return;} - overviewer.collections.worlds.each(function(world, index, list) { - var nv = new overviewer.views.WorldView({model: world}); - overviewer.collections.worldViews.push(nv); - }); + var ovconf = currTileset.tileSetConfig; - overviewer.mapModel = new overviewer.models.GoogleMapModel({}); - overviewer.mapView = new overviewer.views.GoogleMapView({el: document.getElementById(overviewerConfig.CONST.mapDivId), model:overviewer.mapModel}); + w_coords = overviewer.util.fromLatLngToWorld(latlng.lat, latlng.lng, ovconf); - // any controls must be created after the GoogleMapView is created - // controls should be added in the order they should appear on screen, - // with controls on the outside of the page being added first + var r_x = Math.floor(Math.floor(w_coords.x / 16.0) / 32.0); + var r_z = Math.floor(Math.floor(w_coords.z / 16.0) / 32.0); + var r_name = "r." + r_x + "." + r_z + ".mca"; - var compass = new overviewer.views.CompassView({tagName: 'DIV', model:overviewer.mapModel}); - // no need to render the compass now. it's render event will get fired by - // the maptypeid_chagned event - - var coordsdiv = new overviewer.views.CoordboxView({tagName: 'DIV'}); - coordsdiv.render(); - - var progressdiv = new overviewer.views.ProgressView({tagName: 'DIV'}); - progressdiv.render(); - progressdiv.updateProgress(); - - if (overviewer.collections.haveSigns) { - var signs = new overviewer.views.SignControlView(); - signs.registerEvents(signs); - } - - var overlayControl = new overviewer.views.OverlayControlView(); - - var spawnmarker = new overviewer.views.SpawnIconView(); - - // Update coords on mousemove - google.maps.event.addListener(overviewer.map, 'mousemove', function (event) { - coordsdiv.updateCoords(event.latLng); + this.coord_box.innerHTML = "X " + + Math.round(w_coords.x) + + " Z " + Math.round(w_coords.z) + + " (" + r_name + ")"; + }, + onAdd: function() { + return this.coord_box; + } }); - google.maps.event.addListener(overviewer.map, 'idle', function (event) { + overviewer.compassClass = L.Control.extend({ + initialize: function(imagedict, options) { + L.Util.setOptions(this, options); + this.compass_img = L.DomUtil.create('img', 'compass'); + this.imagedict = imagedict; + }, + render: function(direction) { + this.compass_img.src = this.imagedict[direction]; + }, + onAdd: function() { + return this.compass_img; + } + }); + overviewer.control = L.Control.extend({ + initialize: function(options) { + L.Util.setOptions(this, options); + + this.container = L.DomUtil.create('div', 'worldcontrol'); + this.select = L.DomUtil.create('select'); + this.select.onchange = this.onChange; + this.container.appendChild(this.select); + }, + addWorld: function(world) { + var option = L.DomUtil.create('option'); + option.value = world; + option.innerText = world; + this.select.appendChild(option); + }, + onChange: function(ev) { + console.log(ev.target); + console.log(ev.target.value); + var selected_world = ev.target.value; + + + // save current view for the current_world + overviewer.collections.centers[overviewer.current_world][0] = overviewer.map.getCenter(); + overviewer.collections.centers[overviewer.current_world][1] = overviewer.map.getZoom(); + + overviewer.layerCtrl.remove(); + + overviewer.layerCtrl = L.control.layers( + overviewer.collections.mapTypes[selected_world], + overviewer.collections.overlays[selected_world], + {collapsed: false}) + .addTo(overviewer.map); + + for (var world_name in overviewer.collections.mapTypes) { + for (var tset_name in overviewer.collections.mapTypes[world_name]) { + var lyr = overviewer.collections.mapTypes[world_name][tset_name]; + if (world_name != selected_world) { + if (overviewer.map.hasLayer(lyr)) + overviewer.map.removeLayer(lyr); + } + } + + for (var tset_name in overviewer.collections.overlays[world_name]) { + var lyr = overviewer.collections.overlays[world_name][tset_name]; + if (world_name != selected_world) { + if (overviewer.map.hasLayer(lyr)) + overviewer.map.removeLayer(lyr); + } + } + } + + var center = overviewer.collections.centers[selected_world]; + overviewer.map.setView(center[0], center[1]); + + overviewer.current_world = selected_world; + + if (overviewer.collections.mapTypes[selected_world] && overviewer.current_layer[selected_world]) { + overviewer.map.addLayer(overviewer.collections.mapTypes[selected_world][overviewer.current_layer[selected_world].tileSetConfig.name]); + } else { + var tset_name = Object.keys(overviewer.collections.mapTypes[selected_world])[0] + overviewer.map.addLayer(overviewer.collections.mapTypes[selected_world][tset_name]); + } + }, + onAdd: function() { + console.log("onAdd mycontrol"); + + return this.container + } + }); + + + + overviewer.map = L.map('mcmap', { + crs: L.CRS.Simple, + minZoom: 0}); + + overviewer.map.attributionControl.setPrefix( + 'Overviewer/Leaflet'); + + overviewer.map.on('baselayerchange', function(ev) { + overviewer.current_layer[overviewer.current_world] = ev.layer; + var ovconf = ev.layer.tileSetConfig; + + // Change the compass + overviewer.compass.render(ovconf.north_direction); + + // Set the background colour + document.getElementById("mcmap").style.backgroundColor = ovconf.bgcolor; + + if (overviewer.collections.locationMarker) { + overviewer.collections.locationMarker.remove(); + } + // Remove old spawn marker, add new one + if (overviewer.collections.spawnMarker) { + overviewer.collections.spawnMarker.remove(); + } + if (typeof(ovconf.spawn) == "object") { + var spawnIcon = L.icon({ + iconUrl: overviewerConfig.CONST.image.spawnMarker, + iconRetinaUrl: overviewerConfig.CONST.image.spawnMarker2x, + iconSize: [32, 37], + iconAnchor: [15, 33], + }); + var latlng = overviewer.util.fromWorldToLatLng(ovconf.spawn[0], + ovconf.spawn[1], + ovconf.spawn[2], + ovconf); + var ohaimark = L.marker(latlng, {icon: spawnIcon, title: "Spawn"}); + ohaimark.on('click', function(ev) { + overviewer.map.setView(ev.latlng); + }); + overviewer.collections.spawnMarker = ohaimark + overviewer.collections.spawnMarker.addTo(overviewer.map); + } else { + overviewer.collections.spawnMarker = null; + } + + // reset the markers control with the markers for this layer + if (ovconf.marker_groups) { + console.log("markers for", ovconf.marker_groups); + markerCtrl = L.control.layers( + [], + ovconf.marker_groups, {collapsed: false}).addTo(overviewer.map); + } + overviewer.util.updateHash(); }); - google.maps.event.addListener(overviewer.map, 'maptypeid_changed', function(event) { - // it's handy to keep track of the currently visible tileset. we let - // the GoogleMapView manage this - overviewer.mapView.updateCurrentTileset(); - - compass.render(); - spawnmarker.render(); - if (overviewer.collections.locationMarker) { - overviewer.collections.locationMarker.setMap(null); - overviewer.collections.locationMarker = null; - } - - // update list of spawn overlays - overlayControl.render(); - - // re-center on the last viewport - var currentWorldView = overviewer.mapModel.get("currentWorldView"); - if (currentWorldView.options.lastViewport) { - var x = currentWorldView.options.lastViewport[0]; - var y = currentWorldView.options.lastViewport[1]; - var z = currentWorldView.options.lastViewport[2]; - var zoom = currentWorldView.options.lastViewport[3]; - - var latlngcoords = overviewer.util.fromWorldToLatLng(x, y, z, - overviewer.mapView.options.currentTileSet); - overviewer.map.setCenter(latlngcoords); - - if (zoom == 'max') { - zoom = overviewer.mapView.options.currentTileSet.get('maxZoom'); - } else if (zoom == 'min') { - zoom = overviewer.mapView.options.currentTileSet.get('minZoom'); - } else { - zoom = parseInt(zoom); - if (zoom < 0) { - // if zoom is negative, treat it as a "zoom out from max" - zoom += overviewer.mapView.options.currentTileSet.get('maxZoom'); - } else { - // fall back to default zoom - zoom = overviewer.mapView.options.currentTileSet.get('defaultZoom'); - } - } - - // clip zoom - if (zoom > overviewer.mapView.options.currentTileSet.get('maxZoom')) - zoom = overviewer.mapView.options.currentTileSet.get('maxZoom'); - if (zoom < overviewer.mapView.options.currentTileSet.get('minZoom')) - zoom = overviewer.mapView.options.currentTileSet.get('minZoom'); - - overviewer.map.setZoom(zoom); - } - + overviewer.map.on('moveend', function(ev) { + overviewer.util.updateHash(); + }); + + var tset = overviewerConfig.tilesets[0]; + overviewer.map.on("click", function(e) { + console.log(e.latlng); + var point = overviewer.util.fromLatLngToWorld(e.latlng.lat, e.latlng.lng, tset); + console.log(point); }); + var tilesetLayers = {} - // hook up some events - - overviewer.mapModel.bind("change:currentWorldView", overviewer.mapView.render, overviewer.mapView); - - overviewer.mapView.render(); - - // Jump to the hash if given (and do so for any further hash changes) - overviewer.util.initHash(); - $(window).on('hashchange', function() { overviewer.util.initHash(); }); - - // create this control after initHash so it can correctly select the current world - var worldSelector = new overviewer.views.WorldSelectorView({tagName:'DIV'}); - overviewer.collections.worlds.bind("add", worldSelector.render, worldSelector); - + overviewer.worldCtrl = new overviewer.control(); + overviewer.compass = new overviewer.compassClass( + overviewerConfig.CONST.image.compass); + overviewer.coord_box = new overviewer.coordBoxClass(); - overviewer.util.initializeMarkers(); - /* - overviewer.util.initializeMapTypes(); - overviewer.util.initializeMap(); - overviewer.util.initializeRegions(); - overviewer.util.createMapControls(); - */ - - // run ready callbacks now - google.maps.event.addListenerOnce(overviewer.map, 'idle', function(){ - // ok now.. - overviewer.util.runReadyQueue(); - overviewer.util.isReady = true; + $.each(overviewerConfig.worlds, function(idx, world_name) { + overviewer.collections.mapTypes[world_name] = {} + overviewer.collections.overlays[world_name] = {} + overviewer.worldCtrl.addWorld(world_name); }); + + overviewer.compass.addTo(overviewer.map); + overviewer.worldCtrl.addTo(overviewer.map); + overviewer.coord_box.addTo(overviewer.map); + + overviewer.map.on('mousemove', function(ev) { + overviewer.coord_box.render(ev.latlng); + }); + + $.each(overviewerConfig.tilesets, function(idx, obj) { + var myLayer = new L.tileLayer('', { + tileSize: overviewerConfig.CONST.tileSize, + noWrap: true, + maxZoom: obj.maxZoom, + minZoom: obj.minZoom, + errorTileUrl: obj.base + obj.path + "/blank." + obj.imgextension, + }); + myLayer.getTileUrl = overviewer.util.getTileUrlGenerator(obj.path, obj.base, obj.imgextension); + + if (obj.isOverlay) { + overviewer.collections.overlays[obj.world][obj.name] = myLayer; + } else { + overviewer.collections.mapTypes[obj.world][obj.name] = myLayer; + } + + obj.marker_groups = {}; + + if (overviewer.collections.haveSigns == true) { + // if there are markers for this tileset, create them now + if ((typeof markers !== 'undefined') && (obj.path in markers)) { + console.log("this tileset has markers:", obj); + + for (var mkidx = 0; mkidx < markers[obj.path].length; mkidx++) { + var marker_group = new L.layerGroup(); + var marker_entry = markers[obj.path][mkidx]; + var icon = L.icon({iconUrl: marker_entry.icon, + iconSize: [32, 32]}); + console.log("marker group:", marker_entry.displayName, marker_entry.groupName); + + for (var dbidx = 0; dbidx < markersDB[marker_entry.groupName].raw.length; dbidx++) { + var db = markersDB[marker_entry.groupName].raw[dbidx]; + var latlng = overviewer.util.fromWorldToLatLng(db.x, db.y, db.z, obj); + console.log(latlng); + marker_group.addLayer(new L.marker(latlng, { + icon: icon + })); + } + obj.marker_groups[marker_entry.displayName] = marker_group; + } + + + //var latlng = overviewer.util.fromWorldToLatLng( + // ovconf.spawn[0], + // ovconf.spawn[1], + // ovconf.spawn[2], + // obj); + //marker_group.addLayer(L.marker( + } + } + + myLayer["tileSetConfig"] = obj; + + + if (typeof(obj.spawn) == "object") { + var latlng = overviewer.util.fromWorldToLatLng(obj.spawn[0], obj.spawn[1], obj.spawn[2], obj); + overviewer.collections.centers[obj.world] = [ latlng, 1 ]; + } else { + overviewer.collections.centers[obj.world] = [ [0, 0], 1 ]; + } + + }); + + overviewer.layerCtrl = L.control.layers( + overviewer.collections.mapTypes[overviewerConfig.worlds[0]], + overviewer.collections.overlays[overviewerConfig.worlds[0]], + {collapsed: false}) + .addTo(overviewer.map); + overviewer.current_world = overviewerConfig.worlds[0]; + + //myLayer.addTo(overviewer.map); + overviewer.map.setView(overviewer.util.fromWorldToLatLng(tset.spawn[0], tset.spawn[1], tset.spawn[2], tset), 1); + + if (!overviewer.util.initHash()) { + overviewer.worldCtrl.onChange({target: {value: overviewer.current_world}}); + } + + }, 'injectMarkerScript': function(url) { @@ -180,10 +331,32 @@ overviewer.util = { }, 'initializeMarkers': function() { + if (overviewer.collections.haveSigns=true) { + console.log("initializeMarkers"); + + + //Object.keys( + // + } return; }, + /** Any polyfills needed to improve browser compatibility + */ + 'initializePolyfills': function() { + // From https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove + // IE is missing this + if (!('remove' in Element.prototype)) { + Element.prototype.remove = function() { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }; + } + + }, + /** * This adds some methods to these classes because Javascript is stupid @@ -262,15 +435,6 @@ overviewer.util = { "pregQuote": function(str) { return (str+'').replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1"); }, - /** - * Change the map's div's background color according to the mapType's bg_color setting - * - * @param string mapTypeId - * @return string - */ - 'getMapTypeBackgroundColor': function(id) { - return overviewerConfig.tilesets[id].bgcolor; - }, /** * Gee, I wonder what this does. * @@ -316,10 +480,10 @@ overviewer.util = { * * @return google.maps.LatLng */ - 'fromWorldToLatLng': function(x, y, z, model) { + 'fromWorldToLatLng': function(x, y, z, tset) { - var zoomLevels = model.get("zoomLevels"); - var north_direction = model.get('north_direction'); + var zoomLevels = tset.zoomLevels; + var north_direction = tset.north_direction; // the width and height of all the highest-zoom tiles combined, // inverted @@ -367,7 +531,7 @@ overviewer.util = { // add on 12 px to the X coordinate to center our point lng += 12 * perPixel; - return new google.maps.LatLng(lat, lng); + return [-lat*overviewerConfig.CONST.tileSize, lng*overviewerConfig.CONST.tileSize] }, /** * The opposite of fromWorldToLatLng @@ -379,9 +543,15 @@ overviewer.util = { * * @return Array */ - 'fromLatLngToWorld': function(lat, lng, model) { - var zoomLevels = model.get("zoomLevels"); - var north_direction = model.get("north_direction"); + 'fromLatLngToWorld': function(lat, lng, tset) { + var zoomLevels = tset.zoomLevels; + var north_direction = tset.north_direction; + + lat = -lat/overviewerConfig.CONST.tileSize; + lng = lng/overviewerConfig.CONST.tileSize; + + // lat lng will always be between (0,0) -- top left corner + // (-384, 384) -- bottom right corner // Initialize world x/y/z object to be returned var point = Array(); @@ -482,52 +652,42 @@ overviewer.util = { overviewer.util.goToHash(); // Clean up the hash. overviewer.util.updateHash(); + return true; + } else { + return false; // signal to caller that we didn't goto any hash } } }, 'setHash': function(x, y, z, zoom, w, maptype) { // save this info is a nice easy to parse format - var currentWorldView = overviewer.mapModel.get("currentWorldView"); - currentWorldView.options.lastViewport = [x,y,z,zoom]; - var newHash = "#/" + Math.floor(x) + "/" + Math.floor(y) + "/" + Math.floor(z) + "/" + zoom + "/" + w + "/" + maptype; + var newHash = "#/" + Math.floor(x) + "/" + Math.floor(y) + "/" + Math.floor(z) + "/" + zoom + "/" + encodeURI(w) + "/" + encodeURI(maptype); overviewer.util.lastHash = newHash; // this should not trigger initHash window.location.replace(newHash); }, 'updateHash': function() { - var currTileset = overviewer.mapView.options.currentTileSet; + // name of current world + var currWorld = overviewer.current_world; + if (currWorld == null) {return;} + + var currTileset = overviewer.current_layer[currWorld]; if (currTileset == null) {return;} - var coordinates = overviewer.util.fromLatLngToWorld(overviewer.map.getCenter().lat(), - overviewer.map.getCenter().lng(), - currTileset); + + var ovconf = currTileset.tileSetConfig; + + var coordinates = overviewer.util.fromLatLngToWorld(overviewer.map.getCenter().lat, + overviewer.map.getCenter().lng, + ovconf); var zoom = overviewer.map.getZoom(); - var maptype = overviewer.map.getMapTypeId(); - // convert mapType into a index - var currentWorldView = overviewer.mapModel.get("currentWorldView"); - var maptypeId = -1; - for (id in currentWorldView.options.mapTypeIds) { - if (currentWorldView.options.mapTypeIds[id] == maptype) { - maptypeId = id; - } - } - - var worldId = -1; - for (id in overviewer.collections.worldViews) { - if (overviewer.collections.worldViews[id] == currentWorldView) { - worldId = id; - } - } - - - if (zoom >= currTileset.get('maxZoom')) { + if (zoom >= ovconf.maxZoom) { zoom = 'max'; - } else if (zoom <= currTileset.get('minZoom')) { + } else if (zoom <= ovconf.minZoom) { zoom = 'min'; } else { // default to (map-update friendly) negative zooms - zoom -= currTileset.get('maxZoom'); + zoom -= ovconf.maxZoom; } - overviewer.util.setHash(coordinates.x, coordinates.y, coordinates.z, zoom, worldId, maptypeId); + overviewer.util.setHash(coordinates.x, coordinates.y, coordinates.z, zoom, currWorld, ovconf.name); }, 'goToHash': function() { // Note: the actual data begins at coords[1], coords[0] is empty. @@ -535,52 +695,104 @@ overviewer.util = { var zoom; - var worldid = -1; - var maptyped = -1; + var world_name = null; + var tileset_name = null; // The if-statements try to prevent unexpected behaviour when using incomplete hashes, e.g. older links if (coords.length > 4) { zoom = coords[4]; } if (coords.length > 6) { - worldid = coords[5]; - maptypeid = coords[6]; + world_name = decodeURI(coords[5]); + tileset_name = decodeURI(coords[6]); } - var worldView = overviewer.collections.worldViews[worldid]; - overviewer.mapModel.set({currentWorldView: worldView}); - var maptype = worldView.options.mapTypeIds[maptypeid]; - overviewer.map.setMapTypeId(maptype); - var tsetModel = worldView.model.get("tileSets").at(maptypeid); - + var target_layer = overviewer.collections.mapTypes[world_name][tileset_name]; + var ovconf = target_layer.tileSetConfig; + var latlngcoords = overviewer.util.fromWorldToLatLng(parseInt(coords[1]), parseInt(coords[2]), parseInt(coords[3]), - tsetModel); + ovconf); if (zoom == 'max') { - zoom = tsetModel.get('maxZoom'); + zoom = ovconf.maxZoom; } else if (zoom == 'min') { - zoom = tsetModel.get('minZoom'); + zoom = ovconf.minZoom; } else { zoom = parseInt(zoom); if (zoom < 0) { // if zoom is negative, treat it as a "zoom out from max" - zoom += tsetModel.get('maxZoom'); + zoom += ovconf.maxZoom; } else { // fall back to default zoom - zoom = tsetModel.get('defaultZoom'); + zoom = ovconf.defaultZoom; } } // clip zoom - if (zoom > tsetModel.get('maxZoom')) - zoom = tsetModel.get('maxZoom'); - if (zoom < tsetModel.get('minZoom')) - zoom = tsetModel.get('minZoom'); + if (zoom > ovconf.maxZoom) + zoom = ovconf.maxZoom; + if (zoom < ovconf.minZoom) + zoom = ovconf.minZoom; - overviewer.map.setCenter(latlngcoords); - overviewer.map.setZoom(zoom); - var locationmarker = new overviewer.views.LocationIconView(); - locationmarker.render(); + // build a fake event for the world switcher control + overviewer.worldCtrl.onChange({target: {value: world_name}}); + overviewer.worldCtrl.select.value = world_name; + if (!overviewer.map.hasLayer(target_layer)) { + overviewer.map.addLayer(target_layer); + } + + overviewer.map.setView(latlngcoords, zoom); + + if (ovconf.showlocationmarker) { + var locationIcon = L.icon({ + iconUrl: overviewerConfig.CONST.image.queryMarker, + iconRetinaUrl: overviewerConfig.CONST.image.queryMarker2x, + iconSize: [32, 37], + iconAnchor: [15, 33], + }); + var locationm = L.marker(latlngcoords, { icon: locationIcon, + title: "Linked location"}); + overviewer.collections.locationMarker = locationm + overviewer.collections.locationMarker.on('contextmenu', function(ev) { + overviewer.collections.locationMarker.remove(); + }); + overviewer.collections.locationMarker.on('click', function(ev) { + overviewer.map.setView(ev.latlng); + }); + overviewer.collections.locationMarker.addTo(overviewer.map); + } + }, + /** + * Generate a function to get the path to a tile at a particular location + * and zoom level. + * + * @param string path + * @param string pathBase + * @param string pathExt + */ + 'getTileUrlGenerator': function(path, pathBase, pathExt) { + return function(o) { + var url = path; + var zoom = o.z; + var urlBase = ( pathBase ? pathBase : '' ); + if(o.x < 0 || o.x >= Math.pow(2, zoom) || + o.y < 0 || o.y >= Math.pow(2, zoom)) { + url += '/blank'; + } else if(zoom === 0) { + url += '/base'; + } else { + for(var z = zoom - 1; z >= 0; --z) { + var x = Math.floor(o.x / Math.pow(2, z)) % 2; + var y = Math.floor(o.y / Math.pow(2, z)) % 2; + url += '/' + (x + 2 * y); + } + } + url = url + '.' + pathExt; + if(typeof overviewerConfig.map.cacheTag !== 'undefined') { + url += '?c=' + overviewerConfig.map.cacheTag; + } + return(urlBase + url); + }; } }; diff --git a/overviewer_core/data/js_src/views.js b/overviewer_core/data/js_src/views.js deleted file mode 100644 index 6259223..0000000 --- a/overviewer_core/data/js_src/views.js +++ /dev/null @@ -1,621 +0,0 @@ -overviewer.views= {} - - -overviewer.views.WorldView = Backbone.View.extend({ - initialize: function(opts) { - this.options.mapTypes = []; - this.options.overlayMapTypes = []; - this.options.mapTypeIds = []; - this.options.overlayMapTypeIds = []; - - var curTileSet = this.model.get("tileSets").at(0); - var spawn = curTileSet.get("spawn"); - if (spawn == "false") { - var spawn = [0,64,0]; - } - this.options.lastViewport = [spawn[0],spawn[1],spawn[2],curTileSet.get("defaultZoom")]; - - this.model.get("tileSets").each(function(tset, index, list) { - // ignore overlays: - var ops = { - getTileUrl: overviewer.gmap.getTileUrlGenerator(tset.get("path"), tset.get("base"), tset.get("imgextension")), - 'tileSize': new google.maps.Size( - overviewerConfig.CONST.tileSize, - overviewerConfig.CONST.tileSize), - 'maxZoom': tset.get("maxZoom"), - 'minZoom': tset.get("minZoom"), - 'isPng': (tset.get("imgextension")=="png") - }; - var newMapType = new google.maps.ImageMapType(ops); - newMapType.name = tset.get("name"); - newMapType.shortname = tset.get("name"); - newMapType.alt = "Minecraft " + tset.get("name") + " Map"; - newMapType.projection = new overviewer.classes.MapProjection(); - newMapType._ov_tileSet = tset; - - if (tset.get("isOverlay")) { - newMapType.tiles = tset.get("tilesets"); - this.options.overlayMapTypes.push(newMapType); - this.options.overlayMapTypeIds.push(overviewerConfig.CONST.mapDivId + this.model.get("name") + tset.get("name")); - } else { - this.options.mapTypes.push(newMapType); - this.options.mapTypeIds.push(overviewerConfig.CONST.mapDivId + this.model.get("name") + tset.get("name")); - } - - }, this); - - this.model.get("tileSets").each(function(tset, index, list) { - // ignore non-overlays: - if (!tset.get("isOverlay")) { return; }; - - }); - } -}); - - - -overviewer.views.WorldSelectorView = Backbone.View.extend({ - initialize: function() { - if(overviewer.collections.worldViews.length > 1) { - $(this.el).addClass("customControl"); - - // a div will have already been created for us, we just - // need to register it with the google maps control - var selectBox = document.createElement('select'); - $.each(overviewer.collections.worldViews, function(index, elem) { - var o = document.createElement("option"); - o.value = elem.model.get("name"); - o.innerHTML = elem.model.get("name"); - if (elem.model == overviewer.mapModel.get("currentWorldView").model) { - o.selected=true; - } - $(o).data("viewObj", elem); - selectBox.appendChild(o); - - }); - - this.el.appendChild(selectBox); - overviewer.map.controls[google.maps.ControlPosition.TOP_LEFT].push(this.el); - } - }, - events: { - "change select": "changeWorld" - }, - changeWorld: function() { - var selectObj = this.$("select")[0]; - var selectedOption = selectObj.options[selectObj.selectedIndex]; - - overviewer.mapModel.set({currentWorldView: $(selectedOption).data("viewObj")}); - // - }, - render: function(t) { - //console.log("WorldSelectorView::render() TODO implement this (low priority)"); - } -}); - - - -overviewer.views.CompassView = Backbone.View.extend({ - initialize: function() { - this.el.index=0; - var compassImg = document.createElement('IMG'); - compassImg.src = ''; // this will be set properly in the render function (below) - this.el.appendChild(compassImg); - - overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(this.el); - }, - /** - * CompassView::render - */ - render: function() { - var tsetModel = overviewer.mapView.options.currentTileSet; - var northdir = tsetModel.get("north_direction"); - if (northdir == overviewerConfig.CONST.UPPERLEFT) - this.$("IMG").attr("src","compass_upper-left.png"); - if (northdir == overviewerConfig.CONST.UPPERRIGHT) - this.$("IMG").attr("src", "compass_upper-right.png"); - if (northdir == overviewerConfig.CONST.LOWERLEFT) - this.$("IMG").attr("src", "compass_lower-left.png"); - if (northdir == overviewerConfig.CONST.LOWERRIGHT) - this.$("IMG").attr("src", "compass_lower-right.png"); - } -}); - - -overviewer.views.CoordboxView = Backbone.View.extend({ - initialize: function() { - // Coords box - this.el.id = 'coordsDiv'; - this.el.innerHTML = 'coords here'; - overviewer.map.controls[google.maps.ControlPosition.BOTTOM_LEFT].push(this.el); - }, - updateCoords: function(latLng) { - var worldcoords = overviewer.util.fromLatLngToWorld(latLng.lat(), - latLng.lng(), - overviewer.mapView.options.currentTileSet); - - var regionfileX = Math.floor(Math.floor(worldcoords.x / 16.0) / 32.0); - var regionfileZ = Math.floor(Math.floor(worldcoords.z / 16.0) / 32.0); - var regionfilename = "r." + regionfileX + "." + regionfileZ + ".mca"; - - this.el.innerHTML = "X " + Math.round(worldcoords.x) + - " Z " + Math.round(worldcoords.z) + - " (" + regionfilename + ")"; - } -}); - -overviewer.views.ProgressView = Backbone.View.extend({ - initialize: function() { - this.el.id = 'progressDiv'; - this.el.innerHTML = 'Current Render Progress'; - overviewer.map.controls[google.maps.ControlPosition.BOTTOM_RIGHT].push(this.el); - $(this.el).hide(); - $.ajaxSetup({cache: false}); - }, - updateProgress: function() { - e = this; - $.getJSON('progress.json', null, function(d){ - if (!(d == null||d=='')) { - $(e.el).show(); - e.el.innerHTML = d['message']; - if (d.update > 0) { - setTimeout("e.updateProgress()", d.update); - } else { - setTimeout("e.updateProgress()", 60000); - e.el.innerHTML="Hidden - d.update < 0"; - $(e.el).hide(); - } - } else { - e.el.innerHTML="Hidden - !!d==false"; - $(e.el).hide(); - } - }); - } -}); - -/* GoogleMapView is responsible for dealing with the GoogleMaps API to create the - */ - -overviewer.views.GoogleMapView = Backbone.View.extend({ - initialize: function(opts) { - this.options.map = null; - var curWorld = this.model.get("currentWorldView").model; - - var curTset = curWorld.get("tileSets").at(0); - var spawn = curTset.get("spawn"); - if (spawn == "false") { - var spawn = [0,64,0]; - } - var mapcenter = overviewer.util.fromWorldToLatLng( - spawn[0], - spawn[1], - spawn[2], - curTset); - - - this.options.mapTypes=[]; - this.options.mapTypeIds=[]; - var opts = this.options; - - var mapOptions = {}; - // - // init the map with some default options. use the first tileset in the first world - this.options.mapOptions = { - zoom: curTset.get("defaultZoom"), - center: mapcenter, - panControl: true, - scaleControl: false, - mapTypeControl: true, - //mapTypeControlOptions: { - //mapTypeIds: this.options.mapTypeIds - //}, - mapTypeId: '', - streetViewControl: false, - overviewMapControl: true, - zoomControl: true, - backgroundColor: curTset.get("bgcolor") - }; - - - overviewer.map = new google.maps.Map(this.el, this.options.mapOptions); - - // register every ImageMapType with the map - $.each(overviewer.collections.worldViews, function( index, worldView) { - $.each(worldView.options.mapTypes, function(i_index, maptype) { - overviewer.map.mapTypes.set(overviewerConfig.CONST.mapDivId + - worldView.model.get("name") + maptype.shortname , maptype); - }); - }); - - }, - /* GoogleMapView::render() - * Should be called when the current world has changed in GoogleMapModel - */ - render: function() { - var view = this.model.get("currentWorldView"); - this.options.mapOptions.mapTypeControlOptions = { - mapTypeIds: view.options.mapTypeIds}; - this.options.mapOptions.mapTypeId = view.options.mapTypeIds[0]; - overviewer.map.setOptions(this.options.mapOptions); - - - return this; - }, - /** - * GoogleMapView::updateCurrentTileset() - * Keeps track of the currently visible tileset - */ - updateCurrentTileset: function() { - var currentWorldView = this.model.get("currentWorldView"); - var gmapCurrent = overviewer.map.getMapTypeId(); - for (id in currentWorldView.options.mapTypeIds) { - if (currentWorldView.options.mapTypeIds[id] == gmapCurrent) { - this.options.currentTileSet = currentWorldView.options.mapTypes[id]._ov_tileSet; - } - } - - // for this world, remember our current viewport (as worldcoords, not LatLng) - // - - } - -}); - - -/** - * OverlayControlView - */ -overviewer.views.OverlayControlView = Backbone.View.extend({ - /** OverlayControlVIew::initialize - */ - initialize: function(opts) { - $(this.el).addClass("customControl"); - overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(this.el); - }, - registerEvents: function(me) { - overviewer.mapModel.bind("change:currentWorldView", me.render, me); - }, - - /** - * OverlayControlView::render - */ - render: function() { - this.el.innerHTML=""; - - // hide all visible overlays: - overviewer.map.overlayMapTypes.clear() - - // if this world has no overlays, don't create this control - var mapTypes = overviewer.mapModel.get('currentWorldView').options.overlayMapTypes; - if (mapTypes.length == 0) { return; } - - var controlText = document.createElement('DIV'); - controlText.innerHTML = "Overlays"; - - var controlBorder = document.createElement('DIV'); - $(controlBorder).addClass('top'); - this.el.appendChild(controlBorder); - controlBorder.appendChild(controlText); - - var dropdownDiv = document.createElement('DIV'); - $(dropdownDiv).addClass('dropDown'); - this.el.appendChild(dropdownDiv); - dropdownDiv.innerHTML=''; - - $(controlText).click(function() { - $(controlBorder).toggleClass('top-active'); - $(dropdownDiv).toggle(); - }); - - var currentTileSetPath = overviewer.mapView.options.currentTileSet.get('path'); - - for (i in mapTypes) { - var mt = mapTypes[i]; - // if this overlay specifies a list of valid tilesets, then skip over any invalid tilesets - if ((mt.tiles.length > 0) && (mt.tiles.indexOf(currentTileSetPath) ==-1)) { - continue; - } - this.addItem({label: mt.name, - name: mt.name, - mt: mt, - - action: function(this_item, checked) { - if (checked) { - overviewer.map.overlayMapTypes.push(this_item.mt); - } else { - var idx_to_delete = -1; - overviewer.map.overlayMapTypes.forEach(function(e, j) { - if (e == this_item.mt) { - idx_to_delete = j; - } - }); - if (idx_to_delete >= 0) { - overviewer.map.overlayMapTypes.removeAt(idx_to_delete); - } - } - - } - }); - } - - - }, - - addItem: function(item) { - var itemDiv = document.createElement('div'); - var itemInput = document.createElement('input'); - itemInput.type='checkbox'; - - // if this overlay is already visible, set the checkbox - // to checked - overviewer.map.overlayMapTypes.forEach(function(e, j) { - if (e == item.mt) { - itemInput.checked=true; - } - }); - - // give it a name - $(itemInput).attr("_mc_overlayname", item.name); - jQuery(itemInput).click((function(local_item) { - return function(e) { - item.action(local_item, e.target.checked); - }; - })(item)); - - this.$(".dropDown")[0].appendChild(itemDiv); - itemDiv.appendChild(itemInput); - var textNode = document.createElement('text'); - textNode.innerHTML = item.label + '
'; - - itemDiv.appendChild(textNode); - } -}); - - -/** - * SignControlView - */ -overviewer.views.SignControlView = Backbone.View.extend({ - /** SignControlView::initialize - */ - initialize: function(opts) { - $(this.el).addClass("customControl"); - overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(this.el); - - }, - registerEvents: function(me) { - google.maps.event.addListener(overviewer.map, 'maptypeid_changed', function(event) { - overviewer.mapView.updateCurrentTileset(); - - // workaround IE issue. bah! - if (typeof markers=="undefined") { return; } - me.render(); - - - // hide markers that are part of other tilesets than this - // for each markerSet, check: - // if the markerSet isnot part of this tileset, hide all of the markers - var curMarkerSet = overviewer.mapView.options.currentTileSet.get("path"); - var dataRoot = markers[curMarkerSet]; - - jQuery.each(markers, function(key, markerSet) { - if (key != curMarkerSet) { - jQuery.each(markerSet, function(i, markerGroup) { - if (typeof markerGroup.markerObjs != "undefined") { - jQuery.each(markerGroup.markerObjs, function(j, markerObj) { - markerObj.setVisible(false); - }); - } - }); - } - }); - - return; - - }); - - }, - /** - * SignControlView::render - */ - render: function() { - - var curMarkerSet = overviewer.mapView.options.currentTileSet.get("path"); - //var dataRoot = overviewer.collections.markerInfo[curMarkerSet]; - var dataRoot = markers[curMarkerSet]; - - this.el.innerHTML=""; - - // if we have no markerSets for this tileset, do nothing: - if (!dataRoot) { return; } - - - var controlText = document.createElement('DIV'); - controlText.innerHTML = overviewer.mapView.options.currentTileSet.get("poititle"); - - var controlBorder = document.createElement('DIV'); - $(controlBorder).addClass('top'); - this.el.appendChild(controlBorder); - controlBorder.appendChild(controlText); - - var dropdownDiv = document.createElement('DIV'); - $(dropdownDiv).addClass('dropDown'); - this.el.appendChild(dropdownDiv); - dropdownDiv.innerHTML=''; - - // add the functionality to toggle visibility of the items - $(controlText).click(function() { - $(controlBorder).toggleClass('top-active'); - $(dropdownDiv).toggle(); - }); - - - - //dataRoot['markers'] = []; - // - for (i in dataRoot) { - var groupName = dataRoot[i].groupName; - if (!dataRoot[i].created) { - dataRoot[i].markerObjs = []; - for (j in markersDB[groupName].raw) { - var entity = markersDB[groupName].raw[j]; - if (entity['icon']) { - iconURL = entity['icon']; - } else { - iconURL = dataRoot[i].icon; - } - var marker = new google.maps.Marker({ - 'position': overviewer.util.fromWorldToLatLng(entity.x, - entity.y, entity.z, overviewer.mapView.options.currentTileSet), - 'map': overviewer.map, - 'title': jQuery.trim(entity.hovertext), - 'content': jQuery.trim(entity.text), - 'icon': iconURL, - 'visible': false - }); - if(entity['createInfoWindow'] == true) { - overviewer.util.createMarkerInfoWindow(marker); - } else { - if(dataRoot[i].createInfoWindow == true) { - overviewer.util.createMarkerInfoWindow(marker); - } - } - dataRoot[i].markerObjs.push(marker); - // Polyline stuff added by FreakusGeekus. Probably needs work. - if (typeof entity['polyline'] != 'undefined') { - var polypath = new Array(); - for (point in entity.polyline) { - polypath.push(overviewer.util.fromWorldToLatLng(entity.polyline[point].x, entity.polyline[point].y, entity.polyline[point].z, overviewer.mapView.options.currentTileSet)); - } - - var polyline = new google.maps.Polyline({ - 'path': polypath, - 'clickable': false, - 'map': overviewer.map, - 'visible': false, - 'strokeColor': entity['strokeColor'] - }); - dataRoot[i].markerObjs.push(polyline); - } - - // Polygons - if (typeof entity['polygon'] != 'undefined') { - var polypath = new Array(); - for (point in entity.polygon) { - polypath.push(overviewer.util.fromWorldToLatLng(entity.polygon[point].x, entity.polygon[point].y, entity.polygon[point].z, overviewer.mapView.options.currentTileSet)); - } - - var polygon = new google.maps.Polygon({ - 'clickable': false, - 'fillColor': entity['fillColor'], - 'fillOpacity': entity['fillOpacity'], - 'map': overviewer.map, - 'path': polypath, - 'strokeColor': entity['strokeColor'], - 'strokeOpacity': entity['strokeOpacity'], - 'visible': false - }); - dataRoot[i].markerObjs.push(polygon); - } - } - dataRoot[i].created = true; - } - } - - // add some menus - for (i in dataRoot) { - var group = dataRoot[i]; - this.addItem({group: group, action:function(this_item, checked) { - this_item.group.checked = checked; - jQuery.each(this_item.group.markerObjs, function(i, markerObj) { - markerObj.setVisible(checked); - }); - }}); - if (group.checked) { - jQuery.each(group.markerObjs, function(i, markerObj) { - markerObj.setVisible(true); - }); - } - } - - - }, - addItem: function(item) { - var itemDiv = document.createElement('div'); - var itemInput = document.createElement('input'); - itemInput.type='checkbox'; - - if (item.group.checked) { - itemInput.checked="true"; - } - - // give it a name - $(itemInput).data('label',item.group.displayName); - $(itemInput).attr("_mc_groupname", item.group.gropuName); - jQuery(itemInput).click((function(local_item) { - return function(e) { - item.action(local_item, e.target.checked); - }; - })(item)); - - this.$(".dropDown")[0].appendChild(itemDiv); - itemDiv.appendChild(itemInput); - var textNode = document.createElement('text'); - if(item.icon) { - textNode.innerHTML = '' + item.group.displayName + ' 
'; - } else { - textNode.innerHTML = item.group.displayName + ' 
'; - } - - itemDiv.appendChild(textNode); - itemDiv.style.whiteSpace = "nowrap"; - - } -}); - -/** - * SpawnIconView - */ -overviewer.views.SpawnIconView = Backbone.View.extend({ - render: function() { - // - var curTileSet = overviewer.mapView.options.currentTileSet; - if (overviewer.collections.spawnMarker) { - overviewer.collections.spawnMarker.setMap(null); - overviewer.collections.spawnMarker = null; - } - var spawn = curTileSet.get("spawn"); - if (spawn) { - overviewer.collections.spawnMarker = new google.maps.Marker({ - 'position': overviewer.util.fromWorldToLatLng(spawn[0], - spawn[1], spawn[2], overviewer.mapView.options.currentTileSet), - 'map': overviewer.map, - 'title': 'spawn', - 'icon': overviewerConfig.CONST.image.spawnMarker, - 'visible': false - }); - overviewer.collections.spawnMarker.setVisible(true); - } - } -}); - -overviewer.views.LocationIconView = Backbone.View.extend({ - render: function() { - // - if (overviewer.collections.locationMarker) { - overviewer.collections.locationMarker.setMap(null); - overviewer.collections.locationMarker = null; - } - overviewer.collections.locationMarker = new google.maps.Marker({ - 'position': overviewer.map.getCenter(), - 'map': overviewer.map, - 'title': 'location', - 'icon': overviewerConfig.CONST.image.queryMarker, - 'visible': false - }); - overviewer.collections.locationMarker.setVisible(overviewer.mapView.options.currentTileSet.get("showlocationmarker")); - - } -}); - diff --git a/overviewer_core/data/textures/icon_src/marker_home.svg b/overviewer_core/data/textures/icon_src/marker_home.svg new file mode 100644 index 0000000..f8b2b66 --- /dev/null +++ b/overviewer_core/data/textures/icon_src/marker_home.svg @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/overviewer_core/data/textures/icon_src/marker_location.svg b/overviewer_core/data/textures/icon_src/marker_location.svg new file mode 100644 index 0000000..e12530b --- /dev/null +++ b/overviewer_core/data/textures/icon_src/marker_location.svg @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/overviewer_core/data/web_assets/backbone.js b/overviewer_core/data/web_assets/backbone.js deleted file mode 100644 index b2e4932..0000000 --- a/overviewer_core/data/web_assets/backbone.js +++ /dev/null @@ -1,1158 +0,0 @@ -// Backbone.js 0.5.3 -// (c) 2010 Jeremy Ashkenas, DocumentCloud Inc. -// Backbone may be freely distributed under the MIT license. -// For all details and documentation: -// http://documentcloud.github.com/backbone - -(function(){ - - // Initial Setup - // ------------- - - // Save a reference to the global object. - var root = this; - - // Save the previous value of the `Backbone` variable. - var previousBackbone = root.Backbone; - - // The top-level namespace. All public Backbone classes and modules will - // be attached to this. Exported for both CommonJS and the browser. - var Backbone; - if (typeof exports !== 'undefined') { - Backbone = exports; - } else { - Backbone = root.Backbone = {}; - } - - // Current version of the library. Keep in sync with `package.json`. - Backbone.VERSION = '0.5.3'; - - // Require Underscore, if we're on the server, and it's not already present. - var _ = root._; - if (!_ && (typeof require !== 'undefined')) _ = require('underscore')._; - - // For Backbone's purposes, jQuery or Zepto owns the `$` variable. - var $ = root.jQuery || root.Zepto; - - // Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable - // to its previous owner. Returns a reference to this Backbone object. - Backbone.noConflict = function() { - root.Backbone = previousBackbone; - return this; - }; - - // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option will - // fake `"PUT"` and `"DELETE"` requests via the `_method` parameter and set a - // `X-Http-Method-Override` header. - Backbone.emulateHTTP = false; - - // Turn on `emulateJSON` to support legacy servers that can't deal with direct - // `application/json` requests ... will encode the body as - // `application/x-www-form-urlencoded` instead and will send the model in a - // form param named `model`. - Backbone.emulateJSON = false; - - // Backbone.Events - // ----------------- - - // A module that can be mixed in to *any object* in order to provide it with - // custom events. You may `bind` or `unbind` a callback function to an event; - // `trigger`-ing an event fires all callbacks in succession. - // - // var object = {}; - // _.extend(object, Backbone.Events); - // object.bind('expand', function(){ alert('expanded'); }); - // object.trigger('expand'); - // - Backbone.Events = { - - // Bind an event, specified by a string name, `ev`, to a `callback` function. - // Passing `"all"` will bind the callback to all events fired. - bind : function(ev, callback, context) { - var calls = this._callbacks || (this._callbacks = {}); - var list = calls[ev] || (calls[ev] = []); - list.push([callback, context]); - return this; - }, - - // Remove one or many callbacks. If `callback` is null, removes all - // callbacks for the event. If `ev` is null, removes all bound callbacks - // for all events. - unbind : function(ev, callback) { - var calls; - if (!ev) { - this._callbacks = {}; - } else if (calls = this._callbacks) { - if (!callback) { - calls[ev] = []; - } else { - var list = calls[ev]; - if (!list) return this; - for (var i = 0, l = list.length; i < l; i++) { - if (list[i] && callback === list[i][0]) { - list[i] = null; - break; - } - } - } - } - return this; - }, - - // Trigger an event, firing all bound callbacks. Callbacks are passed the - // same arguments as `trigger` is, apart from the event name. - // Listening for `"all"` passes the true event name as the first argument. - trigger : function(eventName) { - var list, calls, ev, callback, args; - var both = 2; - if (!(calls = this._callbacks)) return this; - while (both--) { - ev = both ? eventName : 'all'; - if (list = calls[ev]) { - for (var i = 0, l = list.length; i < l; i++) { - if (!(callback = list[i])) { - list.splice(i, 1); i--; l--; - } else { - args = both ? Array.prototype.slice.call(arguments, 1) : arguments; - callback[0].apply(callback[1] || this, args); - } - } - } - } - return this; - } - - }; - - // Backbone.Model - // -------------- - - // Create a new model, with defined attributes. A client id (`cid`) - // is automatically generated and assigned for you. - Backbone.Model = function(attributes, options) { - var defaults; - attributes || (attributes = {}); - if (defaults = this.defaults) { - if (_.isFunction(defaults)) defaults = defaults.call(this); - attributes = _.extend({}, defaults, attributes); - } - this.attributes = {}; - this._escapedAttributes = {}; - this.cid = _.uniqueId('c'); - this.set(attributes, {silent : true}); - this._changed = false; - this._previousAttributes = _.clone(this.attributes); - if (options && options.collection) this.collection = options.collection; - this.initialize(attributes, options); - }; - - // Attach all inheritable methods to the Model prototype. - _.extend(Backbone.Model.prototype, Backbone.Events, { - - // A snapshot of the model's previous attributes, taken immediately - // after the last `"change"` event was fired. - _previousAttributes : null, - - // Has the item been changed since the last `"change"` event? - _changed : false, - - // The default name for the JSON `id` attribute is `"id"`. MongoDB and - // CouchDB users may want to set this to `"_id"`. - idAttribute : 'id', - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // Return a copy of the model's `attributes` object. - toJSON : function() { - return _.clone(this.attributes); - }, - - // Get the value of an attribute. - get : function(attr) { - return this.attributes[attr]; - }, - - // Get the HTML-escaped value of an attribute. - escape : function(attr) { - var html; - if (html = this._escapedAttributes[attr]) return html; - var val = this.attributes[attr]; - return this._escapedAttributes[attr] = escapeHTML(val == null ? '' : '' + val); - }, - - // Returns `true` if the attribute contains a value that is not null - // or undefined. - has : function(attr) { - return this.attributes[attr] != null; - }, - - // Set a hash of model attributes on the object, firing `"change"` unless you - // choose to silence it. - set : function(attrs, options) { - - // Extract attributes and options. - options || (options = {}); - if (!attrs) return this; - if (attrs.attributes) attrs = attrs.attributes; - var now = this.attributes, escaped = this._escapedAttributes; - - // Run validation. - if (!options.silent && this.validate && !this._performValidation(attrs, options)) return false; - - // Check for changes of `id`. - if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; - - // We're about to start triggering change events. - var alreadyChanging = this._changing; - this._changing = true; - - // Update attributes. - for (var attr in attrs) { - var val = attrs[attr]; - if (!_.isEqual(now[attr], val)) { - now[attr] = val; - delete escaped[attr]; - this._changed = true; - if (!options.silent) this.trigger('change:' + attr, this, val, options); - } - } - - // Fire the `"change"` event, if the model has been changed. - if (!alreadyChanging && !options.silent && this._changed) this.change(options); - this._changing = false; - return this; - }, - - // Remove an attribute from the model, firing `"change"` unless you choose - // to silence it. `unset` is a noop if the attribute doesn't exist. - unset : function(attr, options) { - if (!(attr in this.attributes)) return this; - options || (options = {}); - var value = this.attributes[attr]; - - // Run validation. - var validObj = {}; - validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - // Remove the attribute. - delete this.attributes[attr]; - delete this._escapedAttributes[attr]; - if (attr == this.idAttribute) delete this.id; - this._changed = true; - if (!options.silent) { - this.trigger('change:' + attr, this, void 0, options); - this.change(options); - } - return this; - }, - - // Clear all attributes on the model, firing `"change"` unless you choose - // to silence it. - clear : function(options) { - options || (options = {}); - var attr; - var old = this.attributes; - - // Run validation. - var validObj = {}; - for (attr in old) validObj[attr] = void 0; - if (!options.silent && this.validate && !this._performValidation(validObj, options)) return false; - - this.attributes = {}; - this._escapedAttributes = {}; - this._changed = true; - if (!options.silent) { - for (attr in old) { - this.trigger('change:' + attr, this, void 0, options); - } - this.change(options); - } - return this; - }, - - // Fetch the model from the server. If the server's representation of the - // model differs from its current attributes, they will be overriden, - // triggering a `"change"` event. - fetch : function(options) { - options || (options = {}); - var model = this; - var success = options.success; - options.success = function(resp, status, xhr) { - if (!model.set(model.parse(resp, xhr), options)) return false; - if (success) success(model, resp); - }; - options.error = wrapError(options.error, model, options); - return (this.sync || Backbone.sync).call(this, 'read', this, options); - }, - - // Set a hash of model attributes, and sync the model to the server. - // If the server returns an attributes hash that differs, the model's - // state will be `set` again. - save : function(attrs, options) { - options || (options = {}); - if (attrs && !this.set(attrs, options)) return false; - var model = this; - var success = options.success; - options.success = function(resp, status, xhr) { - if (!model.set(model.parse(resp, xhr), options)) return false; - if (success) success(model, resp, xhr); - }; - options.error = wrapError(options.error, model, options); - var method = this.isNew() ? 'create' : 'update'; - return (this.sync || Backbone.sync).call(this, method, this, options); - }, - - // Destroy this model on the server if it was already persisted. Upon success, the model is removed - // from its collection, if it has one. - destroy : function(options) { - options || (options = {}); - if (this.isNew()) return this.trigger('destroy', this, this.collection, options); - var model = this; - var success = options.success; - options.success = function(resp) { - model.trigger('destroy', model, model.collection, options); - if (success) success(model, resp); - }; - options.error = wrapError(options.error, model, options); - return (this.sync || Backbone.sync).call(this, 'delete', this, options); - }, - - // Default URL for the model's representation on the server -- if you're - // using Backbone's restful methods, override this to change the endpoint - // that will be called. - url : function() { - var base = getUrl(this.collection) || this.urlRoot || urlError(); - if (this.isNew()) return base; - return base + (base.charAt(base.length - 1) == '/' ? '' : '/') + encodeURIComponent(this.id); - }, - - // **parse** converts a response into the hash of attributes to be `set` on - // the model. The default implementation is just to pass the response along. - parse : function(resp, xhr) { - return resp; - }, - - // Create a new model with identical attributes to this one. - clone : function() { - return new this.constructor(this); - }, - - // A model is new if it has never been saved to the server, and lacks an id. - isNew : function() { - return this.id == null; - }, - - // Call this method to manually fire a `change` event for this model. - // Calling this will cause all objects observing the model to update. - change : function(options) { - this.trigger('change', this, options); - this._previousAttributes = _.clone(this.attributes); - this._changed = false; - }, - - // Determine if the model has changed since the last `"change"` event. - // If you specify an attribute name, determine if that attribute has changed. - hasChanged : function(attr) { - if (attr) return this._previousAttributes[attr] != this.attributes[attr]; - return this._changed; - }, - - // Return an object containing all the attributes that have changed, or false - // if there are no changed attributes. Useful for determining what parts of a - // view need to be updated and/or what attributes need to be persisted to - // the server. - changedAttributes : function(now) { - now || (now = this.attributes); - var old = this._previousAttributes; - var changed = false; - for (var attr in now) { - if (!_.isEqual(old[attr], now[attr])) { - changed = changed || {}; - changed[attr] = now[attr]; - } - } - return changed; - }, - - // Get the previous value of an attribute, recorded at the time the last - // `"change"` event was fired. - previous : function(attr) { - if (!attr || !this._previousAttributes) return null; - return this._previousAttributes[attr]; - }, - - // Get all of the attributes of the model at the time of the previous - // `"change"` event. - previousAttributes : function() { - return _.clone(this._previousAttributes); - }, - - // Run validation against a set of incoming attributes, returning `true` - // if all is well. If a specific `error` callback has been passed, - // call that instead of firing the general `"error"` event. - _performValidation : function(attrs, options) { - var error = this.validate(attrs); - if (error) { - if (options.error) { - options.error(this, error, options); - } else { - this.trigger('error', this, error, options); - } - return false; - } - return true; - } - - }); - - // Backbone.Collection - // ------------------- - - // Provides a standard collection class for our sets of models, ordered - // or unordered. If a `comparator` is specified, the Collection will maintain - // its models in sort order, as they're added and removed. - Backbone.Collection = function(models, options) { - options || (options = {}); - if (options.comparator) this.comparator = options.comparator; - _.bindAll(this, '_onModelEvent', '_removeReference'); - this._reset(); - if (models) this.reset(models, {silent: true}); - this.initialize.apply(this, arguments); - }; - - // Define the Collection's inheritable methods. - _.extend(Backbone.Collection.prototype, Backbone.Events, { - - // The default model for a collection is just a **Backbone.Model**. - // This should be overridden in most cases. - model : Backbone.Model, - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // The JSON representation of a Collection is an array of the - // models' attributes. - toJSON : function() { - return this.map(function(model){ return model.toJSON(); }); - }, - - // Add a model, or list of models to the set. Pass **silent** to avoid - // firing the `added` event for every new model. - add : function(models, options) { - if (_.isArray(models)) { - for (var i = 0, l = models.length; i < l; i++) { - this._add(models[i], options); - } - } else { - this._add(models, options); - } - return this; - }, - - // Remove a model, or a list of models from the set. Pass silent to avoid - // firing the `removed` event for every model removed. - remove : function(models, options) { - if (_.isArray(models)) { - for (var i = 0, l = models.length; i < l; i++) { - this._remove(models[i], options); - } - } else { - this._remove(models, options); - } - return this; - }, - - // Get a model from the set by id. - get : function(id) { - if (id == null) return null; - return this._byId[id.id != null ? id.id : id]; - }, - - // Get a model from the set by client id. - getByCid : function(cid) { - return cid && this._byCid[cid.cid || cid]; - }, - - // Get the model at the given index. - at: function(index) { - return this.models[index]; - }, - - // Force the collection to re-sort itself. You don't need to call this under normal - // circumstances, as the set will maintain sort order as each item is added. - sort : function(options) { - options || (options = {}); - if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); - this.models = this.sortBy(this.comparator); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - - // Pluck an attribute from each model in the collection. - pluck : function(attr) { - return _.map(this.models, function(model){ return model.get(attr); }); - }, - - // When you have more items than you want to add or remove individually, - // you can reset the entire set with a new list of models, without firing - // any `added` or `removed` events. Fires `reset` when finished. - reset : function(models, options) { - models || (models = []); - options || (options = {}); - this.each(this._removeReference); - this._reset(); - this.add(models, {silent: true}); - if (!options.silent) this.trigger('reset', this, options); - return this; - }, - - // Fetch the default set of models for this collection, resetting the - // collection when they arrive. If `add: true` is passed, appends the - // models to the collection instead of resetting. - fetch : function(options) { - options || (options = {}); - var collection = this; - var success = options.success; - options.success = function(resp, status, xhr) { - collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options); - if (success) success(collection, resp); - }; - options.error = wrapError(options.error, collection, options); - return (this.sync || Backbone.sync).call(this, 'read', this, options); - }, - - // Create a new instance of a model in this collection. After the model - // has been created on the server, it will be added to the collection. - // Returns the model, or 'false' if validation on a new model fails. - create : function(model, options) { - var coll = this; - options || (options = {}); - model = this._prepareModel(model, options); - if (!model) return false; - var success = options.success; - options.success = function(nextModel, resp, xhr) { - coll.add(nextModel, options); - if (success) success(nextModel, resp, xhr); - }; - model.save(null, options); - return model; - }, - - // **parse** converts a response into a list of models to be added to the - // collection. The default implementation is just to pass it through. - parse : function(resp, xhr) { - return resp; - }, - - // Proxy to _'s chain. Can't be proxied the same way the rest of the - // underscore methods are proxied because it relies on the underscore - // constructor. - chain: function () { - return _(this.models).chain(); - }, - - // Reset all internal state. Called when the collection is reset. - _reset : function(options) { - this.length = 0; - this.models = []; - this._byId = {}; - this._byCid = {}; - }, - - // Prepare a model to be added to this collection - _prepareModel: function(model, options) { - if (!(model instanceof Backbone.Model)) { - var attrs = model; - model = new this.model(attrs, {collection: this}); - if (model.validate && !model._performValidation(attrs, options)) model = false; - } else if (!model.collection) { - model.collection = this; - } - return model; - }, - - // Internal implementation of adding a single model to the set, updating - // hash indexes for `id` and `cid` lookups. - // Returns the model, or 'false' if validation on a new model fails. - _add : function(model, options) { - options || (options = {}); - model = this._prepareModel(model, options); - if (!model) return false; - var already = this.getByCid(model); - if (already) throw new Error(["Can't add the same model to a set twice", already.id]); - this._byId[model.id] = model; - this._byCid[model.cid] = model; - var index = options.at != null ? options.at : - this.comparator ? this.sortedIndex(model, this.comparator) : - this.length; - this.models.splice(index, 0, model); - model.bind('all', this._onModelEvent); - this.length++; - if (!options.silent) model.trigger('add', model, this, options); - return model; - }, - - // Internal implementation of removing a single model from the set, updating - // hash indexes for `id` and `cid` lookups. - _remove : function(model, options) { - options || (options = {}); - model = this.getByCid(model) || this.get(model); - if (!model) return null; - delete this._byId[model.id]; - delete this._byCid[model.cid]; - this.models.splice(this.indexOf(model), 1); - this.length--; - if (!options.silent) model.trigger('remove', model, this, options); - this._removeReference(model); - return model; - }, - - // Internal method to remove a model's ties to a collection. - _removeReference : function(model) { - if (this == model.collection) { - delete model.collection; - } - model.unbind('all', this._onModelEvent); - }, - - // Internal method called every time a model in the set fires an event. - // Sets need to update their indexes when models change ids. All other - // events simply proxy through. "add" and "remove" events that originate - // in other collections are ignored. - _onModelEvent : function(ev, model, collection, options) { - if ((ev == 'add' || ev == 'remove') && collection != this) return; - if (ev == 'destroy') { - this._remove(model, options); - } - if (model && ev === 'change:' + model.idAttribute) { - delete this._byId[model.previous(model.idAttribute)]; - this._byId[model.id] = model; - } - this.trigger.apply(this, arguments); - } - - }); - - // Underscore methods that we want to implement on the Collection. - var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', - 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', - 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', - 'first', 'rest', 'last', 'without', 'indexOf', 'lastIndexOf', 'isEmpty', 'groupBy']; - - // Mix in each Underscore method as a proxy to `Collection#models`. - _.each(methods, function(method) { - Backbone.Collection.prototype[method] = function() { - return _[method].apply(_, [this.models].concat(_.toArray(arguments))); - }; - }); - - // Backbone.Router - // ------------------- - - // Routers map faux-URLs to actions, and fire events when routes are - // matched. Creating a new one sets its `routes` hash, if not set statically. - Backbone.Router = function(options) { - options || (options = {}); - if (options.routes) this.routes = options.routes; - this._bindRoutes(); - this.initialize.apply(this, arguments); - }; - - // Cached regular expressions for matching named param parts and splatted - // parts of route strings. - var namedParam = /:([\w\d]+)/g; - var splatParam = /\*([\w\d]+)/g; - var escapeRegExp = /[-[\]{}()+?.,\\^$|#\s]/g; - - // Set up all inheritable **Backbone.Router** properties and methods. - _.extend(Backbone.Router.prototype, Backbone.Events, { - - // Initialize is an empty function by default. Override it with your own - // initialization logic. - initialize : function(){}, - - // Manually bind a single named route to a callback. For example: - // - // this.route('search/:query/p:num', 'search', function(query, num) { - // ... - // }); - // - route : function(route, name, callback) { - Backbone.history || (Backbone.history = new Backbone.History); - if (!_.isRegExp(route)) route = this._routeToRegExp(route); - Backbone.history.route(route, _.bind(function(fragment) { - var args = this._extractParameters(route, fragment); - callback.apply(this, args); - this.trigger.apply(this, ['route:' + name].concat(args)); - }, this)); - }, - - // Simple proxy to `Backbone.history` to save a fragment into the history. - navigate : function(fragment, triggerRoute) { - Backbone.history.navigate(fragment, triggerRoute); - }, - - // Bind all defined routes to `Backbone.history`. We have to reverse the - // order of the routes here to support behavior where the most general - // routes can be defined at the bottom of the route map. - _bindRoutes : function() { - if (!this.routes) return; - var routes = []; - for (var route in this.routes) { - routes.unshift([route, this.routes[route]]); - } - for (var i = 0, l = routes.length; i < l; i++) { - this.route(routes[i][0], routes[i][1], this[routes[i][1]]); - } - }, - - // Convert a route string into a regular expression, suitable for matching - // against the current location hash. - _routeToRegExp : function(route) { - route = route.replace(escapeRegExp, "\\$&") - .replace(namedParam, "([^\/]*)") - .replace(splatParam, "(.*?)"); - return new RegExp('^' + route + '$'); - }, - - // Given a route, and a URL fragment that it matches, return the array of - // extracted parameters. - _extractParameters : function(route, fragment) { - return route.exec(fragment).slice(1); - } - - }); - - // Backbone.History - // ---------------- - - // Handles cross-browser history management, based on URL fragments. If the - // browser does not support `onhashchange`, falls back to polling. - Backbone.History = function() { - this.handlers = []; - _.bindAll(this, 'checkUrl'); - }; - - // Cached regex for cleaning hashes. - var hashStrip = /^#*/; - - // Cached regex for detecting MSIE. - var isExplorer = /msie [\w.]+/; - - // Has the history handling already been started? - var historyStarted = false; - - // Set up all inheritable **Backbone.History** properties and methods. - _.extend(Backbone.History.prototype, { - - // The default interval to poll for hash changes, if necessary, is - // twenty times a second. - interval: 50, - - // Get the cross-browser normalized URL fragment, either from the URL, - // the hash, or the override. - getFragment : function(fragment, forcePushState) { - if (fragment == null) { - if (this._hasPushState || forcePushState) { - fragment = window.location.pathname; - var search = window.location.search; - if (search) fragment += search; - if (fragment.indexOf(this.options.root) == 0) fragment = fragment.substr(this.options.root.length); - } else { - fragment = window.location.hash; - } - } - return decodeURIComponent(fragment.replace(hashStrip, '')); - }, - - // Start the hash change handling, returning `true` if the current URL matches - // an existing route, and `false` otherwise. - start : function(options) { - - // Figure out the initial configuration. Do we need an iframe? - // Is pushState desired ... is it available? - if (historyStarted) throw new Error("Backbone.history has already been started"); - this.options = _.extend({}, {root: '/'}, this.options, options); - this._wantsPushState = !!this.options.pushState; - this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState); - var fragment = this.getFragment(); - var docMode = document.documentMode; - var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); - if (oldIE) { - this.iframe = $('