Пример #1
0
export function uiCommit(context) {
    var dispatch = d3_dispatch('cancel', 'save');
    var _userDetails;
    var _selection;

    var changesetEditor = uiChangesetEditor(context)
        .on('change', changeTags);
    var rawTagEditor = uiRawTagEditor(context)
        .on('change', changeTags);
    var commitChanges = uiCommitChanges(context);
    var commitWarnings = uiCommitWarnings(context);


    function commit(selection) {
        _selection = selection;

        var osm = context.connection();
        if (!osm) return;

        // expire stored comment, hashtags, source after cutoff datetime - #3947 #4899
        var commentDate = +context.storage('commentDate') || 0;
        var currDate = Date.now();
        var cutoff = 2 * 86400 * 1000;   // 2 days
        if (commentDate > currDate || currDate - commentDate > cutoff) {
            context.storage('comment', null);
            context.storage('hashtags', null);
            context.storage('source', null);
        }

        var tags;
        if (!_changeset) {
            var detected = utilDetect();
            tags = {
                comment: context.storage('comment') || '',
                created_by: ('iD ' + context.version).substr(0, 255),
                host: detected.host.substr(0, 255),
                locale: detected.locale.substr(0, 255)
            };

            // call findHashtags initially - this will remove stored
            // hashtags if any hashtags are found in the comment - #4304
            findHashtags(tags, true);

            var hashtags = context.storage('hashtags');
            if (hashtags) {
                tags.hashtags = hashtags;
            }

            // iD 2.8.1 could write a literal 'undefined' here.. see #5021
            // (old source values expire after 2 days, so 'undefined' checks can go away in v2.9)
            var source = context.storage('source');
            if (source && source !== 'undefined') {
                tags.source = source;
            } else if (source === 'undefined') {
                context.storage('source', null);
            }

            _changeset = new osmChangeset({ tags: tags });
        }

        tags = _clone(_changeset.tags);
        tags.imagery_used = context.history().imageryUsed().join(';').substr(0, 255);
        _changeset = _changeset.update({ tags: tags });

        var header = selection.selectAll('.header')
            .data([0]);

        header.enter()
            .append('div')
            .attr('class', 'header fillL')
            .append('h3')
            .text(t('commit.title'));

        var body = selection.selectAll('.body')
            .data([0]);

        body = body.enter()
            .append('div')
            .attr('class', 'body')
            .merge(body);


        // Changeset Section
        var changesetSection = body.selectAll('.changeset-editor')
            .data([0]);

        changesetSection = changesetSection.enter()
            .append('div')
            .attr('class', 'modal-section changeset-editor')
            .merge(changesetSection);

        changesetSection
            .call(changesetEditor
                .changesetID(_changeset.id)
                .tags(tags)
            );


        // Warnings
        body.call(commitWarnings);


        // Upload Explanation
        var saveSection = body.selectAll('.save-section')
            .data([0]);

        saveSection = saveSection.enter()
            .append('div')
            .attr('class','modal-section save-section fillL cf')
            .merge(saveSection);

        var prose = saveSection.selectAll('.commit-info')
            .data([0]);

        prose = prose.enter()
            .append('p')
            .attr('class', 'commit-info')
            .text(t('commit.upload_explanation'))
            .merge(prose);

        osm.userDetails(function(err, user) {
            if (err) return;

            var userLink = d3_select(document.createElement('div'));

            _userDetails = user;

            if (user.image_url) {
                userLink
                    .append('img')
                    .attr('src', user.image_url)
                    .attr('class', 'icon pre-text user-icon');
            }

            userLink
                .append('a')
                .attr('class', 'user-info')
                .text(user.display_name)
                .attr('href', osm.userURL(user.display_name))
                .attr('tabindex', -1)
                .attr('target', '_blank');

            prose
                .html(t('commit.upload_explanation_with_user', { user: userLink.html() }));
        });


        // Request Review
        var requestReview = saveSection.selectAll('.request-review')
            .data([0]);

        // Enter
        var requestReviewEnter = requestReview.enter()
            .append('div')
            .attr('class', 'request-review');

        var labelEnter = requestReviewEnter
            .append('label')
            .attr('for', 'commit-input-request-review');

        labelEnter
            .append('input')
            .attr('type', 'checkbox')
            .attr('id', 'commit-input-request-review');

        labelEnter
            .append('span')
            .text(t('commit.request_review'));

        // Update
        requestReview = requestReview
            .merge(requestReviewEnter);

        var requestReviewInput = requestReview.selectAll('input')
            .property('checked', isReviewRequested(_changeset.tags))
            .on('change', toggleRequestReview);


        // Buttons
        var buttonSection = saveSection.selectAll('.buttons')
            .data([0]);

        // enter
        var buttonEnter = buttonSection.enter()
            .append('div')
            .attr('class', 'buttons fillL cf');

        buttonEnter
            .append('button')
            .attr('class', 'secondary-action button cancel-button')
            .append('span')
            .attr('class', 'label')
            .text(t('commit.cancel'));

        buttonEnter
            .append('button')
            .attr('class', 'action button save-button')
            .append('span')
            .attr('class', 'label')
            .text(t('commit.save'));

        // update
        buttonSection = buttonSection
            .merge(buttonEnter);

        buttonSection.selectAll('.cancel-button')
            .on('click.cancel', function() {
                var selectedID = commitChanges.entityID();
                dispatch.call('cancel', this, selectedID);
            });

        buttonSection.selectAll('.save-button')
            .attr('disabled', function() {
                var n = d3_select('#preset-input-comment').node();
                return (n && n.value.length) ? null : true;
            })
            .on('click.save', function() {
                this.blur();    // avoid keeping focus on the button - #4641
                dispatch.call('save', this, _changeset);
            });


        // Raw Tag Editor
        var tagSection = body.selectAll('.tag-section.raw-tag-editor')
            .data([0]);

        tagSection = tagSection.enter()
            .append('div')
            .attr('class', 'modal-section tag-section raw-tag-editor')
            .merge(tagSection);

        var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty();
        tagSection
            .call(rawTagEditor
                .expanded(expanded)
                .readOnlyTags(readOnlyTags)
                .tags(_clone(_changeset.tags))
            );


        // Change summary
        body.call(commitChanges);


        function toggleRequestReview() {
            var rr = requestReviewInput.property('checked');
            updateChangeset({ review_requested: (rr ? 'yes' : undefined) });

            var expanded = !tagSection.selectAll('a.hide-toggle.expanded').empty();
            tagSection
                .call(rawTagEditor
                    .expanded(expanded)
                    .readOnlyTags(readOnlyTags)
                    .tags(_clone(_changeset.tags))
                );
        }
    }


    function changeTags(changed, onInput) {
        if (changed.hasOwnProperty('comment')) {
            if (changed.comment === undefined) {
                changed.comment = '';
            }
            if (!onInput) {
                context.storage('comment', changed.comment);
                context.storage('commentDate', Date.now());
            }
        }
        if (changed.hasOwnProperty('source')) {
            if (changed.source === undefined) {
                context.storage('source', null);
            } else if (!onInput) {
                context.storage('source', changed.source);
                context.storage('commentDate', Date.now());
            }
        }

        updateChangeset(changed, onInput);

        if (_selection) {
            _selection.call(commit);
        }
    }


    function findHashtags(tags, commentOnly) {
        var inComment = commentTags();
        var inHashTags = hashTags();

        if (inComment !== null) {                    // when hashtags are detected in comment...
            context.storage('hashtags', null);       // always remove stored hashtags - #4304
            if (commentOnly) { inHashTags = null; }  // optionally override hashtags field
        }
        return _unionBy(inComment, inHashTags, function (s) {
            return s.toLowerCase();
        });

        // Extract hashtags from `comment`
        function commentTags() {
            return tags.comment
                .replace(/http\S*/g, '')  // drop anything that looks like a URL - #4289
                .match(hashtagRegex);
        }

        // Extract and clean hashtags from `hashtags`
        function hashTags() {
            var t = tags.hashtags || '';
            return t
                .split(/[,;\s]+/)
                .map(function (s) {
                    if (s[0] !== '#') { s = '#' + s; }    // prepend '#'
                    var matched = s.match(hashtagRegex);
                    return matched && matched[0];
                }).filter(Boolean);                       // exclude falsey
        }
    }


    function isReviewRequested(tags) {
        var rr = tags.review_requested;
        if (rr === undefined) return false;
        rr = rr.trim().toLowerCase();
        return !(rr === '' || rr === 'no');
    }


    function updateChangeset(changed, onInput) {
        var tags = _clone(_changeset.tags);

        _forEach(changed, function(v, k) {
            k = k.trim().substr(0, 255);
            if (readOnlyTags.indexOf(k) !== -1) return;

            if (k !== '' && v !== undefined) {
                if (onInput) {
                    tags[k] = v;
                } else {
                    tags[k] = v.trim().substr(0, 255);
                }
            } else {
                delete tags[k];
            }
        });

        if (!onInput) {
            // when changing the comment, override hashtags with any found in comment.
            var commentOnly = changed.hasOwnProperty('comment') && (changed.comment !== '');
            var arr = findHashtags(tags, commentOnly);
            if (arr.length) {
                tags.hashtags = arr.join(';').substr(0, 255);
                context.storage('hashtags', tags.hashtags);
            } else {
                delete tags.hashtags;
                context.storage('hashtags', null);
            }
        }

        // always update userdetails, just in case user reauthenticates as someone else
        if (_userDetails && _userDetails.changesets_count !== undefined) {
            var changesetsCount = parseInt(_userDetails.changesets_count, 10) + 1;  // #4283
            tags.changesets_count = String(changesetsCount);

            // first 100 edits - new user
            if (changesetsCount <= 100) {
                var s;
                s = context.storage('walkthrough_completed');
                if (s) {
                    tags['ideditor:walkthrough_completed'] = s;
                }

                s = context.storage('walkthrough_progress');
                if (s) {
                    tags['ideditor:walkthrough_progress'] = s;
                }

                s = context.storage('walkthrough_started');
                if (s) {
                    tags['ideditor:walkthrough_started'] = s;
                }
            }
        } else {
            delete tags.changesets_count;
        }

        if (!_isEqual(_changeset.tags, tags)) {
            _changeset = _changeset.update({ tags: tags });
        }
    }


    commit.reset = function() {
        _changeset = null;
    };


    return utilRebind(commit, dispatch, 'on');
}
Пример #2
0
export function uiIntroLine(context, reveal) {
    var dispatch = d3_dispatch('done');
    var timeouts = [];
    var _tulipRoadID = null;
    var flowerRoadID = 'w646';
    var tulipRoadStart = [-85.6297754121684, 41.95805253325314];
    var tulipRoadMidpoint = [-85.62975395449628, 41.95787501510204];
    var tulipRoadIntersection = [-85.62974496187628, 41.95742515554585];
    var roadCategory = context.presets().item('category-road');
    var residentialPreset = context.presets().item('highway/residential');
    var woodRoadID = 'w525';
    var woodRoadEndID = 'n2862';
    var woodRoadAddNode = [-85.62390110349587, 41.95397111462291];
    var woodRoadDragEndpoint = [-85.623867390213, 41.95466987786487];
    var woodRoadDragMidpoint = [-85.62386254803509, 41.95430395953872];
    var washingtonStreetID = 'w522';
    var twelfthAvenueID = 'w1';
    var eleventhAvenueEndID = 'n3550';
    var twelfthAvenueEndID = 'n5';
    var _washingtonSegmentID = null;
    var eleventhAvenueEnd = context.entity(eleventhAvenueEndID).loc;
    var twelfthAvenueEnd = context.entity(twelfthAvenueEndID).loc;
    var deleteLinesLoc = [-85.6219395542764, 41.95228033922477];
    var twelfthAvenue = [-85.62219310052491, 41.952505413152956];


    var chapter = {
        title: 'intro.lines.title'
    };


    function timeout(f, t) {
        timeouts.push(window.setTimeout(f, t));
    }


    function eventCancel() {
        d3_event.stopPropagation();
        d3_event.preventDefault();
    }


    function revealEditMenu(loc, text, options) {
        var rect = context.surfaceRect();
        var point = context.curtainProjection(loc);
        var pad = 40;
        var width = 250 + (2 * pad);
        var height = 350;
        var startX = rect.left + point[0];
        var left = (textDirection === 'rtl') ? (startX - width + pad) : (startX - pad);
        var box = {
            left: left,
            top: point[1] + rect.top - 60,
            width: width,
            height: height
        };
        reveal(box, text, options);
    }


    function addLine() {
        context.enter(modeBrowse(context));
        context.history().reset('initial');

        var msec = transitionTime(tulipRoadStart, context.map().center());
        if (msec) { reveal(null, null, { duration: 0 }); }
        context.map().zoom(18.5).centerEase(tulipRoadStart, msec);

        timeout(function() {
            var tooltip = reveal('button.add-line',
                t('intro.lines.add_line', { button: icon('#iD-icon-line', 'pre-text') }));

            tooltip.selectAll('.tooltip-inner')
                .insert('svg', 'span')
                .attr('class', 'tooltip-illustration')
                .append('use')
                .attr('xlink:href', '#iD-graphic-lines');

            context.on('enter.intro', function(mode) {
                if (mode.id !== 'add-line') return;
                continueTo(startLine);
            });
        }, msec + 100);

        function continueTo(nextStep) {
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function startLine() {
        if (context.mode().id !== 'add-line') return chapter.restart();

        _tulipRoadID = null;

        var padding = 70 * Math.pow(2, context.map().zoom() - 18);
        var box = pad(tulipRoadStart, padding, context);
        box.height = box.height + 100;
        reveal(box, t('intro.lines.start_line'));

        context.map().on('move.intro drawn.intro', function() {
            padding = 70 * Math.pow(2, context.map().zoom() - 18);
            box = pad(tulipRoadStart, padding, context);
            box.height = box.height + 100;
            reveal(box, t('intro.lines.start_line'), { duration: 0 });
        });

        context.on('enter.intro', function(mode) {
            if (mode.id !== 'draw-line') return chapter.restart();
            continueTo(drawLine);
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function drawLine() {
        if (context.mode().id !== 'draw-line') return chapter.restart();

        _tulipRoadID = context.mode().selectedIDs()[0];
        context.map().centerEase(tulipRoadMidpoint, 500);

        timeout(function() {
            var padding = 200 * Math.pow(2, context.map().zoom() - 18.5);
            var box = pad(tulipRoadMidpoint, padding, context);
            box.height = box.height * 2;
            reveal(box,
                t('intro.lines.intersect', { name: t('intro.graph.name.flower-street') })
            );

            context.map().on('move.intro drawn.intro', function() {
                padding = 200 * Math.pow(2, context.map().zoom() - 18.5);
                box = pad(tulipRoadMidpoint, padding, context);
                box.height = box.height * 2;
                reveal(box,
                    t('intro.lines.intersect', { name: t('intro.graph.name.flower-street') }),
                    { duration: 0 }
                );
            });
        }, 550);  // after easing..

        context.history().on('change.intro', function() {
            if (isLineConnected()) {
                continueTo(continueLine);
            }
        });

        context.on('enter.intro', function(mode) {
            if (mode.id === 'draw-line') {
                return;
            } else if (mode.id === 'select') {
                continueTo(retryIntersect);
                return;
            } else {
                return chapter.restart();
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function isLineConnected() {
        var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);
        if (!entity) return false;

        var drawNodes = context.graph().childNodes(entity);
        return _some(drawNodes, function(node) {
            return _some(context.graph().parentWays(node), function(parent) {
                return parent.id === flowerRoadID;
            });
        });
    }


    function retryIntersect() {
        d3_select(window).on('mousedown.intro', eventCancel, true);

        var box = pad(tulipRoadIntersection, 80, context);
        reveal(box,
            t('intro.lines.retry_intersect', { name: t('intro.graph.name.flower-street') })
        );

        timeout(chapter.restart, 3000);
    }


    function continueLine() {
        if (context.mode().id !== 'draw-line') return chapter.restart();
        var entity = _tulipRoadID && context.hasEntity(_tulipRoadID);
        if (!entity) return chapter.restart();

        context.map().centerEase(tulipRoadIntersection, 500);

        reveal('#surface', t('intro.lines.continue_line'));

        context.on('enter.intro', function(mode) {
            if (mode.id === 'draw-line')
                return;
            else if (mode.id === 'select')
                return continueTo(chooseCategoryRoad);
            else
                return chapter.restart();
        });

        function continueTo(nextStep) {
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function chooseCategoryRoad() {
        if (context.mode().id !== 'select') return chapter.restart();

        context.on('exit.intro', function() {
            return chapter.restart();
        });

        var button = d3_select('.preset-category-road .preset-list-button');
        if (button.empty()) return chapter.restart();

        // disallow scrolling
        d3_select('.inspector-wrap').on('wheel.intro', eventCancel);

        timeout(function() {
            // reset pane, in case user somehow happened to change it..
            d3_select('.inspector-wrap .panewrap').style('right', '-100%');

            reveal(button.node(),
                t('intro.lines.choose_category_road', { category: roadCategory.name() })
            );

            button.on('click.intro', function() {
                continueTo(choosePresetResidential);
            });

        }, 400);  // after editor pane visible

        function continueTo(nextStep) {
            d3_select('.inspector-wrap').on('wheel.intro', null);
            d3_select('.preset-list-button').on('click.intro', null);
            context.on('exit.intro', null);
            nextStep();
        }
    }


    function choosePresetResidential() {
        if (context.mode().id !== 'select') return chapter.restart();

        context.on('exit.intro', function() {
            return chapter.restart();
        });

        var subgrid = d3_select('.preset-category-road .subgrid');
        if (subgrid.empty()) return chapter.restart();

        subgrid.selectAll(':not(.preset-highway-residential) .preset-list-button')
            .on('click.intro', function() {
                continueTo(retryPresetResidential);
            });

        subgrid.selectAll('.preset-highway-residential .preset-list-button')
            .on('click.intro', function() {
                continueTo(nameRoad);
            });

        timeout(function() {
            reveal(subgrid.node(),
                t('intro.lines.choose_preset_residential', { preset: residentialPreset.name() }),
                { tooltipBox: '.preset-highway-residential .preset-list-button', duration: 300 }
            );
        }, 300);

        function continueTo(nextStep) {
            d3_select('.preset-list-button').on('click.intro', null);
            context.on('exit.intro', null);
            nextStep();
        }
    }


    // selected wrong road type
    function retryPresetResidential() {
        if (context.mode().id !== 'select') return chapter.restart();

        context.on('exit.intro', function() {
            return chapter.restart();
        });

        // disallow scrolling
        d3_select('.inspector-wrap').on('wheel.intro', eventCancel);

        timeout(function() {
            var button = d3_select('.entity-editor-pane .preset-list-button');

            reveal(button.node(),
                t('intro.lines.retry_preset_residential', { preset: residentialPreset.name() })
            );

            button.on('click.intro', function() {
                continueTo(chooseCategoryRoad);
            });

        }, 500);

        function continueTo(nextStep) {
            d3_select('.inspector-wrap').on('wheel.intro', null);
            d3_select('.preset-list-button').on('click.intro', null);
            context.on('exit.intro', null);
            nextStep();
        }
    }


    function nameRoad() {
        context.on('exit.intro', function() {
            continueTo(didNameRoad);
        });

        timeout(function() {
            reveal('.entity-editor-pane',
                t('intro.lines.name_road', { button: icon('#iD-icon-apply', 'pre-text') }),
                { tooltipClass: 'intro-lines-name_road' }
            );
        }, 500);

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            nextStep();
        }
    }


    function didNameRoad() {
        context.history().checkpoint('doneAddLine');

        timeout(function() {
            reveal('#surface', t('intro.lines.did_name_road'), {
                buttonText: t('intro.ok'),
                buttonCallback: function() { continueTo(updateLine); }
            });
        }, 500);

        function continueTo(nextStep) {
            nextStep();
        }
    }


    function updateLine() {
        context.history().reset('doneAddLine');
        if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
            return chapter.restart();
        }

        var msec = transitionTime(woodRoadDragMidpoint, context.map().center());
        if (msec) { reveal(null, null, { duration: 0 }); }
        context.map().zoom(19).centerEase(woodRoadDragMidpoint, msec);

        timeout(function() {
            var padding = 250 * Math.pow(2, context.map().zoom() - 19);
            var box = pad(woodRoadDragMidpoint, padding, context);
            var advance = function() { continueTo(addNode); };

            reveal(box, t('intro.lines.update_line'),
                { buttonText: t('intro.ok'), buttonCallback: advance }
            );

            context.map().on('move.intro drawn.intro', function() {
                var padding = 250 * Math.pow(2, context.map().zoom() - 19);
                var box = pad(woodRoadDragMidpoint, padding, context);
                reveal(box, t('intro.lines.update_line'),
                    { duration: 0, buttonText: t('intro.ok'), buttonCallback: advance }
                );
            });
        }, msec + 100);

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            nextStep();
        }
    }


    function addNode() {
        context.history().reset('doneAddLine');
        if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
            return chapter.restart();
        }

        var padding = 40 * Math.pow(2, context.map().zoom() - 19);
        var box = pad(woodRoadAddNode, padding, context);
        reveal(box, t('intro.lines.add_node'));

        context.map().on('move.intro drawn.intro', function() {
            var padding = 40 * Math.pow(2, context.map().zoom() - 19);
            var box = pad(woodRoadAddNode, padding, context);
            reveal(box, t('intro.lines.add_node'), { duration: 0 });
        });

        context.history().on('change.intro', function(changed) {
            if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
                return continueTo(updateLine);
            }
            if (changed.created().length === 1) {
                timeout(function() { continueTo(startDragEndpoint); }, 500);
            }
        });

        context.on('enter.intro', function(mode) {
            if (mode.id !== 'select') {
                continueTo(updateLine);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function startDragEndpoint() {
        if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
            return continueTo(updateLine);
        }
        var padding = 100 * Math.pow(2, context.map().zoom() - 19);
        var box = pad(woodRoadDragEndpoint, padding, context);
        reveal(box, t('intro.lines.start_drag_endpoint'));

        context.map().on('move.intro drawn.intro', function() {
            if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
                return continueTo(updateLine);
            }
            var padding = 100 * Math.pow(2, context.map().zoom() - 19);
            var box = pad(woodRoadDragEndpoint, padding, context);
            reveal(box, t('intro.lines.start_drag_endpoint'), { duration: 0 });

            var entity = context.entity(woodRoadEndID);
            if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) <= 4) {
                continueTo(finishDragEndpoint);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            nextStep();
        }
    }


    function finishDragEndpoint() {
        if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
            return continueTo(updateLine);
        }

        var padding = 100 * Math.pow(2, context.map().zoom() - 19);
        var box = pad(woodRoadDragEndpoint, padding, context);
        reveal(box, t('intro.lines.finish_drag_endpoint'));

        context.map().on('move.intro drawn.intro', function() {
            if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
                return continueTo(updateLine);
            }
            var padding = 100 * Math.pow(2, context.map().zoom() - 19);
            var box = pad(woodRoadDragEndpoint, padding, context);
            reveal(box, t('intro.lines.finish_drag_endpoint'), { duration: 0 });

            var entity = context.entity(woodRoadEndID);
            if (geoSphericalDistance(entity.loc, woodRoadDragEndpoint) > 4) {
                continueTo(startDragEndpoint);
            }
        });

        context.on('enter.intro', function() {
            continueTo(startDragMidpoint);
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function startDragMidpoint() {
        if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
            return continueTo(updateLine);
        }
        if (context.selectedIDs().indexOf(woodRoadID) === -1) {
            context.enter(modeSelect(context, [woodRoadID]));
        }

        var padding = 80 * Math.pow(2, context.map().zoom() - 19);
        var box = pad(woodRoadDragMidpoint, padding, context);
        reveal(box, t('intro.lines.start_drag_midpoint'));

        context.map().on('move.intro drawn.intro', function() {
            if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
                return continueTo(updateLine);
            }
            var padding = 80 * Math.pow(2, context.map().zoom() - 19);
            var box = pad(woodRoadDragMidpoint, padding, context);
            reveal(box, t('intro.lines.start_drag_midpoint'), { duration: 0 });
        });

        context.history().on('change.intro', function(changed) {
            if (changed.created().length === 1) {
                continueTo(continueDragMidpoint);
            }
        });

        context.on('enter.intro', function(mode) {
            if (mode.id !== 'select') {
                // keep Wood Road selected so midpoint triangles are drawn..
                context.enter(modeSelect(context, [woodRoadID]));
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function continueDragMidpoint() {
        if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
            return continueTo(updateLine);
        }

        var padding = 100 * Math.pow(2, context.map().zoom() - 19);
        var box = pad(woodRoadDragEndpoint, padding, context);
        box.height += 400;

        var advance = function() {
            context.history().checkpoint('doneUpdateLine');
            continueTo(deleteLines);
        };

        reveal(box, t('intro.lines.continue_drag_midpoint'),
            { buttonText: t('intro.ok'), buttonCallback: advance }
        );

        context.map().on('move.intro drawn.intro', function() {
            if (!context.hasEntity(woodRoadID) || !context.hasEntity(woodRoadEndID)) {
                return continueTo(updateLine);
            }
            var padding = 100 * Math.pow(2, context.map().zoom() - 19);
            var box = pad(woodRoadDragEndpoint, padding, context);
            box.height += 400;
            reveal(box, t('intro.lines.continue_drag_midpoint'),
                { duration: 0, buttonText: t('intro.ok'), buttonCallback: advance }
            );
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            nextStep();
        }
    }


    function deleteLines() {
        context.history().reset('doneUpdateLine');
        context.enter(modeBrowse(context));

        if (!context.hasEntity(washingtonStreetID) ||
            !context.hasEntity(twelfthAvenueID) ||
            !context.hasEntity(eleventhAvenueEndID)) {
            return chapter.restart();
        }

        var msec = transitionTime(deleteLinesLoc, context.map().center());
        if (msec) { reveal(null, null, { duration: 0 }); }
        context.map().zoom(18).centerEase(deleteLinesLoc, msec);

        timeout(function() {
            var padding = 200 * Math.pow(2, context.map().zoom() - 18);
            var box = pad(deleteLinesLoc, padding, context);
            box.top -= 200;
            box.height += 400;
            var advance = function() { continueTo(rightClickIntersection); };

            reveal(box, t('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),
                { buttonText: t('intro.ok'), buttonCallback: advance }
            );

            context.map().on('move.intro drawn.intro', function() {
                var padding = 200 * Math.pow(2, context.map().zoom() - 18);
                var box = pad(deleteLinesLoc, padding, context);
                box.top -= 200;
                box.height += 400;
                reveal(box, t('intro.lines.delete_lines', { street: t('intro.graph.name.12th-avenue') }),
                    { duration: 0, buttonText: t('intro.ok'), buttonCallback: advance }
                );
            });

            context.history().on('change.intro', function() {
                timeout(function() {
                    continueTo(deleteLines);
                }, 500);  // after any transition (e.g. if user deleted intersection)
            });

        }, msec + 100);

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function rightClickIntersection() {
        context.history().reset('doneUpdateLine');
        context.enter(modeBrowse(context));

        context.map().zoom(18).centerEase(eleventhAvenueEnd, 500);

        timeout(function() {
            var padding = 60 * Math.pow(2, context.map().zoom() - 18);
            var box = pad(eleventhAvenueEnd, padding, context);
            reveal(box, t('intro.lines.rightclick_intersection',
                { street1: t('intro.graph.name.11th-avenue'), street2: t('intro.graph.name.washington-street') })
            );

            context.map().on('move.intro drawn.intro', function() {
                var padding = 60 * Math.pow(2, context.map().zoom() - 18);
                var box = pad(eleventhAvenueEnd, padding, context);
                reveal(box, t('intro.lines.rightclick_intersection',
                    { street1: t('intro.graph.name.11th-avenue'), street2: t('intro.graph.name.washington-street') }),
                    { duration: 0 }
                );
            });

            context.on('enter.intro', function(mode) {
                if (mode.id !== 'select') return;
                var ids = context.selectedIDs();
                if (ids.length !== 1 || ids[0] !== eleventhAvenueEndID) return;

                timeout(function() {
                    var node = selectMenuItem('split').node();
                    if (!node) return;
                    continueTo(splitIntersection);
                }, 300);  // after menu visible
            });

            context.history().on('change.intro', function() {
                timeout(function() {
                    continueTo(deleteLines);
                }, 300);  // after any transition (e.g. if user deleted intersection)
            });

        }, 600);

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function splitIntersection() {
        if (!context.hasEntity(washingtonStreetID) ||
            !context.hasEntity(twelfthAvenueID) ||
            !context.hasEntity(eleventhAvenueEndID)) {
            return continueTo(deleteLines);
        }

        var node = selectMenuItem('split').node();
        if (!node) { return continueTo(rightClickIntersection); }

        var wasChanged = false;
        var menuCoords = context.map().mouseCoordinates();
        _washingtonSegmentID = null;

        revealEditMenu(menuCoords, t('intro.lines.split_intersection',
            { button: icon('#iD-operation-split', 'pre-text'), street: t('intro.graph.name.washington-street') })
        );

        context.map().on('move.intro drawn.intro', function() {
            var node = selectMenuItem('split').node();
            if (!wasChanged && !node) { return continueTo(rightClickIntersection); }

            revealEditMenu(menuCoords, t('intro.lines.split_intersection',
                { button: icon('#iD-operation-split', 'pre-text'), street: t('intro.graph.name.washington-street') }),
                { duration: 0 }
            );
        });

        context.history().on('change.intro', function(changed) {
            wasChanged = true;
            timeout(function() {
                if (context.history().undoAnnotation() === t('operations.split.annotation.line')) {
                    _washingtonSegmentID = changed.created()[0].id;
                    continueTo(didSplit);
                } else {
                    _washingtonSegmentID = null;
                    continueTo(retrySplit);
                }
            }, 300);  // after any transition (e.g. if user deleted intersection)
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function retrySplit() {
        context.enter(modeBrowse(context));
        context.map().zoom(18).centerEase(eleventhAvenueEnd, 500);
        var advance = function() { continueTo(rightClickIntersection); };

        var padding = 60 * Math.pow(2, context.map().zoom() - 18);
        var box = pad(eleventhAvenueEnd, padding, context);
        reveal(box, t('intro.lines.retry_split'),
            { buttonText: t('intro.ok'), buttonCallback: advance }
        );

        context.map().on('move.intro drawn.intro', function() {
            var padding = 60 * Math.pow(2, context.map().zoom() - 18);
            var box = pad(eleventhAvenueEnd, padding, context);
            reveal(box, t('intro.lines.retry_split'),
                { duration: 0, buttonText: t('intro.ok'), buttonCallback: advance }
            );
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            nextStep();
        }
    }


    function didSplit() {
        if (!_washingtonSegmentID ||
            !context.hasEntity(_washingtonSegmentID) ||
            !context.hasEntity(washingtonStreetID) ||
            !context.hasEntity(twelfthAvenueID) ||
            !context.hasEntity(eleventhAvenueEndID)) {
            return continueTo(rightClickIntersection);
        }

        var ids = context.selectedIDs();
        var string = 'intro.lines.did_split_' + (ids.length > 1 ? 'multi' : 'single');
        var street = t('intro.graph.name.washington-street');

        var padding = 200 * Math.pow(2, context.map().zoom() - 18);
        var box = pad(twelfthAvenue, padding, context);
        box.width = box.width / 2;
        reveal(box, t(string, { street1: street, street2: street }),
            { duration: 500 }
        );

        timeout(function() {
            context.map().zoom(18).centerEase(twelfthAvenue, 500);

            context.map().on('move.intro drawn.intro', function() {
                var padding = 200 * Math.pow(2, context.map().zoom() - 18);
                var box = pad(twelfthAvenue, padding, context);
                box.width = box.width / 2;
                reveal(box, t(string, { street1: street, street2: street }),
                    { duration: 0 }
                );
            });
        }, 600);  // after initial reveal and curtain cut

        context.on('enter.intro', function() {
            var ids = context.selectedIDs();
            if (ids.length === 1 && ids[0] === _washingtonSegmentID) {
                continueTo(multiSelect);
            }
        });

        context.history().on('change.intro', function() {
            if (!_washingtonSegmentID ||
                !context.hasEntity(_washingtonSegmentID) ||
                !context.hasEntity(washingtonStreetID) ||
                !context.hasEntity(twelfthAvenueID) ||
                !context.hasEntity(eleventhAvenueEndID)) {
                return continueTo(rightClickIntersection);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function multiSelect() {
        if (!_washingtonSegmentID ||
            !context.hasEntity(_washingtonSegmentID) ||
            !context.hasEntity(washingtonStreetID) ||
            !context.hasEntity(twelfthAvenueID) ||
            !context.hasEntity(eleventhAvenueEndID)) {
            return continueTo(rightClickIntersection);
        }

        var ids = context.selectedIDs();
        var hasWashington = ids.indexOf(_washingtonSegmentID) !== -1;
        var hasTwelfth = ids.indexOf(twelfthAvenueID) !== -1;

        if (hasWashington && hasTwelfth) {
            return continueTo(multiRightClick);
        } else if (!hasWashington && !hasTwelfth) {
            return continueTo(didSplit);
        }

        context.map().zoom(18).centerEase(twelfthAvenue, 500);

        timeout(function() {
            var selected, other, padding, box;
            if (hasWashington) {
                selected = t('intro.graph.name.washington-street');
                other = t('intro.graph.name.12th-avenue');
                padding = 60 * Math.pow(2, context.map().zoom() - 18);
                box = pad(twelfthAvenueEnd, padding, context);
                box.width *= 3;
            } else {
                selected = t('intro.graph.name.12th-avenue');
                other = t('intro.graph.name.washington-street');
                padding = 200 * Math.pow(2, context.map().zoom() - 18);
                box = pad(twelfthAvenue, padding, context);
                box.width /= 2;
            }

            reveal(box,
                t('intro.lines.multi_select', { selected: selected, other1: other, other2: other })
            );

            context.map().on('move.intro drawn.intro', function() {
                if (hasWashington) {
                    selected = t('intro.graph.name.washington-street');
                    other = t('intro.graph.name.12th-avenue');
                    padding = 60 * Math.pow(2, context.map().zoom() - 18);
                    box = pad(twelfthAvenueEnd, padding, context);
                    box.width *= 3;
                } else {
                    selected = t('intro.graph.name.12th-avenue');
                    other = t('intro.graph.name.washington-street');
                    padding = 200 * Math.pow(2, context.map().zoom() - 18);
                    box = pad(twelfthAvenue, padding, context);
                    box.width /= 2;
                }

                reveal(box,
                    t('intro.lines.multi_select', { selected: selected, other1: other, other2: other }),
                    { duration: 0 }
                );
            });

            context.on('enter.intro', function() {
                continueTo(multiSelect);
            });

            context.history().on('change.intro', function() {
                if (!_washingtonSegmentID ||
                    !context.hasEntity(_washingtonSegmentID) ||
                    !context.hasEntity(washingtonStreetID) ||
                    !context.hasEntity(twelfthAvenueID) ||
                    !context.hasEntity(eleventhAvenueEndID)) {
                    return continueTo(rightClickIntersection);
                }
            });
        }, 600);

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function multiRightClick() {
        if (!_washingtonSegmentID ||
            !context.hasEntity(_washingtonSegmentID) ||
            !context.hasEntity(washingtonStreetID) ||
            !context.hasEntity(twelfthAvenueID) ||
            !context.hasEntity(eleventhAvenueEndID)) {
            return continueTo(rightClickIntersection);
        }

        var padding = 200 * Math.pow(2, context.map().zoom() - 18);
        var box = pad(twelfthAvenue, padding, context);
        reveal(box, t('intro.lines.multi_rightclick'));

        context.map().on('move.intro drawn.intro', function() {
            var padding = 200 * Math.pow(2, context.map().zoom() - 18);
            var box = pad(twelfthAvenue, padding, context);
            reveal(box, t('intro.lines.multi_rightclick'), { duration: 0 });
        });

        d3_select(window).on('click.intro contextmenu.intro', function() {
            timeout(function() {
                var ids = context.selectedIDs();
                if (ids.length === 2 &&
                    ids.indexOf(twelfthAvenueID) !== -1 &&
                    ids.indexOf(_washingtonSegmentID) !== -1) {
                        var node = selectMenuItem('delete').node();
                        if (!node) return;
                        continueTo(multiDelete);
                } else if (ids.length === 1 &&
                    ids.indexOf(_washingtonSegmentID) !== -1) {
                    return continueTo(multiSelect);
                } else {
                    return continueTo(didSplit);
                }
            }, 300);  // after edit menu visible
        }, true);

        context.history().on('change.intro', function() {
            if (!_washingtonSegmentID ||
                !context.hasEntity(_washingtonSegmentID) ||
                !context.hasEntity(washingtonStreetID) ||
                !context.hasEntity(twelfthAvenueID) ||
                !context.hasEntity(eleventhAvenueEndID)) {
                return continueTo(rightClickIntersection);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            d3_select(window).on('click.intro contextmenu.intro', null, true);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function multiDelete() {
        if (!_washingtonSegmentID ||
            !context.hasEntity(_washingtonSegmentID) ||
            !context.hasEntity(washingtonStreetID) ||
            !context.hasEntity(twelfthAvenueID) ||
            !context.hasEntity(eleventhAvenueEndID)) {
            return continueTo(rightClickIntersection);
        }

        var node = selectMenuItem('delete').node();
        if (!node) return continueTo(multiRightClick);

        var menuCoords = context.map().mouseCoordinates();
        revealEditMenu(menuCoords,
            t('intro.lines.multi_delete', { button: icon('#iD-operation-delete', 'pre-text') })
        );

        context.map().on('move.intro drawn.intro', function() {
            revealEditMenu(menuCoords,
                t('intro.lines.multi_delete', { button: icon('#iD-operation-delete', 'pre-text') }),
                { duration: 0 }
            );
        });

        context.on('exit.intro', function() {
            if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {
                return continueTo(multiSelect);  // left select mode but roads still exist
            }
        });

        context.history().on('change.intro', function() {
            if (context.hasEntity(_washingtonSegmentID) || context.hasEntity(twelfthAvenueID)) {
                continueTo(retryDelete);         // changed something but roads still exist
            } else {
                continueTo(play);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function retryDelete() {
        context.enter(modeBrowse(context));

        var padding = 200 * Math.pow(2, context.map().zoom() - 18);
        var box = pad(twelfthAvenue, padding, context);
        reveal(box, t('intro.lines.retry_delete'), {
            buttonText: t('intro.ok'),
            buttonCallback: function() { continueTo(multiSelect); }
        });

        function continueTo(nextStep) {
            nextStep();
        }
    }


    function play() {
        dispatch.call('done');
        reveal('#id-container',
            t('intro.lines.play', { next: t('intro.buildings.title') }), {
                tooltipBox: '.intro-nav-wrap .chapter-building',
                buttonText: t('intro.ok'),
                buttonCallback: function() { reveal('#id-container'); }
            }
        );
   }


    chapter.enter = function() {
        addLine();
    };


    chapter.exit = function() {
        timeouts.forEach(window.clearTimeout);
        d3_select(window).on('mousedown.intro', null, true);
        context.on('enter.intro exit.intro', null);
        context.map().on('move.intro drawn.intro', null);
        context.history().on('change.intro', null);
        d3_select('.inspector-wrap').on('wheel.intro', null);
        d3_select('.preset-list-button').on('click.intro', null);
    };


    chapter.restart = function() {
        chapter.exit();
        chapter.enter();
    };


    return utilRebind(chapter, dispatch, 'on');
}
Пример #3
0
/**
 *
 *  data: []
 *  series: [ {label, x, y} ]
 *
 */
export default function timeline() {

	var _id = 'timeline_line_' + Date.now();

	/**
	 * Style stuff
	 */

	// Margin between the main plot group and the svg border
	var _margin = { top: 10, right: 10, bottom: 20, left: 40 };

	// Height and width of the SVG element
	var _height = 100, _width = 600;

	// Render the grid?
	var _displayOptions = {
		xGrid: false,
		yGrid: false,
		pointEvents: false // value, values, series, custom (falsey is off)
	};


	/**
	 * Configuration of accessors to data
	 */

	// Various configuration functions
	var _fn = {
		valueX: function(d) { return d[0]; },

		markerValueX: function(d) { return d[0]; },
		markerLabel: function(d) { return d[1]; },

		pointRadius: function() { return 2; }
	};


	/**
	 * Extents, Axes, and Scales
	 */

	// Extent configuration for x and y dimensions of plot
	var now = Date.now();
	var _extent = {
		x: extent({
			defaultValue: [ now - 60000 * 5, now ],
			getValue: function(d, i) { return _fn.valueX(d, i); }
		}),
		y: extent({
			filter: function(d, i) {
				var x = _fn.valueX(d, i);
				return x >= _scale.x.domain()[0] && x <= _scale.x.domain()[1];
			}
		})
	};
	var _multiExtent = multiExtent();


	// Default scales for x and y dimensions
	var _scale = {
		x: d3_scaleTime(),
		y: d3_scaleLinear()
	};

	// Default Axis definitions
	var _axis = {
		x: d3_axisBottom().scale(_scale.x),
		y: d3_axisLeft().ticks(3).scale(_scale.y),

		xGrid: d3_axisBottom().tickFormat('').tickSizeOuter(0).scale(_scale.x),
		yGrid: d3_axisLeft().tickFormat('').tickSizeOuter(0).ticks(3).scale(_scale.y)
	};


	/**
	 * Generators
	 */

	var _line = d3_line()
		.x(function(d, i) { return _scale.x(_fn.valueX(d, i)); });

	var _area = d3_area()
		.x(function(d, i) { return _scale.x(_fn.valueX(d, i)); });

	// Voronoi that we'll use for hovers
	var _voronoi = d3_voronoi()
		.x(function(d, i) {
			return _scale.x(d.x, i);
		})
		.y(function(d, i) {
			return _scale.y(d.y, i);
		});


	/**
	 * Brush and Events
	 */

	// Brush Management
	var _brush = timelineBrush({ brush: d3_brushX(), scale: _scale.x });
	_brush.dispatch()
		.on('end', function() {
			updateBrush();
			_dispatch.call('brushEnd', this, getBrush());
		})
		.on('start', function() {
			updateBrush();
			_dispatch.call('brushStart', this, getBrush());
		})
		.on('brush', function() {
			updateBrush();
			_dispatch.call('brush', this, getBrush());
		});

	// The dispatch object and all events
	var _dispatch = d3_dispatch(
		'brush', 'brushStart', 'brushEnd',
		'markerClick', 'markerMouseover', 'markerMouseout',
		'pointMouseover', 'pointMouseout', 'pointClick');


	/**
	 * Keep track of commonly access DOM elements
	 */

	// Storage for commonly used DOM elements
	var _element = {
		svg: undefined,

		g: {
			container: undefined,
			plots: undefined,
			plotBrushes: undefined,
			points: undefined,
			voronoi: undefined,

			xAxis: undefined,
			yAxis: undefined,
			xAxisGrid: undefined,
			yAxisGrid: undefined,

			markers: undefined,
			brush: undefined
		},

		plotClipPath: undefined,
		plotBrushClipPath: undefined,
		markerClipPath: undefined
	};


	/**
	 * Data and Series and Markers
	 */

	// The main data array
	var _data = [];

	// The definition of the series to draw
	var _series = [];

	// Markers data
	var _markers = [];

	/**
	 * Explodes the data into an array with one point per unique point
	 * in the data (according to the series).
	 *
	 * I.e.,
	 *
	 * data: [{ x: 0, y1: 1, y2: 2}]
	 * series: [
	 *     { key: 's1', getValue: function(d) { return d.y1; } },
	 *     { key: 's2', getValue: function(d) { return d.y2; } }
	 * ]
	 *
	 * ==>
	 *
	 * [
	 *     { x: 0, y: 1, series: { key: 's1', ... }, data: { x: 0, y1: 1, y2: 2 },
	 *     { x: 0, y: 2, series: { key: 's2', ... }, data: { x: 0, y1: 1, y2: 2 },
	 * ]
	 *
	 * @param series
	 * @param data
	 */
	function getVoronoiData(series, data, getXValue) {
		var toReturn = [];

		// Loop over each series
		series.forEach(function(s, i) {

			// Convert the data to x/y series
			toReturn = toReturn.concat(data.map(function(d, ii) {
				return {
					x: getXValue(d, ii),
					y: s.getValue(d, ii),
					series: s,
					data: d
				};
			}));

		});

		return toReturn;
	}

	function highlightValues(hovered) {
		if (null != hovered) {

			var join = _element.g.points.selectAll('circle')
				.data(_series.map(function(d) {
					return {
						x: _fn.valueX(hovered.data),
						y: d.getValue(hovered.data),
						category: d.category
					};
				}));
			var enter = join.enter().append('circle');
			var update = join.selectAll('circle');

			enter.merge(update)
				.attr('class', function(d, i) { return d.category; })
				.attr('cx', function(d, i) { return _scale.x(d.x); })
				.attr('cy', function(d, i) { return _scale.y(d.y); })
				.attr('r', 3);
		}
		else {
			_element.g.points.selectAll('circle').remove();
		}
	}

	function highlightValue(hovered) {}
	function highlightSeries(hovered) {}


	function onPointMouseover(d, i) {

		var pointAction = _displayOptions.pointEvents;
		if('value' === pointAction) {
			highlightValue(d.data);
		}
		else if('values' === pointAction) {
			highlightValues(d.data);
		}
		else if('series' === pointAction) {
			highlightSeries(d.data);
		}

		_dispatch.call('pointMouseover', this, d.data, i);
	}

	function onPointMouseout(d, i) {

		var pointAction = _displayOptions.pointEvents;
		if('value' === pointAction) {
			highlightValue();
		}
		else if('values' === pointAction) {
			highlightValues();
		}
		else if('series' === pointAction) {
			highlightSeries();
		}

		_dispatch.call('pointMouseout', this, d.data, i);
	}

	function onPointClick(d, i) {
		_dispatch.call('pointClick', this, d.data, i);
	}

	/**
	 * Get the current brush state in terms of the x data domain, in ms epoch time
	 */
	function getBrush() {

		// Try to get the node from the brush group selection
		var node = (null != _element.g.brush)? _element.g.brush.node() : null;

		// Get the current brush selection
		return _brush.getSelection(node);

	}

	function getBrushSelection() {

		// Try to get the node from the brush group selection
		var node = (null != _element.g.brush)? _element.g.brush.node() : null;

		// Get the current brush selection
		return _brush.getBrushSelection(node);

	}

	function getBrushHandlePath(d) {
		var w = 8, h = 12, ch = 4;
		var y = (_scale.y.range()[0] / 2) + (h / 2);

		return 'M' + (w / 2) + ' ' + y + ' c 0 ' + ch + ', ' + (-w) + ' ' + ch + ', ' + (-w) + ' 0 v0 ' + (-h) + ' c 0 ' + (-ch) + ', ' + w + ' ' + (-ch) + ', ' + w + ' 0 Z M0' + ' ' + y + ' v' + (-h);
	}

	/**
	 * Set the current brush state in terms of the x data domain
	 * @param v The new value of the brush
	 *
	 */
	function setBrush(v) {
		_brush.setSelection(_element.g.brush, v);
	}


	/**
	 * Update the state of the brush (as part of redrawing everything)
	 *
	 * The purpose of this function is to update the state of the brush to reflect changes
	 * to the rest of the chart as part of a normal update/redraw cycle. When the x extent
	 * changes, the brush needs to move to stay correctly aligned with the x axis. Normally,
	 * we are only updating the drawn position of the brush, so the brushSelection doesn't
	 * actually change. However, if the change results in the brush extending partially or
	 * wholly outside of the x extent, we might have to clip or clear the brush, which will
	 * result in brush change events being propagated.
	 *
	 * @param previousExtent The previous state of the brush extent. Must be provided to
	 *        accurately determine the extent of the brush in terms of the x data domain
	 */
	function updateBrush(previousExtent) {

		// If there was no previous extent, then there is no brush to update
		if (null != previousExtent) {

			// Derive the overall plot extent from the collection of series
			var plotExtent = _extent.x.getExtent(_data);

			if(null != plotExtent && Array.isArray(plotExtent) && plotExtent.length == 2) {

				// Clip extent by the full extent of the plot (this is in case we've slipped off the visible plot)
				var newExtent = [ Math.max(plotExtent[0], previousExtent[0]), Math.min(plotExtent[1], previousExtent[1]) ];
				setBrush(newExtent);

			}
			else {
				// There is no plot/data so just clear the brush
				setBrush(undefined);
			}
		}

		_element.g.brush
			.style('display', (_brush.enabled())? 'unset' : 'none')
			.call(_brush.brush());


		/*
		 * Update the clip path for the brush plot
		 */
		var brushExtent = getBrushSelection();
		if (null != brushExtent) {

			var height = _scale.y.range()[0];

			// Update the brush clip path
			_element.plotBrushClipPath
				.attr('transform', 'translate(' + brushExtent[0] + ', -1)')
				.attr('width', Math.max(0, brushExtent[1] - brushExtent[0]))
				.attr('height', Math.max(0, height) + 2);

			// Create/Update the handles
			var handleJoin = _element.g.brush
				.selectAll('.resize-handle').data([ { type: 'w' }, { type: 'e' } ]);

			var handleEnter = handleJoin.enter().append('g')
				.attr('class', 'resize-handle')
				.attr('cursor', 'ew-resize');

			handleEnter.append('path').attr('class', 'handle-line');
			handleEnter.append('path').attr('class', 'handle-grip');

			var merge = handleEnter.merge(handleJoin);
			merge.attr('transform', function(d, i) { return 'translate(' + brushExtent[i] + ', 0)'; });
			merge.select('.handle-line')
				.attr('d', 'M0 ' + height + ' v' + (-height));
			merge.select('.handle-grip')
				.attr('d', getBrushHandlePath);

		}
		else {

			// Empty the clip path
			_element.plotBrushClipPath
				.attr('transform', 'translate(-1, -1)')
				.attr('width', 0)
				.attr('height', 0);

			// Remove the handles
			_element.g.brush.selectAll('.resize-handle')
				.remove();

		}

	}


	function updateAxes() {
		if (null != _axis.x) {
			_element.g.xAxis.call(_axis.x);
		}
		if (null != _axis.xGrid && _displayOptions.xGrid) {
			_element.g.xAxisGrid.call(_axis.xGrid);
		}
		if (null != _axis.y) {
			_element.g.yAxis.call(_axis.y);
		}
		if (null != _axis.yGrid && _displayOptions.yGrid) {
			_element.g.yAxisGrid.call(_axis.yGrid);
		}
	}


	function updatePlots() {

		// Join
		var plotJoin = _element.g.plots
			.selectAll('.plot')
			.data(_series, function(d) { return d.key; });

		// Enter
		var plotEnter = plotJoin.enter().append('g').attr('class', 'plot');

		var lineEnter = plotEnter.append('g').append('path')
			.attr('class', function(d) { return ((d.category)? d.category : '') + ' line'; });
		var areaEnter = plotEnter.append('g').append('path')
			.attr('class', function(d) { return ((d.category)? d.category : '') + ' area'; });

		var lineUpdate = plotJoin.select('.line');
		var areaUpdate = plotJoin.select('.area');

		// Enter + Update
		lineEnter.merge(lineUpdate)
			.attr('d', function(series) {
				return _line.y(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data);
			});

		areaEnter.merge(areaUpdate)
			.attr('d', function(series) {
				return _area
					.y0(_scale.y.range()[0])
					.y1(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data);
			});


		// Remove the previous voronoi
		_element.g.voronoi.selectAll('path').remove();

		if (_displayOptions.pointEvents) {

			// check range against width
			var extent = _scale.x.domain();
			var voronoiData = getVoronoiData(_series, _data, _fn.valueX)
				.filter(function(d) {
					// Filter out points that are outside of the extent
					return (extent[0] <= d.x && d.x <= extent[1]);
				});

			// Filter out paths that are null
			voronoiData  = _voronoi.polygons(voronoiData)
				.filter(function (d) { return (null != d); });

			// Draw the voronoi overlay polygons
			_element.g.voronoi.selectAll('path').data(voronoiData).enter().append('path')
				.attr('d', function (d) { return (null != d) ? 'M' + d.join('L') + 'Z' : null; })
				.on('mouseover', onPointMouseover)
				.on('mouseout', onPointMouseout)
				.on('click', onPointClick);

		}

		// Exit
		var plotExit = plotJoin.exit();
		plotExit.remove();
	}

	function updatePlotBrushes() {

		// Join
		var plotJoin = _element.g.plotBrushes
			.selectAll('.plot-brush')
			.data((_brush.enabled())? _series : [], function(d) { return d.key; });

		// Enter
		var plotEnter = plotJoin.enter().append('g').attr('class', 'plot plot-brush');

		var lineEnter = plotEnter.append('g').append('path')
			.attr('class', function(d) { return ((d.category)? d.category : '') + ' line'; });
		var areaEnter = plotEnter.append('g').append('path')
			.attr('class', function(d) { return ((d.category)? d.category : '') + ' area'; });

		var lineUpdate = plotJoin.select('.line');
		var areaUpdate = plotJoin.select('.area');

		// Enter + Update
		lineEnter.merge(lineUpdate)
			.attr('d', function(series) {
				return _line.y(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data);
			});

		areaEnter.merge(areaUpdate)
			.attr('d', function(series) {
				return _area
					.y0(_scale.y.range()[0])
					.y1(function (d, i) { return _scale.y(series.getValue(d, i)); })(_data);
			});

		// Exit
		var plotExit = plotJoin.exit();
		plotExit.remove();

	}

	function updateMarkers() {

		// Join
		var markerJoin = _element.g.markers
			.selectAll('.marker')
			.data(_markers, _fn.markerValueX);

		// Enter
		var markerEnter = markerJoin.enter().append('g')
			.attr('class', 'marker')
			.on('mouseover', function(d, i) { _dispatch.call('markerMouseover', this, d, i); })
			.on('mouseout', function(d, i) { _dispatch.call('markerMouseout', this, d, i); })
			.on('click', function(d, i) { _dispatch.call('markerClick', this, d, i); });

		var lineEnter = markerEnter.append('line');
		var textEnter = markerEnter.append('text');

		lineEnter
			.attr('y1', function(d) { return _scale.y.range()[1]; })
			.attr('y2', function(d) { return _scale.y.range()[0]; });

		textEnter
			.attr('dy', '0em')
			.attr('y', -3)
			.attr('text-anchor', 'middle')
			.text(_fn.markerLabel);

		// Enter + Update
		var lineUpdate = markerJoin.select('line');
		var textUpdate = markerJoin.select('text');

		lineEnter.merge(lineUpdate)
			.attr('x1', function(d, i) { return _scale.x(_fn.markerValueX(d, i)); })
			.attr('x2', function(d, i) { return _scale.x(_fn.markerValueX(d)); });

		textEnter.merge(textUpdate)
			.attr('x', function(d, i) { return _scale.x(_fn.markerValueX(d)); });

		// Exit
		markerJoin.exit().remove();

	}


	// Chart create/init method
	function _instance() {}


	/**
	 * Initialize the chart (only called once). Performs all initial chart creation/setup
	 *
	 * @param container The container element to which to apply the chart
	 * @returns {_instance} Instance of the chart
	 */
	_instance.init = function(container) {

		// Create a container div
		_element.div = container.append('div').attr('class', 'sentio timeline');

		// Create the SVG element
		_element.svg = _element.div.append('svg');

		// Add the defs and add the clip path definition
		var defs = _element.svg.append('defs');
		_element.plotBrushClipPath = defs.append('clipPath').attr('id', 'plotBrush_' + _id).append('rect');
		_element.plotClipPath = defs.append('clipPath').attr('id', 'plot_' + _id).append('rect');
		_element.markerClipPath = defs.append('clipPath').attr('id', 'marker_' + _id).append('rect');

		// Append a container for everything
		_element.g.container = _element.svg.append('g');

		// Append the grid
		_element.g.grid = _element.g.container.append('g').attr('class', 'grid');
		_element.g.xAxisGrid = _element.g.grid.append('g').attr('class', 'x');
		_element.g.yAxisGrid = _element.g.grid.append('g').attr('class', 'y');

		// Append the path group (which will have the clip path and the line path
		_element.g.plots = _element.g.container.append('g').attr('class', 'plots');
		_element.g.plots.attr('clip-path', 'url(#plot_' + _id + ')');

		// Append the path group (which will have the clip path and the line path
		_element.g.plotBrushes = _element.g.container.append('g').attr('class', 'plot-brushes');
		_element.g.plotBrushes.attr('clip-path', 'url(#plotBrush_' + _id + ')');
		_element.g.plotBrushHandles = _element.g.container.append('g').attr('class', 'plot-brush-handles');

		// Append groups for the axes
		_element.g.axes = _element.g.container.append('g').attr('class', 'axis');
		_element.g.xAxis = _element.g.axes.append('g').attr('class', 'x');
		_element.g.yAxis = _element.g.axes.append('g').attr('class', 'y');

		// Append a group for the voronoi and the points
		_element.g.points = _element.g.container.append('g').attr('class', 'points');
		_element.g.points.attr('clip-path', 'url(#marker_' + _id + ')');
		_element.g.voronoi = _element.g.container.append('g').attr('class', 'voronoi');


		// Append a group for the markers
		_element.g.markers = _element.g.container.append('g').attr('class', 'markers');
		_element.g.markers.attr('clip-path', 'url(#marker_' + _id + ')');

		// Add the brush element
		_element.g.brush = _element.g.container.append('g').attr('class', 'x brush');

		_instance.resize();

		return _instance;
	};

	/*
	 * Set the data to drive the chart
	 */
	_instance.data = function(v) {
		if (!arguments.length) { return _data; }
		_data = (null != v)? v : [];

		return _instance;
	};

	/*
	 * Define the series to show on the chart
	 */
	_instance.series = function(v) {
		if (!arguments.length) { return _series; }
		_series = (null != v)? v : [];

		return _instance;
	};

	/*
	 * Set the markers data
	 */
	_instance.markers = function(v) {
		if (!arguments.length) { return _markers; }
		_markers = (null != v)? v : [];
		return _instance;
	};

	/*
	 * Updates all the elements that depend on the size of the various components
	 */
	_instance.resize = function() {

		// Need to grab the brush extent before we change anything
		var brushSelection = getBrush();


		// Resize the SVG Pane
		_element.svg.attr('width', _width).attr('height', _height);

		// Update the margins on the main draw group
		_element.g.container.attr('transform', 'translate(' + _margin.left + ',' + _margin.top + ')');


		// Resize Scales
		_scale.x.range([ 0, Math.max(0, _width - _margin.left - _margin.right) ]);
		_scale.y.range([ Math.max(0, _height - _margin.top - _margin.bottom), 0 ]);


		/**
		 * Resize clip paths
		 */

		// Plot Brush clip path is only the plot pane
		_element.plotBrushClipPath
			.attr('transform', 'translate(-1, -1)')
			.attr('width', Math.max(0, _scale.x.range()[1]) + 2)
			.attr('height', Math.max(0, _scale.y.range()[0]) + 2);

		// Plot clip path is only the plot pane
		_element.plotClipPath
			.attr('transform', 'translate(-1, -1)')
			.attr('width', Math.max(0, _scale.x.range()[1]) + 2)
			.attr('height', Math.max(0, _scale.y.range()[0]) + 2);

		// Marker clip path includes top margin by default
		_element.markerClipPath
			.attr('transform', 'translate(0, -' + _margin.top + ')')
			.attr('width', Math.max(0, _width - _margin.left - _margin.right))
			.attr('height', Math.max(0, _height - _margin.bottom));

		// Resize the clip extent of the plot
		_voronoi.extent([
			[ 0, 0 ],
			[ _width - _margin.left - _margin.right, _height - _margin.top - _margin.bottom ]
		]);


		/**
		 * Update axis and grids
		 */

		// Reset axis and grid positions
		_element.g.xAxis.attr('transform', 'translate(0,' + _scale.y.range()[0] + ')');
		_element.g.xAxisGrid.attr('transform', 'translate(0,' + _scale.y.range()[0] + ')');


		// Resize the x grid ticks
		if (_displayOptions.xGrid) {
			_axis.xGrid.tickSizeInner(-(_height - _margin.top - _margin.bottom));
		}
		else {
			_axis.xGrid.tickSizeInner(0);
		}

		// Resize the y grid ticks
		if (_displayOptions.yGrid) {
			_axis.yGrid.tickSizeInner(-(_width - _margin.left - _margin.right));
		}
		else {
			_axis.yGrid.tickSizeInner(0);
		}


		/**
		 * Update the brush
		 */

		// Resize and position the brush g element
		_element.g.brush.selectAll('rect')
			.attr('y', -1).attr('x', 0)
			.attr('width', _scale.x.range()[1])
			.attr('height', _scale.y.range()[0] + 2);

		// Resize the brush
		_brush.brush()
			.extent([ [ 0, 0 ], [ _scale.x.range()[1], _scale.y.range()[0] + 2 ] ]);

		updateBrush(brushSelection);


		return _instance;
	};


	/*
	 * Redraw the graphic
	 */
	_instance.redraw = function() {

		// Need to grab the brush extent before we change anything
		var brushSelection = getBrush();

		// Update the x domain (to the latest time window)
		_scale.x.domain(_extent.x.getExtent(_data));

		// Update the y domain (based on configuration and data)
		_scale.y.domain(_multiExtent.extent(_extent.y).series(_series).getExtent(_data));

		// Update the plot elements
		updateAxes();
		updatePlots();
		updatePlotBrushes();
		updateMarkers();
		updateBrush(brushSelection);

		return _instance;
	};


	// Basic Getters/Setters
	_instance.width = function(v) {
		if (!arguments.length) { return _width; }
		_width = v;
		return _instance;
	};
	_instance.height = function(v) {
		if (!arguments.length) { return _height; }
		_height = v;
		return _instance;
	};
	_instance.margin = function(v) {
		if (!arguments.length) { return _margin; }
		_margin = v;
		return _instance;
	};
	_instance.showXGrid = function(v) {
		if (!arguments.length) { return _displayOptions.xGrid; }
		_displayOptions.xGrid = v;
		return _instance;
	};
	_instance.showYGrid = function(v) {
		if (!arguments.length) { return _displayOptions.yGrid; }
		_displayOptions.yGrid = v;
		return _instance;
	};
	_instance.showGrid = function(v) {
		_displayOptions.xGrid = _displayOptions.yGrid = v;
		return _instance;
	};
	_instance.pointEvents = function(v) {
		if (!arguments.length) { return _displayOptions.pointEvents; }
		_displayOptions.pointEvents = v;
		return _instance;
	};

	_instance.curve = function(v) {
		if (!arguments.length) { return _line.curve(); }
		_line.curve(v);
		_area.curve(v);
		return _instance;
	};

	_instance.xAxis = function(v) {
		if (!arguments.length) { return _axis.x; }
		_axis.x = v;
		return _instance;
	};
	_instance.xGridAxis = function(v) {
		if (!arguments.length) { return _axis.xGrid; }
		_axis.xGrid = v;
		return _instance;
	};
	_instance.yAxis = function(v) {
		if (!arguments.length) { return _axis.y; }
		_axis.y = v;
		return _instance;
	};
	_instance.yGridAxis = function(v) {
		if (!arguments.length) { return _axis.yGrid; }
		_axis.yGrid = v;
		return _instance;
	};
	_instance.xScale = function(v) {
		if (!arguments.length) { return _scale.x; }
		_scale.x = v;
		if (null != _axis.x) {
			_axis.x.scale(v);
		}
		if (null != _axis.xGrid) {
			_axis.xGrid.scale(v);
		}
		if (null != _brush) {
			_brush.scale(v);
		}
		return _instance;
	};
	_instance.yScale = function(v) {
		if (!arguments.length) { return _scale.y; }
		_scale.y = v;
		if (null != _axis.y) {
			_axis.y.scale(v);
		}
		if (null != _axis.yGrid) {
			_axis.yGrid.scale(v);
		}
		return _instance;
	};
	_instance.xValue = function(v) {
		if (!arguments.length) { return _fn.valueX; }
		_fn.valueX = v;
		return _instance;
	};
	_instance.yExtent = function(v) {
		if (!arguments.length) { return _extent.y; }
		_extent.y = v;
		return _instance;
	};
	_instance.xExtent = function(v) {
		if (!arguments.length) { return _extent.x; }
		_extent.x = v;
		return _instance;
	};

	_instance.markerXValue = function(v) {
		if (!arguments.length) { return _fn.markerValueX; }
		_fn.markerValueX = v;
		return _instance;
	};
	_instance.markerLabel = function(v) {
		if (!arguments.length) { return _fn.markerLabel; }
		_fn.markerLabel = v;
		return _instance;
	};

	_instance.dispatch = function(v) {
		if (!arguments.length) { return _dispatch; }
		return _instance;
	};

	_instance.brush = function(v) {
		if (!arguments.length) { return _brush.enabled(); }
		_brush.enabled(v);
		return _instance;
	};
	_instance.setBrush = function(v) {
		setBrush(v);
		return _instance;
	};
	_instance.getBrush = function() {
		return getBrush();
	};

	return _instance;
}
Пример #4
0
function brush(dim) {
  var extent = defaultExtent,
      filter = defaultFilter,
      listeners = dispatch(brush, "start", "brush", "end"),
      handleSize = 6,
      touchending;

  function brush(group) {
    var overlay = group
        .property("__brush", initialize)
      .selectAll(".overlay")
      .data([type("overlay")]);

    overlay.enter().append("rect")
        .attr("class", "overlay")
        .attr("pointer-events", "all")
        .attr("cursor", cursors.overlay)
      .merge(overlay)
        .each(function() {
          var extent = local(this).extent;
          select(this)
              .attr("x", extent[0][0])
              .attr("y", extent[0][1])
              .attr("width", extent[1][0] - extent[0][0])
              .attr("height", extent[1][1] - extent[0][1]);
        });

    group.selectAll(".selection")
      .data([type("selection")])
      .enter().append("rect")
        .attr("class", "selection")
        .attr("cursor", cursors.selection)
        .attr("fill", "#777")
        .attr("fill-opacity", 0.3)
        .attr("stroke", "#fff")
        .attr("shape-rendering", "crispEdges");

    var handle = group.selectAll(".handle")
      .data(dim.handles, function(d) { return d.type; });

    handle.exit().remove();

    handle.enter().append("rect")
        .attr("class", function(d) { return "handle handle--" + d.type; })
        .attr("cursor", function(d) { return cursors[d.type]; });

    group
        .each(redraw)
        .attr("fill", "none")
        .attr("pointer-events", "all")
        .style("-webkit-tap-highlight-color", "rgba(0,0,0,0)")
        .on("mousedown.brush touchstart.brush", started);
  }

  brush.move = function(group, selection) {
    if (group.selection) {
      group
          .on("start.brush", function() { emitter(this, arguments).beforestart().start(); })
          .on("interrupt.brush end.brush", function() { emitter(this, arguments).end(); })
          .tween("brush", function() {
            var that = this,
                state = that.__brush,
                emit = emitter(that, arguments),
                selection0 = state.selection,
                selection1 = dim.input(typeof selection === "function" ? selection.apply(this, arguments) : selection, state.extent),
                i = interpolate(selection0, selection1);

            function tween(t) {
              state.selection = t === 1 && empty(selection1) ? null : i(t);
              redraw.call(that);
              emit.brush();
            }

            return selection0 && selection1 ? tween : tween(1);
          });
    } else {
      group
          .each(function() {
            var that = this,
                args = arguments,
                state = that.__brush,
                selection1 = dim.input(typeof selection === "function" ? selection.apply(that, args) : selection, state.extent),
                emit = emitter(that, args).beforestart();

            interrupt(that);
            state.selection = selection1 == null || empty(selection1) ? null : selection1;
            redraw.call(that);
            emit.start().brush().end();
          });
    }
  };

  function redraw() {
    var group = select(this),
        selection = local(this).selection;

    if (selection) {
      group.selectAll(".selection")
          .style("display", null)
          .attr("x", selection[0][0])
          .attr("y", selection[0][1])
          .attr("width", selection[1][0] - selection[0][0])
          .attr("height", selection[1][1] - selection[0][1]);

      group.selectAll(".handle")
          .style("display", null)
          .attr("x", function(d) { return d.type[d.type.length - 1] === "e" ? selection[1][0] - handleSize / 2 : selection[0][0] - handleSize / 2; })
          .attr("y", function(d) { return d.type[0] === "s" ? selection[1][1] - handleSize / 2 : selection[0][1] - handleSize / 2; })
          .attr("width", function(d) { return d.type === "n" || d.type === "s" ? selection[1][0] - selection[0][0] + handleSize : handleSize; })
          .attr("height", function(d) { return d.type === "e" || d.type === "w" ? selection[1][1] - selection[0][1] + handleSize : handleSize; });
    }

    else {
      group.selectAll(".selection,.handle")
          .style("display", "none")
          .attr("x", null)
          .attr("y", null)
          .attr("width", null)
          .attr("height", null);
    }
  }

  function emitter(that, args) {
    return that.__brush.emitter || new Emitter(that, args);
  }

  function Emitter(that, args) {
    this.that = that;
    this.args = args;
    this.state = that.__brush;
    this.active = 0;
  }

  Emitter.prototype = {
    beforestart: function() {
      if (++this.active === 1) this.state.emitter = this, this.starting = true;
      return this;
    },
    start: function() {
      if (this.starting) this.starting = false, this.emit("start");
      return this;
    },
    brush: function() {
      this.emit("brush");
      return this;
    },
    end: function() {
      if (--this.active === 0) delete this.state.emitter, this.emit("end");
      return this;
    },
    emit: function(type) {
      customEvent(new BrushEvent(brush, type, dim.output(this.state.selection)), listeners.apply, listeners, [type, this.that, this.args]);
    }
  };

  function started() {
    if (event.touches) { if (event.changedTouches.length < event.touches.length) return noevent(); }
    else if (touchending) return;
    if (!filter.apply(this, arguments)) return;

    var that = this,
        type = event.target.__data__.type,
        mode = (event.metaKey ? type = "overlay" : type) === "selection" ? MODE_DRAG : (event.altKey ? MODE_CENTER : MODE_HANDLE),
        signX = dim === Y ? null : signsX[type],
        signY = dim === X ? null : signsY[type],
        state = local(that),
        extent = state.extent,
        selection = state.selection,
        W = extent[0][0], w0, w1,
        N = extent[0][1], n0, n1,
        E = extent[1][0], e0, e1,
        S = extent[1][1], s0, s1,
        dx,
        dy,
        moving,
        shifting = signX && signY && event.shiftKey,
        lockX,
        lockY,
        point0 = mouse(that),
        point = point0,
        emit = emitter(that, arguments).beforestart();

    if (type === "overlay") {
      state.selection = selection = [
        [w0 = dim === Y ? W : point0[0], n0 = dim === X ? N : point0[1]],
        [e0 = dim === Y ? E : w0, s0 = dim === X ? S : n0]
      ];
    } else {
      w0 = selection[0][0];
      n0 = selection[0][1];
      e0 = selection[1][0];
      s0 = selection[1][1];
    }

    w1 = w0;
    n1 = n0;
    e1 = e0;
    s1 = s0;

    var group = select(that)
        .attr("pointer-events", "none");

    var overlay = group.selectAll(".overlay")
        .attr("cursor", cursors[type]);

    if (event.touches) {
      group
          .on("touchmove.brush", moved, true)
          .on("touchend.brush touchcancel.brush", ended, true);
    } else {
      var view = select(event.view)
          .on("keydown.brush", keydowned, true)
          .on("keyup.brush", keyupped, true)
          .on("mousemove.brush", moved, true)
          .on("mouseup.brush", ended, true);

      dragDisable(event.view);
    }

    nopropagation();
    interrupt(that);
    redraw.call(that);
    emit.start();

    function moved() {
      var point1 = mouse(that);
      if (shifting && !lockX && !lockY) {
        if (Math.abs(point1[0] - point[0]) > Math.abs(point1[1] - point[1])) lockY = true;
        else lockX = true;
      }
      point = point1;
      moving = true;
      noevent();
      move();
    }

    function move() {
      var t;

      dx = point[0] - point0[0];
      dy = point[1] - point0[1];

      switch (mode) {
        case MODE_SPACE:
        case MODE_DRAG: {
          if (signX) dx = Math.max(W - w0, Math.min(E - e0, dx)), w1 = w0 + dx, e1 = e0 + dx;
          if (signY) dy = Math.max(N - n0, Math.min(S - s0, dy)), n1 = n0 + dy, s1 = s0 + dy;
          break;
        }
        case MODE_HANDLE: {
          if (signX < 0) dx = Math.max(W - w0, Math.min(E - w0, dx)), w1 = w0 + dx, e1 = e0;
          else if (signX > 0) dx = Math.max(W - e0, Math.min(E - e0, dx)), w1 = w0, e1 = e0 + dx;
          if (signY < 0) dy = Math.max(N - n0, Math.min(S - n0, dy)), n1 = n0 + dy, s1 = s0;
          else if (signY > 0) dy = Math.max(N - s0, Math.min(S - s0, dy)), n1 = n0, s1 = s0 + dy;
          break;
        }
        case MODE_CENTER: {
          if (signX) w1 = Math.max(W, Math.min(E, w0 - dx * signX)), e1 = Math.max(W, Math.min(E, e0 + dx * signX));
          if (signY) n1 = Math.max(N, Math.min(S, n0 - dy * signY)), s1 = Math.max(N, Math.min(S, s0 + dy * signY));
          break;
        }
      }

      if (e1 < w1) {
        signX *= -1;
        t = w0, w0 = e0, e0 = t;
        t = w1, w1 = e1, e1 = t;
        if (type in flipX) overlay.attr("cursor", cursors[type = flipX[type]]);
      }

      if (s1 < n1) {
        signY *= -1;
        t = n0, n0 = s0, s0 = t;
        t = n1, n1 = s1, s1 = t;
        if (type in flipY) overlay.attr("cursor", cursors[type = flipY[type]]);
      }

      if (state.selection) selection = state.selection; // May be set by brush.move!
      if (lockX) w1 = selection[0][0], e1 = selection[1][0];
      if (lockY) n1 = selection[0][1], s1 = selection[1][1];

      if (selection[0][0] !== w1
          || selection[0][1] !== n1
          || selection[1][0] !== e1
          || selection[1][1] !== s1) {
        state.selection = [[w1, n1], [e1, s1]];
        redraw.call(that);
        emit.brush();
      }
    }

    function ended() {
      nopropagation();
      if (event.touches) {
        if (event.touches.length) return;
        if (touchending) clearTimeout(touchending);
        touchending = setTimeout(function() { touchending = null; }, 500); // Ghost clicks are delayed!
        group.on("touchmove.brush touchend.brush touchcancel.brush", null);
      } else {
        dragEnable(event.view, moving);
        view.on("keydown.brush keyup.brush mousemove.brush mouseup.brush", null);
      }
      group.attr("pointer-events", "all");
      overlay.attr("cursor", cursors.overlay);
      if (state.selection) selection = state.selection; // May be set by brush.move (on start)!
      if (empty(selection)) state.selection = null, redraw.call(that);
      emit.end();
    }

    function keydowned() {
      switch (event.keyCode) {
        case 16: { // SHIFT
          shifting = signX && signY;
          break;
        }
        case 18: { // ALT
          if (mode === MODE_HANDLE) {
            if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
            if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
            mode = MODE_CENTER;
            move();
          }
          break;
        }
        case 32: { // SPACE; takes priority over ALT
          if (mode === MODE_HANDLE || mode === MODE_CENTER) {
            if (signX < 0) e0 = e1 - dx; else if (signX > 0) w0 = w1 - dx;
            if (signY < 0) s0 = s1 - dy; else if (signY > 0) n0 = n1 - dy;
            mode = MODE_SPACE;
            overlay.attr("cursor", cursors.selection);
            move();
          }
          break;
        }
        default: return;
      }
      noevent();
    }

    function keyupped() {
      switch (event.keyCode) {
        case 16: { // SHIFT
          if (shifting) {
            lockX = lockY = shifting = false;
            move();
          }
          break;
        }
        case 18: { // ALT
          if (mode === MODE_CENTER) {
            if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
            if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
            mode = MODE_HANDLE;
            move();
          }
          break;
        }
        case 32: { // SPACE
          if (mode === MODE_SPACE) {
            if (event.altKey) {
              if (signX) e0 = e1 - dx * signX, w0 = w1 + dx * signX;
              if (signY) s0 = s1 - dy * signY, n0 = n1 + dy * signY;
              mode = MODE_CENTER;
            } else {
              if (signX < 0) e0 = e1; else if (signX > 0) w0 = w1;
              if (signY < 0) s0 = s1; else if (signY > 0) n0 = n1;
              mode = MODE_HANDLE;
            }
            overlay.attr("cursor", cursors[type]);
            move();
          }
          break;
        }
        default: return;
      }
      noevent();
    }
  }

  function initialize() {
    var state = this.__brush || {selection: null};
    state.extent = extent.apply(this, arguments);
    state.dim = dim;
    return state;
  }

  brush.extent = function(_) {
    return arguments.length ? (extent = typeof _ === "function" ? _ : constant([[+_[0][0], +_[0][1]], [+_[1][0], +_[1][1]]]), brush) : extent;
  };

  brush.filter = function(_) {
    return arguments.length ? (filter = typeof _ === "function" ? _ : constant(!!_), brush) : filter;
  };

  brush.handleSize = function(_) {
    return arguments.length ? (handleSize = +_, brush) : handleSize;
  };

  brush.on = function() {
    var value = listeners.on.apply(listeners, arguments);
    return value === listeners ? brush : value;
  };

  return brush;
}
Пример #5
0
export function uiRawTagEditor(context) {
    var taginfo = services.taginfo,
        dispatch = d3_dispatch('change'),
        expandedPreference = (context.storage('raw_tag_editor.expanded') === 'true'),
        expandedCurrent = expandedPreference,
        updatePreference = true,
        readOnlyTags = [],
        showBlank = false,
        newRow,
        state,
        preset,
        tags,
        id;


    function rawTagEditor(selection) {
        var count = Object.keys(tags).filter(function(d) { return d; }).length;

        selection.call(uiDisclosure()
            .title(t('inspector.all_tags') + ' (' + count + ')')
            .expanded(expandedCurrent)
            .on('toggled', toggled)
            .content(content)
        );

        function toggled(expanded) {
            expandedCurrent = expanded;
            if (updatePreference) {
                expandedPreference = expanded;
                context.storage('raw_tag_editor.expanded', expanded);
            }
            if (expanded) {
                selection.node().parentNode.scrollTop += 200;
            }
        }
    }


    function content(wrap) {
        var entries = _map(tags, function(v, k) {
            return { key: k, value: v };
        });

        if (!entries.length || showBlank) {
            showBlank = false;
            entries.push({key: '', value: ''});
            newRow = '';
        }

        var list = wrap.selectAll('.tag-list')
            .data([0]);

        list = list.enter()
            .append('ul')
            .attr('class', 'tag-list')
            .merge(list);

        var newTag = wrap.selectAll('.add-tag')
            .data([0]);

        newTag.enter()
            .append('button')
            .attr('class', 'add-tag')
            .on('click', addTag)
            .call(svgIcon('#icon-plus', 'light'));


        var items = list.selectAll('.tag-row')
            .data(entries, function(d) { return d.key; });

        items.exit()
            .each(unbind)
            .remove();

        // Enter

        var enter = items.enter()
            .append('li')
            .attr('class', 'tag-row cf')
            .classed('readonly', isReadOnly);

        enter
            .append('div')
            .attr('class', 'key-wrap')
            .append('input')
            .property('type', 'text')
            .attr('class', 'key')
            .attr('maxlength', 255)
            .call(utilNoAuto)
            .on('blur', keyChange)
            .on('change', keyChange);

        enter
            .append('div')
            .attr('class', 'input-wrap-position')
            .append('input')
            .property('type', 'text')
            .attr('class', 'value')
            .attr('maxlength', 255)
            .call(utilNoAuto)
            .on('blur', valueChange)
            .on('change', valueChange)
            .on('keydown.push-more', pushMore);

        enter
            .append('button')
            .attr('tabindex', -1)
            .attr('class', 'remove minor')
            .call(svgIcon('#operation-delete'));


        // Update

        items = items
            .merge(enter)
            .sort(function(a, b) {
                return (a.key === newRow && b.key !== newRow) ? 1
                    : (a.key !== newRow && b.key === newRow) ? -1
                    : d3_ascending(a.key, b.key);
            });

        items
            .each(function(tag) {
                var row = d3_select(this),
                    key = row.select('input.key'),      // propagate bound data to child
                    value = row.select('input.value');  // propagate bound data to child

                if (id && taginfo) {
                    bindTypeahead(key, value);
                }

                var isRelation = (id && context.entity(id).type === 'relation'),
                    reference;

                if (isRelation && tag.key === 'type') {
                    reference = uiTagReference({ rtype: tag.value }, context);
                } else {
                    reference = uiTagReference({ key: tag.key, value: tag.value }, context);
                }

                if (state === 'hover') {
                    reference.showing(false);
                }

                row
                    .call(reference.button)
                    .call(reference.body);
            });

        items.selectAll('input.key')
            .attr('title', function(d) { return d.key; })
            .call(utilGetSetValue, function(d) { return d.key; })
            .property('disabled', isReadOnly);

        items.selectAll('input.value')
            .attr('title', function(d) { return d.value; })
            .call(utilGetSetValue, function(d) { return d.value; })
            .property('disabled', isReadOnly);

        items.selectAll('button.remove')
            .on('click', removeTag);



        function isReadOnly(d) {
            for (var i = 0; i < readOnlyTags.length; i++) {
                if (d.key.match(readOnlyTags[i]) !== null) {
                    return true;
                }
            }
            return false;
        }


        function pushMore() {
            if (d3_event.keyCode === 9 && !d3_event.shiftKey &&
                list.selectAll('li:last-child input.value').node() === this) {
                addTag();
            }
        }


        function bindTypeahead(key, value) {
            if (isReadOnly({ key: key })) return;
            var geometry = context.geometry(id);

            key.call(d3_combobox()
                .container(context.container())
                .fetcher(function(value, callback) {
                    taginfo.keys({
                        debounce: true,
                        geometry: geometry,
                        query: value
                    }, function(err, data) {
                        if (!err) callback(sort(value, data));
                    });
                }));

            value.call(d3_combobox()
                .container(context.container())
                .fetcher(function(value, callback) {
                    taginfo.values({
                        debounce: true,
                        key: utilGetSetValue(key),
                        geometry: geometry,
                        query: value
                    }, function(err, data) {
                        if (!err) callback(sort(value, data));
                    });
                }));


            function sort(value, data) {
                var sameletter = [],
                    other = [];
                for (var i = 0; i < data.length; i++) {
                    if (data[i].value.substring(0, value.length) === value) {
                        sameletter.push(data[i]);
                    } else {
                        other.push(data[i]);
                    }
                }
                return sameletter.concat(other);
            }
        }


        function unbind() {
            var row = d3_select(this);

            row.selectAll('input.key')
                .call(d3_combobox.off);

            row.selectAll('input.value')
                .call(d3_combobox.off);
        }


        function keyChange(d) {
            var kOld = d.key,
                kNew = this.value.trim(),
                tag = {};


            if (isReadOnly({ key: kNew })) {
                this.value = kOld;
                return;
            }

            if (kNew && kNew !== kOld) {
                var match = kNew.match(/^(.*?)(?:_(\d+))?$/),
                    base = match[1],
                    suffix = +(match[2] || 1);
                while (tags[kNew]) {  // rename key if already in use
                    kNew = base + '_' + suffix++;
                }
            }
            tag[kOld] = undefined;
            tag[kNew] = d.value;

            d.key = kNew; // Maintain DOM identity through the subsequent update.

            if (newRow === kOld) {  // see if this row is still a new row
                newRow = ((d.value === '' || kNew === '') ? kNew : undefined);
            }

            this.value = kNew;
            dispatch.call('change', this, tag);
        }


        function valueChange(d) {
            if (isReadOnly(d)) return;
            var tag = {};
            tag[d.key] = this.value;

            if (newRow === d.key && d.key !== '' && d.value !== '') {   // not a new row anymore
                newRow = undefined;
            }

            dispatch.call('change', this, tag);
        }


        function removeTag(d) {
            if (isReadOnly(d)) return;
            var tag = {};
            tag[d.key] = undefined;
            dispatch.call('change', this, tag);
            d3_select(this.parentNode).remove();
        }


        function addTag() {
            // Wrapped in a setTimeout in case it's being called from a blur
            // handler. Without the setTimeout, the call to `content` would
            // wipe out the pending value change.
            setTimeout(function() {
                showBlank = true;
                content(wrap);
                list.selectAll('li:last-child input.key').node().focus();
            }, 0);
        }
    }


    rawTagEditor.state = function(_) {
        if (!arguments.length) return state;
        state = _;
        return rawTagEditor;
    };


    rawTagEditor.preset = function(_) {
        if (!arguments.length) return preset;
        preset = _;
        if (preset.isFallback()) {
            expandedCurrent = true;
            updatePreference = false;
        } else {
            expandedCurrent = expandedPreference;
            updatePreference = true;
        }
        return rawTagEditor;
    };


    rawTagEditor.tags = function(_) {
        if (!arguments.length) return tags;
        tags = _;
        return rawTagEditor;
    };


    rawTagEditor.entityID = function(_) {
        if (!arguments.length) return id;
        id = _;
        return rawTagEditor;
    };


    rawTagEditor.expanded = function(_) {
        if (!arguments.length) return expandedCurrent;
        expandedCurrent = _;
        updatePreference = false;
        return rawTagEditor;
    };


    rawTagEditor.readOnlyTags = function(_) {
        if (!arguments.length) return readOnlyTags;
        readOnlyTags = _;
        return rawTagEditor;
    };


    return utilRebind(rawTagEditor, dispatch, 'on');
}
Пример #6
0
export function coreContext() {
    var context = {};
    context.version = '2.10.0';

    // create a special translation that contains the keys in place of the strings
    var tkeys = _cloneDeep(dataEn);
    var parents = [];

    function traverser(v, k, obj) {
        parents.push(k);
        if (_isObject(v)) {
            _forOwn(v, traverser);
        } else if (_isString(v)) {
            obj[k] = parents.join('.');
        }
        parents.pop();
    }

    _forOwn(tkeys, traverser);
    addTranslation('_tkeys_', tkeys);

    addTranslation('en', dataEn);
    setLocale('en');

    var dispatch = d3_dispatch('enter', 'exit', 'change');

    // https://github.com/openstreetmap/iD/issues/772
    // http://mathiasbynens.be/notes/localstorage-pattern#comment-9
    var storage;
    try { storage = localStorage; } catch (e) {}  // eslint-disable-line no-empty
    storage = storage || (function() {
        var s = {};
        return {
            getItem: function(k) { return s[k]; },
            setItem: function(k, v) { s[k] = v; },
            removeItem: function(k) { delete s[k]; }
        };
    })();

    context.storage = function(k, v) {
        try {
            if (arguments.length === 1) return storage.getItem(k);
            else if (v === null) storage.removeItem(k);
            else storage.setItem(k, v);
        } catch (e) {
            // localstorage quota exceeded
            /* eslint-disable no-console */
            if (typeof console !== 'undefined') console.error('localStorage quota exceeded');
            /* eslint-enable no-console */
        }
    };


    /* Straight accessors. Avoid using these if you can. */
    var ui, connection, history;
    context.ui = function() { return ui; };
    context.connection = function() { return connection; };
    context.history = function() { return history; };


    /* Connection */
    context.preauth = function(options) {
        if (connection) {
            connection.switch(options);
        }
        return context;
    };

    context.loadTiles = utilCallWhenIdle(function(projection, callback) {
        var cid;
        function done(err, result) {
            if (connection.getConnectionId() !== cid) {
                if (callback) callback({ message: 'Connection Switched', status: -1 });
                return;
            }
            if (!err) history.merge(result.data, result.extent);
            if (callback) callback(err, result);
        }
        if (connection && context.editable()) {
            cid = connection.getConnectionId();
            connection.loadTiles(projection, done);
        }
    });

    context.loadEntity = function(entityID, callback) {
        var cid;
        function done(err, result) {
            if (connection.getConnectionId() !== cid) {
                if (callback) callback({ message: 'Connection Switched', status: -1 });
                return;
            }
            if (!err) history.merge(result.data, result.extent);
            if (callback) callback(err, result);
        }
        if (connection) {
            cid = connection.getConnectionId();
            connection.loadEntity(entityID, done);
        }
    };

    context.zoomToEntity = function(entityID, zoomTo) {
        if (zoomTo !== false) {
            this.loadEntity(entityID, function(err, result) {
                if (err) return;
                var entity = _find(result.data, function(e) { return e.id === entityID; });
                if (entity) { map.zoomTo(entity); }
            });
        }

        map.on('drawn.zoomToEntity', function() {
            if (!context.hasEntity(entityID)) return;
            map.on('drawn.zoomToEntity', null);
            context.on('enter.zoomToEntity', null);
            context.enter(modeSelect(context, [entityID]));
        });

        context.on('enter.zoomToEntity', function() {
            if (mode.id !== 'browse') {
                map.on('drawn.zoomToEntity', null);
                context.on('enter.zoomToEntity', null);
            }
        });
    };

    var minEditableZoom = 16;
    context.minEditableZoom = function(_) {
        if (!arguments.length) return minEditableZoom;
        minEditableZoom = _;
        if (connection) {
            connection.tileZoom(_);
        }
        return context;
    };


    /* History */
    var inIntro = false;
    context.inIntro = function(_) {
        if (!arguments.length) return inIntro;
        inIntro = _;
        return context;
    };

    context.save = function() {
        // no history save, no message onbeforeunload
        if (inIntro || d3_select('.modal').size()) return;

        var canSave;
        if (mode && mode.id === 'save') {
            canSave = false;
        } else {
            canSave = context.selectedIDs().every(function(id) {
                var entity = context.hasEntity(id);
                return entity && !entity.isDegenerate();
            });
        }

        if (canSave) {
            history.save();
        }
        if (history.hasChanges()) {
            return t('save.unsaved_changes');
        }
    };


    /* Graph */
    context.hasEntity = function(id) {
        return history.graph().hasEntity(id);
    };
    context.entity = function(id) {
        return history.graph().entity(id);
    };
    context.childNodes = function(way) {
        return history.graph().childNodes(way);
    };
    context.geometry = function(id) {
        return context.entity(id).geometry(history.graph());
    };


    /* Modes */
    var mode;
    context.mode = function() {
        return mode;
    };
    context.enter = function(newMode) {
        if (mode) {
            mode.exit();
            dispatch.call('exit', this, mode);
        }

        mode = newMode;
        mode.enter();
        dispatch.call('enter', this, mode);
    };

    context.selectedIDs = function() {
        if (mode && mode.selectedIDs) {
            return mode.selectedIDs();
        } else {
            return [];
        }
    };

    context.activeID = function() {
        return mode && mode.activeID && mode.activeID();
    };

    var _selectedNoteID;
    context.selectedNoteID = function(noteID) {
        if (!arguments.length) return _selectedNoteID;
        _selectedNoteID = noteID;
        return context;
    };


    /* Behaviors */
    context.install = function(behavior) {
        context.surface().call(behavior);
    };
    context.uninstall = function(behavior) {
        context.surface().call(behavior.off);
    };


    /* Copy/Paste */
    var copyIDs = [], copyGraph;
    context.copyGraph = function() { return copyGraph; };
    context.copyIDs = function(_) {
        if (!arguments.length) return copyIDs;
        copyIDs = _;
        copyGraph = history.graph();
        return context;
    };


    /* Background */
    var background;
    context.background = function() { return background; };


    /* Features */
    var features;
    context.features = function() { return features; };
    context.hasHiddenConnections = function(id) {
        var graph = history.graph(),
            entity = graph.entity(id);
        return features.hasHiddenConnections(entity, graph);
    };


    /* Presets */
    var presets;
    context.presets = function() { return presets; };


    /* Map */
    var map;
    context.map = function() { return map; };
    context.layers = function() { return map.layers; };
    context.surface = function() { return map.surface; };
    context.editable = function() { return map.editable(); };
    context.surfaceRect = function() {
        return map.surface.node().getBoundingClientRect();
    };


    /* Debug */
    var debugFlags = {
        tile: false,        // tile boundaries
        collision: false,   // label collision bounding boxes
        imagery: false,     // imagery bounding polygons
        community: false,   // community bounding polygons
        imperial: false,    // imperial (not metric) bounding polygons
        driveLeft: false,   // driveLeft bounding polygons
        target: false       // touch targets
    };
    context.debugFlags = function() {
        return debugFlags;
    };
    context.setDebug = function(flag, val) {
        if (arguments.length === 1) val = true;
        debugFlags[flag] = val;
        dispatch.call('change');
        return context;
    };
    context.getDebug = function(flag) {
        return flag && debugFlags[flag];
    };


    /* Container */
    var container = d3_select(document.body);
    context.container = function(_) {
        if (!arguments.length) return container;
        container = _;
        container.classed('id-container', true);
        return context;
    };
    var embed;
    context.embed = function(_) {
        if (!arguments.length) return embed;
        embed = _;
        return context;
    };


    /* Assets */
    var assetPath = '';
    context.assetPath = function(_) {
        if (!arguments.length) return assetPath;
        assetPath = _;
        return context;
    };

    var assetMap = {};
    context.assetMap = function(_) {
        if (!arguments.length) return assetMap;
        assetMap = _;
        return context;
    };

    context.asset = function(_) {
        var filename = assetPath + _;
        return assetMap[filename] || filename;
    };

    context.imagePath = function(_) {
        return context.asset('img/' + _);
    };


    /* locales */
    // `locale` variable contains a "requested locale".
    // It won't become the `currentLocale` until after loadLocale() is called.
    var locale, localePath;

    context.locale = function(loc, path) {
        if (!arguments.length) return currentLocale;
        locale = loc;
        localePath = path;
        return context;
    };

    context.loadLocale = function(callback) {
        if (locale && locale !== 'en' && dataLocales.hasOwnProperty(locale)) {
            localePath = localePath || context.asset('locales/' + locale + '.json');
            d3_json(localePath, function(err, result) {
                if (!err) {
                    addTranslation(locale, result[locale]);
                    setLocale(locale);
                    utilDetect(true);
                }
                if (callback) {
                    callback(err);
                }
            });
        } else {
            if (locale) {
                setLocale(locale);
                utilDetect(true);
            }
            if (callback) {
                callback();
            }
        }
    };


    /* reset (aka flush) */
    context.reset = context.flush = function() {
        context.debouncedSave.cancel();
        _each(services, function(service) {
            if (service && typeof service.reset === 'function') {
                service.reset(context);
            }
        });
        features.reset();
        history.reset();
        return context;
    };


    /* Init */

    context.projection = geoRawMercator();
    context.curtainProjection = geoRawMercator();

    locale = utilDetect().locale;
    if (locale && !dataLocales.hasOwnProperty(locale)) {
        locale = locale.split('-')[0];
    }

    history = coreHistory(context);
    context.graph = history.graph;
    context.changes = history.changes;
    context.intersects = history.intersects;

    // Debounce save, since it's a synchronous localStorage write,
    // and history changes can happen frequently (e.g. when dragging).
    context.debouncedSave = _debounce(context.save, 350);
    function withDebouncedSave(fn) {
        return function() {
            var result = fn.apply(history, arguments);
            context.debouncedSave();
            return result;
        };
    }

    context.perform = withDebouncedSave(history.perform);
    context.replace = withDebouncedSave(history.replace);
    context.pop = withDebouncedSave(history.pop);
    context.overwrite = withDebouncedSave(history.overwrite);
    context.undo = withDebouncedSave(history.undo);
    context.redo = withDebouncedSave(history.redo);

    ui = uiInit(context);

    connection = services.osm;
    background = rendererBackground(context);
    features = rendererFeatures(context);
    presets = presetIndex();

    map = rendererMap(context);
    context.mouse = map.mouse;
    context.extent = map.extent;
    context.pan = map.pan;
    context.zoomIn = map.zoomIn;
    context.zoomOut = map.zoomOut;
    context.zoomInFurther = map.zoomInFurther;
    context.zoomOutFurther = map.zoomOutFurther;
    context.redrawEnable = map.redrawEnable;

    _each(services, function(service) {
        if (service && typeof service.init === 'function') {
            service.init(context);
        }
    });

    background.init();
    features.init();
    presets.init();
    areaKeys = presets.areaKeys();


    return utilRebind(context, dispatch, 'on');
}
Пример #7
0
import osmAuth from 'osm-auth';
import { JXON } from '../util/jxon';
import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
import { geoExtent } from '../geo';
import {
    osmEntity,
    osmNode,
    osmRelation,
    osmWay
} from '../osm';

import { utilRebind, utilIdleWorker } from '../util';


var dispatch = d3_dispatch('authLoading', 'authDone', 'change', 'loading', 'loaded'),
    urlroot = 'https://www.openstreetmap.org',
    blacklists = ['.*\.google(apis)?\..*/(vt|kh)[\?/].*([xyz]=.*){3}.*'],
    inflight = {},
    loadedTiles = {},
    entityCache = {},
    connectionId = 1,
    tileZoom = 16,
    oauth = osmAuth({
        url: urlroot,
        oauth_consumer_key: '5A043yRSEugj4DJ5TljuapfnrflWDte8jTOcWLlT',
        oauth_secret: 'aB3jKq1TRsCOUrfOIZ6oQMEDmv2ptV76PA54NGLL',
        loading: authLoading,
        done: authDone
    }),
    rateLimitError,
Пример #8
0
export function uiDisclosure() {
    var dispatch = d3_dispatch('toggled'),
        title,
        expanded = false,
        content = function () {};


    var disclosure = function(selection) {
        var hideToggle = selection.selectAll('.hide-toggle')
            .data([0]);

        hideToggle = hideToggle.enter()
            .append('a')
            .attr('href', '#')
            .attr('class', 'hide-toggle')
            .merge(hideToggle);

        hideToggle
            .text(title)
            .on('click', toggle)
            .classed('expanded', expanded);


        var wrap = selection.selectAll('div')
            .data([0]);

        wrap = wrap.enter()
            .append('div')
            .merge(wrap);

        wrap
            .classed('hide', !expanded)
            .call(content);


        function toggle() {
            expanded = !expanded;
            hideToggle.classed('expanded', expanded);
            wrap.call(uiToggle(expanded));
            dispatch.call('toggled', this, expanded);
        }
    };


    disclosure.title = function(_) {
        if (!arguments.length) return title;
        title = _;
        return disclosure;
    };


    disclosure.expanded = function(_) {
        if (!arguments.length) return expanded;
        expanded = _;
        return disclosure;
    };


    disclosure.content = function(_) {
        if (!arguments.length) return content;
        content = _;
        return disclosure;
    };


    return utilRebind(disclosure, dispatch, 'on');
}
Пример #9
0
export function uiFieldWikipedia(field, context) {
    var dispatch = d3_dispatch('change');
    var wikipedia = services.wikipedia;
    var wikidata = services.wikidata;
    var lang = d3_select(null);
    var title = d3_select(null);
    var _wikiURL = '';
    var _entity;

    var langCombo = uiCombobox(context, 'wikipedia-lang')
        .fetcher(function(value, cb) {
            var v = value.toLowerCase();

            cb(dataWikipedia.filter(function(d) {
                return d[0].toLowerCase().indexOf(v) >= 0 ||
                    d[1].toLowerCase().indexOf(v) >= 0 ||
                    d[2].toLowerCase().indexOf(v) >= 0;
            }).map(function(d) {
                return { value: d[1] };
            }));
        });

    var titleCombo = uiCombobox(context, 'wikipedia-title')
        .fetcher(function(value, cb) {
            if (!value && _entity) {
                value = context.entity(_entity.id).tags.name || '';
            }

            var searchfn = value.length > 7 ? wikipedia.search : wikipedia.suggestions;
            searchfn(language()[2], value, function(query, data) {
                cb(data.map(function(d) {
                    return { value: d };
                }));
            });
        });


    function wiki(selection) {
        var wrap = selection.selectAll('.form-field-input-wrap')
            .data([0]);

        wrap = wrap.enter()
            .append('div')
            .attr('class', 'form-field-input-wrap form-field-input-' + field.type)
            .merge(wrap);


        var langRow = wrap.selectAll('.wiki-lang-container')
            .data([0]);

        langRow = langRow.enter()
            .append('div')
            .attr('class', 'wiki-lang-container')
            .merge(langRow);


        lang = langRow.selectAll('input.wiki-lang')
            .data([0]);

        lang = lang.enter()
            .append('input')
            .attr('type', 'text')
            .attr('class', 'wiki-lang')
            .attr('placeholder', t('translate.localized_translation_language'))
            .call(utilNoAuto)
            .call(langCombo)
            .merge(lang);

        utilGetSetValue(lang, language()[1]);

        lang
            .on('blur', changeLang)
            .on('change', changeLang);


        var titleRow = wrap.selectAll('.wiki-title-container')
            .data([0]);

        titleRow = titleRow.enter()
            .append('div')
            .attr('class', 'wiki-title-container')
            .merge(titleRow);

        title = titleRow.selectAll('input.wiki-title')
            .data([0]);

        title = title.enter()
            .append('input')
            .attr('type', 'text')
            .attr('class', 'wiki-title')
            .attr('id', 'preset-input-' + field.safeid)
            .call(utilNoAuto)
            .call(titleCombo)
            .merge(title);

        title
            .on('blur', blur)
            .on('change', change);


        var link = titleRow.selectAll('.wiki-link')
            .data([0]);

        link = link.enter()
            .append('button')
            .attr('class', 'form-field-button wiki-link')
            .attr('tabindex', -1)
            .call(svgIcon('#iD-icon-out-link'))
            .merge(link);

        link
            .on('click', function() {
                d3_event.preventDefault();
                if (_wikiURL) window.open(_wikiURL, '_blank');
            });
    }


    function language() {
        var value = utilGetSetValue(lang).toLowerCase();
        var locale = utilDetect().locale.toLowerCase();
        var localeLanguage;
        return _find(dataWikipedia, function(d) {
            if (d[2] === locale) localeLanguage = d;
            return d[0].toLowerCase() === value ||
                d[1].toLowerCase() === value ||
                d[2] === value;
        }) || localeLanguage || ['English', 'English', 'en'];
    }


    function changeLang() {
        utilGetSetValue(lang, language()[1]);
        change(true);
    }


    function blur() {
        change(true);
    }


    function change(skipWikidata) {
        var value = utilGetSetValue(title);
        var m = value.match(/https?:\/\/([-a-z]+)\.wikipedia\.org\/(?:wiki|\1-[-a-z]+)\/([^#]+)(?:#(.+))?/);
        var l = m && _find(dataWikipedia, function(d) { return m[1] === d[2]; });
        var syncTags = {};

        if (l) {
            // Normalize title http://www.mediawiki.org/wiki/API:Query#Title_normalization
            value = decodeURIComponent(m[2]).replace(/_/g, ' ');
            if (m[3]) {
                var anchor;
                try {
                    // Best-effort `anchordecode:` implementation
                    anchor = decodeURIComponent(m[3].replace(/\.([0-9A-F]{2})/g, '%$1'));
                } catch (e) {
                    anchor = decodeURIComponent(m[3]);
                }
                value += '#' + anchor.replace(/_/g, ' ');
            }
            value = value.slice(0, 1).toUpperCase() + value.slice(1);
            utilGetSetValue(lang, l[1]);
            utilGetSetValue(title, value);
        }

        if (value) {
            syncTags.wikipedia = language()[2] + ':' + value;
        } else {
            syncTags.wikipedia = undefined;
            syncTags.wikidata = undefined;
        }

        dispatch.call('change', this, syncTags);


        if (skipWikidata || !value || !language()[2]) return;

        // attempt asynchronous update of wikidata tag..
        var initGraph = context.graph();
        var initEntityID = _entity.id;

        wikidata.itemsByTitle(language()[2], value, function(err, data) {
            if (err) return;

            // If graph has changed, we can't apply this update.
            if (context.graph() !== initGraph) return;

            if (!data || !Object.keys(data).length) return;

            var qids = Object.keys(data);
            var value = qids && _find(qids, function(id) { return id.match(/^Q\d+$/); });
            var currTags = _clone(context.entity(initEntityID).tags);

            currTags.wikidata = value;

            // Coalesce the update of wikidata tag into the previous tag change
            context.overwrite(
                actionChangeTags(initEntityID, currTags),
                context.history().undoAnnotation()
            );

            // do not dispatch.call('change') here, because entity_editor
            // changeTags() is not intended to be called asynchronously
        });
    }


    wiki.tags = function(tags) {
        var value = tags[field.key] || '';
        var m = value.match(/([^:]+):([^#]+)(?:#(.+))?/);
        var l = m && _find(dataWikipedia, function(d) { return m[1] === d[2]; });
        var anchor = m && m[3];

        // value in correct format
        if (l) {
            utilGetSetValue(lang, l[1]);
            utilGetSetValue(title, m[2] + (anchor ? ('#' + anchor) : ''));
            if (anchor) {
                try {
                    // Best-effort `anchorencode:` implementation
                    anchor = encodeURIComponent(anchor.replace(/ /g, '_')).replace(/%/g, '.');
                } catch (e) {
                    anchor = anchor.replace(/ /g, '_');
                }
            }
            _wikiURL = 'https://' + m[1] + '.wikipedia.org/wiki/' +
                m[2].replace(/ /g, '_') + (anchor ? ('#' + anchor) : '');

        // unrecognized value format
        } else {
            utilGetSetValue(title, value);
            if (value && value !== '') {
                utilGetSetValue(lang, '');
                _wikiURL = 'https://en.wikipedia.org/wiki/Special:Search?search=' + value;
            } else {
                _wikiURL = '';
            }
        }
    };


    wiki.entity = function(val) {
        if (!arguments.length) return _entity;
        _entity = val;
        return wiki;
    };


    wiki.focus = function() {
        title.node().focus();
    };


    return utilRebind(wiki, dispatch, 'on');
}
Пример #10
0
export function rendererBackground(context) {
    var dispatch = d3_dispatch('change'),
        baseLayer = rendererTileLayer(context).projection(context.projection),
        overlayLayers = [],
        backgroundSources;


    function background(selection) {
        var base = selection.selectAll('.layer-background')
            .data([0]);

        base.enter()
            .insert('div', '.layer-data')
            .attr('class', 'layer layer-background')
            .merge(base)
            .call(baseLayer);

        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() {
        if (context.inIntro()) return;

        var b = background.baseLayerSource(),
            o = overlayLayers
                .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); })
                .map(function (d) { return d.source().id; })
                .join(','),
            meters = geoOffsetToMeters(b.offset()),
            epsilon = 0.01,
            x = +meters[0].toFixed(2),
            y = +meters[1].toFixed(2),
            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 = [b.imageryUsed()];

        overlayLayers
            .filter(function (d) { return !d.source().isLocatorOverlay() && !d.source().isHidden(); })
            .forEach(function (d) { imageryUsed.push(d.source().imageryUsed()); });

        var gpx = context.layers().layer('gpx');
        if (gpx && gpx.enabled() && gpx.hasGpx()) {
            // Include a string like '.gpx data file' or '.geojson data file'
            var match = gpx.getSrc().match(/(kml|gpx|(?:geo)?json)$/i);
            var extension = match ? ('.' + match[0].toLowerCase() + ' ') : '';
            imageryUsed.push(extension + 'data file');
        }

        var mapillary_images = context.layers().layer('mapillary-images');
        if (mapillary_images && mapillary_images.enabled()) {
            imageryUsed.push('Mapillary Images');
        }

        var mapillary_signs = context.layers().layer('mapillary-signs');
        if (mapillary_signs && mapillary_signs.enabled()) {
            imageryUsed.push('Mapillary Signs');
        }

        var openstreetcam_images = context.layers().layer('openstreetcam-images');
        if (openstreetcam_images && openstreetcam_images.enabled()) {
            imageryUsed.push('OpenStreetCam Images');
        }

        context.history().imageryUsed(imageryUsed);
    };


    background.sources = function(extent) {
        return backgroundSources.filter(function(source) {
            return source.intersects(extent);
        });
    };


    background.dimensions = function(_) {
        if (!_) return;
        baseLayer.dimensions(_);

        overlayLayers.forEach(function(layer) {
            layer.dimensions(_);
        });
    };


    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(),
            fail = false,
            tested = 0,
            regex, i;

        for (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 _find(backgroundSources, function(d) {
            return d.id && d.id === id;
        });
    };


    background.bing = function() {
        background.baseLayerSource(background.findSource('Bing'));
    };


    background.showsLayer = function(d) {
        return d.id === baseLayer.source().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.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 dataImagery = data.imagery || [],
            q = utilStringQs(window.location.hash.substring(1)),
            requested = q.background || q.layer,
            extent = parseMap(q.map),
            first,
            best;

        // Add all the available imagery sources
        backgroundSources = dataImagery.map(function(source) {
            if (source.type === 'bing') {
                return rendererBackgroundSource.Bing(source, dispatch);
            } else if (source.id === 'EsriWorldImagery') {
                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 = _find(this.sources(extent), 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('Bing') ||
                first ||
                background.findSource('none')
            );
        }

        var locator = _find(backgroundSources, 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('gpx');
            if (gpx) {
                gpx.url(q.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');
}
Пример #11
0
    return function module() {

        let margin = {
                top: 70,
                right: 30,
                bottom: 60,
                left: 70
            },
            width = 960,
            height = 500,

            xScale, xAxis, xMonthAxis,
            yScale, yAxis,

            aspectRatio = null,

            monthAxisPadding = 30,
            verticalTicks = 5,
            yTickTextYOffset = -8,
            yTickTextXOffset = -20,
            tickPadding = 5,

            colorSchema = colorHelper.colorSchemas.britechartsColorSchema,

            areaOpacity = 0.64,
            categoryColorMap,
            order,

            forceAxisSettings = null,
            forcedXTicks = null,
            forcedXFormat = null,
            locale,

            baseLine,

            layers,
            layersInitial,
            area,

            // Area Animation
            maxAreaNumber = 8,
            areaAnimationDelayStep = 20,
            areaAnimationDelays = d3Array.range(areaAnimationDelayStep, maxAreaNumber* areaAnimationDelayStep, areaAnimationDelayStep),

            overlay,

            verticalMarkerContainer,
            verticalMarker,
            epsilon,

            dataPoints            = {},
            pointsSize            = 1.5,
            pointsColor           = '#c0c6cc',
            pointsBorderColor     = '#ffffff',

            isAnimated = false,
            ease = d3Ease.easeQuadInOut,
            areaAnimationDuration = 1000,

            svg,
            chartWidth, chartHeight,
            data,
            dataByDate,
            dataByDateFormatted,
            dataByDateZeroed,

            verticalGridLines,
            horizontalGridLines,
            grid = null,

            tooltipThreshold = 480,

            xAxisPadding = {
                top: 0,
                left: 15,
                bottom: 0,
                right: 0
            },

            dateLabel = 'date',
            valueLabel = 'value',
            keyLabel = 'name',

            // getters
            getName = ({name}) => name,
            getDate = ({date}) => date,

            // events
            dispatcher = d3Dispatch.dispatch('customMouseOver', 'customMouseOut', 'customMouseMove');

       /**
         * This function creates the graph using the selection and data provided
         * @param {D3Selection} _selection A d3 selection that represents
         * the container(s) where the chart(s) will be rendered
         * @param {areaChartData} _data The data to attach and generate the chart
         */
        function exports(_selection) {
            _selection.each(function(_data) {
                chartWidth = width - margin.left - margin.right;
                chartHeight = height - margin.top - margin.bottom;
                data = cleanData(_data);
                dataByDate = getDataByDate(data);

                buildLayers();
                buildScales();
                buildSVG(this);
                buildAxis();
                drawAxis();
                drawStackedAreas();

                if(shouldShowTooltip()) {
                    drawHoverOverlay();
                    drawVerticalMarker();
                    addMouseEvents();
                }
            });
        }

        /**
         * Adds events to the container group if the environment is not mobile
         * Adding: mouseover, mouseout and mousemove
         */
        function addMouseEvents() {
            svg
                .on('mouseover', handleMouseOver)
                .on('mouseout', handleMouseOut)
                .on('mousemove', handleMouseMove);
        }

        /**
         * Formats the value depending on its characteristics
         * @param  {Number} value Value to format
         * @return {Number}       Formatted value
         */
        function getFormattedValue(value) {
            let format;

            if (isInteger(value)) {
                format = formatIntegerValue;
            } else {
                format = formatDecimalValue;
            }

            return format(value);
        }

        /**
         * Creates the d3 x and y axis, setting orientations
         * @private
         */
        function buildAxis() {
            let dataSpan = yScale.domain()[1] - yScale.domain()[0];
            let yTickNumber = dataSpan < verticalTicks - 1 ? dataSpan : verticalTicks;
            let minor, major;

            if (forceAxisSettings === 'custom' && typeof forcedXFormat === 'string') {
                minor = {
                    tick: forcedXTicks,
                    format:  d3TimeFormat.timeFormat(forcedXFormat)
                };
                major = null;
            } else {
                ({minor, major} = getXAxisSettings(dataByDate, width, forceAxisSettings, locale));

                xMonthAxis = d3Axis.axisBottom(xScale)
                    .ticks(major.tick)
                    .tickSize(0, 0)
                    .tickFormat(major.format);
            }

            xAxis = d3Axis.axisBottom(xScale)
                .ticks(minor.tick)
                .tickSize(10, 0)
                .tickPadding(tickPadding)
                .tickFormat(minor.format);


            yAxis = d3Axis.axisRight(yScale)
                .ticks(yTickNumber)
                .tickSize([0])
                .tickPadding(tickPadding)
                .tickFormat(getFormattedValue);

            drawGridLines(minor.tick, yTickNumber);
        }

        /**
         * Builds containers for the chart, the axis and a wrapper for all of them
         * NOTE: The order of drawing of this group elements is really important,
         * as everything else will be drawn on top of them
         * @private
         */
        function buildContainerGroups() {
            let container = svg
              .append('g')
                .classed('container-group', true)
                .attr('transform', `translate(${margin.left},${margin.top})`);

            container
              .append('g').classed('x-axis-group', true)
              .append('g').classed('x axis', true);
            container.selectAll('.x-axis-group')
              .append('g').classed('month-axis', true);
            container
              .append('g').classed('y-axis-group axis', true);
            container
              .append('g').classed('grid-lines-group', true);
            container
              .append('g').classed('chart-group', true);
            container
              .append('g').classed('metadata-group', true);
        }

        /**
         * Builds the stacked layers layout
         * @return {D3Layout} Layout for drawing the chart
         * @private
         */
        function buildLayers() {
            dataByDateFormatted = dataByDate
                .map(d => assign({}, d, d.values))
                .map(d => {
                    Object.keys(d).forEach(k => {
                        const entry = d[k];

                        if (entry && entry.name) {
                            d[entry.name] = entry.value;
                        }
                    });

                    return assign({}, d, {
                        date: new Date(d['key'])
                    });
                });

            dataByDateZeroed = dataByDate
                .map(d => assign({}, d, d.values))
                .map(d => {
                    Object.keys(d).forEach(k => {
                        const entry = d[k];

                        if (entry && entry.name) {
                            d[entry.name] = 0;
                        }
                    });

                    return assign({}, d, {
                        date: new Date(d['key'])
                    });
                });

            let initialTotalsObject = uniq(data.map(({name}) => name))
                                        .reduce((memo, key) => (
                                            assign({}, memo, {[key]: 0})
                                        ), {});

            let totals = data.reduce((memo, item) => (
                assign({}, memo, {[item.name]: memo[item.name]  += item.value})
            ), initialTotalsObject);

            order = formatOrder(totals);

            let stack3 = d3Shape.stack()
                .keys(order)
                .order(d3Shape.stackOrderNone)
                .offset(d3Shape.stackOffsetNone);

            layersInitial = stack3(dataByDateZeroed);
            layers = stack3(dataByDateFormatted);
        }

        /**
         * Takes an object with all topics as keys and their aggregate totals as values,
         * sorts them into a list by descending total value and
         * moves "Other" to the end
         * @param  {Object} totals  Keys of all the topics and their corresponding totals
         * @return {Array}          List of topic names in aggregate order
         */
        function formatOrder(totals) {
            let order = Object.keys(totals)
                .sort((a, b) => {
                    if (totals[a] > totals[b]) return -1;
                    if (totals[a] === totals[b]) return 0;
                    return 1;
                });

            let otherIndex = order.indexOf('Other');

            if (otherIndex >= 0) {
                let other = order.splice(otherIndex, 1);

                order = order.concat(other);
            }

            return order;
        }

        /**
         * Creates the x, y and color scales of the chart
         * @private
         */
        function buildScales() {
            xScale = d3Scale.scaleTime()
                .domain(d3Array.extent(dataByDate, ({date}) => date))
                .rangeRound([0, chartWidth]);

            yScale = d3Scale.scaleLinear()
                .domain([0, getMaxValueByDate()])
                .rangeRound([chartHeight, 0])
                .nice();

            categoryColorMap =  order.reduce((memo, topic, index) => (
                assign({}, memo, {[topic]: colorSchema[index]})
            ), {});
        }

        /**
         * @param  {HTMLElement} container DOM element that will work as the container of the graph
         * @private
         */
        function buildSVG(container) {
            if (!svg) {
                svg = d3Selection.select(container)
                    .append('svg')
                    .classed('britechart stacked-area', true);

                buildContainerGroups();
            }

            svg
                .attr('width', width)
                .attr('height', height);
        }

        /**
         * Parses dates and values into JS Date objects and numbers
         * @param  {obj} data Raw data from JSON file
         * @return {obj}      Parsed data with values and dates
         */
        function cleanData(data) {
            return data.map((d) => {
                d.date = new Date(d[dateLabel]),
                d.value = +d[valueLabel]

                return d;
            });
        }

        /**
         * Draws the x and y axis on the svg object within their
         * respective groups
         * @private
         */
        function drawAxis() {
            svg.select('.x-axis-group .axis.x')
                .attr('transform', `translate( 0, ${chartHeight} )`)
                .call(xAxis);

            if (forceAxisSettings !== 'custom') {
                svg.select('.x-axis-group .month-axis')
                    .attr('transform', `translate(0, ${(chartHeight + monthAxisPadding)})`)
                    .call(xMonthAxis);
            }

            svg.select('.y-axis-group.axis')
                .attr('transform', `translate( ${-xAxisPadding.left}, 0)`)
                .call(yAxis)
                .call(adjustYTickLabels);

            // Moving the YAxis tick labels to the right side
            // d3Selection.selectAll('.y-axis-group .tick text')
            //     .attr('transform', `translate( ${-chartWidth - yTickTextXOffset}, ${yTickTextYOffset})` );
        }

        /**
         * Adjusts the position of the y axis' ticks
         * @param  {D3Selection} selection Y axis group
         * @return void
         */
        function adjustYTickLabels(selection) {
            selection.selectAll('.tick text')
                .attr('transform', `translate(${yTickTextXOffset}, ${yTickTextYOffset})`);
        }

        /**
         * Creates SVG dot elements for each data entry and draws them
         * TODO: Plug
         */
        function drawDataReferencePoints() {
            // Creates Dots on Data points
            var points = svg.select('.chart-group').selectAll('.dots')
                .data(layers)
              .enter().append('g')
                .attr('class', 'dots')
                .attr('d', ({values}) => area(values))
                .attr('clip-path', 'url(#clip)');

            // Processes the points
            // TODO: Optimize this code
            points.selectAll('.dot')
                .data(({values}, index) => values.map((point) => ({index, point})))
                .enter()
                .append('circle')
                .attr('class','dot')
                .attr('r', () => pointsSize)
                .attr('fill', () => pointsColor)
                .attr('stroke-width', '0')
                .attr('stroke', pointsBorderColor)
                .attr('transform', function(d) {
                    let {point} = d;
                    let key = xScale(point.date);

                    dataPoints[key] = dataPoints[key] || [];
                    dataPoints[key].push(d);

                    let {date, y, y0} = point;
                    return `translate( ${xScale(date)}, ${yScale(y + y0)} )`;
                });
        }

        /**
         * Draws grid lines on the background of the chart
         * @return void
         */
        function drawGridLines(xTicks, yTicks) {
            if (grid === 'horizontal' || grid === 'full') {
                horizontalGridLines = svg.select('.grid-lines-group')
                    .selectAll('line.horizontal-grid-line')
                    .data(yScale.ticks(yTicks))
                    .enter()
                        .append('line')
                        .attr('class', 'horizontal-grid-line')
                        .attr('x1', (-xAxisPadding.left - 30))
                        .attr('x2', chartWidth)
                        .attr('y1', (d) => yScale(d))
                        .attr('y2', (d) => yScale(d));
            }

            if (grid === 'vertical' || grid === 'full') {
                verticalGridLines = svg.select('.grid-lines-group')
                    .selectAll('line.vertical-grid-line')
                    .data(xScale.ticks(xTicks))
                    .enter()
                        .append('line')
                        .attr('class', 'vertical-grid-line')
                        .attr('y1', 0)
                        .attr('y2', chartHeight)
                        .attr('x1', (d) => xScale(d))
                        .attr('x2', (d) => xScale(d));
            }

            //draw a horizontal line to extend x-axis till the edges
            baseLine = svg.select('.grid-lines-group')
                .selectAll('line.extended-x-line')
                .data([0])
                .enter()
              .append('line')
                .attr('class', 'extended-x-line')
                .attr('x1', (-xAxisPadding.left - 30))
                .attr('x2', chartWidth)
                .attr('y1', height - margin.bottom - margin.top)
                .attr('y2', height - margin.bottom - margin.top);
        }

        /**
         * Draws an overlay element over the graph
         * @private
         */
        function drawHoverOverlay() {
            overlay = svg.select('.metadata-group')
                .append('rect')
                .attr('class', 'overlay')
                .attr('y1', 0)
                .attr('y2', chartHeight)
                .attr('height', chartHeight)
                .attr('width', chartWidth)
                .attr('fill', 'rgba(0,0,0,0)')
                .style('display', 'none');
        }

        /**
         * Draws the different areas into the chart-group element
         * @private
         */
        function drawStackedAreas() {
            let series;

            area = d3Shape.area()
                .curve(d3Shape.curveMonotoneX)
                .x( ({data}) => xScale(data.date) )
                .y0( (d) => yScale(d[0]) )
                .y1( (d) => yScale(d[1]) );

            if (isAnimated) {
                series = svg.select('.chart-group').selectAll('.layer')
                    .data(layersInitial)
                    .enter()
                  .append('g')
                    .classed('layer-container', true);

                series
                  .append('path')
                    .attr('class', 'layer')
                    .attr('d', area)
                    .style('fill', ({key}) => categoryColorMap[key]);

                // Update
                svg.select('.chart-group').selectAll('.layer')
                    .data(layers)
                    .transition()
                    .delay( (_, i) => areaAnimationDelays[i])
                    .duration(areaAnimationDuration)
                    .ease(ease)
                    .attr('d', area)
                    .style('opacity', areaOpacity)
                    .style('fill', ({key}) => categoryColorMap[key]);
            } else {
                series = svg.select('.chart-group').selectAll('.layer')
                    .data(layers)
                    .enter()
                  .append('g')
                    .classed('layer-container', true);

                series
                  .append('path')
                    .attr('class', 'layer')
                    .attr('d', area)
                    .style('fill', ({key}) => categoryColorMap[key]);

                // Update
                series
                    .attr('d', area)
                    .style('opacity', areaOpacity)
                    .style('fill', ({key}) => categoryColorMap[key]);
            }

            // Exit
            series.exit()
                .transition()
                .style('opacity', 0)
                .remove();
        }

        /**
         * Creates the vertical marker
         * @return void
         */
        function drawVerticalMarker() {
            verticalMarkerContainer = svg.select('.metadata-group')
                .append('g')
                .attr('class', 'vertical-marker-container')
                .attr('transform', 'translate(9999, 0)');

            verticalMarker = verticalMarkerContainer.selectAll('path')
                .data([{
                    x1: 0,
                    y1: 0,
                    x2: 0,
                    y2: 0
                }])
                .enter()
              .append('line')
                .classed('vertical-marker', true)
                .attr('x1', 0)
                .attr('y1', chartHeight)
                .attr('x2', 0)
                .attr('y2', 0);
        }

        /**
         * Removes all the datapoints highlighter circles added to the marker container
         * @return void
         */
        function eraseDataPointHighlights() {
            verticalMarkerContainer.selectAll('.circle-container').remove();
        }

        /**
         * Orders the data by date for consumption on the chart tooltip
         * @param  {areaChartData} data    Chart data
         * @return {Object[]}               Chart data ordered by date
         * @private
         */
        function getDataByDate(data) {
            return d3Collection.nest()
                                .key(getDate)
                                .entries(
                                    data.sort((a, b) => a.date - b.date)
                                )
                                .map(d => {
                                    return assign({}, d, {
                                        date: new Date(d.key)
                                    });
                                });

            // let b =  d3Collection.nest()
            //                     .key(getDate).sortKeys(d3Array.ascending)
            //                     .entries(data);
        }

        /**
         * Computes the maximum sum of values for any date
         *
         * @return {Number} Max value
         */
        function getMaxValueByDate() {
            let keys = uniq(data.map(o => o.name));
            let maxValueByDate = d3Array.max(dataByDateFormatted, function(d){
                let vals = keys.map((key) => d[key]);

                return d3Array.sum(vals);
            });

            return maxValueByDate;
        }

        /**
         * Extract X position on the chart from a given mouse event
         * @param  {obj} event D3 mouse event
         * @return {Number}       Position on the x axis of the mouse
         * @private
         */
        function getMouseXPosition(event) {
            return d3Selection.mouse(event)[0];
        }

        /**
         * Finds out the data entry that is closer to the given position on pixels
         * @param  {Number} mouseX X position of the mouse
         * @return {obj}        Data entry that is closer to that x axis position
         */
        function getNearestDataPoint(mouseX) {
            let points = dataByDate.filter(({date}) => Math.abs(xScale(date) - mouseX) <= epsilon);

            if (points.length) {
                return points[0];
            }
        }

        /**
         * Epsilon is the value given to the number representing half of the distance in
         * pixels between two date data points
         * @return {Number} half distance between any two points
         */
        function setEpsilon() {
            let dates = dataByDate.map(({date}) => date);

            epsilon = (xScale(dates[1]) - xScale(dates[0])) / 2;
        }

        /**
         * MouseMove handler, calculates the nearest dataPoint to the cursor
         * and updates metadata related to it
         * @private
         */
        function handleMouseMove() {
            epsilon || setEpsilon();

            let dataPoint = getNearestDataPoint(getMouseXPosition(this) - margin.left),
                dataPointXPosition;

            if(dataPoint) {
                dataPointXPosition = xScale(new Date( dataPoint.key ));
                // Move verticalMarker to that datapoint
                moveVerticalMarker(dataPointXPosition);
                // Add data points highlighting
                highlightDataPoints(dataPoint);
                // Emit event with xPosition for tooltip or similar feature
                dispatcher.call('customMouseMove', this, dataPoint, categoryColorMap, dataPointXPosition);
            }
        }

        /**
         * MouseOut handler, hides overlay and removes active class on verticalMarkerLine
         * It also resets the container of the vertical marker
         * @private
         */
        function handleMouseOut(data) {
            overlay.style('display', 'none');
            verticalMarker.classed('bc-is-active', false);
            verticalMarkerContainer.attr('transform', 'translate(9999, 0)');

            dispatcher.call('customMouseOut', this, data);
        }

        /**
         * Mouseover handler, shows overlay and adds active class to verticalMarkerLine
         * @private
         */
        function handleMouseOver(data) {
            overlay.style('display', 'block');
            verticalMarker.classed('bc-is-active', true);

            dispatcher.call('customMouseOver', this, data);
        }

        /**
         * Creates coloured circles marking where the exact data y value is for a given data point
         * @param  {obj} dataPoint Data point to extract info from
         * @private
         */
        function highlightDataPoints({values}) {
            let accumulator = 0;

            eraseDataPointHighlights();

            // ensure order stays constant
            values = values
                        .filter(v => !!v)
                        .sort((a,b) => order.indexOf(a.name) > order.indexOf(b.name))

            values.forEach(({name}, index) => {
                let marker = verticalMarkerContainer
                                .append('g')
                                .classed('circle-container', true),
                    circleSize = 12;

                accumulator = accumulator + values[index][valueLabel];

                marker.append('circle')
                    .classed('data-point-highlighter', true)
                    .attr('cx', circleSize)
                    .attr('cy', 0)
                    .attr('r', 5)
                    .style('stroke-width', 2)
                    .style('stroke', categoryColorMap[name]);

                marker.attr('transform', `translate( ${(- circleSize)}, ${(yScale(accumulator))} )` );
            });
        }

        /**
         * Helper method to update the x position of the vertical marker
         * @param  {obj} dataPoint Data entry to extract info
         * @return void
         */
        function moveVerticalMarker(verticalMarkerXPosition) {
            verticalMarkerContainer.attr('transform', `translate(${verticalMarkerXPosition},0)`);
        }

        /**
         * Determines if we should add the tooltip related logic depending on the
         * size of the chart and the tooltipThreshold variable value
         * @return {boolean} Should we build the tooltip?
         * @private
         */
        function shouldShowTooltip() {
            return width > tooltipThreshold;
        }

        // Accessors

        /**
         * Gets or Sets the opacity of the stacked areas in the chart (all of them will have the same opacity)
         * @param  {Object} _x                  Opacity to get/set
         * @return { opacity | module}          Current opacity or Area Chart module to chain calls
         * @public
         */
        exports.areaOpacity = function(_x) {
            if (!arguments.length) {
                return areaOpacity;
            }
            areaOpacity = _x;

            return this;
        };

        /**
         * Gets or Sets the aspect ratio of the chart
         * @param  {Number} _x Desired aspect ratio for the graph
         * @return { (Number | Module) } Current aspect ratio or Area Chart module to chain calls
         * @public
         */
        exports.aspectRatio = function(_x) {
            if (!arguments.length) {
                return aspectRatio;
            }
            aspectRatio = _x;

            return this;
        };

        /**
         * Gets or Sets the colorSchema of the chart
         * @param  {String[]} _x Desired colorSchema for the graph
         * @return { colorSchema | module} Current colorSchema or Chart module to chain calls
         * @public
         */
        exports.colorSchema = function(_x) {
            if (!arguments.length) {
                return colorSchema;
            }
            colorSchema = _x;

            return this;
        };

        /**
         * Gets or Sets the dateLabel of the chart
         * @param  {Number} _x Desired dateLabel for the graph
         * @return { dateLabel | module} Current dateLabel or Chart module to chain calls
         * @public
         */
        exports.dateLabel = function(_x) {
            if (!arguments.length) {
                return dateLabel;
            }
            dateLabel = _x;

            return this;
        };

        /**
         * Exposes the ability to force the chart to show a certain x axis grouping
         * @param  {String} _x Desired format
         * @return { (String|Module) }    Current format or module to chain calls
         * @example
         *     area.forceAxisFormat(area.axisTimeCombinations.HOUR_DAY)
         */
        exports.forceAxisFormat = function(_x) {
            if (!arguments.length) {
              return forceAxisSettings;
            }
            forceAxisSettings = _x;

            return this;
        };

        /**
         * Exposes the ability to force the chart to show a certain x format
         * It requires a `forceAxisFormat` of 'custom' in order to work.
         * NOTE: localization not supported
         * @param  {String} _x              Desired format for x axis
         * @return { (String|Module) }      Current format or module to chain calls
         */
        exports.forcedXFormat = function(_x) {
            if (!arguments.length) {
              return forcedXFormat;
            }
            forcedXFormat = _x;

            return this;
        };

        /**
         * Exposes the ability to force the chart to show a certain x ticks. It requires a `forceAxisFormat` of 'custom' in order to work.
         * NOTE: This value needs to be a multiple of 2, 5 or 10. They won't always work as expected, as D3 decides at the end
         * how many and where the ticks will appear.
         *
         * @param  {Number} _x              Desired number of x axis ticks (multiple of 2, 5 or 10)
         * @return { (Number|Module) }      Current number or ticks or module to chain calls
         */
        exports.forcedXTicks = function(_x) {
            if (!arguments.length) {
              return forcedXTicks;
            }
            forcedXTicks = _x;

            return this;
        };

        /**
         * Gets or Sets the grid mode.
         *
         * @param  {String} _x Desired mode for the grid ('vertical'|'horizontal'|'full')
         * @return { String | module} Current mode of the grid or Area Chart module to chain calls
         * @public
         */
        exports.grid = function(_x) {
            if (!arguments.length) {
                return grid;
            }
            grid = _x;

            return this;
        };

        /**
         * Gets or Sets the height of the chart
         * @param  {Number} _x Desired width for the graph
         * @return { height | module} Current height or Area Chart module to chain calls
         * @public
         */
        exports.height = function(_x) {
            if (!arguments.length) {
                return height;
            }
            if (aspectRatio) {
                width = Math.ceil(_x / aspectRatio);
            }
            height = _x;

            return this;
        };

        /**
         * Gets or Sets the isAnimated property of the chart, making it to animate when render.
         * By default this is 'false'
         *
         * @param  {Boolean} _x Desired animation flag
         * @return { isAnimated | module} Current isAnimated flag or Chart module
         * @public
         */
        exports.isAnimated = function(_x) {
            if (!arguments.length) {
                return isAnimated;
            }
            isAnimated = _x;

            return this;
        };

        /**
         * Gets or Sets the keyLabel of the chart
         * @param  {Number} _x Desired keyLabel for the graph
         * @return { keyLabel | module} Current keyLabel or Chart module to chain calls
         * @public
         */
        exports.keyLabel = function(_x) {
            if (!arguments.length) {
                return keyLabel;
            }
            keyLabel = _x;

            return this;
        };

        /**
         * Gets or Sets the margin of the chart
         * @param  {Object} _x Margin object to get/set
         * @return { margin | module} Current margin or Area Chart module to chain calls
         * @public
         */
        exports.margin = function(_x) {
            if (!arguments.length) {
                return margin;
            }
            margin = _x;

            return this;
        };

        /**
         * Gets or Sets the minimum width of the graph in order to show the tooltip
         * NOTE: This could also depend on the aspect ratio
         *
         * @param  {Object} _x Margin object to get/set
         * @return { tooltipThreshold | module} Current tooltipThreshold or Area Chart module to chain calls
         * @public
         */
        exports.tooltipThreshold = function(_x) {
            if (!arguments.length) {
                return tooltipThreshold;
            }
            tooltipThreshold = _x;

            return this;
        };

        /**
         * Gets or Sets the valueLabel of the chart
         * @param  {Number} _x Desired valueLabel for the graph
         * @return { valueLabel | module} Current valueLabel or Chart module to chain calls
         * @public
         */
        exports.valueLabel = function(_x) {
            if (!arguments.length) {
                return valueLabel;
            }
            valueLabel = _x;

            return this;
        };

        /**
         * Gets or Sets the number of verticalTicks of the yAxis on the chart
         * @param  {Number} _x Desired verticalTicks
         * @return { verticalTicks | module} Current verticalTicks or Chart module to chain calls
         * @public
         */
        exports.verticalTicks = function(_x) {
            if (!arguments.length) {
                return verticalTicks;
            }
            verticalTicks = _x;

            return this;
        };

        /**
         * Gets or Sets the width of the chart
         * @param  {Number} _x Desired width for the graph
         * @return { width | module} Current width or Area Chart module to chain calls
         * @public
         */
        exports.width = function(_x) {
            if (!arguments.length) {
                return width;
            }
            if (aspectRatio) {
                height = Math.ceil(_x * aspectRatio);
            }
            width = _x;

            return this;
        };

        /**
         * Pass language tag for the tooltip to localize the date.
         * Feature uses Intl.DateTimeFormat, for compatability and support, refer to
         * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
         * @param  {String} _x  must be a language tag (BCP 47) like 'en-US' or 'fr-FR'
         * @return { (String|Module) }    Current locale or module to chain calls
         */
        exports.locale = function(_x) {
            if (!arguments.length) {
                return locale;
            }
            locale = _x;

            return this;
        };

        /**
         * Chart exported to png and a download action is fired
         * @public
         */
        exports.exportChart = function(filename, title) {
            exportChart.call(exports, svg, filename, title);
        };

        /**
         * Exposes an 'on' method that acts as a bridge with the event dispatcher
         * We are going to expose this events:
         * customMouseOver, customMouseMove and customMouseOut
         *
         * @return {module} Bar Chart
         * @public
         */
        exports.on = function() {
            let value = dispatcher.on.apply(dispatcher, arguments);

            return value === dispatcher ? exports : value;
        };

        /**
         * Exposes the constants to be used to force the x axis to respect a certain granularity
         * current options: MINUTE_HOUR, HOUR_DAY, DAY_MONTH, MONTH_YEAR
         * @example
         *     area.forceAxisFormat(area.axisTimeCombinations.HOUR_DAY)
         */
        exports.axisTimeCombinations = axisTimeCombinations;

        return exports;
    };
Пример #12
0
export function uiFieldMaxspeed(field, context) {
    var dispatch = d3_dispatch('change');
    var unitInput = d3_select(null);
    var input = d3_select(null);
    var combobox;
    var _entity;
    var _isImperial;

    var metricValues = [20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120];
    var imperialValues = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80];


    function maxspeed(selection) {
        combobox = d3_combobox()
            .container(context.container());

        var unitCombobox = d3_combobox()
            .container(context.container())
            .data(['km/h', 'mph'].map(comboValues));

        var wrap = selection.selectAll('.form-field-input-wrap')
            .data([0]);

        wrap = wrap.enter()
            .append('div')
            .attr('class', 'form-field-input-wrap form-field-input-' + field.type)
            .merge(wrap);


        input = wrap.selectAll('#preset-input-' + field.safeid)
            .data([0]);

        input = input.enter()
            .append('input')
            .attr('type', 'text')
            .attr('id', 'preset-input-' + field.safeid)
            .attr('placeholder', field.placeholder())
            .call(utilNoAuto)
            .call(combobox)
            .merge(input);

        input
            .on('change', change)
            .on('blur', change);

        var loc;
        if (_entity.type === 'node') {
            loc = _entity.loc;
        } else {
            var childNodes = context.graph().childNodes(context.entity(_entity.id));
            loc = childNodes[~~(childNodes.length/2)].loc;
        }

        _isImperial = _some(dataImperial.features, function(f) {
            return _some(f.geometry.coordinates, function(d) {
                return geoPointInPolygon(loc, d);
            });
        });

        unitInput = wrap.selectAll('input.maxspeed-unit')
            .data([0]);

        unitInput = unitInput.enter()
            .append('input')
            .attr('type', 'text')
            .attr('class', 'maxspeed-unit')
            .call(unitCombobox)
            .merge(unitInput);

        unitInput
            .on('blur', changeUnits)
            .on('change', changeUnits);


        function changeUnits() {
            _isImperial = utilGetSetValue(unitInput) === 'mph';
            utilGetSetValue(unitInput, _isImperial ? 'mph' : 'km/h');
            setSuggestions();
            change();
        }
    }


    function setSuggestions() {
        combobox.data((_isImperial ? imperialValues : metricValues).map(comboValues));
        utilGetSetValue(unitInput, _isImperial ? 'mph' : 'km/h');
    }


    function comboValues(d) {
        return {
            value: d.toString(),
            title: d.toString()
        };
    }


    function change() {
        var tag = {};
        var value = utilGetSetValue(input);

        if (!value) {
            tag[field.key] = undefined;
        } else if (isNaN(value) || !_isImperial) {
            tag[field.key] = value;
        } else {
            tag[field.key] = value + ' mph';
        }

        dispatch.call('change', this, tag);
    }


    maxspeed.tags = function(tags) {
        var value = tags[field.key];

        if (value && value.indexOf('mph') >= 0) {
            value = parseInt(value, 10);
            _isImperial = true;
        } else if (value) {
            _isImperial = false;
        }

        setSuggestions();
        utilGetSetValue(input, value || '');
    };


    maxspeed.focus = function() {
        input.node().focus();
    };


    maxspeed.entity = function(val) {
        _entity = val;
    };


    return utilRebind(maxspeed, dispatch, 'on');
}
Пример #13
0
export function uiFieldCombo(field, context) {
    var dispatch = d3_dispatch('change');
    var nominatim = services.geocoder;
    var taginfo = services.taginfo;
    var isMulti = (field.type === 'multiCombo');
    var isNetwork = (field.type === 'networkCombo');
    var isSemi = (field.type === 'semiCombo');
    var optstrings = field.strings && field.strings.options;
    var optarray = field.options;
    var snake_case = (field.snake_case || (field.snake_case === undefined));
    var caseSensitive = field.caseSensitive;
    var combobox = uiCombobox(context, 'combo-' + field.safeid)
        .caseSensitive(caseSensitive)
        .minItems(isMulti || isSemi ? 1 : 2);
    var container = d3_select(null);
    var inputWrap = d3_select(null);
    var input = d3_select(null);
    var _comboData = [];
    var _multiData = [];
    var _entity;
    var _country;

    // ensure multiCombo field.key ends with a ':'
    if (isMulti && /[^:]$/.test(field.key)) {
        field.key += ':';
    }


    function snake(s) {
        return s.replace(/\s+/g, '_');
    }

    function unsnake(s) {
        return s.replace(/_+/g, ' ');
    }

    function clean(s) {
        return s.split(';')
            .map(function(s) { return s.trim(); })
            .join(';');
    }


    // returns the tag value for a display value
    // (for multiCombo, dval should be the key suffix, not the entire key)
    function tagValue(dval) {
        dval = clean(dval || '');

        if (optstrings) {
            var found = _comboData.find(function(o) {
                return o.key && clean(o.value) === dval;
            });
            if (found) {
                return found.key;
            }
        }

        if (field.type === 'typeCombo' && !dval) {
            return 'yes';
        }

        return (snake_case ? snake(dval) : dval) || undefined;
    }


    // returns the display value for a tag value
    // (for multiCombo, tval should be the key suffix, not the entire key)
    function displayValue(tval) {
        tval = tval || '';

        if (optstrings) {
            var found = _comboData.find(function(o) {
                return o.key === tval && o.value;
            });
            if (found) {
                return found.value;
            }
        }

        if (field.type === 'typeCombo' && tval.toLowerCase() === 'yes') {
            return '';
        }

        return snake_case ? unsnake(tval) : tval;
    }


    // Compute the difference between arrays of objects by `value` property
    //
    // objectDifference([{value:1}, {value:2}, {value:3}], [{value:2}])
    // > [{value:1}, {value:3}]
    //
    function objectDifference(a, b) {
        return a.filter(function(d1) {
            return !b.some(function(d2) { return d1.value === d2.value; });
        });
    }


    function initCombo(selection, attachTo) {
        if (optstrings) {
            selection.attr('readonly', 'readonly');
            selection.call(combobox, attachTo);
            setStaticValues(setPlaceholder);

        } else if (optarray) {
            selection.call(combobox, attachTo);
            setStaticValues(setPlaceholder);

        } else if (taginfo) {
            selection.call(combobox.fetcher(setTaginfoValues), attachTo);
            setTaginfoValues('', setPlaceholder);
        }
    }


    function setStaticValues(callback) {
        if (!(optstrings || optarray)) return;

        if (optstrings) {
            _comboData = Object.keys(optstrings).map(function(k) {
                var v = field.t('options.' + k, { 'default': optstrings[k] });
                return {
                    key: k,
                    value: v,
                    title: v
                };
            });

        } else if (optarray) {
            _comboData = optarray.map(function(k) {
                var v = snake_case ? unsnake(k) : k;
                return {
                    key: k,
                    value: v,
                    title: v
                };
            });
        }

        combobox.data(objectDifference(_comboData, _multiData));
        if (callback) callback(_comboData);
    }


    function setTaginfoValues(q, callback) {
        var fn = isMulti ? 'multikeys' : 'values';
        var query = (isMulti ? field.key : '') + q;
        var hasCountryPrefix = isNetwork && _country && _country.indexOf(q.toLowerCase()) === 0;
        if (hasCountryPrefix) {
            query = _country + ':';
        }

        var params = {
            debounce: (q !== ''),
            key: field.key,
            query: query
        };

        if (_entity) {
            params.geometry = context.geometry(_entity.id);
        }

        taginfo[fn](params, function(err, data) {
            if (err) return;

            var deprecatedValues = osmEntity.deprecatedTagValuesByKey()[field.key];
            if (deprecatedValues) {
                // don't suggest deprecated tag values
                data = data.filter(function(d) {
                    return deprecatedValues.indexOf(d.value) === -1;
                });
            }

            if (hasCountryPrefix) {
                data = data.filter(function(d) {
                    return d.value.toLowerCase().indexOf(_country + ':') === 0;
                });
            }

            // hide the caret if there are no suggestions
            container.classed('empty-combobox', data.length === 0);

            _comboData = data.map(function(d) {
                var k = d.value;
                if (isMulti) k = k.replace(field.key, '');
                var v = snake_case ? unsnake(k) : k;
                return {
                    key: k,
                    value: v,
                    title: isMulti ? v : d.title
                };
            });

            _comboData = objectDifference(_comboData, _multiData);
            if (callback) callback(_comboData);
        });
    }


    function setPlaceholder(values) {
        var ph;

        if (isMulti || isSemi) {
            ph = field.placeholder() || t('inspector.add');
        } else {
            var vals = values
                .map(function(d) { return d.value; })
                .filter(function(s) { return s.length < 20; });

            var placeholders = vals.length > 1 ? vals : values.map(function(d) { return d.key; });
            ph = field.placeholder() || placeholders.slice(0, 3).join(', ');
        }

        if (!/(…|\.\.\.)$/.test(ph)) {
            ph += '…';
        }

        container.selectAll('input')
            .attr('placeholder', ph);
    }


    function change() {
        var t = {};
        var val;

        if (isMulti || isSemi) {
            val = tagValue(utilGetSetValue(input).replace(/,/g, ';')) || '';
            container.classed('active', false);
            utilGetSetValue(input, '');

            var vals = val.split(';').filter(Boolean);
            if (!vals.length) return;

            if (isMulti) {
                utilArrayUniq(vals).forEach(function(v) {
                    var key = field.key + v;
                    if (_entity) {
                        // don't set a multicombo value to 'yes' if it already has a non-'no' value
                        // e.g. `language:de=main`
                        var old = _entity.tags[key] || '';
                        if (old && old.toLowerCase() !== 'no') return;
                    }
                    field.keys.push(key);
                    t[key] = 'yes';
                });

            } else if (isSemi) {
                var arr = _multiData.map(function(d) { return d.key; });
                arr = arr.concat(vals);
                t[field.key] = utilArrayUniq(arr).filter(Boolean).join(';');
            }

            window.setTimeout(function() { input.node().focus(); }, 10);

        } else {
            val = tagValue(utilGetSetValue(input));
            t[field.key] = val;
        }

        dispatch.call('change', this, t);
    }


    function removeMultikey(d) {
        d3_event.stopPropagation();
        var t = {};
        if (isMulti) {
            t[d.key] = undefined;
        } else if (isSemi) {
            var arr = _multiData.map(function(md) {
                return md.key === d.key ? null : md.key;
            }).filter(Boolean);

            arr = utilArrayUniq(arr);
            t[field.key] = arr.length ? arr.join(';') : undefined;
        }
        dispatch.call('change', this, t);
    }


    function combo(selection) {
        container = selection.selectAll('.form-field-input-wrap')
            .data([0]);

        var type = (isMulti || isSemi) ? 'multicombo': 'combo';
        container = container.enter()
            .append('div')
            .attr('class', 'form-field-input-wrap form-field-input-' + type)
            .merge(container);

        if (isMulti || isSemi) {
            container = container.selectAll('.chiplist')
                .data([0]);

            var listClass = 'chiplist';

            // Use a separate line for each value in the Destinations field
            // to mimic highway exit signs
            if (field.id === 'destination_oneway') {
                listClass += ' full-line-chips';
            }

            container = container.enter()
                .append('ul')
                .attr('class', listClass)
                .on('click', function() {
                    window.setTimeout(function() { input.node().focus(); }, 10);
                })
                .merge(container);


            inputWrap = container.selectAll('.input-wrap')
                .data([0]);

            inputWrap = inputWrap.enter()
                .append('li')
                .attr('class', 'input-wrap')
                .merge(inputWrap);

            input = inputWrap.selectAll('input')
                .data([0]);
        } else {
            input = container.selectAll('input')
                .data([0]);
        }

        input = input.enter()
            .append('input')
            .attr('type', 'text')
            .attr('id', 'preset-input-' + field.safeid)
            .call(utilNoAuto)
            .call(initCombo, selection)
            .merge(input);

        if (isNetwork && nominatim && _entity) {
            var center = _entity.extent(context.graph()).center();
            nominatim.countryCode(center, function (err, code) {
                _country = code;
            });
        }

        input
            .on('change', change)
            .on('blur', change);

        input
            .on('keydown.field', function() {
                switch (d3_event.keyCode) {
                    case 13: // ↩ Return
                        input.node().blur(); // blurring also enters the value
                        d3_event.stopPropagation();
                        break;
                }
            });

        if (isMulti || isSemi) {
            combobox
                .on('accept', function() {
                    input.node().blur();
                    input.node().focus();
                });

            input
                .on('focus', function() { container.classed('active', true); });
        }
    }


    combo.tags = function(tags) {
        if (isMulti || isSemi) {
            _multiData = [];

            if (isMulti) {
                // Build _multiData array containing keys already set..
                for (var k in tags) {
                    if (k.indexOf(field.key) !== 0) continue;
                    var v = (tags[k] || '').toLowerCase();
                    if (v === '' || v === 'no') continue;

                    var suffix = k.substring(field.key.length);
                    _multiData.push({
                        key: k,
                        value: displayValue(suffix)
                    });
                }

                // Set keys for form-field modified (needed for undo and reset buttons)..
                field.keys = _multiData.map(function(d) { return d.key; });

            } else if (isSemi) {
                var arr = utilArrayUniq((tags[field.key] || '').split(';')).filter(Boolean);
                _multiData = arr.map(function(k) {
                    return {
                        key: k,
                        value: displayValue(k)
                    };
                });
            }

            // Exclude existing multikeys from combo options..
            var available = objectDifference(_comboData, _multiData);
            combobox.data(available);

            // Hide 'Add' button if this field uses fixed set of
            // translateable optstrings and they're all currently used..
            container.selectAll('.combobox-input, .combobox-caret')
                .classed('hide', optstrings && !available.length);


            // Render chips
            var chips = container.selectAll('.chips')
                .data(_multiData);

            chips.exit()
                .remove();

            var enter = chips.enter()
                .insert('li', '.input-wrap')
                .attr('class', 'chips');

            enter.append('span');
            enter.append('a');

            chips = chips.merge(enter);

            chips.select('span')
                .text(function(d) { return d.value; });

            chips.select('a')
                .on('click', removeMultikey)
                .attr('class', 'remove')
                .text('×');

        } else {
            utilGetSetValue(input, displayValue(tags[field.key]));
        }
    };


    combo.focus = function() {
        input.node().focus();
    };


    combo.entity = function(val) {
        if (!arguments.length) return _entity;
        _entity = val;
        return combo;
    };


    return utilRebind(combo, dispatch, 'on');
}
Пример #14
0
export function behaviorHover(context) {
    var dispatch = d3_dispatch('hover');
    var _selection = d3_select(null);
    var _newId = null;
    var _buttonDown;
    var _altDisables;
    var _target;


    function keydown() {
        if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
            _selection.selectAll('.hover')
                .classed('hover-suppressed', true)
                .classed('hover', false);

            _selection
                .classed('hover-disabled', true);

            dispatch.call('hover', this, null);
        }
    }


    function keyup() {
        if (_altDisables && d3_event.keyCode === d3_keybinding.modifierCodes.alt) {
            _selection.selectAll('.hover-suppressed')
                .classed('hover-suppressed', false)
                .classed('hover', true);

            _selection
                .classed('hover-disabled', false);

            dispatch.call('hover', this, _target ? _target.id : null);
        }
    }


    var hover = function(selection) {
        _selection = selection;
        _newId = null;

        _selection
            .on('mouseover.hover', mouseover)
            .on('mouseout.hover', mouseout)
            .on('mousedown.hover', mousedown);

        d3_select(window)
            .on('keydown.hover', keydown)
            .on('keyup.hover', keyup);


        function mouseover() {
            if (_buttonDown) return;
            var target = d3_event.target;
            enter(target ? target.__data__ : null);
        }


        function mouseout() {
            if (_buttonDown) return;
            var target = d3_event.relatedTarget;
            enter(target ? target.__data__ : null);
        }


        function mousedown() {
            _buttonDown = true;
            d3_select(window)
                .on('mouseup.hover', mouseup, true);
        }


        function mouseup() {
            _buttonDown = false;
            d3_select(window)
                .on('mouseup.hover', null, true);
        }


        function enter(datum) {
            if (datum === _target) return;
            _target = datum;

            _selection.selectAll('.hover')
                .classed('hover', false);
            _selection.selectAll('.hover-suppressed')
                .classed('hover-suppressed', false);

            var entity;
            if (datum instanceof osmNote || datum instanceof osmEntity) {
                entity = datum;
            } else {
                entity = datum && datum.properties && datum.properties.entity;
            }

            if (entity && entity.id !== _newId) {
                // If drawing a way, don't hover on a node that was just placed. #3974
                var mode = context.mode() && context.mode().id;
                if ((mode === 'draw-line' || mode === 'draw-area') && !_newId && entity.type === 'node') {
                    _newId = entity.id;
                    return;
                }

                var selector =  (datum instanceof osmNote) ? 'note-' + entity.id : '.' + entity.id;

                if (entity.type === 'relation') {
                    entity.members.forEach(function(member) {
                        selector += ', .' + member.id;
                    });
                }

                var suppressed = _altDisables && d3_event && d3_event.altKey;

                _selection.selectAll(selector)
                    .classed(suppressed ? 'hover-suppressed' : 'hover', true);

                if (datum instanceof osmNote) {
                    dispatch.call('hover', this, !suppressed && entity);
                } else {
                    dispatch.call('hover', this, !suppressed && entity.id);
                }

            } else {
                dispatch.call('hover', this, null);
            }
        }

    };


    hover.off = function(selection) {
        selection.selectAll('.hover')
            .classed('hover', false);
        selection.selectAll('.hover-suppressed')
            .classed('hover-suppressed', false);
        selection
            .classed('hover-disabled', false);

        selection
            .on('mouseover.hover', null)
            .on('mouseout.hover', null)
            .on('mousedown.hover', null);

        d3_select(window)
            .on('keydown.hover', null)
            .on('keyup.hover', null);
    };


    hover.altDisables = function(_) {
        if (!arguments.length) return _altDisables;
        _altDisables = _;
        return hover;
    };


    return utilRebind(hover, dispatch, 'on');
}
Пример #15
0
export function uiSettingsCustomBackground(context) {
    var dispatch = d3_dispatch('change');

    function render(selection) {
        // keep separate copies of original and current settings
        var _origSettings = {
            template: context.storage('background-custom-template')
        };
        var _currSettings = {
            template: context.storage('background-custom-template')
        };

        var example = 'https://{switch:a,b,c}.tile.openstreetmap.org/{zoom}/{x}/{y}.png';
        var modal = uiConfirm(selection).okButton();

        modal
            .classed('settings-modal settings-custom-background', true);

        modal.select('.modal-section.header')
            .append('h3')
            .text(t('settings.custom_background.header'));


        var textSection = modal.select('.modal-section.message-text');

        textSection
            .append('pre')
            .attr('class', 'instructions-template')
            .text(t('settings.custom_background.instructions', { example: example }));

        textSection
            .append('textarea')
            .attr('class', 'field-template')
            .attr('placeholder', t('settings.custom_background.template.placeholder'))
            .call(utilNoAuto)
            .property('value', _currSettings.template);


        // insert a cancel button
        var buttonSection = modal.select('.modal-section.buttons');

        buttonSection
            .insert('button', '.ok-button')
            .attr('class', 'button cancel-button secondary-action')
            .text(t('confirm.cancel'));


        buttonSection.select('.cancel-button')
            .on('click.cancel', clickCancel);

        buttonSection.select('.ok-button')
            .attr('disabled', isSaveDisabled)
            .on('click.save', clickSave);


        function isSaveDisabled() {
            return null;
        }


        // restore the original template
        function clickCancel() {
            textSection.select('.field-template').property('value', _origSettings.template);
            context.storage('background-custom-template', _origSettings.template);
            this.blur();
            modal.close();
        }

        // accept the current template
        function clickSave() {
            _currSettings.template = textSection.select('.field-template').property('value');
            context.storage('background-custom-template', _currSettings.template);
            this.blur();
            modal.close();
            dispatch.call('change', this, _currSettings);
        }
    }

    return utilRebind(render, dispatch, 'on');
}
Пример #16
0
module.exports = function() {
  var DEFAULT_WIDTH = 256,
      DEFAULT_HEIGHT = 256,
      size = [DEFAULT_WIDTH, DEFAULT_HEIGHT],
      text = cloudText,
      font = cloudFont,
      fontSize = cloudFontSize,
      fontStyle = cloudFontNormal,
      fontWeight = cloudFontNormal,
      rotate = cloudRotate,
      padding = cloudPadding,
      spiral = archimedeanSpiral,
      words = [],
      timeInterval = Infinity,
      event = dispatch("word", "end"),
      timer = null,
      random = Math.random,
      overflow = false,
      cloud = {},
      canvas = cloudCanvas;

  cloud.version = "1.2.5-rc1";
  cloud.canvas = function(_) {
    return arguments.length ? (canvas = functor(_), cloud) : canvas;
  };

  cloud.start = function() {
    var contextAndRatio = getContext(canvas()),
        board = zeroArray((size[0] >> 5) * size[1]),
        bounds = null,
        n = words.length,
        i = -1,
        tags = [],
        data = words.map(function(d, i) {
          d.text = text.call(this, d, i);
          d.font = font.call(this, d, i);
          d.style = fontStyle.call(this, d, i);
          d.weight = fontWeight.call(this, d, i);
          d.rotate = rotate.call(this, d, i);
          d.size = ~~fontSize.call(this, d, i);
          d.padding = padding.call(this, d, i);
          return d;
        }).sort(function(a, b) { return b.size - a.size; });

    if (timer) clearInterval(timer);
    timer = setInterval(step, 0);
    step();

    return cloud;

    function step() {
      var start = Date.now(),
        missing = 0,
        d, x, y;
      while (Date.now() - start < timeInterval && ++i < n && timer) {
        d = data[i];
        x = d.x = (size[0] * (random() + .5)) >> 1;
        y = d.y = (size[1] * (random() + .5)) >> 1;
        cloudSprite(contextAndRatio, d, data, i);
        while (d.hasText) {
          if (place(board, d, bounds)) {
            tags.push(d);
            event.call("word", cloud, d);
            if (bounds) cloudBounds(bounds, d);
            else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
            // Temporary hack
            d.x -= size[0] >> 1;
            d.y -= size[1] >> 1;
            break;
          } else {
            // If the word wont be noticed (the cloud contains 200+ words)
            // Or impossible to get fit, give up on this one
            if (data.length > 200 || (d.retries && d.retries === 3)) {
              missing++;
              // console.log('giving up', d);
              d.sprite = d.sprite || [];
              break;
            }
            d.retries = d.retries || 1;
            d.retries++;
            // reset
            // d.sprite = []
            delete d.sprite;
            d.sprite = null;
            d.x = x;
            d.y = y;

            // decrement the size until it fits
            d.size = d.size * 0.7;
            // d.size = d.size - 3;
            if (~~d.size > 0) {
              cloudSprite(contextAndRatio, d, data, i);
            }
          }
        }
      }
      if (i >= n) {
        if (missing) {
          console.warn(missing + " words were unable to fit, they were not rendered.");
        }
        cloud.stop();
        event.call("end", cloud, tags, bounds);
      }
    }
  }

  cloud.stop = function() {
    if (timer) {
      clearInterval(timer);
      timer = null;
    }
    return cloud;
  };

  function getContext(canvas) {
    canvas.width = canvas.height = 1;
    var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
    canvas.width = (cw << 5) / ratio;
    canvas.height = ch / ratio;

    var context = canvas.getContext("2d");
    context.fillStyle = context.strokeStyle = "red";
    context.textAlign = "center";

    return {context: context, ratio: ratio};
  }

  function place(board, tag, bounds) {
    var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
        startX = tag.x,
        startY = tag.y,
        maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
        s = spiral(size),
        dt = random() < .5 ? 1 : -1,
        t = -dt,
        dxdy,
        dx,
        dy;

    while (dxdy = s(t += dt)) {
      dx = ~~dxdy[0];
      dy = ~~dxdy[1];

      if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;

      tag.x = startX + dx;
      tag.y = startY + dy;

      if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
          tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) {
        if (!overflow) {
          continue;
        }
      }
      // TODO only check for collisions within current bounds.
      if (!bounds || !cloudCollide(tag, board, size[0])) {
        if (!bounds || collideRects(tag, bounds)) {
          var sprite = tag.sprite,
              w = tag.width >> 5,
              sw = size[0] >> 5,
              lx = tag.x - (w << 4),
              sx = lx & 0x7f,
              msx = 32 - sx,
              h = tag.y1 - tag.y0,
              x = (tag.y + tag.y0) * sw + (lx >> 5),
              last;
          for (var j = 0; j < h; j++) {
            last = 0;
            for (var i = 0; i <= w; i++) {
              board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
            }
            x += sw;
          }
          delete tag.sprite;
          return true;
        }
      }
    }
    return false;
  }

  cloud.timeInterval = function(_) {
    return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
  };

  cloud.words = function(_) {
    return arguments.length ? (words = _, cloud) : words;
  };

  cloud.size = function(_) {
    if (!arguments.length) return size;
    if (!_[0] || _[0] < 0) _[0] = DEFAULT_WIDTH;
    if (!_[1] || _[1] < 1) _[1] = DEFAULT_HEIGHT;
    return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
  };

  cloud.font = function(_) {
    return arguments.length ? (font = functor(_), cloud) : font;
  };

  cloud.fontStyle = function(_) {
    return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
  };

  cloud.fontWeight = function(_) {
    return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
  };

  cloud.rotate = function(_) {
    return arguments.length ? (rotate = functor(_), cloud) : rotate;
  };

  cloud.text = function(_) {
    return arguments.length ? (text = functor(_), cloud) : text;
  };

  cloud.spiral = function(_) {
    return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
  };

  cloud.fontSize = function(_) {
    return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
  };

  cloud.padding = function(_) {
    return arguments.length ? (padding = functor(_), cloud) : padding;
  };

  cloud.overflow = function(_) {
    return arguments.length ? (overflow = functor(_), cloud) : overflow;
  };

  cloud.random = function(_) {
    return arguments.length ? (random = _, cloud) : random;
  };

  cloud.on = function() {
    var value = event.on.apply(event, arguments);
    return value === event ? cloud : value;
  };

  return cloud;
};
Пример #17
0
export function uiIntroPoint(context, reveal) {
    var dispatch = d3_dispatch('done');
    var timeouts = [];
    var intersection = [-85.63279, 41.94394];
    var building = [-85.632422, 41.944045];
    var cafePreset = context.presets().item('amenity/cafe');
    var _pointID = null;


    var chapter = {
        title: 'intro.points.title'
    };


    function timeout(f, t) {
        timeouts.push(window.setTimeout(f, t));
    }


    function revealEditMenu(loc, text, options) {
        var rect = context.surfaceRect();
        var point = context.curtainProjection(loc);
        var pad = 40;
        var width = 250 + (2 * pad);
        var height = 250;
        var startX = rect.left + point[0];
        var left = (textDirection === 'rtl') ? (startX - width + pad) : (startX - pad);
        var box = {
            left: left,
            top: point[1] + rect.top - 60,
            width: width,
            height: height
        };
        reveal(box, text, options);
    }


    function eventCancel() {
        d3_event.stopPropagation();
        d3_event.preventDefault();
    }


    function addPoint() {
        context.enter(modeBrowse(context));
        context.history().reset('initial');

        var msec = transitionTime(intersection, context.map().center());
        if (msec) { reveal(null, null, { duration: 0 }); }
        context.map().centerZoomEase(intersection, 19, msec);

        timeout(function() {
            var tooltip = reveal('button.add-point',
                t('intro.points.add_point', { button: icon('#iD-icon-point', 'pre-text') }));

            _pointID = null;

            tooltip.selectAll('.tooltip-inner')
                .insert('svg', 'span')
                .attr('class', 'tooltip-illustration')
                .append('use')
                .attr('xlink:href', '#iD-graphic-points');

            context.on('enter.intro', function(mode) {
                if (mode.id !== 'add-point') return;
                continueTo(placePoint);
            });
        }, msec + 100);

        function continueTo(nextStep) {
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function placePoint() {
        if (context.mode().id !== 'add-point') {
            return chapter.restart();
        }

        var pointBox = pad(building, 150, context);
        reveal(pointBox, t('intro.points.place_point'));

        context.map().on('move.intro drawn.intro', function() {
            pointBox = pad(building, 150, context);
            reveal(pointBox, t('intro.points.place_point'), { duration: 0 });
        });

        context.on('enter.intro', function(mode) {
            if (mode.id !== 'select') return chapter.restart();
            _pointID = context.mode().selectedIDs()[0];
            continueTo(searchPreset);
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function searchPreset() {
        if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
            return addPoint();
        }

        // disallow scrolling
        d3_select('.inspector-wrap').on('wheel.intro', eventCancel);

        d3_select('.preset-search-input')
            .on('keydown.intro', null)
            .on('keyup.intro', checkPresetSearch);

        reveal('.preset-search-input',
            t('intro.points.search_cafe', { preset: cafePreset.name() })
        );

        context.on('enter.intro', function(mode) {
            if (!_pointID || !context.hasEntity(_pointID)) {
                return continueTo(addPoint);
            }

            var ids = context.selectedIDs();
            if (mode.id !== 'select' || !ids.length || ids[0] !== _pointID) {
                // keep the user's point selected..
                context.enter(modeSelect(context, [_pointID]));

                // disallow scrolling
                d3_select('.inspector-wrap').on('wheel.intro', eventCancel);

                d3_select('.preset-search-input')
                    .on('keydown.intro', null)
                    .on('keyup.intro', checkPresetSearch);

                reveal('.preset-search-input',
                    t('intro.points.search_cafe', { preset: cafePreset.name() })
                );

                context.history().on('change.intro', null);
            }
        });


        function checkPresetSearch() {
            var first = d3_select('.preset-list-item:first-child');

            if (first.classed('preset-amenity-cafe')) {
                d3_select('.preset-search-input')
                    .on('keydown.intro', eventCancel, true)
                    .on('keyup.intro', null);

                reveal(first.select('.preset-list-button').node(),
                    t('intro.points.choose_cafe', { preset: cafePreset.name() }),
                    { duration: 300 }
                );

                context.history().on('change.intro', function() {
                    continueTo(aboutFeatureEditor);
                });
            }
        }

        function continueTo(nextStep) {
            context.on('enter.intro', null);
            context.history().on('change.intro', null);
            d3_select('.inspector-wrap').on('wheel.intro', null);
            d3_select('.preset-search-input').on('keydown.intro keyup.intro', null);
            nextStep();
        }
    }


    function aboutFeatureEditor() {
        if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
            return addPoint();
        }

        timeout(function() {
            reveal('.entity-editor-pane', t('intro.points.feature_editor'), {
                tooltipClass: 'intro-points-describe',
                buttonText: t('intro.ok'),
                buttonCallback: function() { continueTo(addName); }
            });
        }, 400);

        context.on('exit.intro', function() {
            // if user leaves select mode here, just continue with the tutorial.
            continueTo(reselectPoint);
        });

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            nextStep();
        }
    }


    function addName() {
        if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
            return addPoint();
        }

        // reset pane, in case user happened to change it..
        d3_select('.inspector-wrap .panewrap').style('right', '0%');

        timeout(function() {
            // It's possible for the user to add a name in a previous step..
            // If so, don't tell them to add the name in this step.
            // Give them an OK button instead.
            var entity = context.entity(_pointID);
            if (entity.tags.name) {
                var tooltip = reveal('.entity-editor-pane', t('intro.points.add_name'), {
                    tooltipClass: 'intro-points-describe',
                    buttonText: t('intro.ok'),
                    buttonCallback: function() { continueTo(addCloseEditor); }
                });
                tooltip.select('.instruction').style('display', 'none');

            } else {
                reveal('.entity-editor-pane', t('intro.points.add_name'),
                    { tooltipClass: 'intro-points-describe' }
                );
            }
        }, 400);

        context.history().on('change.intro', function() {
            continueTo(addCloseEditor);
        });

        context.on('exit.intro', function() {
            // if user leaves select mode here, just continue with the tutorial.
            continueTo(reselectPoint);
        });

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function addCloseEditor() {
        // reset pane, in case user happened to change it..
        d3_select('.inspector-wrap .panewrap').style('right', '0%');

        var selector = '.entity-editor-pane button.preset-close svg use';
        var href = d3_select(selector).attr('href') || '#iD-icon-close';

        context.on('exit.intro', function() {
            continueTo(reselectPoint);
        });

        reveal('.entity-editor-pane',
            t('intro.points.add_close', { button: icon(href, 'pre-text') })
        );

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            nextStep();
        }
    }


    function reselectPoint() {
        if (!_pointID) return chapter.restart();
        var entity = context.hasEntity(_pointID);
        if (!entity) return chapter.restart();

        // make sure it's still a cafe, in case user somehow changed it..
        var oldPreset = context.presets().match(entity, context.graph());
        context.replace(actionChangePreset(_pointID, oldPreset, cafePreset));

        context.enter(modeBrowse(context));

        var msec = transitionTime(entity.loc, context.map().center());
        if (msec) { reveal(null, null, { duration: 0 }); }
        context.map().centerEase(entity.loc, msec);

        timeout(function() {
            var box = pointBox(entity.loc, context);
            reveal(box, t('intro.points.reselect'), { duration: 600 });

            timeout(function() {
                context.map().on('move.intro drawn.intro', function() {
                    var entity = context.hasEntity(_pointID);
                    if (!entity) return chapter.restart();
                    var box = pointBox(entity.loc, context);
                    reveal(box, t('intro.points.reselect'), { duration: 0 });
                });
            }, 600); // after reveal..

            context.on('enter.intro', function(mode) {
                if (mode.id !== 'select') return;
                continueTo(updatePoint);
            });

        }, msec + 100);

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            nextStep();
        }
    }


    function updatePoint() {
        if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
            return continueTo(reselectPoint);
        }

        // reset pane, in case user happened to untag the point..
        d3_select('.inspector-wrap .panewrap').style('right', '0%');

        context.on('exit.intro', function() {
            continueTo(reselectPoint);
        });

        context.history().on('change.intro', function() {
            continueTo(updateCloseEditor);
        });

        timeout(function() {
            reveal('.entity-editor-pane', t('intro.points.update'),
                { tooltipClass: 'intro-points-describe' }
            );
        }, 400);

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function updateCloseEditor() {
        if (context.mode().id !== 'select' || !_pointID || !context.hasEntity(_pointID)) {
            return continueTo(reselectPoint);
        }

        // reset pane, in case user happened to change it..
        d3_select('.inspector-wrap .panewrap').style('right', '0%');

        context.on('exit.intro', function() {
            continueTo(rightClickPoint);
        });

        timeout(function() {
            reveal('.entity-editor-pane',
                t('intro.points.update_close', { button: icon('#iD-icon-apply', 'pre-text') })
            );
        }, 500);

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            nextStep();
        }
    }


    function rightClickPoint() {
        if (!_pointID) return chapter.restart();
        var entity = context.hasEntity(_pointID);
        if (!entity) return chapter.restart();

        context.enter(modeBrowse(context));

        var box = pointBox(entity.loc, context);
        reveal(box, t('intro.points.rightclick'), { duration: 600 });

        timeout(function() {
            context.map().on('move.intro drawn.intro', function() {
                var entity = context.hasEntity(_pointID);
                if (!entity) return chapter.restart();
                var box = pointBox(entity.loc, context);
                reveal(box, t('intro.points.rightclick'), { duration: 0 });
            });
        }, 600); // after reveal

        context.on('enter.intro', function(mode) {
            if (mode.id !== 'select') return;
            var ids = context.selectedIDs();
            if (ids.length !== 1 || ids[0] !== _pointID) return;

            timeout(function() {
                var node = selectMenuItem('delete').node();
                if (!node) return;
                continueTo(enterDelete);
            }, 300);  // after menu visible
        });

        function continueTo(nextStep) {
            context.on('enter.intro', null);
            context.map().on('move.intro drawn.intro', null);
            nextStep();
        }
    }


    function enterDelete() {
        if (!_pointID) return chapter.restart();
        var entity = context.hasEntity(_pointID);
        if (!entity) return chapter.restart();

        var node = selectMenuItem('delete').node();
        if (!node) { return continueTo(rightClickPoint); }

        revealEditMenu(entity.loc,
            t('intro.points.delete', { button: icon('#iD-operation-delete', 'pre-text') })
        );

        timeout(function() {
            context.map().on('move.intro drawn.intro', function() {
                revealEditMenu(entity.loc,
                    t('intro.points.delete', { button: icon('#iD-operation-delete', 'pre-text') }),
                    { duration: 0}
                );
            });
        }, 300); // after menu visible

        context.on('exit.intro', function() {
            if (!_pointID) return chapter.restart();
            var entity = context.hasEntity(_pointID);
            if (entity) return continueTo(rightClickPoint);  // point still exists
        });

        context.history().on('change.intro', function(changed) {
            if (changed.deleted().length) {
                continueTo(undo);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            context.on('exit.intro', null);
            nextStep();
        }
    }


    function undo() {
        context.history().on('change.intro', function() {
            continueTo(play);
        });

        var iconName = '#iD-icon-' + (textDirection === 'rtl' ? 'redo' : 'undo');
        reveal('#bar button.undo-button',
            t('intro.points.undo', { button: icon(iconName, 'pre-text') })
        );

        function continueTo(nextStep) {
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function play() {
        dispatch.call('done');
        reveal('#id-container',
            t('intro.points.play', { next: t('intro.areas.title') }), {
                tooltipBox: '.intro-nav-wrap .chapter-area',
                buttonText: t('intro.ok'),
                buttonCallback: function() { reveal('#id-container'); }
            }
        );
    }


    chapter.enter = function() {
        addPoint();
    };


    chapter.exit = function() {
        timeouts.forEach(window.clearTimeout);
        context.on('enter.intro exit.intro', null);
        context.map().on('move.intro drawn.intro', null);
        context.history().on('change.intro', null);
        d3_select('.inspector-wrap').on('wheel.intro', eventCancel);
        d3_select('.preset-search-input').on('keydown.intro keyup.intro', null);
    };


    chapter.restart = function() {
        chapter.exit();
        chapter.enter();
    };


    return utilRebind(chapter, dispatch, 'on');
}
Пример #18
0
import rbush from 'rbush';

import { d3geoTile as d3_geoTile } from '../lib/d3.geo.tile';
import { geoExtent } from '../geo';

import {
    utilQsString,
    utilRebind,
    utilSetTransform
} from '../util';


var apibase = 'https://openstreetcam.org',
    maxResults = 1000,
    tileZoom = 14,
    dispatch = d3_dispatch('loadedImages'),
    imgZoom = d3_zoom()
        .extent([[0, 0], [320, 240]])
        .translateExtent([[0, 0], [320, 240]])
        .scaleExtent([1, 15])
        .on('zoom', zoomPan),
    _oscCache,
    _oscSelectedImage;


function abortRequest(i) {
    i.abort();
}


function nearNullIsland(x, y, z) {
Пример #19
0
export function uiIntroNavigation(context, reveal) {
    var dispatch = d3_dispatch('done');
    var timeouts = [];
    var hallId = 'n2061';
    var townHall = [-85.63591, 41.94285];
    var springStreetId = 'w397';
    var springStreetEndId = 'n1834';
    var springStreet = [-85.63582, 41.94255];
    var onewayField = context.presets().field('oneway');
    var maxspeedField = context.presets().field('maxspeed');


    var chapter = {
        title: 'intro.navigation.title'
    };


    function timeout(f, t) {
        timeouts.push(window.setTimeout(f, t));
    }


    function eventCancel() {
        d3_event.stopPropagation();
        d3_event.preventDefault();
    }


    function isTownHallSelected() {
        var ids = context.selectedIDs();
        return ids.length === 1 && ids[0] === hallId;
    }


    function dragMap() {
        context.enter(modeBrowse(context));
        context.history().reset('initial');

        var msec = transitionTime(townHall, context.map().center());
        if (msec) { reveal(null, null, { duration: 0 }); }
        context.map().centerZoomEase(townHall, 19, msec);

        timeout(function() {
            var centerStart = context.map().center();

            reveal('#surface', t('intro.navigation.drag'));
            context.map().on('drawn.intro', function() {
                reveal('#surface', t('intro.navigation.drag'), { duration: 0 });
            });

            context.map().on('move.intro', function() {
                var centerNow = context.map().center();
                if (centerStart[0] !== centerNow[0] || centerStart[1] !== centerNow[1]) {
                    context.map().on('move.intro', null);
                    timeout(function() { continueTo(zoomMap); }, 3000);
                }
            });

        }, msec + 100);

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            nextStep();
        }
    }


    function zoomMap() {
        var zoomStart = context.map().zoom();

        reveal('#surface',
            t('intro.navigation.zoom', {
                plus: icon('#iD-icon-plus', 'pre-text'),
                minus: icon('#iD-icon-minus', 'pre-text')
            })
        );

        context.map().on('drawn.intro', function() {
            reveal('#surface',
                t('intro.navigation.zoom', {
                    plus: icon('#iD-icon-plus', 'pre-text'),
                    minus: icon('#iD-icon-minus', 'pre-text')
                }), { duration: 0 }
            );
        });

        context.map().on('move.intro', function() {
            if (context.map().zoom() !== zoomStart) {
                context.map().on('move.intro', null);
                timeout(function() { continueTo(features); }, 3000);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            nextStep();
        }
    }


    function features() {
        var onClick = function() { continueTo(pointsLinesAreas); };

        reveal('#surface', t('intro.navigation.features'),
            { buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        context.map().on('drawn.intro', function() {
            reveal('#surface', t('intro.navigation.features'),
                { duration: 0, buttonText: t('intro.ok'), buttonCallback: onClick }
            );
        });

        function continueTo(nextStep) {
            context.map().on('drawn.intro', null);
            nextStep();
        }
    }

    function pointsLinesAreas() {
        var onClick = function() { continueTo(nodesWays); };

        reveal('#surface', t('intro.navigation.points_lines_areas'),
            { buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        context.map().on('drawn.intro', function() {
            reveal('#surface', t('intro.navigation.points_lines_areas'),
                { duration: 0, buttonText: t('intro.ok'), buttonCallback: onClick }
            );
        });

        function continueTo(nextStep) {
            context.map().on('drawn.intro', null);
            nextStep();
        }
    }

    function nodesWays() {
        var onClick = function() { continueTo(clickTownHall); };

        reveal('#surface', t('intro.navigation.nodes_ways'),
            { buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        context.map().on('drawn.intro', function() {
            reveal('#surface', t('intro.navigation.nodes_ways'),
                { duration: 0, buttonText: t('intro.ok'), buttonCallback: onClick }
            );
        });

        function continueTo(nextStep) {
            context.map().on('drawn.intro', null);
            nextStep();
        }
    }

    function clickTownHall() {
        context.enter(modeBrowse(context));
        context.history().reset('initial');

        var entity = context.hasEntity(hallId);
        if (!entity) return;
        reveal(null, null, { duration: 0 });
        context.map().centerZoomEase(entity.loc, 19, 500);

        timeout(function() {
            var entity = context.hasEntity(hallId);
            if (!entity) return;
            var box = pointBox(entity.loc, context);
            reveal(box, t('intro.navigation.click_townhall'));

            context.map().on('move.intro drawn.intro', function() {
                var entity = context.hasEntity(hallId);
                if (!entity) return;
                var box = pointBox(entity.loc, context);
                reveal(box, t('intro.navigation.click_townhall'), { duration: 0 });
            });

            context.on('enter.intro', function() {
                if (isTownHallSelected()) continueTo(selectedTownHall);
            });

        }, 550);  // after centerZoomEase

        context.history().on('change.intro', function() {
            if (!context.hasEntity(hallId)) {
                continueTo(clickTownHall);
            }
        });

        function continueTo(nextStep) {
            context.on('enter.intro', null);
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function selectedTownHall() {
        if (!isTownHallSelected()) return clickTownHall();

        var entity = context.hasEntity(hallId);
        if (!entity) return clickTownHall();

        var box = pointBox(entity.loc, context);
        var onClick = function() { continueTo(editorTownHall); };

        reveal(box, t('intro.navigation.selected_townhall'),
            { buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        context.map().on('move.intro drawn.intro', function() {
            var entity = context.hasEntity(hallId);
            if (!entity) return;
            var box = pointBox(entity.loc, context);
            reveal(box, t('intro.navigation.selected_townhall'),
                { duration: 0, buttonText: t('intro.ok'), buttonCallback: onClick }
            );
        });

        context.history().on('change.intro', function() {
            if (!context.hasEntity(hallId)) {
                continueTo(clickTownHall);
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function editorTownHall() {
        if (!isTownHallSelected()) return clickTownHall();

        // disallow scrolling
        d3_select('.inspector-wrap').on('wheel.intro', eventCancel);

        var onClick = function() { continueTo(presetTownHall); };

        reveal('.entity-editor-pane',
            t('intro.navigation.editor_townhall'),
            { buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        context.on('exit.intro', function() {
            continueTo(clickTownHall);
        });

        context.history().on('change.intro', function() {
            if (!context.hasEntity(hallId)) {
                continueTo(clickTownHall);
            }
        });

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            d3_select('.inspector-wrap').on('wheel.intro', null);
            nextStep();
        }
    }


    function presetTownHall() {
        if (!isTownHallSelected()) return clickTownHall();

        // reset pane, in case user happened to change it..
        d3_select('.inspector-wrap .panewrap').style('right', '0%');
        // disallow scrolling
        d3_select('.inspector-wrap').on('wheel.intro', eventCancel);

        // preset match, in case the user happened to change it.
        var entity = context.entity(context.selectedIDs()[0]);
        var preset = context.presets().match(entity, context.graph());

        var onClick = function() { continueTo(fieldsTownHall); };

        context.on('exit.intro', function() {
            continueTo(clickTownHall);
        });

        context.history().on('change.intro', function() {
            if (!context.hasEntity(hallId)) {
                continueTo(clickTownHall);
            }
        });

        reveal('.inspector-body .preset-list-item.inspector-inner',
            t('intro.navigation.preset_townhall', { preset: preset.name() }),
            { buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            d3_select('.inspector-wrap').on('wheel.intro', null);
            nextStep();
        }
    }


    function fieldsTownHall() {
        if (!isTownHallSelected()) return clickTownHall();

        // reset pane, in case user happened to change it..
        d3_select('.inspector-wrap .panewrap').style('right', '0%');
        // disallow scrolling
        d3_select('.inspector-wrap').on('wheel.intro', eventCancel);

        var onClick = function() { continueTo(closeTownHall); };

        reveal('.inspector-body .preset-editor',
            t('intro.navigation.fields_townhall'),
            { buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        context.on('exit.intro', function() {
            continueTo(clickTownHall);
        });

        context.history().on('change.intro', function() {
            if (!context.hasEntity(hallId)) {
                continueTo(clickTownHall);
            }
        });

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            d3_select('.inspector-wrap').on('wheel.intro', null);
            nextStep();
        }
    }


    function closeTownHall() {
        if (!isTownHallSelected()) return clickTownHall();

        var selector = '.entity-editor-pane button.preset-close svg use';
        var href = d3_select(selector).attr('href') || '#iD-icon-close';

        reveal('.entity-editor-pane',
            t('intro.navigation.close_townhall', { button: icon(href, 'pre-text') })
        );

        context.on('exit.intro', function() {
            continueTo(searchStreet);
        });

        context.history().on('change.intro', function() {
            // update the close icon in the tooltip if the user edits something.
            var selector = '.entity-editor-pane button.preset-close svg use';
            var href = d3_select(selector).attr('href') || '#iD-icon-close';

            reveal('.entity-editor-pane',
                t('intro.navigation.close_townhall', { button: icon(href, 'pre-text') }),
                { duration: 0 }
            );
        });

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function searchStreet() {
        context.enter(modeBrowse(context));
        context.history().reset('initial');  // ensure spring street exists

        var msec = transitionTime(springStreet, context.map().center());
        if (msec) { reveal(null, null, { duration: 0 }); }
        context.map().centerZoomEase(springStreet, 19, msec);  // ..and user can see it

        timeout(function() {
            reveal('.search-header input',
                t('intro.navigation.search_street', { name: t('intro.graph.name.spring-street') })
            );

            d3_select('.search-header input')
                .on('keyup.intro', checkSearchResult);
        }, msec + 100);
    }


    function checkSearchResult() {
        var first = d3_select('.feature-list-item:nth-child(0n+2)');  // skip "No Results" item
        var firstName = first.select('.entity-name');
        var name = t('intro.graph.name.spring-street');

        if (!firstName.empty() && firstName.text() === name) {
            reveal(first.node(),
                t('intro.navigation.choose_street', { name: name }),
                { duration: 300 }
            );

            context.on('exit.intro', function() {
                continueTo(selectedStreet);
            });

            d3_select('.search-header input')
                .on('keydown.intro', eventCancel, true)
                .on('keyup.intro', null);
        }

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            d3_select('.search-header input')
                .on('keydown.intro', null)
                .on('keyup.intro', null);
            nextStep();
        }
    }


    function selectedStreet() {
        if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {
            return searchStreet();
        }

        var onClick = function() { continueTo(editorStreet); };
        var entity = context.entity(springStreetEndId);
        var box = pointBox(entity.loc, context);
        box.height = 500;

        reveal(box,
            t('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),
            { duration: 600, buttonText: t('intro.ok'), buttonCallback: onClick }
        );

        timeout(function() {
            context.map().on('move.intro drawn.intro', function() {
                var entity = context.hasEntity(springStreetEndId);
                if (!entity) return;
                var box = pointBox(entity.loc, context);
                box.height = 500;
                reveal(box,
                    t('intro.navigation.selected_street', { name: t('intro.graph.name.spring-street') }),
                    { duration: 0, buttonText: t('intro.ok'), buttonCallback: onClick }
                );
            });
        }, 600);  // after reveal.

        context.on('enter.intro', function(mode) {
            if (!context.hasEntity(springStreetId)) {
                return continueTo(searchStreet);
            }
            var ids = context.selectedIDs();
            if (mode.id !== 'select' || !ids.length || ids[0] !== springStreetId) {
                // keep Spring Street selected..
                context.enter(modeSelect(context, [springStreetId]));
            }
        });

        context.history().on('change.intro', function() {
            if (!context.hasEntity(springStreetEndId) || !context.hasEntity(springStreetId)) {
                timeout(function() {
                    continueTo(searchStreet);
                }, 300);  // after any transition (e.g. if user deleted intersection)
            }
        });

        function continueTo(nextStep) {
            context.map().on('move.intro drawn.intro', null);
            context.on('enter.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function editorStreet() {
        var selector = '.entity-editor-pane button.preset-close svg use';
        var href = d3_select(selector).attr('href') || '#iD-icon-close';

        reveal('.entity-editor-pane',
            t('intro.navigation.editor_street', {
                button: icon(href, 'pre-text'),
                field1: onewayField.label(),
                field2: maxspeedField.label()
            })
        );

        context.on('exit.intro', function() {
            continueTo(play);
        });

        context.history().on('change.intro', function() {
            // update the close icon in the tooltip if the user edits something.
            var selector = '.entity-editor-pane button.preset-close svg use';
            var href = d3_select(selector).attr('href') || '#iD-icon-close';

            reveal('.entity-editor-pane',
                t('intro.navigation.editor_street', {
                    button: icon(href, 'pre-text'),
                    field1: onewayField.label().toLowerCase(),
                    field2: maxspeedField.label().toLowerCase()
                }), { duration: 0 }
            );
        });

        function continueTo(nextStep) {
            context.on('exit.intro', null);
            context.history().on('change.intro', null);
            nextStep();
        }
    }


    function play() {
        dispatch.call('done');
        reveal('#id-container',
            t('intro.navigation.play', { next: t('intro.points.title') }), {
                tooltipBox: '.intro-nav-wrap .chapter-point',
                buttonText: t('intro.ok'),
                buttonCallback: function() { reveal('#id-container'); }
            }
        );
    }


    chapter.enter = function() {
        dragMap();
    };


    chapter.exit = function() {
        timeouts.forEach(window.clearTimeout);
        context.on('enter.intro exit.intro', null);
        context.map().on('move.intro drawn.intro', null);
        context.history().on('change.intro', null);
        d3_select('.inspector-wrap').on('wheel.intro', null);
        d3_select('.search-header input').on('keydown.intro keyup.intro', null);
    };


    chapter.restart = function() {
        chapter.exit();
        chapter.enter();
    };


    return utilRebind(chapter, dispatch, 'on');
}
Пример #20
0
import {dispatch} from "d3-dispatch";
import {timer, timeout} from "d3-timer";

var emptyOn = dispatch("start", "end", "cancel", "interrupt");
var emptyTween = [];

export var CREATED = 0;
export var SCHEDULED = 1;
export var STARTING = 2;
export var STARTED = 3;
export var RUNNING = 4;
export var ENDING = 5;
export var ENDED = 6;

export default function(node, name, id, index, group, timing) {
  var schedules = node.__transition;
  if (!schedules) node.__transition = {};
  else if (id in schedules) return;
  create(node, id, {
    name: name,
    index: index, // For context during callback.
    group: group, // For context during callback.
    on: emptyOn,
    tween: emptyTween,
    time: timing.time,
    delay: timing.delay,
    duration: timing.duration,
    ease: timing.ease,
    timer: null,
    state: CREATED
  });
Пример #21
0
module.exports = function() {
  var size = [256, 256],
      text = cloudText,
      font = cloudFont,
      fontSize = cloudFontSize,
      fontStyle = cloudFontNormal,
      fontWeight = cloudFontNormal,
      rotate = cloudRotate,
      padding = cloudPadding,
      spiral = archimedeanSpiral,
      words = [],
      timeInterval = Infinity,
      event = dispatch("word", "end"),
      timer = null,
      random = Math.random,
      cloud = {},
      canvas = cloudCanvas;

  cloud.canvas = function(_) {
    return arguments.length ? (canvas = functor(_), cloud) : canvas;
  };

  cloud.start = function() {
    var contextAndRatio = getContext(canvas()),
        board = zeroArray((size[0] >> 5) * size[1]),
        bounds = null,
        n = words.length,
        i = -1,
        tags = [],
        data = words.map(function(d, i) {
          d.text = text.call(this, d, i);
          d.font = font.call(this, d, i);
          d.style = fontStyle.call(this, d, i);
          d.weight = fontWeight.call(this, d, i);
          d.rotate = rotate.call(this, d, i);
          d.size = ~~fontSize.call(this, d, i);
          d.padding = padding.call(this, d, i);
          return d;
        }).sort(function(a, b) { return b.size - a.size; });

    if (timer) clearInterval(timer);
    timer = setInterval(step, 0);
    step();

    return cloud;

    function step() {
      var start = Date.now();
      while (Date.now() - start < timeInterval && ++i < n && timer) {
        var d = data[i];
        d.x = (size[0] * (random() + .5)) >> 1;
        d.y = (size[1] * (random() + .5)) >> 1;
        cloudSprite(contextAndRatio, d, data, i);
        if (d.hasText && place(board, d, bounds)) {
          tags.push(d);
          event.call("word", cloud, d);
          if (bounds) cloudBounds(bounds, d);
          else bounds = [{x: d.x + d.x0, y: d.y + d.y0}, {x: d.x + d.x1, y: d.y + d.y1}];
          // Temporary hack
          d.x -= size[0] >> 1;
          d.y -= size[1] >> 1;
        }
      }
      if (i >= n) {
        cloud.stop();
        event.call("end", cloud, tags, bounds);
      }
    }
  }

  cloud.stop = function() {
    if (timer) {
      clearInterval(timer);
      timer = null;
    }
    return cloud;
  };

  function getContext(canvas) {
    canvas.width = canvas.height = 1;
    var ratio = Math.sqrt(canvas.getContext("2d").getImageData(0, 0, 1, 1).data.length >> 2);
    canvas.width = (cw << 5) / ratio;
    canvas.height = ch / ratio;

    var context = canvas.getContext("2d");
    context.fillStyle = context.strokeStyle = "red";
    context.textAlign = "center";

    return {context: context, ratio: ratio};
  }

  function place(board, tag, bounds) {
    var perimeter = [{x: 0, y: 0}, {x: size[0], y: size[1]}],
        startX = tag.x,
        startY = tag.y,
        maxDelta = Math.sqrt(size[0] * size[0] + size[1] * size[1]),
        s = spiral(size),
        dt = random() < .5 ? 1 : -1,
        t = -dt,
        dxdy,
        dx,
        dy;

    while (dxdy = s(t += dt)) {
      dx = ~~dxdy[0];
      dy = ~~dxdy[1];

      if (Math.min(Math.abs(dx), Math.abs(dy)) >= maxDelta) break;

      tag.x = startX + dx;
      tag.y = startY + dy;

      if (tag.x + tag.x0 < 0 || tag.y + tag.y0 < 0 ||
          tag.x + tag.x1 > size[0] || tag.y + tag.y1 > size[1]) continue;
      // TODO only check for collisions within current bounds.
      if (!bounds || !cloudCollide(tag, board, size[0])) {
        if (!bounds || collideRects(tag, bounds)) {
          var sprite = tag.sprite,
              w = tag.width >> 5,
              sw = size[0] >> 5,
              lx = tag.x - (w << 4),
              sx = lx & 0x7f,
              msx = 32 - sx,
              h = tag.y1 - tag.y0,
              x = (tag.y + tag.y0) * sw + (lx >> 5),
              last;
          for (var j = 0; j < h; j++) {
            last = 0;
            for (var i = 0; i <= w; i++) {
              board[x + i] |= (last << msx) | (i < w ? (last = sprite[j * w + i]) >>> sx : 0);
            }
            x += sw;
          }
          delete tag.sprite;
          return true;
        }
      }
    }
    return false;
  }

  cloud.timeInterval = function(_) {
    return arguments.length ? (timeInterval = _ == null ? Infinity : _, cloud) : timeInterval;
  };

  cloud.words = function(_) {
    return arguments.length ? (words = _, cloud) : words;
  };

  cloud.size = function(_) {
    return arguments.length ? (size = [+_[0], +_[1]], cloud) : size;
  };

  cloud.font = function(_) {
    return arguments.length ? (font = functor(_), cloud) : font;
  };

  cloud.fontStyle = function(_) {
    return arguments.length ? (fontStyle = functor(_), cloud) : fontStyle;
  };

  cloud.fontWeight = function(_) {
    return arguments.length ? (fontWeight = functor(_), cloud) : fontWeight;
  };

  cloud.rotate = function(_) {
    return arguments.length ? (rotate = functor(_), cloud) : rotate;
  };

  cloud.text = function(_) {
    return arguments.length ? (text = functor(_), cloud) : text;
  };

  cloud.spiral = function(_) {
    return arguments.length ? (spiral = spirals[_] || _, cloud) : spiral;
  };

  cloud.fontSize = function(_) {
    return arguments.length ? (fontSize = functor(_), cloud) : fontSize;
  };

  cloud.padding = function(_) {
    return arguments.length ? (padding = functor(_), cloud) : padding;
  };

  cloud.random = function(_) {
    return arguments.length ? (random = _, cloud) : random;
  };

  cloud.on = function() {
    var value = event.on.apply(event, arguments);
    return value === event ? cloud : value;
  };

  return cloud;
};
Пример #22
0
export function coreValidator(context) {
    var dispatch = d3_dispatch('reload');
    var self = {};
    var _issues = [];
    var _issuesByEntityID = {};

    var _disabledValidations = {};

    var validations = _filter(Validations, _isFunction).reduce(function(obj, validation) {
        var func = validation();
        obj[func.type] = func;
        return obj;
    }, {});

    var entityValidationIDs = [];
    var changesValidationIDs = [];

    for (var key in validations) {
        var validation = validations[key];
        if (validation.inputType && validation.inputType === 'changes') {
            changesValidationIDs.push(key);
        } else {
            entityValidationIDs.push(key);
        }
    }

    var validationIDsToDisplay = Object.keys(validations).filter(function(rule) {
        return rule !== 'maprules';
    });
    validationIDsToDisplay.sort(function(rule1, rule2) {
        return t('issues.' + rule1 + '.title') > t('issues.' + rule2 + '.title');
    });

    //self.featureApplicabilityOptions = ['edited', 'all'];

    /*var featureApplicability = context.storage('issue-features') || 'edited';

    self.getFeatureApplicability = function() {
        return featureApplicability;
    };

    self.setFeatureApplicability = function(applicability) {
        featureApplicability = applicability;
        context.storage('issue-features', applicability);
    };*/

    self.getIssues = function() {
        return _issues;
    };

    self.getWarnings = function() {
        return _issues.filter(function(d) { return d.severity === 'warning'; });
    };

    self.getErrors = function() {
        return _issues.filter(function(d) { return d.severity === 'error'; });
    };

    self.getIssuesForEntityWithID = function(entityID) {
        if (!context.hasEntity(entityID)) return [];
        var entity = context.entity(entityID);
        var key = osmEntity.key(entity);

        if (!_issuesByEntityID[key]) {
            _issuesByEntityID[key] = validateEntity(entity);
        }
        return _issuesByEntityID[key];
    };

    self.getRuleIDs = function(){
        return validationIDsToDisplay;
    };

    self.getDisabledRules = function(){
        return _disabledValidations;
    };

    self.toggleRule = function(ruleID) {
        if (_disabledValidations[ruleID]) {
            delete _disabledValidations[ruleID];
        } else {
            _disabledValidations[ruleID] = true;
        }
        self.validate();
    };

    function validateEntity(entity) {
        var _issues = [];
        var ran = {};

        // runs validation and appends resulting issues, returning true if validation passed
        function runValidation(which) {
            if (ran[which]) return true;

            if (_disabledValidations[which]) {
                // don't run disabled validations but mark as having run
                ran[which] = true;
                return true;
            }

            var fn = validations[which];
            var typeIssues = fn(entity, context);
            _issues = _issues.concat(typeIssues);
            ran[which] = true;   // mark this validation as having run
            return !typeIssues.length;
        }

        runValidation('missing_role');
        
        if (entity.type === 'relation') {
            if (!runValidation('old_multipolygon')) {
                // don't flag missing tags if they are on the outer way
                ran.missing_tag = true;
            }
        }

        // other validations require feature to be tagged
        if (!runValidation('missing_tag')) return _issues;

        if (entity.type === 'way') {
            runValidation('crossing_ways');

            // only check for disconnected way if no almost junctions
            if (runValidation('almost_junction')) {
                runValidation('disconnected_way');
            } else {
                ran.disconnected_way = true;
            }

            runValidation('tag_suggests_area');
        }

        // run all validations not yet run manually
        entityValidationIDs.forEach(runValidation);

        return _issues;
    }


    self.validate = function() {
        _issuesByEntityID = {};   // clear cached
        _issues = [];

        for (var validationIndex in validations) {
            if (validations[validationIndex].reset) {
                validations[validationIndex].reset();
            }
        }

        var history = context.history();
        var changes = history.changes();
        var entitiesToCheck = changes.created.concat(changes.modified);
        var graph = history.graph();

        _issues = _flatten(changesValidationIDs.map(function(ruleID) {
            if (_disabledValidations[ruleID]) {
                return [];
            }
            var validation = validations[ruleID];
            return validation(changes, context);
        }));

        entitiesToCheck = _uniq(_flattenDeep(entitiesToCheck.map(function(entity) {
            var entities = [entity];
            if (entity.type === 'node') {  // validate ways if their nodes have changed
                entities = entities.concat(graph.parentWays(entity));
            }
            entities = entities.map(function(entity) {
                if (entity.type !== 'relation') {  // validate relations if their geometries have changed
                    return [entity].concat(graph.parentRelations(entity));
                }
                return entity;
            });
            return entities;
        })));

        var issuesByID = {};

        for (var entityIndex in entitiesToCheck) {
            var entity = entitiesToCheck[entityIndex];
            var entityIssues = validateEntity(entity);
            _issuesByEntityID[entity.id] = entityIssues;
            entityIssues.forEach(function(issue) {
                // Different entities can produce the same issue so store them by
                // the ID to ensure that there are no duplicate issues.
                issuesByID[issue.id()] = issue;
            });
        }

        for (var issueID in issuesByID) {
            _issues.push(issuesByID[issueID]);
        }

        dispatch.call('reload', self, _issues);
    };

    return utilRebind(self, dispatch, 'on');
}
Пример #23
0
export function behaviorHover(context) {
    var dispatch = d3_dispatch('hover');
    var _selection = d3_select(null);
    var _newNodeId = null;
    var _initialNodeID = null;
    var _buttonDown;
    var _altDisables;
    var _ignoreVertex;
    var _target;


    function keydown() {
        if (_altDisables && d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
            _selection.selectAll('.hover')
                .classed('hover-suppressed', true)
                .classed('hover', false);

            _selection
                .classed('hover-disabled', true);

            dispatch.call('hover', this, null);
        }
    }


    function keyup() {
        if (_altDisables && d3_event.keyCode === utilKeybinding.modifierCodes.alt) {
            _selection.selectAll('.hover-suppressed')
                .classed('hover-suppressed', false)
                .classed('hover', true);

            _selection
                .classed('hover-disabled', false);

            dispatch.call('hover', this, _target ? _target.id : null);
        }
    }


    function behavior(selection) {
        _selection = selection;

        if (_initialNodeID) {
            _newNodeId = _initialNodeID;
            _initialNodeID = null;
        } else {
            _newNodeId = null;
        }

        _selection
            .on('mouseover.hover', mouseover)
            .on('mouseout.hover', mouseout)
            .on('mousedown.hover', mousedown);

        d3_select(window)
            .on('keydown.hover', keydown)
            .on('keyup.hover', keyup);


        function mouseover() {
            if (_buttonDown) return;
            var target = d3_event.target;
            enter(target ? target.__data__ : null);
        }


        function mouseout() {
            if (_buttonDown) return;
            var target = d3_event.relatedTarget;
            enter(target ? target.__data__ : null);
        }


        function mousedown() {
            _buttonDown = true;
            d3_select(window)
                .on('mouseup.hover', mouseup, true);
        }


        function mouseup() {
            _buttonDown = false;
            d3_select(window)
                .on('mouseup.hover', null, true);
        }

        function allowsVertex(d) {
            return d.geometry(context.graph()) === 'vertex' || context.presets().allowsVertex(d, context.graph());
        }

        function modeAllowsHover(target) {
            var mode = context.mode();
            if (mode.id === 'add-point') {
                return mode.preset.matchGeometry('vertex') ||
                    (target.type !== 'way' && target.geometry(context.graph()) !== 'vertex');
            }
            return true;
        }

        function enter(datum) {
            if (datum === _target) return;
            _target = datum;

            _selection.selectAll('.hover')
                .classed('hover', false);
            _selection.selectAll('.hover-suppressed')
                .classed('hover-suppressed', false);

            // What are we hovering over?
            var entity, selector;
            if (datum && datum.__featurehash__) {
                entity = datum;
                selector = '.data' + datum.__featurehash__;

            } else if (datum instanceof qaError) {
                entity = datum;
                selector = '.' + datum.service + '.error_id-' + datum.id;

            } else if (datum instanceof osmNote) {
                entity = datum;
                selector = '.note-' + datum.id;

            } else if (datum instanceof osmEntity) {
                entity = datum;
                selector = '.' + entity.id;
                if (entity.type === 'relation') {
                    entity.members.forEach(function(member) { selector += ', .' + member.id; });
                }
            } else if (datum && datum.properties && (datum.properties.entity instanceof osmEntity)) {
                entity = datum.properties.entity;
                selector = '.' + entity.id;
                if (entity.type === 'relation') {
                    entity.members.forEach(function(member) { selector += ', .' + member.id; });
                }
            }

            var mode = context.mode();

            // Update hover state and dispatch event
            if (entity && entity.id !== _newNodeId) {
                // If drawing a way, don't hover on a node that was just placed. #3974

                if ((mode.id === 'draw-line' || mode.id === 'draw-area') && !_newNodeId && entity.type === 'node') {
                    _newNodeId = entity.id;
                    return;
                }

                var suppressed = (_altDisables && d3_event && d3_event.altKey) ||
                    (entity.type === 'node' && _ignoreVertex && !allowsVertex(entity)) ||
                    !modeAllowsHover(entity);
                _selection.selectAll(selector)
                    .classed(suppressed ? 'hover-suppressed' : 'hover', true);

                dispatch.call('hover', this, !suppressed && entity);

            } else {
                dispatch.call('hover', this, null);
            }
        }
    }


    behavior.off = function(selection) {
        selection.selectAll('.hover')
            .classed('hover', false);
        selection.selectAll('.hover-suppressed')
            .classed('hover-suppressed', false);
        selection
            .classed('hover-disabled', false);

        selection
            .on('mouseover.hover', null)
            .on('mouseout.hover', null)
            .on('mousedown.hover', null);

        d3_select(window)
            .on('keydown.hover', null)
            .on('keyup.hover', null);
    };


    behavior.altDisables = function(val) {
        if (!arguments.length) return _altDisables;
        _altDisables = val;
        return behavior;
    };

    behavior.ignoreVertex = function(val) {
        if (!arguments.length) return _ignoreVertex;
        _ignoreVertex = val;
        return behavior;
    };

    behavior.initialNodeID = function(nodeId) {
        _initialNodeID = nodeId;
        return behavior;
    };

    return utilRebind(behavior, dispatch, 'on');
}
Пример #24
0
export function uiFieldWikidata(field) {
    var wikidata = services.wikidata;
    var dispatch = d3_dispatch('change');
    var link = d3_select(null);
    var title = d3_select(null);
    var _wikiURL = '';


    function wiki(selection) {

        var wrap = selection.selectAll('.form-field-input-wrap')
            .data([0]);

        wrap = wrap.enter()
            .append('div')
            .attr('class', 'form-field-input-wrap form-field-input-' + field.type)
            .merge(wrap);


        var list = wrap.selectAll('ul')
            .data([0]);

        list = list.enter()
            .append('ul')
            .attr('class', 'labeled-inputs')
            .merge(list);

        var wikidataProperties = ['identifier', 'label', 'description'];

        var items = list.selectAll('li')
            .data(wikidataProperties);

        // Enter
        var enter = items.enter()
            .append('li')
            .attr('class', function(d) { return 'preset-wikidata-' + d; });

        enter
            .append('span')
            .attr('class', 'label')
            .attr('for', function(d) { return 'preset-input-wikidata-' + d; })
            .text(function(d) { return t('wikidata.' + d); });

        var inputWrap = enter
            .append('div')
            .attr('class', 'input-wrap');

        inputWrap
            .append('input')
            .attr('type', 'text')
            .attr('class', 'preset-input-wikidata')
            .attr('id', function(d) { return 'preset-input-wikidata-' + d; });


        title = wrap.select('.preset-wikidata-identifier input')
            .call(utilNoAuto)
            .merge(title);

        title
            .on('blur', blur)
            .on('change', change);

        var idItem = wrap.select('.preset-wikidata-identifier');

        idItem.select('button')
            .remove();

        link = idItem
            .append('button')
            .attr('class', 'form-field-button wiki-link')
            .attr('title', t('icons.open_wikidata'))
            .attr('tabindex', -1)
            .call(svgIcon('#iD-icon-out-link'))
            .merge(link);

        link
            .on('click', function() {
                d3_event.preventDefault();
                if (_wikiURL) window.open(_wikiURL, '_blank');
            });

        var readOnlyItems = wrap.selectAll('li:not(.preset-wikidata-identifier)');

        readOnlyItems.select('input')
            .classed('disabled', 'true')
            .attr('readonly', 'true');

        readOnlyItems.select('button')
            .remove();

        readOnlyItems.append('button')
            .attr('class', 'form-field-button wiki-link')
            .attr('title', t('icons.copy'))
            .attr('tabindex', -1)
            .call(svgIcon('#iD-operation-copy'))
            .on('click', function() {
                d3_event.preventDefault();
                d3_select(this.parentNode)
                    .select('input')
                    .node()
                    .select();
                document.execCommand('copy');
            });
    }


    function blur() {
        change();
    }


    function change() {
        var syncTags = {
            wikidata: utilGetSetValue(title)
        };
        dispatch.call('change', this, syncTags);
    }


    wiki.tags = function(tags) {
        var value = tags[field.key] || '';
        var matches = value.match(/^Q[0-9]*$/);

        utilGetSetValue(title, value);

        // value in correct format
        if (matches) {
            _wikiURL = 'https://wikidata.org/wiki/' + value;
            wikidata.entityByQID(value, function(qid, entity) {
                var label = '';
                var description = '';

                if (entity.labels && Object.keys(entity.labels).length > 0) {
                    label = entity.labels[Object.keys(entity.labels)[0]].value;
                }
                if (entity.descriptions && Object.keys(entity.descriptions).length > 0) {
                    description = entity.descriptions[Object.keys(entity.descriptions)[0]].value;
                }

                d3_select('.preset-wikidata-label')
                    .style('display', function(){
                        return label.length > 0 ? 'flex' : 'none';
                    })
                    .select('input')
                    .attr('value', label);

                d3_select('.preset-wikidata-description')
                    .style('display', function(){
                        return description.length > 0 ? 'flex' : 'none';
                    })
                    .select('input')
                    .attr('value', description);
            });

        // unrecognized value format
        } else {
            d3_select('.preset-wikidata-label').style('display', 'none');
            d3_select('.preset-wikidata-description').style('display', 'none');
            if (value && value !== '') {
                _wikiURL = 'https://wikidata.org/wiki/Special:Search?search=' + value;
            } else {
                _wikiURL = '';
            }
        }
    };


    wiki.focus = function() {
        title.node().focus();
    };


    return utilRebind(wiki, dispatch, 'on');
}
Пример #25
0
export function uiEntityEditor(context) {
    var dispatch = d3_dispatch('choose');
    var _state = 'select';
    var _coalesceChanges = false;
    var _modified = false;
    var _base;
    var _entityID;
    var _activePreset;
    var _tagReference;

    var presetEditor = uiPresetEditor(context).on('change', changeTags);
    var rawTagEditor = uiRawTagEditor(context).on('change', changeTags);
    var rawMemberEditor = uiRawMemberEditor(context);
    var rawMembershipEditor = uiRawMembershipEditor(context);

    function entityEditor(selection) {
        var entity = context.entity(_entityID);
        var tags = _clone(entity.tags);

        // Header
        var header = selection.selectAll('.header')
            .data([0]);

        // Enter
        var enter = header.enter()
            .append('div')
            .attr('class', 'header fillL cf');

        enter
            .append('button')
            .attr('class', 'fl preset-reset preset-choose')
            .call(svgIcon((textDirection === 'rtl') ? '#iD-icon-forward' : '#iD-icon-backward'));

        enter
            .append('button')
            .attr('class', 'fr preset-close')
            .on('click', function() { context.enter(modeBrowse(context)); })
            .call(svgIcon(_modified ? '#iD-icon-apply' : '#iD-icon-close'));

        enter
            .append('h3')
            .text(t('inspector.edit'));

        // Update
        header = header
            .merge(enter);

        header.selectAll('.preset-reset')
            .on('click', function() {
                dispatch.call('choose', this, _activePreset);
            });


        // Body
        var body = selection.selectAll('.inspector-body')
            .data([0]);

        // Enter
        enter = body.enter()
            .append('div')
            .attr('class', 'inspector-body');

        enter
            .append('div')
            .attr('class', 'preset-list-item inspector-inner')
            .append('div')
            .attr('class', 'preset-list-button-wrap')
            .append('button')
            .attr('class', 'preset-list-button preset-reset')
            .call(tooltip().title(t('inspector.back_tooltip')).placement('bottom'))
            .append('div')
            .attr('class', 'label')
            .append('div')
            .attr('class', 'label-inner');

        enter
            .append('div')
            .attr('class', 'inspector-border preset-editor');

        enter
            .append('div')
            .attr('class', 'inspector-border raw-tag-editor inspector-inner');

        enter
            .append('div')
            .attr('class', 'inspector-border raw-member-editor inspector-inner');

        enter
            .append('div')
            .attr('class', 'raw-membership-editor inspector-inner');

        enter
            .append('input')
            .attr('type', 'text')
            .attr('class', 'key-trap');


        // Update
        body = body
            .merge(enter);

        if (_tagReference) {
            body.selectAll('.preset-list-button-wrap')
                .call(_tagReference.button);

            body.selectAll('.preset-list-item')
                .call(_tagReference.body);
        }

        body.selectAll('.preset-reset')
            .on('click', function() {
                dispatch.call('choose', this, _activePreset);
            });

        body.select('.preset-list-item button')
            .call(uiPresetIcon()
                .geometry(context.geometry(_entityID))
                .preset(_activePreset)
            );


        var label = body.select('.label-inner');
        var nameparts = label.selectAll('.namepart')
            .data(_activePreset.name().split(' - '), function(d) { return d; });

        nameparts.exit()
            .remove();

        nameparts
            .enter()
            .append('div')
            .attr('class', 'namepart')
            .text(function(d) { return d; });


        body.select('.preset-editor')
            .call(presetEditor
                .preset(_activePreset)
                .entityID(_entityID)
                .tags(tags)
                .state(_state)
            );

        body.select('.raw-tag-editor')
            .call(rawTagEditor
                .preset(_activePreset)
                .entityID(_entityID)
                .tags(tags)
                .state(_state)
            );

        if (entity.type === 'relation') {
            body.select('.raw-member-editor')
                .style('display', 'block')
                .call(rawMemberEditor
                    .entityID(_entityID)
                );
        } else {
            body.select('.raw-member-editor')
                .style('display', 'none');
        }

        body.select('.raw-membership-editor')
            .call(rawMembershipEditor
                .entityID(_entityID)
            );

        body.select('.key-trap')
            .on('keydown.key-trap', function() {
                // On tabbing, send focus back to the first field on the inspector-body
                // (probably the `name` field) #4159
                if (d3_event.keyCode === 9 && !d3_event.shiftKey) {
                    d3_event.preventDefault();
                    body.select('input').node().focus();
                }
            });

        context.history()
            .on('change.entity-editor', historyChanged);


        function historyChanged() {
            if (_state === 'hide') return;

            var entity = context.hasEntity(_entityID);
            var graph = context.graph();
            if (!entity) return;

            var match = context.presets().match(entity, graph);
            var activePreset = entityEditor.preset();
            var weakPreset = activePreset && _isEmpty(activePreset.addTags);

            // A "weak" preset doesn't set any tags. (e.g. "Address")
            // Don't replace a weak preset with a fallback preset (e.g. "Point")
            if (!(weakPreset && match.isFallback())) {
                entityEditor.preset(match);
            }
            entityEditor.modified(_base !== graph);
            entityEditor(selection);
        }
    }


    // Tag changes that fire on input can all get coalesced into a single
    // history operation when the user leaves the field.  #2342
    function changeTags(changed, onInput) {
        var entity = context.entity(_entityID);
        var annotation = t('operations.change_tags.annotation');
        var tags = _clone(entity.tags);

        for (var k in changed) {
            if (!k) continue;
            var v = changed[k];
            if (v !== undefined || tags.hasOwnProperty(k)) {
                tags[k] = v;
            }
        }

        if (!onInput) {
            tags = utilCleanTags(tags);
        }

        if (!_isEqual(entity.tags, tags)) {
            if (_coalesceChanges) {
                context.overwrite(actionChangeTags(_entityID, tags), annotation);
            } else {
                context.perform(actionChangeTags(_entityID, tags), annotation);
                _coalesceChanges = !!onInput;
            }
        }
    }


    entityEditor.modified = function(_) {
        if (!arguments.length) return _modified;
        _modified = _;
        d3_selectAll('button.preset-close use')
            .attr('xlink:href', (_modified ? '#iD-icon-apply' : '#iD-icon-close'));
        return entityEditor;
    };


    entityEditor.state = function(_) {
        if (!arguments.length) return _state;
        _state = _;
        return entityEditor;
    };


    entityEditor.entityID = function(_) {
        if (!arguments.length) return _entityID;
        _entityID = _;
        _base = context.graph();
        _coalesceChanges = false;

        // reset the scroll to the top of the inspector
        var body = d3_selectAll('.entity-editor-pane .inspector-body');
        if (!body.empty()) {
            body.node().scrollTop = 0;
        }

        var presetMatch = context.presets().match(context.entity(_entityID), _base);

        return entityEditor
            .preset(presetMatch)
            .modified(false);
    };


    entityEditor.preset = function(_) {
        if (!arguments.length) return _activePreset;
        if (_ !== _activePreset) {
            _activePreset = _;
            _tagReference = uiTagReference(_activePreset.reference(context.geometry(_entityID)), context)
                .showing(false);
        }
        return entityEditor;
    };


    return utilRebind(entityEditor, dispatch, 'on');
}
Пример #26
0
export function rendererBackground(context) {
    var dispatch = d3_dispatch('change');
    var detected = utilDetect();
    var baseLayer = rendererTileLayer(context).projection(context.projection);
    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);
            }
        }

        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() {
        if (context.inIntro()) return;

        var b = background.baseLayerSource();
        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 = [b.imageryUsed()];

        _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 streetside = context.layers().layer('streetside');
        if (streetside && streetside.enabled()) {
            imageryUsed.push('Bing Streetside');
        }

        var mapillary_images = context.layers().layer('mapillary-images');
        if (mapillary_images && mapillary_images.enabled()) {
            imageryUsed.push('Mapillary Images');
        }

        var mapillary_signs = context.layers().layer('mapillary-signs');
        if (mapillary_signs && mapillary_signs.enabled()) {
            imageryUsed.push('Mapillary Signs');
        }

        var openstreetcam_images = context.layers().layer('openstreetcam-images');
        if (openstreetcam_images && openstreetcam_images.enabled()) {
            imageryUsed.push('OpenStreetCam Images');
        }

        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 _find(_backgroundSources, function(d) {
            return d.id && d.id === id;
        });
    };


    background.bing = function() {
        background.baseLayerSource(background.findSource('Bing'));
    };


    background.showsLayer = function(d) {
        return d.id === baseLayer.source().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;
            var feature = {
                type: 'Feature',
                properties: { id: source.id },
                geometry: { type: 'MultiPolygon', coordinates: [ source.polygon ] }
            };

            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 = _find(this.sources(extent), 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 = _find(_backgroundSources, 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');
}
Пример #27
0
export function uiFieldAddress(field, context) {
    var dispatch = d3_dispatch('init', 'change'),
        nominatim = services.geocoder,
        wrap = d3_select(null),
        isInitialized = false,
        entity;

    function getNearStreets() {
        var extent = entity.extent(context.graph()),
            l = extent.center(),
            box = geoExtent(l).padByMeters(200);

        var streets = context.intersects(box)
            .filter(isAddressable)
            .map(function(d) {
                var loc = context.projection([
                    (extent[0][0] + extent[1][0]) / 2,
                    (extent[0][1] + extent[1][1]) / 2]),
                    choice = geoChooseEdge(context.childNodes(d), loc, context.projection);
                return {
                    title: d.tags.name,
                    value: d.tags.name,
                    dist: choice.distance
                };
            })
            .sort(function(a, b) {
                return a.dist - b.dist;
            });

        return _uniqBy(streets, 'value');

        function isAddressable(d) {
            return d.tags.highway && d.tags.name && d.type === 'way';
        }
    }


    function getNearCities() {
        var extent = entity.extent(context.graph()),
            l = extent.center(),
            box = geoExtent(l).padByMeters(200);

        var cities = context.intersects(box)
            .filter(isAddressable)
            .map(function(d) {
                return {
                    title: d.tags['addr:city'] || d.tags.name,
                    value: d.tags['addr:city'] || d.tags.name,
                    dist: geoSphericalDistance(d.extent(context.graph()).center(), l)
                };
            })
            .sort(function(a, b) {
                return a.dist - b.dist;
            });

        return _uniqBy(cities, 'value');


        function isAddressable(d) {
            if (d.tags.name) {
                if (d.tags.admin_level === '8' && d.tags.boundary === 'administrative')
                    return true;
                if (d.tags.border_type === 'city')
                    return true;
                if (d.tags.place === 'city' || d.tags.place === 'town' || d.tags.place === 'village')
                    return true;
            }

            if (d.tags['addr:city'])
                return true;

            return false;
        }
    }

    function getNearValues(key) {
        var extent = entity.extent(context.graph()),
            l = extent.center(),
            box = geoExtent(l).padByMeters(200);

        var results = context.intersects(box)
            .filter(function hasTag(d) {
                return d.tags[key];
            })
            .map(function(d) {
                return {
                    title: d.tags[key],
                    value: d.tags[key],
                    dist: geoSphericalDistance(d.extent(context.graph()).center(), l)
                };
            })
            .sort(function(a, b) {
                return a.dist - b.dist;
            });

        return _uniqBy(results, 'value');
    }


    function initCallback(err, countryCode) {
        if (err) return;

        var addressFormat = _find(dataAddressFormats, function (a) {
            return a && a.countryCodes && _includes(a.countryCodes, countryCode.toLowerCase());
        }) || dataAddressFormats[0];

        var widths = addressFormat.widths || {
            housenumber: 1/3, street: 2/3,
            city: 2/3, state: 1/4, postcode: 1/3
        };

        function row(r) {
            // Normalize widths.
            var total = _reduce(r, function(sum, field) {
                return sum + (widths[field] || 0.5);
            }, 0);

            return r.map(function (field) {
                return {
                    id: field,
                    width: (widths[field] || 0.5) / total
                };
            });
        }

        wrap.selectAll('div.addr-row')
            .data(addressFormat.format)
            .enter()
            .append('div')
            .attr('class', 'addr-row')
            .selectAll('input')
            .data(row)
            .enter()
            .append('input')
            .property('type', 'text')
            .attr('placeholder', function (d) {
                var localkey = d.id + '!' + countryCode.toLowerCase(),
                    tkey = field.strings.placeholders[localkey] ? localkey : d.id;
                return field.t('placeholders.' + tkey);
            })
            .attr('class', function (d) { return 'addr-' + d.id; })
            .call(utilNoAuto)
            .style('width', function (d) { return d.width * 100 + '%'; });

        // Update

        // setup dropdowns for common address tags
        var dropdowns = addressFormat.dropdowns || [
            'city', 'county', 'country', 'district', 'hamlet',
            'neighbourhood', 'place', 'postcode', 'province',
            'quarter', 'state', 'street', 'subdistrict', 'suburb'
        ];

        // If fields exist for any of these tags, create dropdowns to pick nearby values..
        dropdowns.forEach(function(tag) {
            var nearValues = (tag === 'street') ? getNearStreets
                    : (tag === 'city') ? getNearCities
                    : getNearValues;

            wrap.selectAll('input.addr-' + tag)
                .call(d3_combobox()
                    .container(context.container())
                    .minItems(1)
                    .fetcher(function(value, callback) {
                        callback(nearValues('addr:' + tag));
                    })
                );
        });

        wrap.selectAll('input')
            .on('blur', change())
            .on('change', change());

        wrap.selectAll('input:not(.combobox-input)')
            .on('input', change(true));

        dispatch.call('init');
        isInitialized = true;
    }


    function address(selection) {
        isInitialized = false;

        wrap = selection.selectAll('.preset-input-wrap')
            .data([0]);

        wrap = wrap.enter()
            .append('div')
            .attr('class', 'preset-input-wrap')
            .merge(wrap);

        if (nominatim && entity) {
            var center = entity.extent(context.graph()).center();
            nominatim.countryCode(center, initCallback);
        }
    }


    function change(onInput) {
        return function() {
            var tags = {};

            wrap.selectAll('input')
                .each(function (field) {
                    tags['addr:' + field.id] = this.value || undefined;
                });

            dispatch.call('change', this, tags, onInput);
        };
    }


    function updateTags(tags) {
        utilGetSetValue(wrap.selectAll('input'), function (field) {
            return tags['addr:' + field.id] || '';
        });
    }


    address.entity = function(_) {
        if (!arguments.length) return entity;
        entity = _;
        return address;
    };


    address.tags = function(tags) {
        if (isInitialized) {
            updateTags(tags);
        } else {
            dispatch.on('init', function () {
                dispatch.on('init', null);
                updateTags(tags);
            });
        }
    };


    address.focus = function() {
        var node = wrap.selectAll('input').node();
        if (node) node.focus();
    };


    return utilRebind(address, dispatch, 'on');
}
Пример #28
0
import { dispatch as d3_dispatch } from 'd3-dispatch';

import deepEqual from 'fast-deep-equal';
import turf_bboxClip from '@turf/bbox-clip';
import stringify from 'fast-json-stable-stringify';
import martinez from 'martinez-polygon-clipping';

import Protobuf from 'pbf';
import vt from '@mapbox/vector-tile';

import { utilHashcode, utilRebind, utilTiler } from '../util';


var tiler = utilTiler().tileSize(512).margin(1);
var dispatch = d3_dispatch('loadedData');
var _vtCache;


function abortRequest(controller) {
    controller.abort();
}


function vtToGeoJSON(data, tile, mergeCache) {
    var vectorTile = new vt.VectorTile(new Protobuf(data.response));
    var layers = Object.keys(vectorTile.layers);
    if (!Array.isArray(layers)) { layers = [layers]; }

    var features = [];
    layers.forEach(function(layerID) {
        var layer = vectorTile.layers[layerID];
Пример #29
0
  factory: function(el, width, height) {

    var instance = {};

    instance.chart = {};

    var dispatch_ = dispatch("mouseover","mouseleave","click");
    rebind(instance.chart, dispatch_, 'on');

    // Take a 2-column CSV and transform it into a hierarchical structure suitable
    // for a partition layout. The first column is a sequence of step names, from
    // root to leaf, separated by hyphens. The second column is a count of how
    // often that sequence occurred.
    function buildHierarchy(csv) {
      var root = {"name": "root", "children": []};
      for (var i = 0; i < csv.length; i++) {
        var sequence = csv[i][0];
        var size = +csv[i][1];
        if (isNaN(size)) { // e.g. if this is a header row
          continue;
        }
        var parts = sequence.split("-");
        var currentNode = root;
        for (var j = 0; j < parts.length; j++) {
          var children = currentNode["children"];
          var nodeName = parts[j];
          var childNode;
          if (j + 1 < parts.length) {
       // Not yet at the end of the sequence; move down the tree.
     	var foundChild = false;
     	for (var k = 0; k < children.length; k++) {
     	  if (children[k]["name"] == nodeName) {
     	    childNode = children[k];
     	    foundChild = true;
     	    break;
     	  }
     	}
      // If we don't already have a child node for this branch, create it.
     	if (!foundChild) {
     	  childNode = {"name": nodeName, "children": []};
     	  children.push(childNode);
     	}
     	currentNode = childNode;
          } else {
     	// Reached the end of the sequence; create a leaf node.
     	childNode = {"name": nodeName, "size": size};
     	children.push(childNode);
          }
        }
      }
      return root;

    };

    return {

      renderValue: function(x) {

        instance.x = x;

        // x.data should be a data.frame in R so an Javascript Object of Objects
        //     but buildHierarchy expects an Array of Arrays
        //     so use d3.zip and apply to do this
        var json = [];
        if(typeof(x.csvdata) !== "undefined"){
          json = buildHierarchy(
            zip.apply(
              null,
              Object.keys(x.csvdata).map(function(ky){return x.csvdata[ky]})
            )
          );
        } else {
          json = x.data
        }
        instance.json = json;

        draw(el, instance, dispatch_);

      },

      resize: function(width, height) {

        draw(el, instance, dispatch_);

      },

      instance: instance

    };
  }
Пример #30
0
export function uiIntroStartEditing(context, reveal) {
    var dispatch = d3_dispatch('done', 'startEditing'),
        modalSelection = d3_select(null);


    var chapter = {
        title: 'intro.startediting.title'
    };

    function showHelp() {
        reveal('.map-control.help-control',
            t('intro.startediting.help', { button: icon('#icon-help', 'pre-text'), key: t('help.key') }), {
                buttonText: t('intro.ok'),
                buttonCallback: function() { shortcuts(); }
            }
        );
    }

    function shortcuts() {
        reveal('.map-control.help-control',
            t('intro.startediting.shortcuts', { key: t('shortcuts.toggle.key') }), {
                buttonText: t('intro.ok'),
                buttonCallback: function() { showSave(); }
            }
        );
    }

    function showSave() {
        d3_selectAll('.shaded').remove();  // in case user opened keyboard shortcuts
        reveal('#bar button.save',
            t('intro.startediting.save'), {
                buttonText: t('intro.ok'),
                buttonCallback: function() { showStart(); }
            }
        );
    }

    function showStart() {
        d3_selectAll('.shaded').remove();  // in case user opened keyboard shortcuts

        modalSelection = uiModal(context.container());

        modalSelection.select('.modal')
            .attr('class', 'modal-splash modal col6');

        modalSelection.selectAll('.close').remove();

        var startbutton = modalSelection.select('.content')
            .attr('class', 'fillL')
            .append('button')
                .attr('class', 'modal-section huge-modal-button')
                .on('click', function() {
                    modalSelection.remove();
                });

            startbutton
                .append('svg')
                .attr('class', 'illustration')
                .append('use')
                .attr('xlink:href', '#logo-walkthrough');

            startbutton
                .append('h2')
                .text(t('intro.startediting.start'));

        dispatch.call('startEditing');
    }


    chapter.enter = function() {
        showHelp();
    };


    chapter.exit = function() {
        modalSelection.remove();
        d3_selectAll('.shaded').remove();  // in case user opened keyboard shortcuts
    };


    return utilRebind(chapter, dispatch, 'on');
}