Remove old parser

Remove old parser

--- src/parser.coffee
+++ src/parser.coffee
@@ -1,607 +0,0 @@
-URLHandler = require './urlhandler.coffee'
-VASTResponse = require './response.coffee'
-VASTAd = require './ad.coffee'
-VASTAdExtension = require './extension.coffee'
-VASTAdExtensionChild = require './extensionchild.coffee'
-VASTUtil = require './util.coffee'
-VASTCreativeLinear = require('./creative.coffee').VASTCreativeLinear
-VASTCreativeCompanion = require('./creative.coffee').VASTCreativeCompanion
-VASTCreativeNonLinear = require('./creative.coffee').VASTCreativeNonLinear
-VASTMediaFile = require './mediafile.coffee'
-VASTIcon = require './icon.coffee'
-VASTCompanionAd = require './companionad.coffee'
-VASTNonLinear = require './nonlinear.coffee'
-EventEmitter = require('events').EventEmitter
-
-DEFAULT_MAX_WRAPPER_WIDTH = 10
-
-DEFAULT_EVENT_DATA =
-    ERRORCODE  : 900
-    extensions : []
-
-class VASTParser
-    maxWrapperDepth = null
-    URLTemplateFilters = []
-
-    @addURLTemplateFilter: (func) ->
-        URLTemplateFilters.push(func) if typeof func is 'function'
-        return
-
-    @removeURLTemplateFilter: () -> URLTemplateFilters.pop()
-    @countURLTemplateFilters: () -> URLTemplateFilters.length
-    @clearUrlTemplateFilters: () -> URLTemplateFilters = []
-
-    @parse: (url, options, cb) ->
-        if not cb
-            cb = options if typeof options is 'function'
-            options = {}
-
-        maxWrapperDepth = options.wrapperLimit || DEFAULT_MAX_WRAPPER_WIDTH
-        options.wrapperDepth = 0
-
-        @_parse url, null, options, (err, response) ->
-            cb(response, err)
-
-    @load: (xml, options, cb) ->
-        if not cb
-            cb = options if typeof options is 'function'
-            options = {}
-
-        @parseXmlDocument(null, [], options, xml, cb)
-
-    @vent = new EventEmitter()
-    @track: (templates, errorCode, data...) ->
-        @vent.emit 'VAST-error', VASTUtil.merge(DEFAULT_EVENT_DATA, errorCode, data...)
-        VASTUtil.track(templates, errorCode)
-
-    @on: (eventName, cb) ->
-        @vent.on eventName, cb
-
-    @once: (eventName, cb) ->
-        @vent.once eventName, cb
-
-    @off: (eventName, cb) ->
-        @vent.removeListener eventName, cb
-
-    @_parse: (url, parentURLs, options, cb) ->
-        # Process url with defined filter
-        url = filter(url) for filter in URLTemplateFilters
-
-        parentURLs ?= []
-        parentURLs.push url
-
-        @vent.emit 'resolving', { url: url }
-
-        URLHandler.get url, options, (err, xml) =>
-            @vent.emit 'resolved', { url: url }
-
-            return cb(err) if err?
-            @parseXmlDocument(url, parentURLs, options, xml, cb)
-
-    @parseXmlDocument: (url, parentURLs, options, xml, cb) =>
-        # Current VAST depth
-        wrapperDepth = options.wrapperDepth++
-
-        response = new VASTResponse()
-
-        unless xml?.documentElement? and xml.documentElement.nodeName is "VAST"
-            return cb(new Error('Invalid VAST XMLDocument'))
-
-        for node in xml.documentElement.childNodes
-            if node.nodeName is 'Error'
-                response.errorURLTemplates.push (@parseNodeText node)
-
-        for node in xml.documentElement.childNodes
-            if node.nodeName is 'Ad'
-                ad = @parseAdElement node
-                if ad?
-                    response.ads.push ad
-                else
-                    # VAST version of response not supported.
-                    @track(response.errorURLTemplates, ERRORCODE: 101)
-
-        complete = () =>
-            for ad, index in response.ads by -1
-                # Still some Wrappers URL to be resolved -> continue
-                return if ad.nextWrapperURL?
-
-            # We've to wait for all <Ad> elements to be parsed before handling error so we can:
-            # - Send computed extensions data
-            # - Ping all <Error> URIs defined across VAST files
-            if wrapperDepth is 0
-                # No Ad case - The parser never bump into an <Ad> element
-                if response.ads.length is 0
-                    @track(response.errorURLTemplates, ERRORCODE: 303)
-                else
-                    for ad, index in response.ads by -1
-                        # - Error encountred while parsing
-                        # - No Creative case - The parser has dealt with soma <Ad><Wrapper> or/and an <Ad><Inline> elements
-                        # but no creative was found
-                        if ad.errorCode or ad.creatives.length is 0
-                            @track(
-                                ad.errorURLTemplates.concat(response.errorURLTemplates),
-                                { ERRORCODE: ad.errorCode || 303 },
-                                { ERRORMESSAGE: ad.errorMessage || '' },
-                                { extensions : ad.extensions },
-                                { system: ad.system }
-                            )
-                            response.ads.splice(index, 1)
-
-            cb(null, response)
-
-        loopIndex = response.ads.length
-        while loopIndex--
-            ad = response.ads[loopIndex]
-            continue unless ad.nextWrapperURL?
-            do (ad) =>
-                if parentURLs.length >= maxWrapperDepth or ad.nextWrapperURL in parentURLs
-                    # Wrapper limit reached, as defined by the video player.
-                    # Too many Wrapper responses have been received with no InLine response.
-                    ad.errorCode = 302
-                    delete ad.nextWrapperURL
-                    return
-
-                # Get full URL
-                ad.nextWrapperURL = @resolveVastAdTagURI(ad.nextWrapperURL, url)
-
-                @_parse ad.nextWrapperURL, parentURLs, options, (err, wrappedResponse) =>
-                    delete ad.nextWrapperURL
-
-                    if err?
-                        # Timeout of VAST URI provided in Wrapper element, or of VAST URI provided in a subsequent Wrapper element.
-                        # (URI was either unavailable or reached a timeout as defined by the video player.)
-                        ad.errorCode = 301
-                        ad.errorMessage = err.message
-                        complete()
-                        return
-
-                    if wrappedResponse?.errorURLTemplates?
-                        response.errorURLTemplates = response.errorURLTemplates.concat wrappedResponse.errorURLTemplates
-
-                    if wrappedResponse.ads.length == 0
-                        # No ads returned by the wrappedResponse, discard current <Ad><Wrapper> creatives
-                        ad.creatives = []
-                    else
-                        index = response.ads.indexOf(ad)
-                        response.ads.splice(index, 1)
-
-                        for wrappedAd in wrappedResponse.ads
-                            @mergeWrapperAdData wrappedAd, ad
-                            response.ads.splice ++index, 0, wrappedAd
-
-                    complete()
-
-        complete()
-
-    # Convert relative vastAdTagUri
-    @resolveVastAdTagURI: (vastAdTagUrl, originalUrl) ->
-        if vastAdTagUrl.indexOf('//') == 0
-            protocol = location.protocol
-            return "#{protocol}#{vastAdTagUrl}"
-
-        if vastAdTagUrl.indexOf('://') == -1
-            # Resolve relative URLs (mainly for unit testing)
-            baseURL = originalUrl.slice(0, originalUrl.lastIndexOf('/'))
-            return "#{baseURL}/#{vastAdTagUrl}"
-
-        return vastAdTagUrl
-
-    # Merge ad tracking URLs / extensions data into wrappedAd
-    @mergeWrapperAdData: (wrappedAd, ad) ->
-        wrappedAd.errorURLTemplates = ad.errorURLTemplates.concat wrappedAd.errorURLTemplates
-        wrappedAd.impressionURLTemplates = ad.impressionURLTemplates.concat wrappedAd.impressionURLTemplates
-        wrappedAd.extensions = ad.extensions.concat wrappedAd.extensions
-
-        for creative in wrappedAd.creatives
-            if ad.trackingEvents?[creative.type]?
-                for eventName, urls of ad.trackingEvents[creative.type]
-                    creative.trackingEvents[eventName] or= []
-                    creative.trackingEvents[eventName] = creative.trackingEvents[eventName].concat urls
-
-        if ad.videoClickTrackingURLTemplates?.length
-            for creative in wrappedAd.creatives
-                if creative.type is 'linear'
-                    creative.videoClickTrackingURLTemplates = creative.videoClickTrackingURLTemplates.concat ad.videoClickTrackingURLTemplates
-
-        if ad.videoCustomClickURLTemplates?.length
-            for creative in wrappedAd.creatives
-                if creative.type is 'linear'
-                    creative.videoCustomClickURLTemplates = creative.videoCustomClickURLTemplates.concat ad.videoCustomClickURLTemplates
-
-        # VAST 2.0 support - Use Wrapper/linear/clickThrough when Inline/Linear/clickThrough is null
-        if ad.videoClickThroughURLTemplate?
-            for creative in wrappedAd.creatives
-                if creative.type is 'linear' and not creative.videoClickThroughURLTemplate?
-                    creative.videoClickThroughURLTemplate = ad.videoClickThroughURLTemplate
-
-    @childByName: (node, name) ->
-        for child in node.childNodes
-            if child.nodeName is name
-                return child
-
-    @childsByName: (node, name) ->
-        childs = []
-        for child in node.childNodes
-            if child.nodeName is name
-                childs.push child
-        return childs
-
-
-    @parseAdElement: (adElement) ->
-        for adTypeElement in adElement.childNodes
-            continue unless adTypeElement.nodeName in ["Wrapper", "InLine"]
-
-            @copyNodeAttribute "id", adElement, adTypeElement
-            @copyNodeAttribute "sequence", adElement, adTypeElement
-
-            if adTypeElement.nodeName is "Wrapper"
-                return @parseWrapperElement adTypeElement
-            else if adTypeElement.nodeName is "InLine"
-                return @parseInLineElement adTypeElement
-
-    @parseWrapperElement: (wrapperElement) ->
-        ad = @parseInLineElement wrapperElement
-        wrapperURLElement = @childByName wrapperElement, "VASTAdTagURI"
-        if wrapperURLElement?
-            ad.nextWrapperURL = @parseNodeText wrapperURLElement
-        else
-            wrapperURLElement = @childByName wrapperElement, "VASTAdTagURL"
-            if wrapperURLElement?
-                ad.nextWrapperURL = @parseNodeText @childByName wrapperURLElement, "URL"
-
-        for wrapperCreativeElement in ad.creatives
-            if wrapperCreativeElement.type in ['linear', 'nonlinear']
-                # TrackingEvents Linear / NonLinear
-                if wrapperCreativeElement.trackingEvents?
-                    ad.trackingEvents or= {}
-                    ad.trackingEvents[wrapperCreativeElement.type] or= {}
-                    for eventName, urls of wrapperCreativeElement.trackingEvents
-                        ad.trackingEvents[wrapperCreativeElement.type][eventName] or= []
-                        ad.trackingEvents[wrapperCreativeElement.type][eventName].push url for url in urls
-                # ClickTracking
-                if wrapperCreativeElement.videoClickTrackingURLTemplates?
-                    ad.videoClickTrackingURLTemplates or= [] # tmp property to save wrapper tracking URLs until they are merged
-                    ad.videoClickTrackingURLTemplates.push item for item in wrapperCreativeElement.videoClickTrackingURLTemplates
-                # ClickThrough
-                if wrapperCreativeElement.videoClickThroughURLTemplate?
-                    ad.videoClickThroughURLTemplate = wrapperCreativeElement.videoClickThroughURLTemplate
-                # CustomClick
-                if wrapperCreativeElement.videoCustomClickURLTemplates?
-                    ad.videoCustomClickURLTemplates or= [] # tmp property to save wrapper tracking URLs until they are merged
-                    ad.videoCustomClickURLTemplates.push item for item in wrapperCreativeElement.videoCustomClickURLTemplates
-
-        if ad.nextWrapperURL?
-            return ad
-
-    @parseInLineElement: (inLineElement) ->
-        ad = new VASTAd()
-        ad.id = inLineElement.getAttribute("id") || null
-        ad.sequence = inLineElement.getAttribute("sequence") || null
-
-        for node in inLineElement.childNodes
-            switch node.nodeName
-                when "Error"
-                    ad.errorURLTemplates.push (@parseNodeText node)
-
-                when "Impression"
-                    ad.impressionURLTemplates.push (@parseNodeText node)
-
-                when "Creatives"
-                    for creativeElement in @childsByName(node, "Creative")
-                        creativeAttributes =
-                            id           : creativeElement.getAttribute('id') or null
-                            adId         : @parseCreativeAdIdAttribute(creativeElement)
-                            sequence     : creativeElement.getAttribute('sequence') or null
-                            apiFramework : creativeElement.getAttribute('apiFramework') or null
-
-                        for creativeTypeElement in creativeElement.childNodes
-                            switch creativeTypeElement.nodeName
-                                when "Linear"
-                                    creative = @parseCreativeLinearElement creativeTypeElement, creativeAttributes
-                                    if creative
-                                        ad.creatives.push creative
-                                when "NonLinearAds"
-                                    creative = @parseNonLinear creativeTypeElement, creativeAttributes
-                                    if creative
-                                        ad.creatives.push creative
-                                when "CompanionAds"
-                                    creative = @parseCompanionAd creativeTypeElement, creativeAttributes
-                                    if creative
-                                        ad.creatives.push creative
-                when "Extensions"
-                    @parseExtension(ad.extensions, @childsByName(node, "Extension"))
-
-                when "AdSystem"
-                    ad.system =
-                        value : @parseNodeText node
-                        version : node.getAttribute("version") || null
-
-                when "AdTitle"
-                    ad.title = @parseNodeText node
-
-                when "Description"
-                    ad.description = @parseNodeText node
-
-                when "Advertiser"
-                    ad.advertiser = @parseNodeText node
-
-                when "Pricing"
-                    ad.pricing =
-                        value    : @parseNodeText node
-                        model    : node.getAttribute("model") || null
-                        currency : node.getAttribute("currency") || null
-
-                when "Survey"
-                    ad.survey = @parseNodeText node
-
-        return ad
-
-    @parseExtension: (collection, extensions) ->
-        for extNode in extensions
-            ext = new VASTAdExtension()
-
-            if extNode.attributes
-                for extNodeAttr in extNode.attributes
-                    ext.attributes[extNodeAttr.nodeName] = extNodeAttr.nodeValue;
-
-            for childNode in extNode.childNodes
-                txt = @parseNodeText(childNode)
-
-                # ignore comments / empty value
-                if childNode.nodeName isnt '#comment' and txt isnt ''
-                    extChild = new VASTAdExtensionChild()
-                    extChild.name = childNode.nodeName
-                    extChild.value = txt
-
-                    if childNode.attributes
-                        for extChildNodeAttr in childNode.attributes
-                            extChild.attributes[extChildNodeAttr.nodeName] = extChildNodeAttr.nodeValue;
-
-                    ext.children.push extChild
-
-            collection.push ext
-
-    @parseCreativeLinearElement: (creativeElement, creativeAttributes) ->
-        creative = new VASTCreativeLinear(creativeAttributes)
-
-        creative.duration = @parseDuration @parseNodeText(@childByName(creativeElement, "Duration"))
-        skipOffset = creativeElement.getAttribute("skipoffset")
-
-        if not skipOffset? then creative.skipDelay = null
-        else if skipOffset.charAt(skipOffset.length - 1) is "%" and  creative.duration != -1
-            percent = parseInt(skipOffset, 10)
-            creative.skipDelay = creative.duration * (percent / 100)
-        else
-            creative.skipDelay = @parseDuration skipOffset
-
-        videoClicksElement = @childByName(creativeElement, "VideoClicks")
-        if videoClicksElement?
-            creative.videoClickThroughURLTemplate = @parseNodeText(@childByName(videoClicksElement, "ClickThrough"))
-            for clickTrackingElement in @childsByName(videoClicksElement, "ClickTracking")
-                creative.videoClickTrackingURLTemplates.push @parseNodeText(clickTrackingElement)
-            for customClickElement in @childsByName(videoClicksElement, "CustomClick")
-                creative.videoCustomClickURLTemplates.push @parseNodeText(customClickElement)
-
-        adParamsElement = @childByName(creativeElement, "AdParameters")
-        if adParamsElement?
-            creative.adParameters = @parseNodeText(adParamsElement)
-
-        for trackingEventsElement in @childsByName(creativeElement, "TrackingEvents")
-            for trackingElement in @childsByName(trackingEventsElement, "Tracking")
-                eventName = trackingElement.getAttribute("event")
-                trackingURLTemplate = @parseNodeText(trackingElement)
-                if eventName? and trackingURLTemplate?
-                    if eventName == "progress"
-                        offset = trackingElement.getAttribute("offset")
-                        if not offset
-                            continue
-                        if offset.charAt(offset.length - 1) == '%'
-                            eventName = "progress-#{offset}"
-                        else
-                            eventName = "progress-#{Math.round(@parseDuration offset)}"
-
-                    creative.trackingEvents[eventName] ?= []
-                    creative.trackingEvents[eventName].push trackingURLTemplate
-
-        for mediaFilesElement in @childsByName(creativeElement, "MediaFiles")
-            for mediaFileElement in @childsByName(mediaFilesElement, "MediaFile")
-                mediaFile = new VASTMediaFile()
-                mediaFile.id = mediaFileElement.getAttribute("id")
-                mediaFile.fileURL = @parseNodeText(mediaFileElement)
-                mediaFile.deliveryType = mediaFileElement.getAttribute("delivery")
-                mediaFile.codec = mediaFileElement.getAttribute("codec")
-                mediaFile.mimeType = mediaFileElement.getAttribute("type")
-                mediaFile.apiFramework = mediaFileElement.getAttribute("apiFramework")
-                mediaFile.bitrate = parseInt mediaFileElement.getAttribute("bitrate") or 0
-                mediaFile.minBitrate = parseInt mediaFileElement.getAttribute("minBitrate") or 0
-                mediaFile.maxBitrate = parseInt mediaFileElement.getAttribute("maxBitrate") or 0
-                mediaFile.width = parseInt mediaFileElement.getAttribute("width") or 0
-                mediaFile.height = parseInt mediaFileElement.getAttribute("height") or 0
-
-                scalable = mediaFileElement.getAttribute("scalable")
-                if scalable and typeof scalable is "string"
-                  scalable = scalable.toLowerCase()
-                  if scalable is "true" then mediaFile.scalable = true
-                  else if scalable is "false" then mediaFile.scalable = false
-
-                maintainAspectRatio = mediaFileElement.getAttribute("maintainAspectRatio")
-                if maintainAspectRatio and typeof maintainAspectRatio is "string"
-                  maintainAspectRatio = maintainAspectRatio.toLowerCase()
-                  if maintainAspectRatio is "true" then mediaFile.maintainAspectRatio = true
-                  else if maintainAspectRatio is "false" then mediaFile.maintainAspectRatio = false
-
-                creative.mediaFiles.push mediaFile
-
-        iconsElement = @childByName(creativeElement, "Icons")
-        if iconsElement?
-            for iconElement in @childsByName(iconsElement, "Icon")
-                icon = new VASTIcon()
-                icon.program = iconElement.getAttribute("program")
-                icon.height = parseInt iconElement.getAttribute("height") or 0
-                icon.width = parseInt iconElement.getAttribute("width") or 0
-                icon.xPosition = @parseXPosition iconElement.getAttribute("xPosition")
-                icon.yPosition = @parseYPosition iconElement.getAttribute("yPosition")
-                icon.apiFramework = iconElement.getAttribute("apiFramework")
-                icon.offset = @parseDuration iconElement.getAttribute("offset")
-                icon.duration = @parseDuration iconElement.getAttribute("duration")
-
-                for htmlElement in @childsByName(iconElement, "HTMLResource")
-                    icon.type = htmlElement.getAttribute("creativeType") or 'text/html'
-                    icon.htmlResource = @parseNodeText(htmlElement)
-
-                for iframeElement in @childsByName(iconElement, "IFrameResource")
-                    icon.type = iframeElement.getAttribute("creativeType") or 0
-                    icon.iframeResource = @parseNodeText(iframeElement)
-
-                for staticElement in @childsByName(iconElement, "StaticResource")
-                    icon.type = staticElement.getAttribute("creativeType") or 0
-                    icon.staticResource = @parseNodeText(staticElement)
-
-                iconClicksElement = @childByName(iconElement, "IconClicks")
-                if iconClicksElement?
-                    icon.iconClickThroughURLTemplate = @parseNodeText(@childByName(iconClicksElement, "IconClickThrough"))
-                    for iconClickTrackingElement in @childsByName(iconClicksElement, "IconClickTracking")
-                        icon.iconClickTrackingURLTemplates.push @parseNodeText(iconClickTrackingElement)
-
-                icon.iconViewTrackingURLTemplate = @parseNodeText(@childByName(iconElement, "IconViewTracking"))
-
-                creative.icons.push icon
-
-        return creative
-
-    @parseNonLinear: (creativeElement, creativeAttributes) ->
-        creative = new VASTCreativeNonLinear(creativeAttributes)
-
-        for trackingEventsElement in @childsByName(creativeElement, "TrackingEvents")
-          for trackingElement in @childsByName(trackingEventsElement, "Tracking")
-            eventName = trackingElement.getAttribute("event")
-            trackingURLTemplate = @parseNodeText(trackingElement)
-            if eventName? and trackingURLTemplate?
-              creative.trackingEvents[eventName] ?= []
-              creative.trackingEvents[eventName].push trackingURLTemplate
-
-        for nonlinearResource in @childsByName(creativeElement, "NonLinear")
-            nonlinearAd = new VASTNonLinear()
-            nonlinearAd.id = nonlinearResource.getAttribute("id") or null
-            nonlinearAd.width = nonlinearResource.getAttribute("width")
-            nonlinearAd.height = nonlinearResource.getAttribute("height")
-            nonlinearAd.expandedWidth = nonlinearResource.getAttribute("expandedWidth")
-            nonlinearAd.expandedHeight = nonlinearResource.getAttribute("expandedHeight")
-            nonlinearAd.scalable = @parseBoolean nonlinearResource.getAttribute("scalable")
-            nonlinearAd.maintainAspectRatio = @parseBoolean nonlinearResource.getAttribute("maintainAspectRatio")
-            nonlinearAd.minSuggestedDuration = @parseDuration nonlinearResource.getAttribute("minSuggestedDuration")
-            nonlinearAd.apiFramework = nonlinearResource.getAttribute("apiFramework")
-
-            for htmlElement in @childsByName(nonlinearResource, "HTMLResource")
-                nonlinearAd.type = htmlElement.getAttribute("creativeType") or 'text/html'
-                nonlinearAd.htmlResource = @parseNodeText(htmlElement)
-
-            for iframeElement in @childsByName(nonlinearResource, "IFrameResource")
-                nonlinearAd.type = iframeElement.getAttribute("creativeType") or 0
-                nonlinearAd.iframeResource = @parseNodeText(iframeElement)
-
-            for staticElement in @childsByName(nonlinearResource, "StaticResource")
-                nonlinearAd.type = staticElement.getAttribute("creativeType") or 0
-                nonlinearAd.staticResource = @parseNodeText(staticElement)
-
-            adParamsElement = @childByName(nonlinearResource, "AdParameters")
-            if adParamsElement?
-                nonlinearAd.adParameters = @parseNodeText(adParamsElement)
-
-            nonlinearAd.nonlinearClickThroughURLTemplate = @parseNodeText(@childByName(nonlinearResource, "NonLinearClickThrough"))
-            for clickTrackingElement in @childsByName(nonlinearResource, "NonLinearClickTracking")
-                nonlinearAd.nonlinearClickTrackingURLTemplates.push @parseNodeText(clickTrackingElement)
-
-            creative.variations.push nonlinearAd
-
-        return creative
-
-    @parseCompanionAd: (creativeElement, creativeAttributes) ->
-        creative = new VASTCreativeCompanion(creativeAttributes)
-
-        for companionResource in @childsByName(creativeElement, "Companion")
-            companionAd = new VASTCompanionAd()
-            companionAd.id = companionResource.getAttribute("id") or null
-            companionAd.width = companionResource.getAttribute("width")
-            companionAd.height = companionResource.getAttribute("height")
-            companionAd.companionClickTrackingURLTemplates = []
-            for htmlElement in @childsByName(companionResource, "HTMLResource")
-                companionAd.type = htmlElement.getAttribute("creativeType") or 'text/html'
-                companionAd.htmlResource = @parseNodeText(htmlElement)
-            for iframeElement in @childsByName(companionResource, "IFrameResource")
-                companionAd.type = iframeElement.getAttribute("creativeType") or 0
-                companionAd.iframeResource = @parseNodeText(iframeElement)
-            for staticElement in @childsByName(companionResource, "StaticResource")
-                companionAd.type = staticElement.getAttribute("creativeType") or 0
-                for child in @childsByName(companionResource, "AltText")
-                    companionAd.altText = @parseNodeText(child)
-                companionAd.staticResource = @parseNodeText(staticElement)
-            for trackingEventsElement in @childsByName(companionResource, "TrackingEvents")
-                for trackingElement in @childsByName(trackingEventsElement, "Tracking")
-                    eventName = trackingElement.getAttribute("event")
-                    trackingURLTemplate = @parseNodeText(trackingElement)
-                    if eventName? and trackingURLTemplate?
-                        companionAd.trackingEvents[eventName] ?= []
-                        companionAd.trackingEvents[eventName].push trackingURLTemplate
-            for clickTrackingElement in @childsByName(companionResource, "CompanionClickTracking")
-              companionAd.companionClickTrackingURLTemplates.push @parseNodeText(clickTrackingElement)
-            companionAd.companionClickThroughURLTemplate = @parseNodeText(@childByName(companionResource, "CompanionClickThrough"))
-            companionAd.companionClickTrackingURLTemplate = @parseNodeText(@childByName(companionResource, "CompanionClickTracking"))
-            creative.variations.push companionAd
-
-        return creative
-
-    @parseDuration: (durationString) ->
-        unless (durationString?)
-            return -1
-        # Some VAST doesn't have an HH:MM:SS duration format but instead jus the number of seconds
-        if VASTUtil.isNumeric(durationString)
-            return parseInt durationString
-
-        durationComponents = durationString.split(":")
-        if durationComponents.length != 3
-            return -1
-
-        secondsAndMS = durationComponents[2].split(".")
-        seconds = parseInt secondsAndMS[0]
-        if secondsAndMS.length == 2
-            seconds += parseFloat "0." + secondsAndMS[1]
-
-        minutes = parseInt durationComponents[1] * 60
-        hours = parseInt durationComponents[0] * 60 * 60
-
-        if isNaN(hours) or isNaN(minutes) or isNaN(seconds) or minutes > 60 * 60 or seconds > 60
-            return -1
-        return hours + minutes + seconds
-
-    @parseXPosition: (xPosition) ->
-        if xPosition in ["left", "right"]
-            return xPosition
-
-        return parseInt xPosition or 0
-
-    @parseYPosition: (yPosition) ->
-        if yPosition in ["top", "bottom"]
-            return yPosition
-
-        return parseInt yPosition or 0
-
-    @parseBoolean: (booleanString) ->
-        return booleanString in ['true', 'TRUE', '1']
-
-    # Parsing node text for legacy support
-    @parseNodeText: (node) ->
-        return node and (node.textContent or node.text or '').trim()
-
-    @copyNodeAttribute: (attributeName, nodeSource, nodeDestination) ->
-        attributeValue = nodeSource.getAttribute attributeName
-        if attributeValue
-            nodeDestination.setAttribute attributeName, attributeValue
-
-    @parseCreativeAdIdAttribute: (creativeElement) ->
-        return creativeElement.getAttribute('AdID') or  # VAST 2 spec
-               creativeElement.getAttribute('adID') or  # VAST 3 spec
-               creativeElement.getAttribute('adId') or  # VAST 4 spec
-               null
-
-module.exports = VASTParser


