VCOG0002

OmniGraffle: U.S. Map Stencil

Using Omni Automation Voice Commands to import and manipulate elements from the U.S. Map stencil for OmniGraffle.

The free U.S. Map stencil can be DOWNLOADED from the Stenciltown website.

Scope

Language: English (United States 🇺🇸 · Great Britain 🇬🇧 · Australia 🇦🇺 · Canada 🇨🇦)

Scope: OmniGraffle

Requirements

Download & Reference Links

Topics

This webpage provides the Voice Control commands file and a corresponding Omni Automation plug-in for working with the free U.S. Map stencil for OmniGraffle.

Two use-case scenarios are demonstrated and explained in detail:

*DISCLAIMER: Mention of third-party websites and products is for informational purposes only and constitutes neither an endorsement nor a recommendation. OMNI-AUTOMATION.COM assumes no responsibility with regard to the selection, performance or use of information or products found at third-party websites. OMNI-AUTOMATION.COM provides this only as a convenience to our users. OMNI-AUTOMATION.COM has not tested the information found on these sites and makes no representations regarding its accuracy or reliability. There are risks inherent in the use of any information or products found on the Internet, and OMNI-AUTOMATION.COM assumes no responsibility in this regard. Please understand that a third-party site is independent from OMNI-AUTOMATION.COM and that OMNI-AUTOMATION.COM has no control over the content on that website. Please contact the vendor for additional information.

Both scenarios are demonstrated in the following videos:

Data Link with U.S.Map Stencil
Using the U.S. Map stencil as an interface to a local data source.
OpenAI and U.S. Map Stencil
Using the U.S. Map stencil to represent data retrieved from the OpenAI ChatGPT server.

Shared Voice Commands

The following map control commands are used in both example scenarios:

First-Run of Voice Commands

NOTE: (below) The first time a command is activated, a script security dialog will be presented for you to review the script. Once you have scrolled the script  01 , approved its further use without prompting  02 , and run the script  03 , subsequent activations will execute automatically without prompting.

Script security dialog

Topic 1: Linked Local Data Source

In this scenario, the user can ask questions about a topic and have the answers reflected in the various map elements. This is accomplished by the individual voice commands dynamically loading and parsing JSON data from locally placed files.

In Omni Automation, each application’s local Documents folder can be accessed without requiring permission from the user, so the supporting data files are placed in that directory. For convenience, a voice command for opening the app Documents folder on the Desktop has been included in the set of provided voice commands.

PREPARATION: DOWNLOAD data files and use the “Open [the] OmniGraffle [Documents] Folder” to open the OmniGraffle Documents folder and place the unarchived data files within the folder.

Once the data files have been installed locally on the computer, you can use the following commands to query the map for information:

The map elements (states) are identifiable to scripts via their shape names, which conveniently happen to be the postal code for each state. For example, the map element representing the state of New York is named: NY

Because each state shape is named using the corresponding state’s postal code, the code can by used by the script to retrieve relevant information from the imported JSON data arrays, and then use the information to construct a spoken response to the user!

This technique is shown in the example scripts shown below. In the first script, the postal code name of a single state shape selected by the user is used to construct a response about which state the shape represents:

What State is This?


