modules/renderer/background.js (373 lines of code) (raw):

import { dispatch as d3_dispatch } from 'd3-dispatch'; import { interpolateNumber as d3_interpolateNumber } from 'd3-interpolate'; import { select as d3_select } from 'd3-selection'; import whichPolygon from 'which-polygon'; import { data } from '../../data'; import { geoExtent, geoMetersToOffset, geoOffsetToMeters} from '../geo'; import { rendererBackgroundSource } from './background_source'; import { rendererTileLayer } from './tile_layer'; import { utilQsString, utilStringQs } from '../util'; import { utilDetect } from '../util/detect'; import { utilRebind } from '../util/rebind'; export function rendererBackground(context) { var dispatch = d3_dispatch('change'); var detected = utilDetect(); var baseLayer = rendererTileLayer(context).projection(context.projection); var _isValid = true; var _overlayLayers = []; var _backgroundSources = []; var _brightness = 1; var _contrast = 1; var _saturation = 1; var _sharpness = 1; function background(selection) { // If we are displaying an Esri basemap at high zoom, // check its tilemap to see how high the zoom can go if (context.map().zoom() > 18) { var basemap = baseLayer.source(); if (basemap && /^EsriWorldImagery/.test(basemap.id)) { var center = context.map().center(); basemap.fetchTilemap(center); } } // Is the imagery valid here? - #4827 var sources = background.sources(context.map().extent()); var wasValid = _isValid; _isValid = !!sources .filter(function(d) { return d === baseLayer.source(); }).length; if (wasValid !== _isValid) { // change in valid status background.updateImagery(); } var baseFilter = ''; if (detected.cssfilters) { if (_brightness !== 1) { baseFilter += 'brightness(' + _brightness + ')'; } if (_contrast !== 1) { baseFilter += 'contrast(' + _contrast + ')'; } if (_saturation !== 1) { baseFilter += 'saturate(' + _saturation + ')'; } if (_sharpness < 1) { // gaussian blur var blur = d3_interpolateNumber(0.5, 5)(1 - _sharpness); baseFilter += 'blur(' + blur + 'px)'; } } var base = selection.selectAll('.layer-background') .data([0]); base = base.enter() .insert('div', '.layer-data') .attr('class', 'layer layer-background') .merge(base); if (detected.cssfilters) { base.style('filter', baseFilter || null); } else { base.style('opacity', _brightness); } var imagery = base.selectAll('.layer-imagery') .data([0]); imagery.enter() .append('div') .attr('class', 'layer layer-imagery') .merge(imagery) .call(baseLayer); var maskFilter = ''; var mixBlendMode = ''; if (detected.cssfilters && _sharpness > 1) { // apply unsharp mask mixBlendMode = 'overlay'; maskFilter = 'saturate(0) blur(3px) invert(1)'; var contrast = _sharpness - 1; maskFilter += ' contrast(' + contrast + ')'; var brightness = d3_interpolateNumber(1, 0.85)(_sharpness - 1); maskFilter += ' brightness(' + brightness + ')'; } var mask = base.selectAll('.layer-unsharp-mask') .data(detected.cssfilters && _sharpness > 1 ? [0] : []); mask.exit() .remove(); mask.enter() .append('div') .attr('class', 'layer layer-mask layer-unsharp-mask') .merge(mask) .call(baseLayer) .style('filter', maskFilter || null) .style('mix-blend-mode', mixBlendMode || null); var overlays = selection.selectAll('.layer-overlay') .data(_overlayLayers, function(d) { return d.source().name(); }); overlays.exit() .remove(); overlays.enter() .insert('div', '.layer-data') .attr('class', 'layer layer-overlay') .merge(overlays) .each(function(layer) { d3_select(this).call(layer); }); } background.updateImagery = function() { var b = baseLayer.source(); if (context.inIntro() || !b) return; var o = _overlayLayers .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) .map(function (d) { return d.source().id; }) .join(','); var meters = geoOffsetToMeters(b.offset()); var epsilon = 0.01; var x = +meters[0].toFixed(2); var y = +meters[1].toFixed(2); var q = utilStringQs(window.location.hash.substring(1)); var id = b.id; if (id === 'custom') { id = 'custom:' + b.template(); } if (id) { q.background = id; } else { delete q.background; } if (o) { q.overlays = o; } else { delete q.overlays; } if (Math.abs(x) > epsilon || Math.abs(y) > epsilon) { q.offset = x + ',' + y; } else { delete q.offset; } if (!window.mocha) { window.location.replace('#' + utilQsString(q, true)); } var imageryUsed = []; var photoOverlaysUsed = []; var current = b.imageryUsed(); if (current && _isValid) { imageryUsed.push(current); } _overlayLayers .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); }) .forEach(function (d) { imageryUsed.push(d.source().imageryUsed()); }); var data = context.layers().layer('data'); if (data && data.enabled() && data.hasData()) { imageryUsed.push(data.getSrc()); } var photoOverlayLayers = { streetside: 'Bing Streetside', mapillary: 'Mapillary Images', 'mapillary-map-features': 'Mapillary Map Features', 'mapillary-signs': 'Mapillary Signs', openstreetcam: 'OpenStreetCam Images' }; for (var layerID in photoOverlayLayers) { var layer = context.layers().layer(layerID); if (layer && layer.enabled()) { photoOverlaysUsed.push(layerID); imageryUsed.push(photoOverlayLayers[layerID]); } } context.history().photoOverlaysUsed(photoOverlaysUsed); context.history().imageryUsed(imageryUsed); }; background.sources = function(extent) { if (!data.imagery || !data.imagery.query) return []; // called before init()? var matchIDs = {}; var matchImagery = data.imagery.query.bbox(extent.rectangle(), true) || []; matchImagery.forEach(function(d) { matchIDs[d.id] = true; }); return _backgroundSources.filter(function(source) { return matchIDs[source.id] || !source.polygon; // no polygon = worldwide }); }; background.dimensions = function(d) { if (!d) return; baseLayer.dimensions(d); _overlayLayers.forEach(function(layer) { layer.dimensions(d); }); }; background.baseLayerSource = function(d) { if (!arguments.length) return baseLayer.source(); // test source against OSM imagery blacklists.. var osm = context.connection(); if (!osm) return background; var blacklists = context.connection().imageryBlacklists(); var template = d.template(); var fail = false; var tested = 0; var regex; for (var i = 0; i < blacklists.length; i++) { try { regex = new RegExp(blacklists[i]); fail = regex.test(template); tested++; if (fail) break; } catch (e) { /* noop */ } } // ensure at least one test was run. if (!tested) { regex = new RegExp('.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'); fail = regex.test(template); } baseLayer.source(!fail ? d : background.findSource('none')); dispatch.call('change'); background.updateImagery(); return background; }; background.findSource = function(id) { return _backgroundSources.find(function(d) { return d.id && d.id === id; }); }; background.bing = function() { background.baseLayerSource(background.findSource('Bing')); }; background.showsLayer = function(d) { var baseSource = baseLayer.source(); if (!d || !baseSource) return false; return d.id === baseSource.id || _overlayLayers.some(function(layer) { return d.id === layer.source().id; }); }; background.overlayLayerSources = function() { return _overlayLayers.map(function (l) { return l.source(); }); }; background.toggleOverlayLayer = function(d) { var layer; for (var i = 0; i < _overlayLayers.length; i++) { layer = _overlayLayers[i]; if (layer.source() === d) { _overlayLayers.splice(i, 1); dispatch.call('change'); background.updateImagery(); return; } } layer = rendererTileLayer(context) .source(d) .projection(context.projection) .dimensions(baseLayer.dimensions() ); _overlayLayers.push(layer); dispatch.call('change'); background.updateImagery(); }; background.nudge = function(d, zoom) { baseLayer.source().nudge(d, zoom); dispatch.call('change'); background.updateImagery(); return background; }; background.offset = function(d) { if (!arguments.length) return baseLayer.source().offset(); baseLayer.source().offset(d); dispatch.call('change'); background.updateImagery(); return background; }; background.brightness = function(d) { if (!arguments.length) return _brightness; _brightness = d; if (context.mode()) dispatch.call('change'); return background; }; background.contrast = function(d) { if (!arguments.length) return _contrast; _contrast = d; if (context.mode()) dispatch.call('change'); return background; }; background.saturation = function(d) { if (!arguments.length) return _saturation; _saturation = d; if (context.mode()) dispatch.call('change'); return background; }; background.sharpness = function(d) { if (!arguments.length) return _sharpness; _sharpness = d; if (context.mode()) dispatch.call('change'); return background; }; background.init = function() { function parseMap(qmap) { if (!qmap) return false; var args = qmap.split('/').map(Number); if (args.length < 3 || args.some(isNaN)) return false; return geoExtent([args[2], args[1]]); } var q = utilStringQs(window.location.hash.substring(1)); var requested = q.background || q.layer; var extent = parseMap(q.map); var first; var best; data.imagery = data.imagery || []; data.imagery.features = {}; // build efficient index and querying for data.imagery var features = data.imagery.map(function(source) { if (!source.polygon) return null; // Add an extra array nest to each element in `source.polygon` // so the rings are not treated as a bunch of holes: // what we have: [ [[outer],[hole],[hole]] ] // what we want: [ [[outer]],[[outer]],[[outer]] ] var rings = source.polygon.map(function(ring) { return [ring]; }); var feature = { type: 'Feature', properties: { id: source.id }, geometry: { type: 'MultiPolygon', coordinates: rings } }; data.imagery.features[source.id] = feature; return feature; }).filter(Boolean); data.imagery.query = whichPolygon({ type: 'FeatureCollection', features: features }); // Add all the available imagery sources _backgroundSources = data.imagery.map(function(source) { if (source.type === 'bing') { return rendererBackgroundSource.Bing(source, dispatch); } else if (/^EsriWorldImagery/.test(source.id)) { return rendererBackgroundSource.Esri(source); } else { return rendererBackgroundSource(source); } }); first = _backgroundSources.length && _backgroundSources[0]; // Add 'None' _backgroundSources.unshift(rendererBackgroundSource.None()); // Add 'Custom' var template = context.storage('background-custom-template') || ''; var custom = rendererBackgroundSource.Custom(template); _backgroundSources.unshift(custom); // Decide which background layer to display if (!requested && extent) { best = this.sources(extent).find(function(s) { return s.best(); }); } if (requested && requested.indexOf('custom:') === 0) { template = requested.replace(/^custom:/, ''); background.baseLayerSource(custom.template(template)); context.storage('background-custom-template', template); } else { background.baseLayerSource( background.findSource(requested) || best || background.findSource(context.storage('background-last-used')) || background.findSource('Bing') || first || background.findSource('none') ); } var locator = _backgroundSources.find(function(d) { return d.overlay && d.default; }); if (locator) { background.toggleOverlayLayer(locator); } var overlays = (q.overlays || '').split(','); overlays.forEach(function(overlay) { overlay = background.findSource(overlay); if (overlay) { background.toggleOverlayLayer(overlay); } }); if (q.gpx) { var gpx = context.layers().layer('data'); if (gpx) { gpx.url(q.gpx, '.gpx'); } } if (q.offset) { var offset = q.offset.replace(/;/g, ',').split(',').map(function(n) { return !isNaN(n) && n; }); if (offset.length === 2) { background.offset(geoMetersToOffset(offset)); } } }; return utilRebind(background, dispatch, 'on'); }