Load map markers on page load

Load map markers on page load

--- web2/src/views/map.cjsx
+++ web2/src/views/map.cjsx
@@ -56,10 +56,11 @@
         @markerLayer = L.featureGroup([])
         @markerLayer.addTo @map

-        # TODO load markers initially
         @map.on 'moveend', (ev) =>
             @actions.setZoom @map.getZoom()
             @actions.setCenter @map.getCenter()
+
+        @refreshMap()

     componentDidUpdate: (prevProps, prevState) ->
         needsRefresh = false
@@ -77,36 +78,38 @@
         if prevProps.filterUsers isnt @props.filterUsers
             needsRefresh = true

+        @refreshMap() if needsRefresh
+
+    componentWillUnmount: ->
+        @actions.setZoom @map.getZoom()
+        @actions.setCenter @map.getCenter()
+
+    refreshMap: ->
         _qs = (key, values) ->
             # superagent can't deal with arrays in query params :-(
             values
                 .map (v) -> "#{key}[]=#{v}"
                 .join '&'

-        if needsRefresh
-            bounds = @map.getBounds()
-            minll = bounds.getSouthWest()
-            maxll = bounds.getNorthEast()
-            typeIds = @props.selectedTypes.toArray().map @typeToId
-            request
-                .get 'https://gc.funkenburg.net/api/geocaches'
-                .query excludeDisabled: 1
-                .query _qs 'bounds', [minll.lat, minll.lng, maxll.lat, maxll.lng]
-                .query _qs 'typeIds', typeIds
-                .query _qs 'excludeFinds', @props.filterUsers.toArray()
-                .end (err, res) =>
-                    if err or res.status isnt 200
-                        return console.log "Geocache download failed (#{res?.status}): #{err}"
-
-                    @markerLayer.clearLayers()
-                    for gc in res.body
-                        icon = @icons[gc.CacheType.GeocacheTypeId]
-                        marker = L.marker [gc.Latitude, gc.Longitude], {icon}
-                        marker.addTo @markerLayer
-
-    componentWillUnmount: ->
-        @actions.setZoom @map.getZoom()
-        @actions.setCenter @map.getCenter()
+        bounds = @map.getBounds()
+        minll = bounds.getSouthWest()
+        maxll = bounds.getNorthEast()
+        typeIds = @props.selectedTypes.toArray().map @typeToId
+        request
+            .get 'https://gc.funkenburg.net/api/geocaches'
+            .query excludeDisabled: 1
+            .query _qs 'bounds', [minll.lat, minll.lng, maxll.lat, maxll.lng]
+            .query _qs 'typeIds', typeIds
+            .query _qs 'excludeFinds', @props.filterUsers.toArray()
+            .end (err, res) =>
+                if err or res.status isnt 200
+                    return console.log "Geocache download failed (#{res?.status}): #{err}"
+
+                @markerLayer.clearLayers()
+                for gc in res.body
+                    icon = @icons[gc.CacheType.GeocacheTypeId]
+                    marker = L.marker [gc.Latitude, gc.Longitude], {icon}
+                    marker.addTo @markerLayer

     typeToId: (type) ->
         geocaches.types[type]


