UNPKG

mmg

Version:

Simple markers for Modest Maps

559 lines (482 loc) 17.4 kB
function mmg() { var m = {}, // external list of geojson features features = [], // internal list of markers markers = [], // internal list of callbacks callbackManager = new MM.CallbackManager(m, ['drawn', 'markeradded']), // the absolute position of the parent element position = null, // a factory function for creating DOM elements out of // GeoJSON objects factory = null, // a sorter function for sorting GeoJSON objects // in the DOM sorter = null, // a list of urls from which features can be loaded. // these can be templated with {z}, {x}, and {y} urls, // map bounds left = null, right = null; // The parent DOM element m.parent = document.createElement('div'); m.parent.style.cssText = 'position: absolute; top: 0px;' + 'left:0px; width:100%; height:100%; margin:0; padding:0; z-index:0'; // reposition a single marker element function reposition(marker) { // remember the tile coordinate so we don't have to reproject every time if (!marker.coord) marker.coord = m.map.locationCoordinate(marker.location); var pos = m.map.coordinatePoint(marker.coord); var pos_loc; // If this point has wound around the world, adjust its position // to the new, onscreen location if (pos.x < 0) { pos_loc = new MM.Location(marker.location.lat, marker.location.lon); pos_loc.lon += Math.ceil((left.lon - marker.location.lon) / 360) * 360; pos = m.map.locationPoint(pos_loc); marker.coord = m.map.locationCoordinate(pos_loc); } else if (pos.x > m.map.dimensions.x) { pos_loc = new MM.Location(marker.location.lat, marker.location.lon); pos_loc.lon -= Math.ceil((marker.location.lon - right.lon) / 360) * 360; pos = m.map.locationPoint(pos_loc); marker.coord = m.map.locationCoordinate(pos_loc); } pos.scale = 1; pos.width = pos.height = 0; MM.moveElement(marker.element, pos); } // Adding and removing callbacks is mainly a way to enable mmg_interaction to operate. // I think there are better ways to do this, by, for instance, having mmg be able to // register 'binders' to markers, but this is backwards-compatible and equivalent // externally. m.addCallback = function(event, callback) { callbackManager.addCallback(event, callback); return m; }; m.removeCallback = function(event, callback) { callbackManager.removeCallback(event, callback); return m; }; // Draw this layer - reposition all markers on the div. This requires // the markers library to be attached to a map, and will noop otherwise. m.draw = function() { if (!m.map) return; left = m.map.pointLocation(new MM.Point(0, 0)); right = m.map.pointLocation(new MM.Point(m.map.dimensions.x, 0)); callbackManager.dispatchCallback('drawn', m); for (var i = 0; i < markers.length; i++) { reposition(markers[i]); } }; // Add a fully-formed marker to the layer. This fires a `markeradded` event. // This does not require the map element t be attached. m.add = function(marker) { if (!marker || !marker.element) return null; m.parent.appendChild(marker.element); markers.push(marker); callbackManager.dispatchCallback('markeradded', marker); return marker; }; // Remove a fully-formed marker - which must be the same exact marker // object as in the markers array - from the layer. m.remove = function(marker) { if (!marker) return null; m.parent.removeChild(marker.element); for (var i = 0; i < markers.length; i++) { if (markers[i] === marker) { markers.splice(i, 1); return marker; } } return marker; }; m.markers = function(x) { if (!arguments.length) return markers; }; // Add a GeoJSON feature to the markers layer. m.add_feature = function(x) { return m.features(m.features().concat([x])); }; // Public data interface m.features = function(x) { // Return features if (!arguments.length) return features; // Clear features while (m.parent.hasChildNodes()) { // removing lastChild iteratively is faster than // innerHTML = '' // http://jsperf.com/innerhtml-vs-removechild-yo/2 m.parent.removeChild(m.parent.lastChild); } // clear markers representation markers = []; // Set features if (!x) x = []; features = x.slice(); features.sort(sorter); for (var i = 0; i < features.length; i++) { m.add({ element: factory(features[i]), location: new MM.Location( features[i].geometry.coordinates[1], features[i].geometry.coordinates[0]), data: features[i] }); } if (m.map && m.map.coordinate) m.map.draw(); return m; }; // Request features from a URL - either a local URL or a JSONP call. // Expects GeoJSON-formatted features. m.url = function(x, callback) { if (!arguments.length) return urls; if (typeof reqwest === 'undefined') throw 'reqwest is required for url loading'; if (typeof x === 'string') x = [x]; urls = x; function add_features(x) { if (x && x.features) m.features(x.features); if (callback) callback(x.features, m); } reqwest((urls[0].match(/geojsonp$/)) ? { url: urls[0] + (~urls[0].indexOf('?') ? '&' : '?') + 'callback=grid', type: 'jsonp', jsonpCallback: 'callback', success: add_features, error: add_features } : { url: urls[0], type: 'json', success: add_features, error: add_features }); return m; }; m.extent = function() { var ext = [{ lat: Infinity, lon: Infinity }, { lat: -Infinity, lon: -Infinity }]; var ft = m.features(); for (var i = 0; i < ft.length; i++) { var coords = ft[i].geometry.coordinates; if (coords[0] < ext[0].lon) ext[0].lon = coords[0]; if (coords[1] < ext[0].lat) ext[0].lat = coords[1]; if (coords[0] > ext[1].lon) ext[1].lon = coords[0]; if (coords[1] > ext[1].lat) ext[1].lat = coords[1]; } return ext; }; // Factory interface m.factory = function(x) { if (!arguments.length) return factory; factory = x; // re-render all features m.features(m.features()); return m; }; m.factory(function defaultFactory(feature) { var d = document.createElement('div'); d.className = 'mmg-default'; d.style.position = 'absolute'; return d; }); m.sort = function(x) { if (!arguments.length) return sorter; sorter = x; return m; }; m.sort(function(a, b) { return b.geometry.coordinates[1] - a.geometry.coordinates[1]; }); m.destroy = function() { if (m.parent.parentNode) { m.parent.parentNode.removeChild(m.parent); } }; return m; } function mmg_interaction(mmg) { var mi = {}, tooltips = [], exclusive = true, hide_on_move = true, show_on_hover = true, close_timer = null, formatter; mi.formatter = function(x) { if (!arguments.length) return formatter; formatter = x; return mi; }; mi.formatter(function(feature) { var o = '', props = feature.properties; // Tolerate markers without properties at all. if (!props) return null; if (props.title) { o += '<div class="mmg-title">' + props.title + '</div>'; } if (props.description) { o += '<div class="mmg-description">' + props.description + '</div>'; } if (typeof html_sanitize !== undefined) { o = html_sanitize(o, function(url) { if (/^(https?:\/\/|data:image)/.test(url)) return url; }, function(x) { return x; }); } return o; }); mi.hide_on_move = function(x) { if (!arguments.length) return hide_on_move; hide_on_move = x; return mi; }; mi.exclusive = function(x) { if (!arguments.length) return exclusive; exclusive = x; return mi; }; mi.show_on_hover = function(x) { if (!arguments.length) return show_on_hover; show_on_hover = x; return mi; }; mi.hide_tooltips = function() { while (tooltips.length) mmg.remove(tooltips.pop()); for (var i = 0; i < markers.length; i++) { delete markers[i].clicked; } }; mi.bind_marker = function(marker) { var delayed_close = function() { if (!marker.clicked) close_timer = window.setTimeout(function() { mi.hide_tooltips(); }, 200); }; var show = function(e) { var content = formatter(marker.data); // Don't show a popup if the formatter returns an // empty string. This does not do any magic around DOM elements. if (!content) return; if (exclusive && tooltips.length > 0) { mi.hide_tooltips(); // We've hidden all of the tooltips, so let's not close // the one that we're creating as soon as it is created. if (close_timer) window.clearTimeout(close_timer); } var tooltip = document.createElement('div'); tooltip.className = 'wax-movetip'; tooltip.style.width = '100%'; var wrapper = tooltip.appendChild(document.createElement('div')); wrapper.style.cssText = 'position: absolute; pointer-events: none;'; var intip = wrapper.appendChild(document.createElement('div')); intip.className = 'wax-intip'; intip.style.cssText = 'pointer-events: auto;'; if (typeof content == 'string') { intip.innerHTML = content; } else { intip.appendChild(content); } // Align the bottom of the tooltip with the top of its marker wrapper.style.bottom = marker.element.offsetHeight / 2 + 20 + 'px'; if (show_on_hover) { tooltip.onmouseover = function() { if (close_timer) window.clearTimeout(close_timer); }; tooltip.onmouseout = delayed_close; } var t = { element: tooltip, data: {}, interactive: false, location: marker.location.copy() }; tooltips.push(t); mmg.add(t); mmg.draw(); }; marker.element.onclick = marker.element.ontouchstart = function() { show(); marker.clicked = true; }; if (show_on_hover) { marker.element.onmouseover = show; marker.element.onmouseout = delayed_close; } }; function bindPanned() { mmg.map.addCallback('panned', function() { if (hide_on_move) { while (tooltips.length) { mmg.remove(tooltips.pop()); } } }); } if (mmg) { // Remove tooltips on panning mmg.addCallback('drawn', bindPanned); mmg.removeCallback('drawn', bindPanned); // Bind present markers var markers = mmg.markers(); for (var i = 0; i < markers.length; i++) { mi.bind_marker(markers[i]); } // Bind future markers mmg.addCallback('markeradded', function(_, marker) { // Markers can choose to be not-interactive. The main example // of this currently is marker bubbles, which should not recursively // give marker bubbles. if (marker.interactive !== false) mi.bind_marker(marker); }); } return mi; } function mmg_csv(x) { // Extracted from d3 function csv_parse(text) { var header; return csv_parseRows(text, function(row, i) { if (i) { var o = {}, j = -1, m = header.length; while (++j < m) o[header[j]] = row[j]; return o; } else { header = row; return null; } }); } function csv_parseRows (text, f) { var EOL = {}, // sentinel value for end-of-line EOF = {}, // sentinel value for end-of-file rows = [], // output rows re = /\r\n|[,\r\n]/g, // field separator regex n = 0, // the current line number t, // the current token eol; // is the current token followed by EOL? re.lastIndex = 0; // work-around bug in FF 3.6 /** @private Returns the next token. */ function token() { if (re.lastIndex >= text.length) return EOF; // special case: end of file if (eol) { eol = false; return EOL; } // special case: end of line // special case: quotes var j = re.lastIndex; if (text.charCodeAt(j) === 34) { var i = j; while (i++ < text.length) { if (text.charCodeAt(i) === 34) { if (text.charCodeAt(i + 1) !== 34) break; i++; } } re.lastIndex = i + 2; var c = text.charCodeAt(i + 1); if (c === 13) { eol = true; if (text.charCodeAt(i + 2) === 10) re.lastIndex++; } else if (c === 10) { eol = true; } return text.substring(j + 1, i).replace(/""/g, "\""); } // common case var m = re.exec(text); if (m) { eol = m[0].charCodeAt(0) !== 44; return text.substring(j, m.index); } re.lastIndex = text.length; return text.substring(j); } while ((t = token()) !== EOF) { var a = []; while ((t !== EOL) && (t !== EOF)) { a.push(t); t = token(); } if (f && !(a = f(a, n++))) continue; rows.push(a); } return rows; } var features = []; var parsed = csv_parse(x); if (!parsed.length) return callback(features); var latfield = '', lonfield = ''; for (var f in parsed[0]) { if (f.match(/^Lat/i)) latfield = f; if (f.match(/^Lon/i)) lonfield = f; } if (!latfield || !lonfield) { throw 'CSV: Could not find latitude or longitude field'; } for (var i = 0; i < parsed.length; i++) { if (parsed[i][lonfield] !== undefined && parsed[i][lonfield] !== undefined) { features.push({ type: 'Feature', properties: parsed[i], geometry: { type: 'Point', coordinates: [ parsed[i][lonfield], parsed[i][latfield]] } }); } } return features; } function mmg_csv_url(url, callback) { if (typeof reqwest === 'undefined') { throw 'CSV: reqwest required for mmg_csv_url'; } function response(x) { if (x.status >= 400) { throw 'CSV: URL returned 404'; } return callback(mmg_csv(x.responseText)); } reqwest({ url: url, type: 'string', success: response, error: response }); } function simplestyle_factory(feature) { var sizes = { small: [20, 50], medium: [30, 70], large: [35, 90] }; var fp = feature.properties || {}; var size = fp['marker-size'] || 'medium'; var symbol = (fp['marker-symbol']) ? '-' + fp['marker-symbol'] : ''; var color = fp['marker-color'] || '7e7e7e'; color = color.replace('#', ''); var d = document.createElement('img'); d.width = sizes[size][0]; d.height = sizes[size][1]; d.className = 'simplestyle-marker'; d.alt = fp.title || ''; d.src = (simplestyle_factory.baseurl || 'http://a.tiles.mapbox.com/v3/marker/') + 'pin-' + size[0] + symbol + '+' + color + '.png'; var ds = d.style; ds.position = 'absolute'; ds.clip = 'rect(auto auto ' + (sizes[size][1] * 0.75) + 'px auto)'; ds.marginTop = -((sizes[size][1]) / 2) + 'px'; ds.marginLeft = -(sizes[size][0] / 2) + 'px'; ds.cursor = 'pointer'; return d; }