(async () => { try { function createUtterance(textToSpeak){ AlexID = ( (app.platformName === "macOS") ? "com.apple.speech.synthesis.voice.Alex" : "com.apple.speech.voice.Alex" ) voiceObj = Speech.Voice.withIdentifier(AlexID) voiceRate = 0.4 utterance = new Speech.Utterance(textToSpeak) utterance.voice = voiceObj utterance.rate = voiceRate return utterance } // FUNCTION FOR ORDINAL STRINGS: 1st, 2nd, 3rd, 4th... function ordinal(n) { var s = ["th", "st", "nd", "rd"]; var v = n%100; return n + (s[(v-20)%10] || s[v] || s[0]); } synthesizer = new Speech.Synthesizer() // CHECK FOR OPEN DOCUMENT try { document.name } catch(err){ throw { name : "Missing Resource", message: "No document is open." } } fileName = "statedata.json" // RETRIEVE THE STATE DATA FROM THE LOCAL FILE fileURL = URL.documentsDirectory.appendingPathComponent(fileName) request = URL.FetchRequest.fromString(fileURL.string) request.method = 'GET' request.cache = "no-cache" errCode = 1 response = await request.fetch() errCode = 2 stateData = JSON.parse(response.bodyString) stateCodes = stateData.map(obj => obj.code) // CHECK FOR SINGLE SOLID sel = document.windows[0].selection.solids if(sel.length !== 1){ throw { name: "Selection Error", message: "Please select a single solid shape." } } // CHECK TO SEE SELECTED SOLID IS STATE shape = sel[0] if(!stateCodes.includes(shape.name)){ throw { name: "Selection Error", message: "Please select one of the state shapes." } } // RETRIEVE THE STATE DATA USING STATE CODE (GRAPHIC NAME) dataObj = stateData.find(obj => {return obj.code === shape.name}) // CONSTRUCT THE RESPONSE STRING USING RETRIEVED DATA ordinalNumber = ordinal(dataObj.order) response = `${dataObj.name}, ${dataObj.nickname}, is the ${ordinalNumber} state in the union, and its capital is ${dataObj.capital}.` console.log(response) // REMOVE COLOR FROM ALL STATES cnvs = document.windows[0].selection.canvas stateCodes.forEach(code => { cnvs.graphicWithName(code).fillColor = Color.white }) // HIGHLIGHT THE TARGET STATE (50% GRAY) cnvs.graphicWithName(shape.name).fillColor = Color.gray // SPEAK THE RESPONSE utterance = createUtterance(response) synthesizer.speakUtterance(utterance) } catch(err){ if(errCode === 1){ URL.documentsDirectory.open() var msg = `The file “${fileName}” was not found in the local OmniGraffle documents folder.` } else { var msg = err.message } utterance = createUtterance(msg) synthesizer.speakUtterance(utterance) //new Alert(err.name, err.message).show() } })();

A similar technique of accessing a data file, is used to respond to a question about a specified region.

Tell Me About the Midwest Region


(async () => { try { function createUtterance(textToSpeak){ AlexID = ( (app.platformName === "macOS") ? "com.apple.speech.synthesis.voice.Alex" : "com.apple.speech.voice.Alex" ) voiceObj = Speech.Voice.withIdentifier(AlexID) voiceRate = 0.4 utterance = new Speech.Utterance(textToSpeak) utterance.voice = voiceObj utterance.rate = voiceRate return utterance } synthesizer = new Speech.Synthesizer() // CHECK FOR OPEN DOCUMENT try { document.name } catch(err){ throw { name : "Missing Resource", message: "No document is open." } } fileName = "regions.json" targetRegion = "Midwest" // RETRIEVE THE REGION DATA FROM THE LOCAL FILE fileURL = URL.documentsDirectory.appendingPathComponent(fileName) request = URL.FetchRequest.fromString(fileURL.string) request.method = 'GET' request.cache = "no-cache" errCode = 1 response = await request.fetch() errCode = 2 regionData = JSON.parse(response.bodyString) // RETRIEVE THE TARGET REGION targetRegionData = regionData.find(item => {return item.name === targetRegion}) // RETRIEVE THE DIVISIONS divisionData = targetRegionData["divisions"] regionIndex = targetRegionData["index"] str = `Region ${regionIndex} is the ${targetRegion} region, which is comprised of ${divisionData.length} divisions.` utterance = createUtterance(str) utterance.postUtteranceDelay = 0.5 synthesizer.speakUtterance(utterance) cnvs = document.windows[0].selection.canvas bckgndColors = [Color.darkGray, Color.lightGray, Color.darkGray, Color.lightGray] divisionData.forEach((obj,idx) => { divisionColor = bckgndColors[idx] divisionIndex = obj.index divisionName = obj.name divisionStates = obj.states stateNames = divisionStates.map(item => item.name) stateCodes = divisionStates.map(item => item.code) stateCount = stateNames.length nameString = "" for (i = 0; i < stateCount; i++) { stateName = stateNames[i] if(i === 0){ nameString += stateName } else if(i === (stateCount - 1)){ nameString += `, and ${stateName}` } else { nameString += `, ${stateName}` } } str = `Division ${divisionIndex} is ${divisionName}, encompassing ${stateCount} states: ${nameString}.` utterance = createUtterance(str) utterance.postUtteranceDelay = 1.0 stateCodes.forEach(code => { cnvs.graphicWithName(code).fillColor = divisionColor }) synthesizer.speakUtterance(utterance) }) } catch(err){ if(errCode === 1){ URL.documentsDirectory.open() var msg = `The file “${fileName}” was not found in the local OmniGraffle documents folder.` } else { var msg = err.message } utterance = createUtterance(msg) synthesizer.speakUtterance(utterance) new Alert(err.name, err.message).show() } })();

Topic 2: OpenAI Heat Map

In this example, the elements (state shapes) of the U.S. Map stencil are shaded to represent data retrieved from OpenAI services. Specifically, the OpenAI ChatGPT 3.5 service is queried with this prompt:

“What are the top 10 states of the United States in terms of…”

…appended with whatever the user has used as a query completion. For example:

This process is accomplished using an Omni Automation plug-in for OmniGraffle that contains code for contacting the OpenAI server, processing the results, and applying the color shading to the individual state shapes to indicate their relevancy in terms of their order (“heat map”).

After its installation, the plug-in is summoned using this command:

Here is the download link and source code of the plug-in:

OpenAI: Top-10 States Heat Map
 

/*{ "type": "action", "targets": ["omnigraffle"], "author": "Otto Automator", "identifier": "com.omni-automation.og.openai-top-ten-us-states-heat-map", "version": "1.0", "description": "Creates a heat map based upon the results of the “Top-10 States” query. This plug-in uses the Credentials class to create and retrieve log-in pairs. NOTE: to clear stored credentials, hold down Control modifier key when selecting plug-in from Automation menu. To set logging status, hold down the Option key when selecting plug-in from Automation menu.", "label": "OpenAI: Top 10 U.S. States", "shortLabel": "Top 10 States", "paletteLabel": "Top 10 States", "image": "wand.and.stars" }*/ (() => { /* DOCUMENTATION: https://platform.openai.com/docs/api-reference/introduction */ var serviceTitle = "OpenAI"; var serviceURLString = "https://api.openai.com/v1/chat/completions" var credentials = new Credentials() var preferences = new Preferences() // NO ID = PLUG-IN ID var shouldLog var promptString var chosenColor var stateData = [{"name":"Alabama","code":"AL"},{"name":"Alaska","code":"AK"},{"name":"Arizona","code":"AZ"},{"name":"Arkansas","code":"AR"},{"name":"California","code":"CA"},{"name":"Colorado","code":"CO"},{"name":"Connecticut","code":"CT"},{"name":"Delaware","code":"DE"},{"name":"Florida","code":"FL"},{"name":"Georgia","code":"GA"},{"name":"Hawaii","code":"HI"},{"name":"Idaho","code":"ID"},{"name":"Illinois","code":"IL"},{"name":"Indiana","code":"IN"},{"name":"Iowa","code":"IA"},{"name":"Kansas","code":"KS"},{"name":"Kentucky","code":"KY"},{"name":"Louisiana","code":"LA"},{"name":"Maine","code":"ME"},{"name":"Maryland","code":"MD"},{"name":"Massachusetts","code":"MA"},{"name":"Michigan","code":"MI"},{"name":"Minnesota","code":"MN"},{"name":"Mississippi","code":"MS"},{"name":"Missouri","code":"MO"},{"name":"Montana","code":"MT"},{"name":"Nebraska","code":"NE"},{"name":"Nevada","code":"NV"},{"name":"New Hampshire","code":"NH"},{"name":"New Jersey","code":"NJ"},{"name":"New Mexico","code":"NM"},{"name":"New York","code":"NY"},{"name":"North Carolina","code":"NC"},{"name":"North Dakota","code":"ND"},{"name":"Ohio","code":"OH"},{"name":"Oklahoma","code":"OK"},{"name":"Oregon","code":"OR"},{"name":"Pennsylvania","code":"PA"},{"name":"Rhode Island","code":"RI"},{"name":"South Carolina","code":"SC"},{"name":"South Dakota","code":"SD"},{"name":"Tennessee","code":"TN"},{"name":"Texas","code":"TX"},{"name":"Utah","code":"UT"},{"name":"Vermont","code":"VT"},{"name":"Virginia","code":"VA"},{"name":"Washington","code":"WA"},{"name":"West Virginia","code":"WV"},{"name":"Wisconsin","code":"WI"},{"name":"Wyoming","code":"WY"}] function createUtterance(textToSpeak){ AlexID = ( (app.platformName === "macOS") ? "com.apple.speech.synthesis.voice.Alex" : "com.apple.speech.voice.Alex" ) voiceObj = Speech.Voice.withIdentifier(AlexID) voiceRate = 0.4 utterance = new Speech.Utterance(textToSpeak) utterance.voice = voiceObj utterance.rate = voiceRate return utterance } const requestCredentials = async () => { try { // CREATE FORM FOR GATHERING CREDENTIALS inputForm = new Form() // CREATE TEXT FIELDS orgIDField = new Form.Field.String( "organizationID", "Org ID", null ) APIKeyField = new Form.Field.String( "APIkey", "API Key", null ) // ADD THE FIELDS TO THE FORM inputForm.addField(orgIDField) inputForm.addField(APIKeyField) // VALIDATE THE USER INPUT inputForm.validate = formObject => { organizationID = formObject.values["organizationID"] orgIDStatus = (organizationID && organizationID.length > 0) ? true:false APIkey = formObject.values["APIkey"] APIKeyStatus = (APIkey && APIkey.length > 0) ? true:false validation = (orgIDStatus && APIKeyStatus) ? true:false return validation } // PRESENT THE FORM TO THE USER formPrompt = "Enter OpenAI Organization ID and API Key:" formObject = await inputForm.show(formPrompt, "Continue") // RETRIEVE FORM VALUES organizationID = formObject.values["organizationID"] APIkey = formObject.values["APIkey"] // STORE THE VALUES credentials.write(serviceTitle, organizationID, APIkey) if(shouldLog){console.log("# CREDENTIALS STORED IN SYSTEM KEYCHAIN")} } catch(err){ if(!err.causedByUserCancelling){ new Alert(err.name, err.message).show() } } } const fetchData = async (credentialsObj, requestObj) => { try { organizationID = credentialsObj["user"] APIkey = credentialsObj["password"] if(shouldLog){console.log("# ORGANIZATION-ID:", organizationID)} if(shouldLog){console.log("# API-KEY:", APIkey)} if(shouldLog){console.log("# CONSTRUCTING URL.FetchRequest()")} if(shouldLog){console.log("# SERVICE URL STRING:", serviceURLString)} request = URL.FetchRequest.fromString(serviceURLString) request.method = 'POST' request.cache = "no-cache" if(shouldLog){console.log("# CONSTRUCTING REQUEST HEADERS")} authorizationStr = "Bearer" + " " + APIkey headerObj = { "Content-Type": "application/json", "Authorization": authorizationStr, "OpenAI-Organization": organizationID } if(shouldLog){console.log("# HEADER OBJECT", JSON.stringify(headerObj, null, 4))} request.headers = headerObj if(shouldLog){console.log("# REQUEST OBJECT", JSON.stringify(requestObj, null, 4))} request.bodyString = JSON.stringify(requestObj) // EXECUTE REQUEST response = await request.fetch() // PROCESS REQUEST RESULT responseCode = response.statusCode if(shouldLog){console.log("# RESPONSE CODE:", responseCode)} if (responseCode >= 200 && responseCode < 300){ // PROCESS RETRIEVED DATA json = JSON.parse(response.bodyString) // PRETTY-PRINT FULL RESPONSE TO CONSOLE console.log(JSON.stringify(json, null, 4)) // EXTRACT RESPONSE CONTENT responseStr = json.choices[0].message.content responseStr = responseStr.trim() // PLACE RESPONSE STRING ON CLIPBOARD Pasteboard.general.string = responseStr // WRITE RESPONSE INTO CURRENT CANVAS NOTES cnvs = document.windows[0].selection.canvas cnvs.background.notes = responseStr // PARSE RESULTING ORDERED LIST graphs = responseStr.split("\n") listItems = new Array() numbers = ["1", "2", "3", "4", "5", "6", "7", "8", "9"] graphs.forEach(graph => { firstChar = graph.charAt(0) if(numbers.includes(firstChar)){ listItem = graph.replace(/^[0-9]+. /g, '') listItems.push(listItem) } }) // "New Mexico - $40,314" listItems.forEach((item, index) => { x = item.indexOf(" - ") if(x !== -1){ var stateTitle = item.substring(0, x) var notesValue = "(" + String(index + 1) + ") " + item.substring(x + 3) } else { var stateTitle = item var notesValue = String(index + 1) } console.log("stateTitle", stateTitle) console.log("notesValue", notesValue) stObj = stateData.find(obj => {return obj.name === stateTitle}) if(stObj !== undefined){ stCode = stObj.code console.log("stCode", stCode) grphk = cnvs.graphicWithName(stCode) grphk.notes = notesValue colorValue = index * 0.1 // 0.02 console.log("colorValue", colorValue) switch(chosenColor){ case "Red": colorObj = Color.RGB(1, colorValue, colorValue, 1) break case "Green": colorObj = Color.RGB(colorValue, 1, colorValue, 1) break case "Blue": colorObj = Color.RGB(colorValue, colorValue, 1, 1) } grphk.fillColor = colorObj } }) utterance = createUtterance(responseStr) synthesizer = new Speech.Synthesizer() synthesizer.speakUtterance(utterance) alertMessage = "The response has been logged to the console " alertMessage += "and placed on the clipboard." alert = new Alert("Successful Query", alertMessage) alert.addOption("Stop Speaking") alert.show(buttonIndex => { if (buttonIndex === 0){ synthesizer.stopSpeaking(Speech.Boundary.Word) } }) } else if (responseCode === 401){ alertMessage = "Problem authenticating account with server." alert = new Alert(String(responseCode), alertMessage) alert.show().then(() => {requestCredentials()}) } else { new Alert(String(responseCode), "An error occurred.").show() console.error(JSON.stringify(response.headers)) } } catch(err){ console.error(err.name, err.message) new Alert(err.name, err.message).show() } } const action = new PlugIn.Action(async function(selection, sender){ try { // READ PREFERENCES booleanValue = preferences.readBoolean("shouldLog") if(!booleanValue){ shouldLog = false preferences.write("shouldLog", false) } if(shouldLog){console.clear()} loggingMsg = (shouldLog) ? "# LOGGING IS ON":"# LOGGING IS OFF" console.log(loggingMsg) if (app.controlKeyDown){ // TO REMOVE CREDENTIALS HOLD DOWN CONTROL KEY WHEN SELECTING PLUG-IN credentialsObj = credentials.read(serviceTitle) if(!credentialsObj){ alertMessage = "There are no stored credentials to remove." new Alert("Missing Resource", alertMessage).show() } else { alertMessage = "Remove the stored credentials?" alert = new Alert("Confirmation Required", alertMessage) alert.addOption("Reset") alert.addOption("Cancel") alert.show(buttonIndex => { if (buttonIndex === 0){ console.log(`Removing Service “${serviceTitle}”`) credentials.remove(serviceTitle) console.log(`Service “${serviceTitle}” Removed`) } }) } } else if (app.optionKeyDown){ alertMessage = (shouldLog) ? "Logging is on." : "Logging is off." alert = new Alert("Logging Status", alertMessage) alert.addOption("Turn On") alert.addOption("Turn Off") alert.show(buttonIndex => { if (buttonIndex === 0){ preferences.write("shouldLog", true) shouldLog = true } else { preferences.write("shouldLog", false) shouldLog = false } loggingMsg = (shouldLog) ? "# LOGGING IS ON":"# LOGGING IS OFF" console.log(loggingMsg) }) } else { credentialsObj = credentials.read(serviceTitle) if (credentialsObj){ // CREATE TEXT FIELD OBJECT defaultQueryString = null textInputField = new Form.Field.String( "textInput", null, defaultQueryString, null ) colorNames = ["Red", "Green", "Blue"] menuElement = new Form.Field.Option( "mappingColor", null, [0, 1, 2], colorNames, 0 ) // CREATE NEW FORM AND ADD FIELD var inputForm = new Form() inputForm.addField(textInputField) inputForm.addField(menuElement) // VALIDATE USER INPUT inputForm.validate = function(formObject){ textInput = formObject.values["textInput"] if (!textInput){return false} return true } // DISPLAY THE FORM qStr = "What are the top 10 states of the United States in terms of" var formPrompt = `${qStr}:` var buttonTitle = "Continue" formObject = await inputForm.show(formPrompt, buttonTitle) // RETRIVE FORM INPUT textInput = formObject.values["textInput"] textInput = qStr + " " + textInput colorIndex = formObject.values["mappingColor"] chosenColor = colorNames[colorIndex] // LOG THE PROMPT console.log("# PROMPT:", textInput) promptString = textInput escapedText = encodeURIComponent(textInput) requestObj = { "model": "gpt-3.5-turbo", "messages": [{"role": "user", "content": escapedText}] } if(shouldLog){console.log("# PASSING CREDENTIALS AND REQUEST TO fetchData()")} fetchData(credentialsObj, requestObj) } else { if(shouldLog){console.log("# NO CREDENTIALS RETRIEVED, REQUESTING CREDENTIALS.")} requestCredentials() } } } catch(err){ if(!err.causedByUserCancelling){ console.error(err.name, err.message) } } }); action.validate = function(selection, sender){ // validation code return true }; return action; })();

 

 

 

LEGAL

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Mention of third-party websites and products is for informational purposes only and constitutes neither an endorsement nor a recommendation. OMNI-AUTOMATION.COM assumes no responsibility with regard to the selection, performance or use of information or products found at third-party websites. OMNI-AUTOMATION.COM provides this only as a convenience to our users. OMNI-AUTOMATION.COM has not tested the information found on these sites and makes no representations regarding its accuracy or reliability. There are risks inherent in the use of any information or products found on the Internet, and OMNI-AUTOMATION.COM assumes no responsibility in this regard. Please understand that a third-party site is independent from OMNI-AUTOMATION.COM and that OMNI-AUTOMATION.COM has no control over the content on that website. Please contact the vendor for additional information.

📨 (Also, your are most welcome!)