Add experimental Player#_nextActionOnComplete.

Not tested or supported in tests yet.

--- src/player.coffee
+++ src/player.coffee
@@ -123,6 +123,7 @@
       @animations.add 'jump', [18..31], 24
       @animations.add 'land', [32,33,18,17], 24
       @animations.add 'end', [34...53], 12
+      @_nextActionOnComplete = null

     _initDebugging: (gui) ->
       @debugNamespace = 'player'
@@ -191,7 +192,10 @@

     _changeAnimation: ->
       unless @_isInMidAir() or @nextAction is 'none'
-        @playAnimation @nextAction, @animation?.loop
+        animation = @playAnimation @nextAction, @animation?.loop
+        if animation and @_nextActionOnComplete?
+          animation.onComplete.addOnce @_nextActionOnComplete, @
+          @_nextActionOnComplete = null
         return

       switch @nextState


spacing

spacing

--- src/routes.coffee
+++ src/routes.coffee
@@ -38,7 +38,7 @@
     console.warn "controller not found: " + controllerName, e
     next()
     return
-  data=null
+  data = null
   if typeof controller[methodName] is 'function'
     actionMethod = controller[methodName].bind controller
     actionMethod req, res, next


Set default delay to 0

Set default delay to 0

--- src/server.coffee
+++ src/server.coffee
@@ -10,7 +10,7 @@
           enabled: false
           send: @_doSendEmail.bind(@)
           # The delay (ms) between sent emails. Useful to prevent spamming recipients and being rejected as spam.
-          delay: 5000
+          delay: 0
           getTemplate: -> @_getDefaultEmailTemplate()
           getTemplateData: (args) -> args
           renderTemplate: (template, templateData) -> _.template(template)(templateData)


345 implementation

345 implementation

--- src/server.coffee
+++ src/server.coffee
@@ -46,31 +46,47 @@
       flags: "w+"

   #
-  # if buffer has content, write buffer directly to the kernel via /dev/ttyO1 pipe
-  # at 15ms resolution. recursively call this function using setInterval()
+  # write buffer directly to the kernel via /dev/ttyO1 pipe
+  # if buffer has content, at 30ms resolution. recursively call this function on success
   #
   _write_pipe: ->
     setInterval (=>
-      if @buffer.length > 0
-        instruction = @buffer.shift()
-        @ws.write(instruction)
-    ), 1
+      return if @buffer.length == 0
+      console.log "writing buffer #{@buffer}"
+      @ws.write(@buffer, (err, written) =>
+        throw err if err
+        @buffer = ''
+      )
+    ), 15

   #
-  # convert halo rgba string to a UART instruction and push to buffer, padding with black if needed
+  # convert halo rgba string to a UART instruction
   # @example
-  #   _data_to_buffer({ color: '000,110,255,000\n000,110,255,000\n' })
+  #   _data_to_instruction({ color: '000,110,255,000\n' })
+  #   # => '4,1,000,110,255,000;'
+  #
+  _data_to_instruction: (data) ->
+    # break colors string into array
+    colors = data.color.split '\n'
+    colors.pop()
+
+    # remove `v` value from each color
+    colors = colors.map (c) -> c.slice(0,-4)
+
+    # build our TTY instruction
+    instruction = '345,5,'
+    instruction += "#{i+1},#{color};" for color, i in colors
+
+    # padding with black if necessary
+    instruction_count = (instruction.match(/;/g)||[]).length
+    instruction += "#{i+1},000,000,000;" for i in [instruction_count...5]
+    return instruction
+
+  #
+  # write socket.io color data to buffer as UART instruction
   #
   _to_buffer: (data) ->
-    # break colors string into array, pop empty last value
-    colors = data.color.split '\n'
-    colors.pop()
-    instruction_count = colors.length
+    @buffer = @_data_to_instruction(data)

-    # build our TTY instruction
-    @buffer.push("4,#{i+1},#{color};") for color, i in colors
-
-    # pad with black if necessary
-    @buffer.push("4,#{i+1},000,000,000,000;") for i in [instruction_count...5]

 module.exports = Server


Using long polling only as heroku doesn't support websockets yet.

--- src/server.coffee
+++ src/server.coffee
@@ -29,6 +29,10 @@
 addr = http.address
 io = socketio.listen(app.listen(port, addr))
 io.set "log level", 1
+if process.env.LONGPOLLING is "true"
+  console.log "using long polling"
+  io.set "transports", ["xhr-polling"]
+  io.set "polling duration", 10

 # this is so the server is also a local vision listener
 if config.self_tunnel


Changing '/' get response content

Changing '/' get response content

--- src/server.coffee
+++ src/server.coffee
@@ -1,7 +1,7 @@
 express = require 'express'
 server  = express()

-server.get('/', (req, res) -> res.send('hello 123 another test') )
+server.get('/', (req, res) -> res.send('this is a LevelGraph PoC app') )

 module.exports = server


Group ID for the field photos

Group ID for the field photos

--- workflows/groups.coffee
+++ workflows/groups.coffee
@@ -13,5 +13,5 @@
   module.exports =
     batch1: '5527dc1fea4cd10469000001'
     batch2: '5527dc1fea4cd10469000003'
-    field: 'TODO'
+    field: '5540f9e599bda504dc000001'


Add previous EU and DE signs back to DE

Add previous EU and DE signs back to DE

--- dev/de.cson
+++ dev/de.cson
@@ -1,3 +1,44 @@
+'ascent':
+  category: 'danger'
+  name: 'steep ascent'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'descent', color: 'black', transform: '{inner_triangle} scale(-1,1)' }
+    { type: 'incline_value', content: '10%', color: 'black', transform: 'translate(0,-25%) {center2tri} scale(.4) rotate(-30deg)' }
+  ]
+
+'descent':
+  category: 'danger'
+  name: 'steep descent'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'descent', color: 'black', transform: '{inner_triangle}' }
+    { type: 'incline_value', content: '10%', color: 'black', transform: 'translate(0,-25%) {center2tri} scale(.4) rotate(30deg)' }
+  ]
+
+'domestic-animals':
+  category: 'danger'
+  name: 'Domestic animals'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'domestic-animal', color: 'black', transform: '{center2tri} translate(-6%,4%) scale(.25)' }
+  ]
+
+'level-crossing':
+  category: 'danger'
+  name: 'level crossing'
+  elements: [
+    { type: 'square-angular', color: 'black', transform: 'rotate(30deg) scale(.15,1)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(-30deg) scale(.15,1)' }
+    { type: 'square-angular', color: 'red', transform: 'rotate(30deg) scale(.14,.99)' }
+    { type: 'square-angular', color: 'red', transform: 'rotate(-30deg) scale(.14,.99)' }
+    { type: 'square-angular', color: 'white', transform: 'rotate(30deg) scale(.14,.6)' }
+    { type: 'square-angular', color: 'white', transform: 'rotate(-30deg) scale(.14,.6)' }
+  ]
+
 'unprotected-quayside-or-riverbank':
   category: 'danger'
   name: 'Unprotected quayside or ferry berth'
@@ -80,6 +121,17 @@
     { type: 'exclamation', color: 'black', transform: 'scale(.65) {center2tri}' }
   ]

+'other-danger':
+  category: 'danger'
+  name: 'danger'
+  variation: 'temporary'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'yellow', transform: '{inner_triangle}' }
+    { type: 'exclamation', color: 'black', transform: 'scale(.65) {center2tri}' }
+  ]
+
+
 'crossroads-ahead':
   category: 'danger'
   name: 'Danger at next intersection'
@@ -106,6 +158,195 @@
     { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
     { type: 'right-of-way-l fit', color: 'black', transform: 'scale(.7) {center2tri} scale(-1,1)' }
   ]
+
+'pedestrian-crossing':
+  category: 'danger'
+  name: 'Pedestrian crossing'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'pedestrian', color: 'black', transform: 'scale(.7) {center2tri}' }
+  ]
+
+
+'railway-crossing-unsecured':
+  category: 'danger'
+  name: 'unsecured railway crossing'
+  elements: [
+    { type: 'tri-rounded', color: 'red'}
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}'}
+    { type: 'train-electric', color: 'black', transform: 'scale(.4) translate(10%,40%)'}
+  ]
+
+'level-crossing':
+  category: 'danger'
+  name: 'Level crossing'
+  variation: 'other'
+  elements: [
+    { type: 'level-crossing', color: 'red' }
+    { type: 'level-crossing', color: 'red', transform: 'scale(-1,1)' }
+    { type: 'square-angular', color: 'white', transform: 'rotate(60deg) scale(.05,1)' }
+    { type: 'square-angular', color: 'white', transform: 'rotate(-60deg) scale(.05,1)' }
+  ]
+
+'railway-crossing-secured':
+  category: 'danger'
+  name: 'secured railway crossing'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'railway-xing-secured', color: 'black', transform: '{center2tri} translate(0,4%) scale(.35)' }
+  ]
+
+'railway-crossing-unsecured':
+  category: 'danger'
+  name: 'unsecured railway crossing'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'railway-xing-unsecured', color: 'black', transform: 'translate(0,13%) scale(.4)' }
+  ]
+
+'snow':
+  category: 'danger'
+  name: 'snow'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'snowflake', color: 'black', transform: '{center2tri} scale(.7)'}
+  ]
+
+'low-flying-aircraft':
+  category: 'danger'
+  name: 'low flying aircraft'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'airplane', color: 'black', transform: '{center2tri} scale(.4)' }
+  ]
+
+'strong-sidewind':
+  category: 'danger'
+  name: 'strong sidewind'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'windbag', color: 'black', transform: '{center2tri} scale(.3)' }
+  ]
+
+'road-bump':
+  category: 'danger'
+  name: 'road bump'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'bump', color: 'black', transform: '{center2tri} scale(.45)'}
+  ]
+
+'road-narrows-ahead':
+  category: 'danger'
+  name: 'Road narrows'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'narrow-road-both', color: 'black', transform: '{center2tri} scale(.6)' }
+  ]
+
+'road-narrows-left-ahead':
+  category: 'danger'
+  name: 'Road narrows left'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'narrow-road-l', color: 'black', transform: '{center2tri} scale(.6)' }
+  ]
+
+'road-narrows-right-ahead':
+  category: 'danger'
+  name: 'road narrows right'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'narrow-road-l', color: 'black', transform: '{center2tri} scale(-.6,.6)' }
+  ]
+
+'roadworks-ahead':
+  category: 'danger'
+  name: 'Roadworks ahead'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'roadworks', color: 'black', transform: 'scale(.7) translate(0,20%)' }
+  ]
+
+'roundabout-ahead':
+  category: 'danger'
+  name: 'roundabout ahead'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'roundabout', color: 'black', transform: '{center2tri} scale(.55)' }
+  ]
+
+'children':
+  category: 'danger'
+  name: 'children'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'children', color: 'black', transform: '{center2tri} scale(.75)'}
+  ]
+
+'slippery-road-surface':
+  category: 'danger'
+  name: 'slippery road surface'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'slippery-road', color: 'black', transform: '{center2tri} scale(.75)'}
+  ]
+
+'traffic-signals-ahead':
+  category: 'danger'
+  name: 'traffic signal'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'circle-bg', color: 'red', transform: '{center2tri} translate(0,-20%) scale(.13)' }
+    { type: 'circle-bg', color: 'yellow', transform: '{center2tri} translate(0,-5%) scale(.13)' }
+    { type: 'circle-bg', color: 'green', transform: '{center2tri} translate(0,10%) scale(.13)' }
+  ]
+
+'two-way-traffic-ahead':
+  category: 'danger'
+  name: 'Two-way traffic ahead'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'DE-arrow-up', color: 'black', transform: '{center2tri} scale(.35) translate(-25%,5%) rotate(180deg)' }
+    { type: 'DE-arrow-up', color: 'black', transform: '{center2tri} scale(.35) translate(25%,-5%)' }
+  ]
+
+'uneven-road':
+  category: 'danger'
+  name: 'uneven road'
+  elements: [
+    { type: 'tri-rounded', color: 'red' }
+    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
+    { type: 'bumps', color: 'black', transform: '{center2tri} scale(.45)'}
+  ]
+
+'border':
+  category: 'information'
+  name: 'European border crossing into Germany'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'europe_stars', color: 'yellow' }
+    { type: 'content-5', color: 'white', content: 'Bundes-', transform: 'scale(.9) translate(0,-130%)'}
+    { type: 'content-5', color: 'white', content: 'republik', transform: 'scale(.9) translate(0,-30%)' }
+    { type: 'content-5', color: 'white', content: 'Deutschland', transform: 'scale(.9) translate(0,70%)' }
+  ]
+

 'pedestrian-crossing':
   category: 'information'
@@ -116,106 +357,49 @@
     { type: 'pedestrian-crossing', color: 'black' }
   ]

-'road-bump':
-  category: 'danger'
-  name: 'road bump'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'bump', color: 'black', transform: '{center2tri} scale(.45)'}
-  ]
-
-'road-narrows-ahead':
-  category: 'danger'
-  name: 'Road narrows'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'narrow-road-both', color: 'black', transform: '{center2tri} scale(.6)' }
-  ]
-
-'road-narrows-left-ahead':
-  category: 'danger'
-  name: 'Road narrows left'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'narrow-road-l', color: 'black', transform: '{center2tri} scale(.6)' }
-  ]
-
-'road-narrows-right-ahead':
-  category: 'danger'
-  name: 'road narrows right'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'narrow-road-l', color: 'black', transform: '{center2tri} scale(-.6,.6)' }
-  ]
-
-'roadworks-ahead':
-  category: 'danger'
-  name: 'Roadworks ahead'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'roadworks', color: 'black', transform: 'scale(.7) translate(0,20%)' }
-  ]
-
-'roundabout-ahead':
-  category: 'danger'
-  name: 'roundabout ahead'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'roundabout', color: 'black', transform: '{center2tri} scale(.55)' }
-  ]
-
-'children':
-  category: 'danger'
-  name: 'children'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'children', color: 'black', transform: '{center2tri} scale(.75)'}
-  ]
-
-'slippery-road-surface':
-  category: 'danger'
-  name: 'slippery road surface'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'slippery-road', color: 'black', transform: '{center2tri} scale(.75)'}
-  ]
-
-'traffic-signals-ahead':
-  category: 'danger'
-  name: 'traffic signal'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'circle-bg', color: 'red', transform: '{center2tri} translate(0,-20%) scale(.13)' }
-    { type: 'circle-bg', color: 'yellow', transform: '{center2tri} translate(0,-5%) scale(.13)' }
-    { type: 'circle-bg', color: 'green', transform: '{center2tri} translate(0,10%) scale(.13)' }
-  ]
-
-'two-way-traffic-ahead':
-  category: 'danger'
-  name: 'Two-way traffic ahead'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'DE-arrow-up', color: 'black', transform: '{center2tri} scale(.35) translate(-25%,5%) rotate(180deg)' }
-    { type: 'DE-arrow-up', color: 'black', transform: '{center2tri} scale(.35) translate(25%,-5%)' }
-  ]
-
-'uneven-road':
-  category: 'danger'
-  name: 'uneven road'
-  elements: [
-    { type: 'tri-rounded', color: 'red' }
-    { type: 'tri-angular', color: 'white', transform: '{inner_triangle}' }
-    { type: 'bumps', color: 'black', transform: '{center2tri} scale(.45)'}
+'deadend':
+  category: 'information'
+  name: 'deadend'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'square-angular', color: 'white', transform: 'translate(0,20%) scale(.15,.6)' }
+    { type: 'square-angular', color: 'red', transform: 'translate(0,-15%) scale(.5,.15)' }
+  ]
+
+'first-aid':
+  category: 'information'
+  name: 'first aid'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'square-rounded', color: 'white', transform: 'scale(.7)' }
+    { type: 'square-angular', color: 'red', transform: 'scale(.15,.5)' }
+    { type: 'square-angular', color: 'red', transform: 'scale(.5,.15)' }
+  ]
+
+'limited-access-road':
+  category: 'information'
+  name: 'limited-access road'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'car', color: 'white', transform: 'scale(1.5)' }
+  ]
+
+'parking-left':
+  category: 'information'
+  name: 'parking (left of this sign)'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'parking', color: 'white' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'translate(25%,30%) rotate(-90deg) scale(.33)'}
+  ]
+
+'parking-right':
+  category: 'information'
+  name: 'parking (right of this sign)'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'parking', color: 'white' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'translate(25%,30%) rotate(90deg) scale(.33)'}
   ]

 'bus-stop':
@@ -252,6 +436,41 @@
     { type: 'pedestrian-crossing', color: 'black' }
   ]

+'equestrians':
+  category: 'mandatory'
+  name: 'equestrian track'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'equestrian', color: 'white', transform: 'scale(.7)' }
+  ]
+
+'minimum-speed':
+  category: 'mandatory'
+  name: 'minimum speed'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'speed_value', color: 'white'}
+  ]
+
+'minimum-speed-end':
+  category: 'mandatory'
+  name: 'end of minimum speed limit'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'speed_value', color: 'white'}
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(45deg)'}
+  ]
+
+'pedestrian-bicycle-dual-track':
+  category: 'mandatory'
+  name: 'pedestrian/bicycle dual track'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'bicycle', color: 'white', transform: '{left_half}' }
+    { type: 'footpath', color: 'white', transform: '{right_half}' }
+    { type: 'circle-separator', color: 'white' }
+  ]
+
 'dual-path-pedestrian-and-cyclists':
   category: 'mandatory'
   name: 'bicycle/pedestrian dual track'
@@ -377,12 +596,239 @@
     { type: 'arrow-l', color: 'white', transform: 'scale(-1.25, 1.25)' }
   ]

+'narrow-location-priority':
+  category: 'priority'
+  name: 'priority at narrow location'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'DE-arrow-up', color: 'red', transform: 'scale(.8) translate(-25%,5%) rotate(180deg)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'scale(.8) translate(25%,-5%)' }
+  ]
+
+'narrow-location-no-priority':
+  category: 'priority'
+  name: 'no priority at narrow location'
+  variation: 'other'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'DE-arrow-up', color: 'black', transform: '{fit_border_circle} scale(.7) translate(-25%,5%) rotate(180deg)' }
+    { type: 'DE-arrow-up', color: 'red', transform: '{fit_border_circle} scale(.7) translate(25%,-5%)' }
+  ]
+
+'priority-road-end':
+  category: 'priority'
+  name: 'end of priority road'
+  variation: 'other'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: '{square_to_diamond}' }
+    { type: 'square-rounded', color: 'white', transform: 'scale(.98) {square_to_diamond}' }
+    { type: 'square-rounded', color: 'yellow', transform: 'scale(.6) {square_to_diamond}' }
+    { type: 'square-angular', color: 'black', transform: '{square_to_diamond} scale(.2,1)' }
+  ]
+
+'priority-road-end':
+  category: 'priority'
+  name: 'end of priority road'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: '{square_to_diamond}' }
+    { type: 'square-rounded', color: 'white', transform: '{square_to_diamond} scale(.98)' }
+    { type: 'square-rounded', color: 'yellow', transform: 'scale(.6) {square_to_diamond}' }
+    { type: 'square-angular', color: 'black', transform: '{square_to_diamond} scale(.05,1)' }
+    { type: 'square-angular', color: 'black', transform: '{square_to_diamond} scale(.05,1) translate(150%,0)' }
+    { type: 'square-angular', color: 'black', transform: '{square_to_diamond} scale(.05,1) translate(-150%,0)' }
+  ]
+
 'give-way':
   category: 'priority'
   name: 'give way'
   elements: [
     { type: 'tri-rounded', color: 'red', transform: 'rotate(180deg)' }
     { type: 'tri-angular', color: 'white', transform: '{tri2center} scale(.7) {center2tri} rotate(180deg)' }
+  ]
+
+'no-pedestrians':
+  category: 'prohibitory'
+  name: 'pedestrians prohibited'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'pedestrian', color: 'black', transform: '{fit_border_circle}' }
+  ]
+
+'no-pedestrians-or-bicycles':
+  category: 'prohibitory'
+  name: 'pedestrians and bicycles prohibited'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'footpath', color: 'black', transform: '{fit_border_circle} {top_half}' }
+    { type: 'bicycle', color: 'black', transform: '{fit_border_circle} {bottom_half}' }
+  ]
+
+'no-bicycles':
+  category: 'prohibitory'
+  name: 'cycling prohibited'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'bicycle', color: 'black', transform: '{fit_border_circle}' }
+  ]
+
+'no-mopeds':
+  category: 'prohibitory'
+  name: 'mopeds prohibited'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'moped', color: 'black', transform: '{fit_border_circle} scale(.7)' }
+  ]
+
+
+'no-motor-vehicles':
+  category: 'prohibitory'
+  name: 'motor vehicles prohibited'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'motorbike', color: 'black', transform: '{fit_border_circle} {top_half}' }
+    { type: 'car', color: 'black', transform: '{fit_border_circle} {bottom_half}' }
+    { type: 'circle-separator', color: 'black', transform: 'rotate(90deg) {fit_border_circle}' }
+  ]
+
+'no-slow-vehicles':
+  category: 'prohibitory'
+  name: 'slow motorized vehicles prohibited'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'tractor', color: 'black', transform: '{fit_border_circle} scale(.7)' }
+  ]
+
+'no-trailers':
+  category: 'prohibitory'
+  name: 'no trailers allowed'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'trailer', color: 'black' , transform: '{fit_border_circle} scale(.8)'}
+  ]
+
+'no-buses':
+  category: 'prohibitory'
+  name: 'no buses'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'bus', color: 'black' , transform: '{fit_border_circle}'}
+  ]
+
+
+'no-trucks':
+  category: 'prohibitory'
+  name: 'no trucks'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'truck', color: 'black' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(45deg)'}
+  ]
+
+'no-trucks-and-buses':
+  category: 'prohibitory'
+  name: 'buses and trucks prohibited'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'truck', color: 'black', transform: '{fit_border_circle} {top_half}' }
+    { type: 'bus', color: 'black', transform: '{fit_border_circle} {bottom_half}' }
+    { type: 'circle-separator', color: 'black', transform: 'rotate(90deg) {fit_border_circle}' }
+  ]
+
+'no-parking-or-stopping-left':
+  category: 'prohibitory'
+  name: 'no parking or stopping'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(-45deg)' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(45deg)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(-90deg) scale(.3) translate(95%)' }
+  ]
+
+'no-parking-or-stopping-left-and-right':
+  category: 'prohibitory'
+  name: 'no parking or stopping'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(-45deg)' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(45deg)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(-90deg) scale(.3) translate(95%)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(90deg) scale(.3) translate(95%)' }
+  ]
+
+'no-parking-or-stopping-right':
+  category: 'prohibitory'
+  name: 'no parking or stopping'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(-45deg)' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(45deg)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(90deg) scale(.3) translate(95%)' }
+  ]
+
+'no-parking-left':
+  category: 'prohibitory'
+  name: 'No parking'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(-45deg)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(90deg) scale(.3) translate(95%)' }
+  ]
+
+'no-parking-left-and-right':
+  category: 'prohibitory'
+  name: 'no_parking'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(-45deg)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(90deg) scale(.3) translate(95%)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(-90deg) scale(.3) translate(95%)' }
+  ]
+
+'no-parking-right':
+  category: 'prohibitory'
+  name: 'no_parking'
+  elements: [
+    { type: 'circle-bg', color: 'blue' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'circle-bar-rounded', color: 'red', transform: 'rotate(-45deg)' }
+    { type: 'DE-arrow-up', color: 'white', transform: 'rotate(-90deg) scale(.3) translate(95%)' }
+  ]
+
+'speed-limit-zone':
+  category: 'prohibitory'
+  name: 'speed limit'
+  elements: [
+    { type: 'square-rounded', color: 'black' }
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95)' }
+    { type: 'circle-bg', color: 'red', transform: 'translate(0,-10%) scale(.65)' }
+    { type: 'circle-bg', color: 'white', transform: 'translate(0,-10%) scale(.65) {fit_border_circle}' }
+    { type: 'speed_zone_value', color: 'black', transform: 'translate(0,-30%) scale(.7)' }
+    { type: 'content-4', content: 'ZONE', color: 'black', transform: 'translate(0,150%) scale(.8)'}
+  ]
+
+'vehicle-weight-limit':
+  category: 'prohibitory'
+  name: 'vehicle weight limit'
+  elements: [
+    { type: 'circle-bg', color: 'white' }
+    { type: 'circle-o', color: 'red' }
+    { type: 'weight_value', color: 'black' }
   ]

 'no-entry':
@@ -420,9 +866,52 @@
     { type: 'stop', color: 'white' }
   ]

-'no-motor-vehicles'
-  category: 'prohibitory'
-  name: 'No motor vehicles'
+'end-of-limits':
+  category: 'prohibitory'
+  name: 'end of all speed and passing limits'
+  elements: [
+    { type: 'circle-bg', color: 'black' }
+    { type: 'circle-bg', color: 'white', transform: 'scale(.95)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.025,.99)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.025,.98) translate(150%,0)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.025,.97) translate(300%,0)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.025,.98) translate(-150%,0)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.025,.97) translate(-300%,0)' }
+  ]
+
+'low-emission-zone':
+  category: 'prohibitory'
+  name: 'low-emission zone'
+  elements: [
+    { type: 'square-rounded', color: 'black' }
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95)' }
+    { type: 'circle-bg', color: 'red', transform: 'translate(0,-8%) scale(.7)' }
+    { type: 'circle-bg', color: 'white', transform: 'translate(0,-8%) scale(.7) {fit_border_circle}' }
+    { type: 'content-4', content: 'Umwelt', color: 'black', transform: 'translate(0,-50%) scale(.8) {fit_border_circle}' }
+    { type: 'content-4', content: 'ZONE', color: 'black', transform: 'translate(0,150%) scale(.8)'}
+  ]
+
+'low-emission-zone-end':
+  category: 'prohibitory'
+  name: 'end of low-emission zone'
+  elements: [
+    { type: 'square-rounded', color: 'black' }
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95)' }
+    { type: 'circle-bg', color: 'grey', transform: 'translate(0,-8%) scale(.7)' }
+    { type: 'circle-bg', color: 'white', transform: 'translate(0,-8%) scale(.7) {fit_border_circle}' }
+    { type: 'content-4', content: 'Umwelt', color: 'grey', transform: 'translate(0,-50%) scale(.8) {fit_border_circle}' }
+    { type: 'content-4', content: 'ZONE', color: 'grey', transform: 'translate(0,150%) scale(.8)'}
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.02,1.35)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.02,1.3) translate(-150%, 0)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.02,1.3) translate(150%, 0)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.02,1.25) translate(-300%, 0)' }
+    { type: 'square-angular', color: 'black', transform: 'rotate(45deg) scale(.02,1.25) translate(300%, 0)' }
+  ]
+
+
+'no-moped-vehicles':
+  category: 'prohibitory'
+  name: 'No moped vehicles'
   elements: [
     { type: 'circle-bg', color: 'white' }
     { type: 'circle-o', color: 'red' }
@@ -441,7 +930,7 @@

 'no-parking':
   category: 'prohibitory'
-  name: 'no_parking'
+  name: 'no parking'
   elements: [
     { type: 'circle-bg', color: 'blue' }
     { type: 'circle-o', color: 'red' }
@@ -564,3 +1053,205 @@
     { type: 'height', color: 'black', transform: 'rotate(90deg)' }
     { type: 'width_value', color: 'black'}
   ]
+
+'destination':
+  category: 'supplementary'
+  name: 'destination traffic excluded'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'content-4', color: 'black', content: 'Anlieger', transform: 'translate(0,-45%) scale(.8)'}
+    { type: 'content-4', color: 'black', content: 'frei', transform: 'translate(0,45%) scale(.8)'}
+  ]
+
+'disabled':
+  category: 'supplementary'
+  name: 'for disabled people'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'disabled-p', color: 'black', transform: 'scale(.6)'}
+  ]
+
+'glaze':
+  category: 'supplementary'
+  name: 'glaze'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'snowflake', color: 'black', transform: 'scale(.8)'}
+  ]
+
+'left-pointing':
+  category: 'supplementary'
+  name: 'left pointing'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'DE-arrow-up', color: 'black', transform: 'scale(.6) rotate(-90deg)'}
+  ]
+
+'both-directions':
+  category: 'supplementary'
+  name: 'both directions'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'DE-arrow-up', color: 'black', transform: 'translate(-15%,0) scale(.5) rotate(180deg)'}
+    { type: 'DE-arrow-up', color: 'black', transform: 'translate(15%,0) scale(.5)'}
+  ]
+
+'pedestrians-use-opposite-sidewalk':
+  category: 'supplementary'
+  name: 'pedestrians should use opposite sidewalk'
+  variation: 'left'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'DE-arrow-up', color: 'black', transform: 'translate(-20%,0) scale(.5) rotate(-90deg)'}
+    { type: 'pedestrians', color: 'black', transform: 'translate(25%,0) scale(.6)'}
+  ]
+
+'pedestrians-use-opposite-sidewalk':
+  category: 'supplementary'
+  name: 'pedestrians should use opposite sidewalk'
+  variation: 'right'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'DE-arrow-up', color: 'black', transform: 'translate(20%,0) scale(.5) rotate(90deg)'}
+    { type: 'pedestrians', color: 'black', transform: 'translate(-25%,0) scale(.6)'}
+  ]
+
+'railway-only':
+  category: 'supplementary'
+  name: 'railway only'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'train-electric', color: 'black', transform: 'scale(.45)'}
+  ]
+
+'right-pointing':
+  category: 'supplementary'
+  name: 'right pointing'
+  elements: [
+    { type: 'square-rounded', color: 'black', transform: 'scale(1,.5)'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .45)'}
+    { type: 'DE-arrow-up', color: 'black', transform: 'scale(.6) rotate(90deg)'}
+  ]
+
+'entry':
+  category: 'other'
+  name: 'entry, one way street'
+  elements: [
+    { type: 'square-rounded', color: 'blue' }
+    { type: 'arrow-oneway', color: 'white', transform: 'scale(.75)' }
+  ]
+
+'priority-road-bends-left-3way':
+  category: 'supplementary'
+  name: 'priority road bends to the left at a 3-way intersection'
+  variation: '1'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,-30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(-.85,.85)'}
+  ]
+
+'priority-road-bends-left-3way':
+  category: 'supplementary'
+  name: 'priority road bends to the left at a 3-way intersection'
+  variation: '2'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'rotate(-90deg) translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(-.85,.85)'}
+    ]
+
+'priority-road-bends-left-4way':
+  category: 'supplementary'
+  name: 'priority road bends to the left at a 4-way intersection'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,-30%) scale(.1,.25)'}
+    { type: 'square-angular', color: 'black', transform: 'rotate(-90deg) translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(-.85,.85)'}
+  ]
+
+'priority-road-bends-right-3way':
+  category: 'supplementary'
+  name: 'priority road bends to the right at a 3-way intersection'
+  variation: '1'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,-30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(.85)'}
+  ]
+'priority-road-bends-right-3way':
+  category: 'supplementary'
+  name: 'priority road bends to the right at a 3-way intersection'
+  variation: '2'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'rotate(90deg) translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(.85)'}
+  ]
+
+'priority-road-bends-right-4way':
+  category: 'supplementary'
+  name: 'priority road bends to the right at a 4-way intersection'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,-30%) scale(.1,.25)'}
+    { type: 'square-angular', color: 'black', transform: 'rotate(90deg) translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(.85)'}
+  ]
+
+'priority-road-from-left-3way':
+  category: 'supplementary'
+  name: 'priority road comes from the left and continues straight at a 3-way intersection'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(-.85,-.85)'}
+    ]
+
+'priority-road-from-left-4way':
+  category: 'supplementary'
+  name: 'priority road comes from the left and continues straight at a 4-way intersection'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,30%) scale(.1,.25)'}
+    { type: 'square-angular', color: 'black', transform: 'rotate(-90deg) translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(-.85,-.85)'}
+  ]
+
+'priority-road-from-right-3way':
+  category: 'supplementary'
+  name: 'priority road comes from the right and continues straight at a 3-way intersection'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(.85,-.85)'}
+  ]
+
+'priority-road-from-right-4way':
+  category: 'supplementary'
+  name: 'priority road comes from the right and continues straight at a 4-way intersection'
+  elements: [
+    { type: 'square-rounded', color: 'black'}
+    { type: 'square-rounded', color: 'white', transform: 'scale(.95, .95)'}
+    { type: 'square-angular', color: 'black', transform: 'translate(0,30%) scale(.1,.25)'}
+    { type: 'square-angular', color: 'black', transform: 'rotate(90deg) translate(0,30%) scale(.1,.25)'}
+    { type: 'bending-priority', color: 'black', transform: 'scale(.85,-.85)'}
+  ]


Add payment_history after IMP.request_pay

--- public/assets/coffee/order-id-payment.coffee
+++ public/assets/coffee/order-id-payment.coffee
@@ -26,37 +26,29 @@
       buyer_addr:   $info.data('address1')
       notice_url:   'https://test-share.theopencloset.net/webhooks/iamport'
     , (res) ->
-      console.log res
-      if res.success
-        $.ajax location.href,
-          type: 'PUT'
-          data: { status_id: STATUS.payment_done }
-          success: (data, textStatus, jqXHR) ->
-            # location.reload()
-            console.log '** SUCCESS'
-          error: (jqXHR, textStatus, errorThrown) ->
-          complete: (jqXHR, textStatus) ->
-            $this.removeClass('disabled')
-      else
-        console.log '** FAIL'
+      data =
+        order_id: order_id
+        dump: JSON.stringify(res)
+        imp_uid: res.imp_uid
+        merchant_uid: res.merchant_uid
+        amount: res.paid_amount
+        status: res.status
+        pg_provider: res.pg_provider
+        pay_method: res.pay_method

-  ###
-  STATUS =
-    choose_address: 49
-    payment_done: 50
+      unless res.success
+        $.growl.error({ title: '결제실패', message: res.error_msg })

-  $('#btn-payment').click (e) ->
-    e.preventDefault()
-    $this = $(@)
-    $this.addClass('disabled')
-    $.ajax location.href,
-      type: 'PUT'
-      data: { status_id: STATUS.payment_done }
-      success: (data, textStatus, jqXHR) ->
-        location.reload()
-      error: (jqXHR, textStatus, errorThrown) ->
-      complete: (jqXHR, textStatus) ->
-        $this.removeClass('disabled')
+      res.order_id = order_id
+      res.dump = JSON.stringify(res)
+      $.ajax '/payments/histories',
+        type: 'POST'
+        dataType: 'json'
+        data: data
+        success: (data, textStatus, jqXHR) ->
+        error: (jqXHR, textStatus, errorThrown) ->
+        complete: (jqXHR, textStatus) ->
+          $this.removeClass('disabled')

   $('#datepicker-wearon-date').datepicker
     language: 'kr'
@@ -92,4 +84,22 @@
       error: (jqXHR, textStatus, errorThrown) ->
       complete: (jqXHR, textStatus) ->
         $this.removeClass('disabled')
+
   ###
+  STATUS =
+    choose_address: 49
+    payment_done: 50
+
+  $('#btn-payment').click (e) ->
+    e.preventDefault()
+    $this = $(@)
+    $this.addClass('disabled')
+    $.ajax location.href,
+      type: 'PUT'
+      data: { status_id: STATUS.payment_done }
+      success: (data, textStatus, jqXHR) ->
+        location.reload()
+      error: (jqXHR, textStatus, errorThrown) ->
+      complete: (jqXHR, textStatus) ->
+        $this.removeClass('disabled')
+  ###


removing Andrew from the team

removing Andrew from the team

--- scripts/offshore.coffee
+++ scripts/offshore.coffee
@@ -89,22 +89,22 @@
     msg.send "Praise to the Lord! Hail to you o mighty Prem."

   robot.respond /who will be victorious/i, (msg)->
-    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :richard: :andrew: :drmatt:"
+    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :richard: :drmatt:"

   robot.respond /who will be triumphant/i, (msg)->
-    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :richard: :andrew: :drmatt:"
+    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :richard: :drmatt:"

   robot.respond /who will prevail/i, (msg)->
-    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :richard: :andrew: :drmatt:"
+    msg.send "The Offshore team! :ahsan: :sachin: :kostas: :richard: :drmatt:"

   robot.respond /who will unleash righteous vengeance upon evil/i, (msg)->
-    msg.send "The heroes of offshore team! :ahsan: :sachin: :kostas: :richard: :andrew: :drmatt:"
+    msg.send "The heroes of offshore team! :ahsan: :sachin: :kostas: :richard: :drmatt:"

   robot.respond /when all hope is lost and darkness is upon us/i, (msg)->
-    msg.send "The offshore team shall know no fear! :ahsan: :sachin: :kostas: :richard: :andrew: :drmatt:"
+    msg.send "The offshore team shall know no fear! :ahsan: :sachin: :kostas: :richard: :drmatt:"

   robot.respond /when Prem is gone and we are lost in darkness/i, (msg)->
-    msg.send "The offshore team shall know no fear! The offshore team will be triumphant! :ahsan: :sachin: :prem: :kostas: :andrew: :drmatt:"
+    msg.send "The offshore team shall know no fear! The offshore team will be triumphant! :ahsan: :sachin: :richard: :kostas: :drmatt:"

   robot.respond /what is the purpose of offshore team/i, (msg)->
     msg.send "To crush our bugs, see them driven before us, and to hear the lamentation of their women"


New arbitary port

New arbitary port

--- spec/server.spec.coffee
+++ spec/server.spec.coffee
@@ -2,7 +2,7 @@
 {RecommenderMock} = require './helper/recommenderMock'
 {Server} = require './../server'
 http = require 'http'
-PORT = process.env.PORT
+PORT = 2055
 completed = false
 allData = null


Reset the loadedPackages before each test.

Also stop including fixturePackagesPath in configs.packageDirPaths. Instead load fixture packages as needed

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -18,19 +18,20 @@
 [bindingSetsToRestore, bindingSetsByFirstKeystrokeToRestore] = []

 # Specs rely on TextMate bundles (but not atom packages)
-pack.load() for pack in atom.getTextMatePackages()
+textMatePackages = atom.loadTextMatePackages()

 beforeEach ->
   window.fixturesProject = new Project(require.resolve('fixtures'))
   window.resetTimeouts()

+  atom.loadedPackages = _.clone(textMatePackages)
+
   # used to reset keymap after each spec
   bindingSetsToRestore = _.clone(keymap.bindingSets)
   bindingSetsByFirstKeystrokeToRestore = _.clone(keymap.bindingSetsByFirstKeystroke)

   # reset config before each spec; don't load or save from/to `config.json`
   window.config = new Config()
-  config.packageDirPaths.unshift(fixturePackagesPath)
   spyOn(config, 'load')
   spyOn(config, 'save')
   config.set "editor.fontSize", 16


Increase timeout on Windows CI

Increase timeout on Windows CI

--- spec/spec-helper.coffee
+++ spec/spec-helper.coffee
@@ -37,7 +37,7 @@

 if process.platform is 'win32' and process.env.JANKY_SHA1
   # Use longer timeout on Windows CI
-  jasmine.getEnv().defaultTimeoutInterval = 30000
+  jasmine.getEnv().defaultTimeoutInterval = 60000
 else
   jasmine.getEnv().defaultTimeoutInterval = 5000


Add persistHighlight() and locals.bit to radix sort

Use persistHighlight() to highlight the working set of each sort call.
Set VA.locals.bit to the current value of bit.

--- graphs.coffee
+++ graphs.coffee
@@ -72,6 +72,8 @@
   """
   radix: """
 sort = (begin, end, bit) ->
+  VA.persistHighlight([begin..end])
+  VA.locals.bit = bit
   i = begin
   j = end
   mask = 1 << bit


Fix command name  =>

Fix command name  =>

--- scripts/turtleme.coffee
+++ scripts/turtleme.coffee
@@ -2,13 +2,13 @@
 #   hubotに亀の写真を拾ってきてもらう
 #
 # Commands:
-#   hubot turtleme - Queries Google Images for turtle and returns a random top result.
+#   hubot turtle me - Queries Google Images for turtle and returns a random top result.

 # Hubotのスクリプトはモジュールとして記述し，
 # Hubot起動時にrequireされてexportした関数が呼び出されます

 module.exports = (robot) ->
-  robot.respond /turtleme/i, (msg) ->
+  robot.respond /turtle me/i, (msg) ->
     turtleMe msg, (url) ->
       msg.send url


Trigger complete beforeunload event

Trigger complete beforeunload event

jquery 2.0 fails to trigger this event using just
$(window).trigger('beforeunload')

--- spec/window-spec.coffee
+++ spec/window-spec.coffee
@@ -52,13 +52,18 @@
       expect(beforeunload).toHaveBeenCalled()

   describe "beforeunload event", ->
+    [beforeUnloadEvent] = []
+
+    beforeEach ->
+      beforeUnloadEvent = $.Event(new Event('beforeunload'))
+
     describe "when pane items are are modified", ->
       it "prompts user to save and and calls rootView.confirmClose", ->
         spyOn(rootView, 'confirmClose').andCallThrough()
         spyOn(atom, "confirmSync").andReturn(2)
         editSession = rootView.openSync("sample.js")
         editSession.insertText("I look different, I feel different.")
-        $(window).trigger 'beforeunload'
+        $(window).trigger(beforeUnloadEvent)
         expect(rootView.confirmClose).toHaveBeenCalled()
         expect(atom.confirmSync).toHaveBeenCalled()

@@ -66,14 +71,14 @@
         spyOn(atom, "confirmSync").andReturn(2)
         editSession = rootView.openSync("sample.js")
         editSession.insertText("I look different, I feel different.")
-        expect(window.onbeforeunload(new Event('beforeunload'))).toBeTruthy()
+        $(window).trigger(beforeUnloadEvent)
         expect(atom.confirmSync).toHaveBeenCalled()

       it "prompts user to save and handler returns false if dialog is canceled", ->
         spyOn(atom, "confirmSync").andReturn(1)
         editSession = rootView.openSync("sample.js")
         editSession.insertText("I look different, I feel different.")
-        expect(window.onbeforeunload(new Event('beforeunload'))).toBeFalsy()
+        $(window).trigger(beforeUnloadEvent)
         expect(atom.confirmSync).toHaveBeenCalled()

   describe ".unloadEditorWindow()", ->


Use trackCalls. Tweak import order.

Use trackCalls. Tweak import order.

--- specsrc/deferred.coffee
+++ specsrc/deferred.coffee
@@ -1,11 +1,7 @@
-{Deferred} = require('backburner-deferred')
-
 {trackCalls} = require('spec-utils')
 WAIT_TIME = 1000

-callTrackingFn = ->
-    return ->
-        arguments.callee.called = true
+{Deferred} = require('backburner-deferred')

 describePromise = (promiseFactory, name) ->
     [promise, resolveFn, rejectFn] = promiseFactory()
@@ -62,7 +58,7 @@
                 [promise, resolveFn, rejectFn] = promiseFactory()

             it 'should register failure callbacks', ->
-                failFn = callTrackingFn()
+                failFn = trackCalls()

                 promise.fail [failFn]


Catch undefined callback

Catch undefined callback

--- lib/actor/hactor.coffee
+++ lib/actor/hactor.coffee
@@ -785,9 +785,12 @@
               result = "QuickFilter added"
               return
         if status isnt undefined
-          return cb status, result
+          if typeof cb is "function"
+            cb status, result
+            return
         else
-          return cb codes.hResultStatus.NOT_AUTHORIZED, "already subscribed to channel " + hChannel
+          if typeof cb is "function"
+            return cb codes.hResultStatus.NOT_AUTHORIZED, "already subscribed to channel " + hChannel

     @send @buildCommand(hChannel, "hSubscribe", {}, {timeout: 3000}), (hResult) =>
       if hResult.payload.status is codes.hResultStatus.OK and hResult.payload.result
@@ -795,10 +798,11 @@
         @inboundAdapters.push channelInbound
         channelInbound.start()
         @subscriptions.push hResult.publisher
-        if cb
+        if typeof cb is "function"
           cb codes.hResultStatus.OK
       else
-        cb hResult.payload.status, hResult.payload.result
+        if typeof cb is "function"
+          cb hResult.payload.status, hResult.payload.result

   #
   # Method called to unsubscribe to a channel.
@@ -828,7 +832,8 @@
       index++

     if subs is false
-      return cb codes.hResultStatus.NOT_AVAILABLE, "user not subscribed to " + hChannel
+      if typeof cb is "function"
+        return cb codes.hResultStatus.NOT_AVAILABLE, "user not subscribed to " + hChannel
     else
       index = 0
       _.forEach @inboundAdapters, (inbound) =>
@@ -839,7 +844,8 @@
                 if result
                   @unsubscribe hChannel, cb
                 else
-                  return cb codes.hResultStatus.OK, "QuickFilter removed"
+                  if typeof cb is "function"
+                    return cb codes.hResultStatus.OK, "QuickFilter removed"
             else
               inbound.stop()
               @inboundAdapters.splice(index, 1)


fix: escape executable path in command line

When the executable path contains spaces (eg: `C:\Program Files\LLVM\bin\clang-format.exe`), `child_process.execSync()` fails.

--- lib/clang-format.coffee
+++ lib/clang-format.coffee
@@ -72,7 +72,7 @@
       options['cwd'] = path.dirname(file_path)

     try
-      stdout = execSync("#{exe} #{args}", options).toString()
+      stdout = execSync("\"#{exe}\" #{args}", options).toString()
         # Update buffer with formatted text. setTextViaDiff minimizes re-rendering
       buffer.setTextViaDiff @getReturnedFormattedText(stdout)
         # Restore cursor position


Remove unused require

Remove unused require

--- lib/fuzzy-finder.coffee
+++ lib/fuzzy-finder.coffee
@@ -1,5 +1,3 @@
-{_} = require 'atom'
-
 module.exports =
   projectPaths: null
   fuzzyFinderView: null


Add longTap to go to timeline

Add longTap to go to timeline

--- www/js/views/app.coffee
+++ www/js/views/app.coffee
@@ -12,6 +12,7 @@
     events:
       'click #back': 'goBack'
       'click #full-modal .accept': 'destroyFullModal'
+      'longTap #back': 'goToTimeline'

     # Initialize the app. First thing we do is check to see if there's a "self"
     # user already present (there should only ever be one). If there is, we'll
@@ -49,6 +50,13 @@
     goBack: ->
       window.history.back()

+    # A long tap on the back button will return the user home, even if they're
+    # deeply nested inside the app.
+    goToTimeline: ->
+      window.router.navigate '',
+        replace: true
+        trigger: true
+
     # Check to see if there's a User with "self" status. This means we have
     # authorization and are signed in. If not, we'll show an intro/login
     # screen so the user can sign in with Foursquare.


previous fix gave us three fixed specs for free

--- spec/motions-spec.coffee
+++ spec/motions-spec.coffee
@@ -677,8 +677,7 @@

         it "deletes the current line plus that many previous lines", ->
           expect(editor.getText()).toBe "1\n6\n"
-          # commented out because the column is wrong due to a bug in `k`; re-enable when `k` is fixed
-          #expect(editor.getCursorScreenPosition()).toEqual [1, 0]
+          expect(editor.getCursorScreenPosition()).toEqual [1, 0]

   describe "the + keybinding", ->
     beforeEach ->
@@ -738,8 +737,7 @@

         it "selects to the first character of the next line", ->
           expect(editor.getText()).toBe "abcdefg\n"
-          # commented out because the column is wrong due to a bug in `j`; re-enable when `j` is fixed
-          #expect(editor.getCursorScreenPosition()).toEqual [0, 0]
+          expect(editor.getCursorScreenPosition()).toEqual [0, 0]

     describe "with a count", ->
       beforeEach ->
@@ -762,8 +760,7 @@

         it "deletes the current line plus that many following lines", ->
           expect(editor.getText()).toBe "1\n6\n"
-          # commented out because the column is wrong due to a bug in `j`; re-enable when `j` is fixed
-          #expect(editor.getCursorScreenPosition()).toEqual [1, 0]
+          expect(editor.getCursorScreenPosition()).toEqual [1, 0]

   describe "the _ keybinding", ->
     beforeEach ->


fix onSuccess callbacks in client.js

fix onSuccess callbacks in client.js

--- src-client/index.coffee
+++ src-client/index.coffee
@@ -213,7 +213,8 @@
 					return onError {err}
 				uri = res.headers.location
 				console.log 'finished'
-				return onSuccess uri
+				_id = Utils.lastUriSegment(uri)
+				return onSuccess {uri, _id}

 	uploadFiles: (opts = {}) ->
 		if typeof opts isnt 'object'
@@ -250,7 +251,8 @@
 						onError {fileIdx, err, file}
 						return done err
 					uri = res.headers.location
-					onSuccess {fileIdx, file, uri}
+					_id = Utils.lastUriSegment(uri)
+					onSuccess {fileIdx, file, uri, _id}
 					done null, uri
 		, (err, uris) ->
 			if err


fullpieagent testing

fullpieagent testing

--- widgets/fullpie_agent/fullpieagent.coffee
+++ widgets/fullpie_agent/fullpieagent.coffee
@@ -50,20 +50,6 @@
     for val in data
       sum += val.value

-# Core
-#    arcs.append("svg:text").attr("transform", (d, i) ->
-#      procent_val = Math.round(data[i].value/sum * 100)
-#      d.innerRadius = (radius * (100-procent_val)/100) - 45  #45=max text size/2
-#      d.outerRadius = radius
-#      "translate(" + arc.centroid(d) + ")")
-#      .attr('fill', "#fff")
-#      .attr("text-anchor", "middle").text((d, i) -> data[i].label).attr('font-size', '28px')
-#      .append('svg:tspan')
-#      .attr('x', 0)
-#      .attr('dy', 25)
-#      .attr('font-size', '90%')
-#      .text((d,i) -> data[i].value + ' (' + Math.round(data[i].value/sum * 100) + '%)')
-
     if !sum
       arcs.append("svg:text")
         .attr('fill', "#fff")


Fixup mate ending animation port.

Fixup mate ending animation port.

For some reason frames 2 and 3 are empty.

--- src/state-play.coffee
+++ src/state-play.coffee
@@ -115,7 +115,7 @@
       y -= 10
       @mate = @add.sprite x, y, 'mate', 1
       @mate.anchor = new Phaser.Point 0.5, 0.5
-      @mate.animations.add 'end', [1..MateLastFrame], 12
+      @mate.animations.add 'end', [4..MateLastFrame], 12

     _addMusic: ->
       @music = @add.audio 'bgm', 0, yes


update error message for new rethinkdb node.js driver

--- salvus/rethink.coffee
+++ salvus/rethink.coffee
@@ -1670,7 +1670,7 @@
         if not @_validate_opts(opts) then return
         @table('projects').get(opts.project_id)('users')(opts.account_id)('group').run (err, group) =>
             if err?
-                if err.name == "RqlRuntimeError"
+                if err.name == "ReqlNonExistenceError"
                     # indicates that there's no opts.account_id key in the table (or users key) -- error is different
                     # (i.e., RqlDriverError) when caused by connection being down.
                     # one more chance -- admin?
@@ -2258,8 +2258,8 @@
             cb         : required  # cb(err, ttl actually used in seconds); ttl=0 for infinite ttl
         @table('blobs').get(opts.uuid).pluck('expire').run (err, x) =>
             if err
-                if err.name == 'RqlRuntimeError'
-                    # Get RqlRuntimeError if the blob not already saved, due to trying to pluck from nothing.
+                if err.name == 'ReqlNonExistenceError'
+                    # Get ReqlNonExistenceError if the blob not already saved, due to trying to pluck from nothing.
                     # We now insert a new blob.
                     x =
                         id         : opts.uuid


[master] regex fix

[master] regex fix

--- translator/baidu-web.coffee
+++ translator/baidu-web.coffee
@@ -46,10 +46,10 @@
       else
         data = JSON.parse body
         if not data.trans_result
-          if data?.error == 3
-            loggerFile.warn 'parse error skipped',
+          if data?.error == 3 or data?.error == 8
+            loggerFile.warn 'error skipped',
               data: data
-              query: JSON.stringify q
+              query: q
             deferred.resolve ''
           else if 0 < retry
             logger.warn 'retry request',
@@ -68,7 +68,8 @@
 exports.translate = translate

 if require.main == module
-  translate '一群乌合之众 又出来乱叫了！~~~\n一跌 就看空 叫空··哈哈··· 一涨 就一片 叫好！'
+  a = '福音\n日本发生了一件千真万确的事：有人为了装修家里，拆开了墙；日式住宅的墙壁通常是中间架了木板后，两边批上泥土，其实里面是空的。他拆墙壁的时候，发现一只\u001d 壁虎被困'
+  translate a.replace(/\u001d/g, '')
   .then (i) ->
     console.log i
   .done()


activity: fix newline handling of input view's cursor position getter [fixes #75889126]

--- client/Social/Activity/views/inputview.coffee
+++ client/Social/Activity/views/inputview.coffee
@@ -108,8 +108,13 @@

     position = 0
     for node in @getEditableElement().childNodes
+      text = node.innerText or node.textContent
+
       break  if node is startContainer or node is parentNode
-      position += node.textContent.length
+      position += text.length
+
+      # take newline at the end of line into account if line if necessary
+      position += 1  if text isnt "\n" and node.nextSibling?.nodeName is 'DIV'

     return position + startOffset


Remote-Extensions: updateInstance: update instances based on the order of timestamp provided in the notification

Also we are using Bongo::Model::updateInstance and passing the whole change to it instead of modifying the data manually

--- client/app/lib/remote-extensions/index.coffee
+++ client/app/lib/remote-extensions/index.coffee
@@ -25,7 +25,6 @@
   getCache: -> globals.__remoteCache

-  updateInstance: (instanceId) -> # , data) ->
   getInstances: (instanceId) -> @getCache()[instanceId] ? []

@@ -38,3 +37,18 @@
     @getCache()[instanceId].push instance

+  updateInstance: (data) ->
+
+    { id: instanceId, change, timestamp } = data
+
+    return  if (instances = @getInstances instanceId).length is 0
+
+    instances
+      .filter (instance) -> instance._events?.update?
+      .filter (instance) ->
+        if instance.__lastUpdate?
+          return instance.__lastUpdate < timestamp
+        return yes
+      .map    (instance) ->
+        instance.__lastUpdate = timestamp
+        instance.emit 'updateInstance', change


allowing passing in 'template_name' option to view

--- shared/base/view.coffee
+++ shared/base/view.coffee
@@ -68,7 +68,7 @@
     data

   getTemplateName: ->
-    @name
+    @options.template_name || @name

   # Get template function
   getTemplate: ->


add commands

add commands

--- src/trello.coffee
+++ src/trello.coffee
@@ -105,8 +105,9 @@
       msg.reply "Yep, ok, I moved that card to #{list_name}." unless err

 module.exports = (robot) ->
+  # fetch our board data when the script is loaded
   ensureConfig console.log
-
+
   robot.respond /trello new ["'](.+)["']\s(.*)/i, (msg) ->
     ensureConfig msg.send
     card_name = msg.match[2]
@@ -134,8 +135,16 @@
     Object.keys(lists).forEach (key) ->
       msg.send " * " + key

+  robot.respond /trello boards/i, (msg) ->
+    showBoards msg
+
+  robot.respond /trello lists/i, (msg) ->
+    showLists msg
+
   robot.respond /trello help/i, (msg) ->
     msg.reply "Here are all the commands for me."
+    msg.send " *  trello boards"
+    msg.send " *  trello lists"
     msg.send " *  trello new \"<ListName>\" <TaskName>"
     msg.send " *  trello list \"<ListName>\""
     msg.send " *  shows * [<card.shortLink>] <card.name> - <card.shortUrl>"


Collaboration: Ignore participant action if realtime manager is not set

--- client/ide/lib/collaborationcontroller.coffee
+++ client/ide/lib/collaborationcontroller.coffee
@@ -266,6 +266,8 @@

   handleParticipantAction: (actionType, changeData) ->

+    return  unless @rtm
+
     kd.utils.wait 2000, => @whenRealtimeReady =>
       participants  = @participants.asArray()
       {sessionId}   = changeData.collaborator


Fix missingThis errors.

Fix missingThis errors.

Add checks for FunctionExpression and function parameters

--- src/transforms.coffee
+++ src/transforms.coffee
@@ -13,11 +13,11 @@
     parents.push node = node.parent
   parents

-getParentsOfType = (node, type) ->
-  _.filter getParents(node), {type: type}
+getParentsOfTypes = (node, types) ->
+  _.filter getParents(node), (elem) -> elem.type in types

 getFunctionNestingLevel = (node) ->
-  getParentsOfType(node, S.FunctionExpression).length
+  getParentsOfTypes(node, [S.FunctionExpression]).length

 ########## Before JS_WALA Normalization ##########

@@ -37,15 +37,18 @@
   return (node) ->
     if node.type is S.VariableDeclarator
       varNames[node.id.name] = true
-    else if node.type is S.FunctionDeclaration# and node.parent.type isnt S.Program
-      varNames[node.id.name] = true
+    else if node.type is S.FunctionDeclaration or node.type is S.FunctionExpression# and node.parent.type isnt S.Program
+      varNames[node.id.name] = true if node.id?
+      varNames[param.name] = true for param in node.params
     else if node.type is S.CallExpression
       v = node.callee.name
+      # console.log v,varNames,varNames[v],varNames['x'],varNames['test']
       if v and not varNames[v] and not global[v]
         # Probably MissingThis, but let's check if we're recursively calling an inner function from itself first.
-        for p in getParentsOfType node, S.FunctionDeclaration
-          varNames[p.id.name] = true
-          return if p.id.name is v
+        for p in getParentsOfTypes node, [S.FunctionDeclaration, S.FunctionExpression]
+          varNames[p.id.name] = true if p.id?
+          varNames[param.name] = true for param in p.params
+          return if varNames[v] is true
         problem = new problems.TranspileProblem @, 'aether', 'MissingThis', {}, null, '', ''  # TODO: last args
         problem.message = "Missing `this.` keyword; should be `this.#{v}`."
         problem.hint = "There is no function `#{v}`, but `this` has a method `#{v}`."