# -*- mode: org; -*- #+HTML_HEAD: #+HTML_HEAD: #+HTML_HEAD: #+HTML_HEAD: #+HTML_HEAD: #+HTML_HEAD: [[./Logo.png]] * Introduction The purpose of this document is to investigate possible User Interaction designs for Decode task 4.4. More specifically the focus is on investigating how the user of a decode wallet grants permission to decode application for a specific set of personal data. ** Privacy levels A prelimary definition of six privacy levels (ordered from most private to least private): #+name: privacy_levels | id | title | description | |----+-----------+---------------------------------------------------------------------------------| | 5 | secret | proofs, passwords, keys etc. | | 4 | private | ssn etc, strict need to know basis stuff | | 3 | intimate | e.g. stuff you share with family | | 2 | affiliate | e.g. stuff you share with work, project etc | | 1 | public | e.g. stuff that everybody may know, your e.g. twitter handle | | 0 | commons | stuff that is intended for the public good / commons, e.g. anonimized IoT stuff | ** Context types A preliminary definition of context types #+name: context_types | id | title | description | generator | |----+-----------+-------------------------------------------------------------------------------------------------------------+----------------------------------------------------------------------------------------------------------| | 0 | personal | data that relates to your personal life, you can have different instances, for example friends, family etc. | Faker::Name.first_name | | 1 | health | health data, you can define different instances (biosignals, stuff to share with dentist, gp, hospital etc) | ['Dentist','Hospital','General Practicioner'].sample | | 2 | education | school / educational data (grades, certificates etc) | Faker::University.name | | 3 | work | stuff you share in a professional context | Faker::Company.name | | 4 | hobby | stuff you share in the context of a pastime | [Faker::Music.instrument,Faker::ProgrammingLanguage.name, Faker::RockBand.name, Faker::Team.name].sample | | 5 | financial | for data about mortgages, insurance, taxes etc. | ['Mortgage', 'Insurance', 'Taxes'].sample | | 6 | other | for everything that doesn't fit the above | Faker::StarWars.call_sign | ** Properties A list of properties with code to eval for fake instances, all properties from id 4 come from gebiedonline. We've made an inventory of the data that is related to a gebied online profile. The question arises what part of the data would theoretically go inside the wallet. This is for the application developer to decide, but we think it is sensible to develop some guidelines on this. For now we have tried to be as inclusive as possible, and put everything in the wallet. Properties that are very application specific have the 'go' (and not the 'decode') namespace. This is pretty speculative, but serves as a discussion starter. #+name: properties | id | title | default_privacy_level | description | generator | |----+-------------------------------------+-----------------------+------------------------------------------------+----------------------------------------------------------------------------------------------------| | 0 | decode:name | 2 | full name | Faker::Name.name | | 1 | decode:email | 2 | Email address | Faker::Internet.email | | 2 | decode:address | 2 | Address | Faker::Address.street_address | | 3 | decode:telephone | 2 | Telephone number | Faker::PhoneNumber.cell_phone | | 4 | go:proof_of_membership | 5 | instead of the password that is used now | SecureRandom.hex | | 5 | decode:first_name | 2 | first name | Faker::Name.first_name | | 6 | decode:last_name | 2 | last name | Faker::Name.last_name | | 7 | go:newsletter_approval | 2 | boolean | [true,false].sample | | 8 | decode:profile_picture | 2 | profile picture | Faker::LoremPixel.image | | 9 | decode:gender | 3 | gender | Faker::Demographic.marital_status | | 10 | decode:birthdate | 3 | date of birthday | Faker::Date.birthday(15,99) | | 11 | decode:homepage | 2 | personal homepage | Faker::Internet.url | | 12 | skype:id | 2 | skype handle | Faker::Internet.user_name | | 13 | twitter:id | 1 | twitter handle | Faker::Internet.user_name | | 14 | facebook:id | 3 | facebook handle | Faker::Internet.email | | 15 | linkedin:id | 1 | linkedin handle | Faker::Internet.email | | 16 | decode:street | 4 | streetname + number | Faker::Address.street_address | | 17 | decode:postcode | 4 | postcode | Faker::Address.postcode | | 18 | decode:persons_in_household | 4 | taken from 'woonsituatie' | Faker::Number.between(1,5) | | 19 | decode:profession | 4 | taken from 'werk en hobbies' | Faker::Job.title | | 20 | go:my_dreams | 3 | taken from 'mijn droom' | Faker::RickAndMorty.quote | | 21 | go:improve_my_environment | 2 | taken from 'mijn omgeving' | Faker::RickAndMorty.location | | 22 | go:expertise | 3 | taken from 'mijn expertise' list structure? | Faker::Educator.course | | 23 | decode:marital_status | 4 | taken from 'woonsituatie' | Faker::Demographic.marital_status | | 24 | go:neighborhood_role | 1 | taken from 'rol & nieuwsbrief' | ["bewoner","toekomstige","ondernemer","professional","scholier","student","geinteresseerd"].sample | | 25 | decode:avg_yearly_gas_use_m3 | 4 | taken from 'jaarlijks warmte verbruik' | Faker::Number.between(1000,2000) | | 26 | decode:avg_yearly_electricy_use_kwh | 4 | taken from 'jaarlijks electriciteits verbruik' | Faker::Number.between(2000,3000) | | 27 | decode:membership_organization | 2 | taken from 'lidmaatschap van organisaties' | Faker::StarTrek.specie | | 28 | go:project_association | 1 | taken from 'associatie met projecten' | Faker::SiliconValley.invention | | 29 | go:offer | 0 | taken from 'vraag en aanbod items' | Faker::SiliconValley.motto | | 30 | go:need | 0 | taken from 'vraag en aanbod items' | Faker::SiliconValley.quote | * Data Model ** Example wallet profile This sample wallet profile datastructure consists of multiple contexts. No assumptions are made about ontology for now so mention of existing e.g. skos/foaf, everything is in the decode namespace which is well known across applications. Each context has a name and groups on or more properties that consist of a well known type and a value. A type can be part of more than one context. Every property instance has a privacy level attached to it, so we can calculate the weight of requests and profiles. It overrides the default privacy level specified by the property type. #+name: profile #+begin_src js :results output var profile = { contexts : [ { title : "personal", context_type : 5,//PERSONAL properties : [ { type : "decode:name", value: "Taco van Dijk", pl: 1 //public, everyone may know my name }, { type : "decode:email", value : "[REDACTED]", pl: 2 //affiliate, parties i have personal business with may know }, { type: "decode:address", value: "[REDACTED]", pl: 2 //affiliate, parties i have personal business with may know }, { type: "decode:phone", value : "[REDACTED]", pl: 2 //affiliate, parties i have personal business with may know } ], pl_sum: 7 //this is a calculated value based on the attributed pl values or default if they were not user specified }, { title: "Waag society", context_type: 2,//WORK properties : [ { type : "decode:name", value : "Taco van Dijk", pl: 1 }, { type : "decode:email", value : "taco@waag.org", pl: 2 }, { type : "decode:address", value : "St. Antoniesbreestraat 69", pl: 1 //since this is shared with all my colleagues i find this public } ], pl_sum: 4 }, { title: "Dyne", context_type: 2,//WORK properties :[{ type : "decode:name", value : "Ocat", pl: 1 }, { type : "decode:email", value: "taco@gogs.dyne.org", pl: 2 } ], pl_sum: 3 } ] }; process.stdout.write(JSON.stringify(profile)); #+end_src #+RESULTS: profile : {"contexts":[{"title":"personal","context_type":5,"properties":[{"type":"decode:name","value":"Taco van Dijk","pl":1},{"type":"decode:email","value":"[REDACTED]","pl":2},{"type":"decode:address","value":"[REDACTED]","pl":2},{"type":"decode:phone","value":"[REDACTED]","pl":2}],"pl_sum":7},{"title":"Waag society","context_type":2,"properties":[{"type":"decode:name","value":"Taco van Dijk","pl":1},{"type":"decode:email","value":"taco@waag.org","pl":2},{"type":"decode:address","value":"St. Antoniesbreestraat 69","pl":1}],"pl_sum":4},{"title":"Dyne","context_type":2,"properties":[{"type":"decode:name","value":"Ocat","pl":1},{"type":"decode:email","value":"taco@gogs.dyne.org","pl":2}],"pl_sum":3}]} ** profile generator #+name: profile_generator #+BEGIN_SRC ruby :var context_types=context_types :var privacy_levels=privacy_levels :var properties=properties :results output require 'faker' require 'json' require 'securerandom' CMIN = 4 #minimum amount of contexts CMAX = 12 #maximum amount of extra contexts PC_MIN = 4 #minimum amount of properties PC_MAX = 12 #maximum amount of extra properties profile = {} contexts = [] #create between CMIN and CMAX contexts in this profile (CMIN + rand(CMAX)).times do context = {} context_type_idx = rand(context_types.count) #pick a random context type context[:title] = eval(context_types[context_type_idx][3]) #eval a fake title based on random context type context[:context_type] = context_types[context_type_idx][0] #context type index context[:properties] = [] #sample a random amount up to 8 properties for each context properties.sample(PC_MIN + rand(PC_MAX)).each do |rec| property = {} property[:type] = rec[1] property[:value] = eval(rec[4]) property[:pl] = rec[2] + rand(2) #default privacy level, + random markup upwards context[:properties] << property end context[:pl_sum] = context[:properties].map{|p|p[:pl]}.reduce(0,:+) #calculate pl sum for each context context[:pl_mean] = (context[:pl_sum] / context[:properties].count) contexts << context end # sort the contexts by contextType sorted = contexts.sort { | a, b | [a[:context_type], a[:pl_mean]] <=> [b[:context_type], b[:pl_mean]] } profile[:contexts] = sorted puts profile.to_json #+END_SRC #+RESULTS: profile_generator : {"contexts":[{"title":"Theresa","context_type":0,"properties":[{"type":"decode:email","value":"buford.dare@gleichnergorczany.name","pl":2},{"type":"go:improve_my_environment","value":"Hideout Planet","pl":3},{"type":"skype:id","value":"eula","pl":3},{"type":"decode:persons_in_household","value":4,"pl":5},{"type":"decode:homepage","value":"http://osinskivandervort.io/tevin","pl":2},{"type":"decode:profile_picture","value":"http://lorempixel.com/300/300","pl":3},{"type":"go:neighborhood_role","value":"geinteresseerd","pl":1},{"type":"go:my_dreams","value":"It's a figure of speech, Morty! They're bureaucrats! I don't respect them. Just keep shooting, Morty! You have no idea what prison is like here!","pl":4}],"pl_sum":23,"pl_mean":2},{"title":"Hospital","context_type":1,"properties":[{"type":"decode:email","value":"alexys@kling.org","pl":2},{"type":"decode:name","value":"Price Miller","pl":2},{"type":"decode:postcode","value":"90304-4175","pl":4},{"type":"decode:address","value":"4296 Turcotte Throughway","pl":3},{"type":"go:proof_of_membership","value":"eb00c76b1f66467dcffbdc49b175d351","pl":5}],"pl_sum":16,"pl_mean":3},{"title":"Hospital","context_type":1,"properties":[{"type":"decode:avg_yearly_electricy_use_kwh","value":2094,"pl":5},{"type":"decode:persons_in_household","value":4,"pl":5},{"type":"decode:homepage","value":"http://schulist.io/sophie","pl":2},{"type":"decode:last_name","value":"Padberg","pl":3},{"type":"decode:marital_status","value":"Never married","pl":5},{"type":"decode:profession","value":"Senior Legal Manager","pl":4},{"type":"decode:first_name","value":"Ryleigh","pl":3},{"type":"go:neighborhood_role","value":"scholier","pl":1}],"pl_sum":28,"pl_mean":3},{"title":"Halvorson, Kohler and Bernhard","context_type":3,"properties":[{"type":"go:project_association","value":"Liquid Shrimp","pl":2},{"type":"twitter:id","value":"lenora_feil","pl":1},{"type":"decode:avg_yearly_gas_use_m3","value":1825,"pl":5},{"type":"go:expertise","value":"Associate Degree in Design","pl":3},{"type":"go:improve_my_environment","value":"Pluto","pl":3},{"type":"decode:telephone","value":"1-153-431-3983","pl":3},{"type":"decode:gender","value":"Widowed","pl":4},{"type":"decode:street","value":"699 Samantha Stream","pl":4},{"type":"go:neighborhood_role","value":"bewoner","pl":2},{"type":"decode:first_name","value":"Ima","pl":3},{"type":"decode:address","value":"533 Kassulke Curve","pl":3}],"pl_sum":33,"pl_mean":3},{"title":"Washington dragons","context_type":4,"properties":[{"type":"go:project_association","value":"Bit Soup","pl":1},{"type":"decode:homepage","value":"http://oberbrunner.net/orlo.wunsch","pl":2},{"type":"facebook:id","value":"anastacio.volkman@borercain.name","pl":4},{"type":"decode:last_name","value":"Leannon","pl":2},{"type":"decode:address","value":"1106 Gerhold Plaza","pl":2},{"type":"linkedin:id","value":"darren@pfannerstillolson.biz","pl":2},{"type":"go:neighborhood_role","value":"ondernemer","pl":1},{"type":"decode:postcode","value":"95406-0979","pl":4},{"type":"go:improve_my_environment","value":"Parblesnops","pl":2},{"type":"go:newsletter_approval","value":false,"pl":2},{"type":"go:expertise","value":"Associate Degree in Creative Arts","pl":3},{"type":"decode:marital_status","value":"Never married","pl":4}],"pl_sum":29,"pl_mean":2},{"title":"Alice In Chains","context_type":4,"properties":[{"type":"decode:email","value":"fredrick@fahey.biz","pl":3},{"type":"decode:profile_picture","value":"http://lorempixel.com/300/300","pl":3},{"type":"go:neighborhood_role","value":"geinteresseerd","pl":2},{"type":"go:expertise","value":"Associate Degree in Engineering","pl":4},{"type":"go:my_dreams","value":"WUBBA LUBBA DUB DUBS!!!","pl":4},{"type":"decode:street","value":"3703 Sanford Rest","pl":4},{"type":"go:newsletter_approval","value":true,"pl":3},{"type":"facebook:id","value":"emerson@barton.org","pl":3},{"type":"decode:homepage","value":"http://mills.com/emely_gulgowski","pl":3},{"type":"decode:avg_yearly_gas_use_m3","value":1263,"pl":4},{"type":"decode:first_name","value":"Clark","pl":3}],"pl_sum":36,"pl_mean":3},{"title":"The Clash","context_type":4,"properties":[{"type":"decode:birthdate","value":"1937-03-13","pl":4},{"type":"decode:gender","value":"Never married","pl":4},{"type":"go:newsletter_approval","value":false,"pl":2},{"type":"go:neighborhood_role","value":"scholier","pl":2},{"type":"decode:profile_picture","value":"http://lorempixel.com/300/300","pl":3}],"pl_sum":15,"pl_mean":3},{"title":"Taxes","context_type":5,"properties":[{"type":"go:project_association","value":"Cold Duck","pl":2},{"type":"decode:name","value":"Shyanne Berge","pl":2},{"type":"decode:homepage","value":"http://boganjacobi.name/lacey","pl":3},{"type":"go:proof_of_membership","value":"40cdec20e4df4de5a0414c252cc023a3","pl":5},{"type":"decode:avg_yearly_electricy_use_kwh","value":2401,"pl":5},{"type":"facebook:id","value":"alfred@nitzsche.name","pl":4},{"type":"decode:profession","value":"Global Design Engineer","pl":4},{"type":"decode:persons_in_household","value":5,"pl":4},{"type":"decode:avg_yearly_gas_use_m3","value":1349,"pl":4},{"type":"decode:first_name","value":"Josiah","pl":2},{"type":"go:improve_my_environment","value":"Hideout Planet","pl":2},{"type":"decode:gender","value":"Separated","pl":4}],"pl_sum":41,"pl_mean":3},{"title":"Taxes","context_type":5,"properties":[{"type":"go:my_dreams","value":"Sometimes science is a lot more art, than science. A lot of people don't get that.","pl":3},{"type":"decode:address","value":"5405 Schneider Court","pl":3},{"type":"facebook:id","value":"miguel.ortiz@kunde.name","pl":3},{"type":"go:offer","value":"Making the world a better place","pl":1},{"type":"go:improve_my_environment","value":"Earth","pl":2},{"type":"decode:marital_status","value":"Widowed","pl":5},{"type":"decode:avg_yearly_electricy_use_kwh","value":2492,"pl":5},{"type":"twitter:id","value":"damian_beer","pl":2},{"type":"decode:birthdate","value":"1932-12-27","pl":4},{"type":"decode:email","value":"jeffery.kris@larsonschiller.co","pl":3},{"type":"go:neighborhood_role","value":"scholier","pl":1},{"type":"decode:homepage","value":"http://robertseichmann.biz/mellie","pl":2},{"type":"go:proof_of_membership","value":"6912d95386c454537a8f038410586c04","pl":6},{"type":"decode:profession","value":"Construction Agent","pl":5}],"pl_sum":45,"pl_mean":3},{"title":"Mortgage","context_type":5,"properties":[{"type":"go:project_association","value":"BamBot","pl":2},{"type":"decode:marital_status","value":"Divorced","pl":4},{"type":"decode:first_name","value":"Marlon","pl":3},{"type":"decode:membership_organization","value":"Vidiian","pl":3},{"type":"go:expertise","value":"Bachelor of Engineering","pl":3},{"type":"decode:homepage","value":"http://west.biz/ray","pl":3},{"type":"decode:persons_in_household","value":5,"pl":4},{"type":"decode:email","value":"stanford.larson@parisian.biz","pl":3},{"type":"decode:postcode","value":"68025","pl":4},{"type":"decode:profile_picture","value":"http://lorempixel.com/300/300","pl":3},{"type":"decode:profession","value":"International Legal Director","pl":4},{"type":"go:improve_my_environment","value":"Screaming Sun Earth","pl":3},{"type":"go:proof_of_membership","value":"9260b7c91284c0cd2177fc34e9f06fe1","pl":5},{"type":"twitter:id","value":"dereck","pl":1},{"type":"go:need","value":"Jian-Yang, what're you doing? This is Palo Alto. People are lunatics about smoking here. We don't enjoy all the freedoms that you have in China.","pl":1}],"pl_sum":46,"pl_mean":3},{"title":"Gray 1","context_type":6,"properties":[{"type":"decode:name","value":"Myrtice Sipes DVM","pl":3},{"type":"decode:persons_in_household","value":2,"pl":4},{"type":"decode:postcode","value":"37078-3579","pl":5},{"type":"go:expertise","value":"Bachelor of Law","pl":3}],"pl_sum":15,"pl_mean":3},{"title":"Green 9","context_type":6,"properties":[{"type":"decode:postcode","value":"51411-7341","pl":4},{"type":"decode:marital_status","value":"Never married","pl":5},{"type":"skype:id","value":"marcellus","pl":2},{"type":"decode:last_name","value":"Schmidt","pl":2},{"type":"twitter:id","value":"sadye_white","pl":1},{"type":"decode:profession","value":"Internal Education Representative","pl":4}],"pl_sum":18,"pl_mean":3},{"title":"Gold Leader","context_type":6,"properties":[{"type":"decode:profile_picture","value":"http://lorempixel.com/300/300","pl":2},{"type":"go:improve_my_environment","value":"Parblesnops","pl":3},{"type":"decode:telephone","value":"785-355-6153","pl":2},{"type":"decode:profession","value":"National Strategist","pl":5},{"type":"go:newsletter_approval","value":false,"pl":2},{"type":"go:neighborhood_role","value":"geinteresseerd","pl":2},{"type":"go:offer","value":"Our products are products, producing unrivaled results","pl":0},{"type":"decode:persons_in_household","value":2,"pl":4},{"type":"decode:street","value":"882 Gutmann Route","pl":4}],"pl_sum":24,"pl_mean":2}]} ** Example request This sample application request consists of an application name, a set of required property types and a set of optional property types. Each application has a default context type attached to it, (so we can assign it a hue). For each request we can calculate the average privacy level, and the cumulative privacy weight by adding the privacy levels of each property in the request. #+name: request #+BEGIN_SRC js :results output var request = { application : "decodeapp:facebook", context_type : 5,//personal required : ["decode:name", "decode:email", "decode:address"], optional : ["decode:phone"] } var data = JSON.stringify(request) + "\n"; process.stdout.write(data); #+END_SRC #+RESULTS: request : {"application":"decodeapp:facebook","context_type":5,"required":["decode:name","decode:email","decode:address"],"optional":["decode:phone"]} ** request generator #+name: request_generator #+BEGIN_SRC ruby :var context_types=context_types :var privacy_levels=privacy_levels :var properties=properties :results output require 'faker' require 'json' require 'securerandom' request = {} request[:application] = "decodeapp:#{Faker::App.name.downcase.gsub(' ','_')}" request[:context_type] = rand(context_types.count) required_recs = properties.sample(1 + rand(3)) optional_recs = (properties - required_recs).sample(2) request[:required] = required_recs.map{|rec| rec[1]} #map property types request[:optional] = optional_recs.map{|rec| rec[1]} #map property types all_recs = required_recs + optional_recs request[:pl_sum] = all_recs.map{|rec|(rec[2] + rand(2))}.reduce(0,:+) #calculate pl sum for each context request[:pl_mean] = (request[:pl_sum] / all_recs.count) puts request.to_json #+END_SRC #+RESULTS: request_generator : {"application":"decodeapp:konklab","context_type":5,"required":["decode:telephone","decode:email"],"optional":["decode:address","decode:name"],"pl_sum":10,"pl_mean":2} * Data Comparison During the interaction we want to give the user insight into a couple of things; - How does the requested set of properties relate to the different contexts? How well does a context match to the request? - What would it mean for the context if the request was accepted? How many / which properties would have to be added to the context in order to fulfill the request? - What would it mean for the cumulative weight of the context? In below ruby code a comparison is made by on creating the intersection and its inverse between the request and each context. #+name: diff_src #+BEGIN_SRC ruby require 'json' require 'nokogiri' #for creating xml request = JSON.parse(request_data) profile = JSON.parse(profile_data) context_diffs = [] profile["contexts"].each do | context | requested = request["required"] + request["optional"] available = context["properties"].map {|p| p["type"]} intersect = available & requested except = requested - available diff = {:context => context["title"], :intersect => intersect, :except => except} context_diffs << diff end #+END_SRC #+RESULTS: diff_src #+name: xml #+BEGIN_SRC ruby :exports none # unfortunately processing.js doesn't support json yet, so we have to use xml doc = Nokogiri::XML::Builder.new do |xml| xml.result { xml.request { xml.application request["application"] xml.contextType request["context_type"] xml.required { request["required"].each do |p| xml.property p end } xml.optional { request["optional"].each do |p| xml.property p end } xml.plSum request["pl_sum"] xml.plMean request["pl_mean"] } xml.diffs { context_diffs.each do |diff| xml.diff { xml.contextName diff[:context] xml.intersect { diff[:intersect].each do |p| xml.property p end } xml.except { diff[:except].each do |p| xml.property p end } } end } xml.profile { profile["contexts"].each do | context | xml.contextObj { xml.contextName context["title"] xml.contextType context["context_type"] xml.plSum context["pl_sum"] xml.plMean context["pl_mean"] xml.properties { context["properties"].each do |property| xml.property { xml.type property["type"] xml.value property["value"] xml.pl property["pl"] } end } } end } } end path = "diff.xml" File.write(path, doc.to_xml) path #+END_SRC #+name: diff #+BEGIN_SRC ruby :exports none :noweb yes :var profile_data=profile_generator :var request_data=request_generator :results value file <> <> #+END_SRC NOTE: We export to file diff.xml here for easy parsing in processing.js below. #+RESULTS: diff [[file:diff.xml]] * Visualization We want to visualize the following things; - The request with the application name and it's size / quality (Who's asking what) - The different contexts with it's name and size / quality relative to the request. (What would it mean to accept?) Per the design of Dyne, we want to use color to indicate the relation between the request and each context. A color should indicate something about privacy level and context type. For now the mapping is as follows; Different hues can be mapped to each context type. Different tones within the hue can be mapped to each privacy level. #+name: colors #+BEGIN_SRC java :exports none //color definitions color a3 = #3A3B58; color b3 = #734246; color d3 = #B4561F; color c3 = #336F60; color f3 = #7A3E2A; color g3 = #A48137; color e2 = #97BBCB; color a4 = #3B4257; color b4 = #6A4345; color d4 = #86451F; color c4 = #345A48; color f4 = #A92F21; color g4 = #BC983B; color a5 = #3D4358; color b5 = #402623; color d5 = #85442D; color c5 = #3B403A; color f5 = #7A150B; color g5 = #252F2B; color a1 = #597099; color e4 = #0A3878; color b1 = #D16365; color d1 = #FFD43B; color c1 = #B7BF98; color e1 = #CAD2C8; color e0 = #F5EDE5; color f1 = #D17978; color g1 = #FDD23E; color a0 = #C5C3CC; color e3 = #0485B1; color b0 = #FFDCD6; color d0 = #FFE9BE; color c0 = #F0E9D5; color f0 = #E4C8BF; color g0 = #FBE6BA; color a2 = #3D4B79; color e5 = #084064; color b2 = #974244; color d2 = #F8AA08; color c2 = #4E937F; color f2 = #8F4330; color g2 = #FFDB03; color colors[][] = { {b0,b1,b2,b3,b4,b5}, {c0,c1,c2,c3,c4,c5}, {a0,a1,a2,a3,a4,a5}, {d0,d1,d2,d3,d4,d5}, {e0,e1,e2,e3,e4,e5}, {f0,f1,f2,f3,f4,f5}, {g0,g1,g2,g3,g4,g5} }; static class PrivacyLevel { public static int SECRET = 5; public static int PRIVATE = 4; public static int INTIMATE = 3; public static int AFFILIATE = 2; public static int PUBLIC = 1; public static int COMMONS = 0; } static class ContextType { public static int PERSONAL = 0; public static int HEALTH = 1; public static int EDUCATION = 2; public static int WORK = 3; public static int HOBBY = 4; public static int FINANCIAL = 5; public static int OTHER = 6; } public int getColor(int privacy_level, int context_type) { return colors[context_type][privacy_level]; } #+END_SRC #+name: pcolors #+BEGIN_SRC java :exports none String[] levels = {"commons", "public", "affiliate", "intimate", "private", "secret"}; String[] contextTypes = {"personal", "health", "education", "work", "hobby", "financial", "other"}; static class PrivacyLevel { public static int SECRET = 5; public static int PRIVATE = 4; public static int INTIMATE = 3; public static int AFFILIATE = 2; public static int PUBLIC = 1; public static int COMMONS = 0; } static class ContextType { public static int PERSONAL = 0; public static int HEALTH = 1; public static int EDUCATION = 2; public static int WORK = 3; public static int HOBBY = 4; public static int FINANCIAL = 5; public static int OTHER = 6; } int[][] colors = generateColors(); //generate colors to maximize contrast int[][] generateColors() { int[][] pcolors = new int[contextTypes.length][levels.length]; //we start with d0 as a seed color color seed = #FFE9BE;//seed color colorMode(HSB); float h_seed = hue(seed); float s_seed = saturation(seed); float v_seed = brightness(seed); float levelshift = 255/levels.length; float contextshift = 255/contextTypes.length + 1; //cycle hue for each level for (int i = 0; i < contextTypes.length; i++){ float h = h_seed + i * contextshift; if(h > 255){h = h - 255;} //cycle brightness for each context for(int j = 0; j < levels.length; j++) { float v = v_seed - (j * levelshift); float s = s_seed + (j * levelshift); color c = color(h,s,v); pcolors[i][j] = c; } } colorMode(RGB); return pcolors; } public int getColor(int privacy_level, int context_type) { return colors[context_type][privacy_level]; } #+END_SRC This table shows all colors for each context / privacy level combination. Click on the diagram to compare colors sets. #+name: color_table #+BEGIN_SRC processing //color definitions color a3 = #3A3B58; color b3 = #734246; color d3 = #B4561F; color c3 = #336F60; color f3 = #7A3E2A; color g3 = #A48137; color e2 = #97BBCB; color a4 = #3B4257; color b4 = #6A4345; color d4 = #86451F; color c4 = #345A48; color f4 = #A92F21; color g4 = #BC983B; color a5 = #3D4358; color b5 = #402623; color d5 = #85442D; color c5 = #3B403A; color f5 = #7A150B; color g5 = #252F2B; color a1 = #597099; color e4 = #0A3878; color b1 = #D16365; color d1 = #FFD43B; color c1 = #B7BF98; color e1 = #CAD2C8; color e0 = #F5EDE5; color f1 = #D17978; color g1 = #FDD23E; color a0 = #C5C3CC; color e3 = #0485B1; color b0 = #FFDCD6; color d0 = #FFE9BE; color c0 = #F0E9D5; color f0 = #E4C8BF; color g0 = #FBE6BA; color a2 = #3D4B79; color e5 = #084064; color b2 = #974244; color d2 = #F8AA08; color c2 = #4E937F; color f2 = #8F4330; color g2 = #FFDB03; color colors[][] = { {b0,b1,b2,b3,b4,b5}, {c0,c1,c2,c3,c4,c5}, {a0,a1,a2,a3,a4,a5}, {d0,d1,d2,d3,d4,d5}, {e0,e1,e2,e3,e4,e5}, {f0,f1,f2,f3,f4,f5}, {g0,g1,g2,g3,g4,g5} }; boolean procedural = true; String[] levels = {"commons", "public", "affiliate", "intimate", "private", "secret"}; String[] contextTypes = {"personal", "health", "education", "work", "hobby", "financial", "other"}; static class PrivacyLevel { public static int SECRET = 5; public static int PRIVATE = 4; public static int INTIMATE = 3; public static int AFFILIATE = 2; public static int PUBLIC = 1; public static int COMMONS = 0; } static class ContextType { public static int PERSONAL = 0; public static int HEALTH = 1; public static int EDUCATION = 2; public static int WORK = 3; public static int HOBBY = 4; public static int FINANCIAL = 5; public static int OTHER = 6; } public int getColor(int privacy_level, int context_type) { if(procedural){ return pcolors[context_type][privacy_level]; } return colors[context_type][privacy_level]; } int[][] pcolors = new int[contextTypes.length][levels.length]; void setup() { generateColors(); size(600,480); } void mousePressed() { //toggle colors to see the difference procedural = !procedural; } //replace colors in the table with generated ones to maximize contrast void generateColors() { //we start with b0 as a seed color color seed = d0; colorMode(HSB); float h_seed = hue(seed); float s_seed = saturation(seed); float v_seed = brightness(seed); float levelshift = 255/levels.length; float contextshift = 255/contextTypes.length + 1; //cycle hue for each level for (int i = 0; i < contextTypes.length; i++){ float h = h_seed + i * contextshift; if(h > 255){h = h - 255;} //cycle brightness for each context for(int j = 0; j < levels.length; j++) { float v = v_seed - (j * levelshift); float s = s_seed + (j * levelshift); color c = color(h,s,v); pcolors[i][j] = c; } } colorMode(RGB); } void draw() { background(200); float row_height = 50; float column_width = 80; fill(0); //draw privacy level headers for (int i = 0; i < levels.length; i++ ){ text(levels[i], (i + 1) * column_width, row_height ); } //draw context headers for (int j = 0; j < contextTypes.length; j++) { text(contextTypes[j], 20, (j+2) * row_height); } //draw colors for(int i = 0; i < levels.length; i++) { float x = 20 + (i + 1) * column_width; for(int j = 0; j < contextTypes.length; j++) { float y = (j + 2) * row_height; color c = getColor(i,j); fill(c); ellipse(x,y, 20, 20); } } } #+END_SRC #+RESULTS: color_table #+BEGIN_EXPORT html #+END_EXPORT #+name: glue #+BEGIN_SRC java :exports none class Request { public String application; public int contextType; public String[] required_properties; public String[] optional_properties; public int plSum; public int plMean; public Request(String app, int contextType, String[] req, String[] opt, int sum, int mean) { this.application = app; this.contextType = contextType; this.required_properties = req; this.optional_properties = opt; this.plSum = sum; this.plMean = mean; } } class Diff { public String context; public String[] intersect; public String[] except; public Diff(String ctx, String[] intersect, String[] except) { this.context = ctx; this.intersect = intersect; this.except = except; } } class Property { public String type; public String value; public int pl;//privacy level public Property(String type, String value, int pl) { this.type = type; this.value = value; this.pl = pl; } } class Context { public String title; public int plSum; public int plMean; public int contextType; public Property[] properties; public Context(String title, int plSum, int plMean, int contextType, Property[] properties) { this.title = title; this.plSum = plSum; this.plMean = plMean; this.properties = properties; this.contextType = contextType; } } XMLElement doc = new XMLElement(this, 'diff.xml'); //create typed versions because this is java :-( Request parseRequest(XMLElement xml) { XMLElement req = xml.getChild(0); String name = req.getChild(0).getContent(); int contextType = req.getChild(1).getContent(); String[] required = new String[req.getChild(2).getChildCount()]; String[] optional = new String[req.getChild(3).getChildCount()]; int plSum = parseInt(req.getChild(4).getContent()); int plMean = parseInt(req.getChild(5).getContent()); for (int i = 0; i < required.length; i++) { required[i] = req.getChild(2).getChild(i).getContent(); } for (int i = 0; i < optional.length; i++) { optional[i] = req.getChild(3).getChild(i).getContent(); } Request r = new Request(name,contextType,required,optional, plSum, plMean); return r; } Context[] parseProfile(XMLElement xml) { XMLElement profile = xml.getChild(2); Context[] contexts = new Context[profile.getChildCount()]; for(int i = 0; i < contexts.length; i++) { String contextName = profile.getChild(i).getChild(0).getContent(); int contextType = parseInt(profile.getChild(i).getChild(1).getContent()); int plSum = parseInt(profile.getChild(i).getChild(2).getContent()); int plMean = parseInt(profile.getChild(i).getChild(3).getContent()); XMLElement propertiesEl = profile.getChild(i).getChild(4); Property[] properties = new Property[propertiesEl.getChildCount()]; for(int j = 0; j < properties.length; j++) { Property prop = parseProperty(propertiesEl.getChild(j)); properties[j] = prop; } Context context = new Context(contextName, plSum, plMean, contextType, properties); contexts[i] = context; } return contexts; } Property parseProperty(XMLElement propertyEl) { String propertyType = propertyEl.getChild(0).getContent(); String value = propertyEl.getChild(1).getContent(); int pl = parseInt(propertyEl.getChild(2).getContent()); return new Property(propertyType, value, pl); } //create typed versions because this is java :-( Diff[] parseDiffs(xml) { XMLElement diffsEl = xml.getChild(1); Diff[] diffs = new Diff[diffsEl.getChildCount()]; for(int i = 0; i < diffs.length; i++) { String contextName = diffsEl.getChild(i).getChild(0).getContent(); String[] intersects = new String[diffsEl.getChild(i).getChild(1).getChildCount()]; String[] except = new String[diffsEl.getChild(i).getChild(2).getChildCount()]; for(int j = 0; j < intersects.length; j++) { intersects[j] = diffsEl.getChild(i).getChild(1).getChild(j).getContent(); } for(int j = 0; j < except.length; j++) { except[j] = diffsEl.getChild(i).getChild(2).getChild(j).getContent(); } Diff diff = new Diff(contextName,intersects,except); diffs[i] = diff; } return diffs; } Request request = parseRequest(doc); Diff[] diffs = parseDiffs(doc); Context[] contexts = parseProfile(doc); #+END_SRC * Interaction We have come up with an interaction for entitlements that is based around a 'context switcher'. The color of the section in the center always represents the current context. The outer ring is divided into separate 'sectors' for each context that you have defined. Each context has a different color, based on the context type and the data that it holds. For example if you are at work you would (or the application will automatically) set the current context to blue by clicking on the blue sector. When a request comes in through decode, it will be positioned in orbit around the context switcher, close by the sector of the calculated best fitted context. Based on the category and the data diff. #+name: fitness_src #+BEGIN_SRC java //calculate the fitness score for the given context and diff for the current request int fitnessScore(c,d) { int score = 0; if(c.contextType == request.contextType) { score += 5; } //add one point for each property that is present score += d.intersect.length; //subtract one point for each property that is missing score -= d.except.length; return score; } #+END_SRC The user can drag the request around the outer ring to compare the impact it would have, accepting the request for each sector in the context switcher. After a suitable context is found, the user can let go of the request, and a dialog appears in the center, asking the user to either accept or decline the request. The example below demonstrates the concept with a randomly generated profile and request. #+name: context_switcher_src #+BEGIN_SRC java :exports none //magic numbers float contextSize = 500;//random(50,100); //random between 50 and 100 float requestSize = 50; float eqd_angle = TWO_PI / contexts.length; float PROPORTION_CENTER = 0.6; float DISTANCE_FACTOR = 1.4; //globals float centerX, centerY; float xOffset, yOffset; float requestX, requestY;//request coordinates, they can move boolean hitRequest = false; boolean dragging = false; int hitContext = -1; int currentContext = 1;//first context is the default context PFont font; PFont titleFont; void setup() { size(800,800); rectMode(RADIUS); smooth(); titleFont = createFont("HelveticaNeue", 20); font = createFont("HelveticaNeue", 14); centerX = width/2; centerY = height/2; int bestContext = bestFit(); PVector position = positionForSectorIndex(bestContext); requestX = position.x; requestY = position.y; } //calculate best fitting sector int bestFit() { int count = 0; int bestContext = -1; int bestScore = -99; for(Context context : contexts) { Diff diff = diffs[count]; int score = fitnessScore(context,diff); if(score > bestScore) { bestScore = score; bestContext = count; } count++; } return bestContext; } //the position should be outside the center of the middle of the sector PVector positionForSectorIndex(int index) { float angle = (PI/2) + (index * eqd_angle) + (eqd_angle/2); float x = centerX + (contextSize/2 * DISTANCE_FACTOR) * cos(angle); //calculate xPos float y = centerY + (contextSize/2 * DISTANCE_FACTOR) * sin(angle); //calculate yPos return new PVector(x,y); } //draw each frame void draw() { background(255); drawTitle(); //drawBorder(); drawSectors(); drawCurrentContext(); drawRequest(); } //draw the request and label void drawRequest() { color strokeColor = hitRequest? 255 : 153; stroke(strokeColor); color reqColor = getColor(request.plMean, request.contextType); fill(reqColor,127); ellipse(requestX,requestY, requestSize,requestSize); colorMode(HSB); fill(darken(reqColor)); textAlign(CENTER,CENTER); text(request.application + "\n(" + contextTypes[request.contextType]+ ")", requestX, requestY + 50/1.5); text("" + request.plSum, requestX, requestY); colorMode(RGB); } //draw current context as center circle over the pie void drawCurrentContext() { Context current = contexts[currentContext]; //set up colors for current context color contextColor = getColor(current.plMean, current.contextType); colorMode(HSB); color darkContextColor = darken(contextColor); colorMode(RGB); fill(contextColor); noStroke(); ellipse(centerX, centerY, contextSize * PROPORTION_CENTER, contextSize * PROPORTION_CENTER); //if we have hit a context draw detailed information in the center if(hitContext > -1) { fill(255); noStroke(); ellipse(centerX, centerY, contextSize * PROPORTION_CENTER * 0.95, contextSize * PROPORTION_CENTER * 0.95); if(dragging == false) { drawButtons(darkContextColor); } else { textFont(font); drawDiff(current, contextColor); } } else //just draw the current context title { textFont(font); fill(255); noStroke(); ellipse(centerX, centerY, contextSize * 0.4, contextSize * 0.4); fill(darkContextColor); textAlign(CENTER,CENTER); text("My \n" + current.title + "\n data", centerX, centerY); } } //darken the given color, expects hsb color mode int darken(int c) { float h = hue(c); float s = saturation(c); float v = brightness(c); return color(h,s,v * 0.6); } //draw the accept / decline buttons void drawButtons(color darkContextColor) { //draw accept and decline button fill(darkContextColor); textFont(font); textAlign(CENTER,CENTER); text("ACCEPT | DECLINE", centerX, centerY); } //show diff information void drawDiff(Context c, color contextColor) { ellipse(centerX, centerY, contextSize * PROPORTION_CENTER * 0.95, contextSize * PROPORTION_CENTER * 0.95); colorMode(HSB); fill(darken(contextColor)); colorMode(RGB); Diff d = diffs[hitContext]; String available = getIntersection(c,d); String missing = getMissing(d); String diffMessage = c.title + " (" + c.plSum + ")\n"; if(d.intersect.length > 0) { diffMessage += "\nAvailable: \n" + available; } if(d.except.length > 0){ diffMessage += "\nMissing: \n" + missing;} textAlign(CENTER,CENTER); text(diffMessage, centerX, centerY); } //draw Sectors for each context as pie pieces void drawSectors() { noStroke(); int count = 0; float begin = (PI/2); float end = begin + eqd_angle; int previousContextType = contexts[0].contextType; for(Context context : contexts) { color contextColor = getColor(context.plMean, context.contextType); colorMode(HSB); color darkContextColor = darken(contextColor); colorMode(RGB); fill(contextColor); arc(centerX, centerY, contextSize, contextSize, begin, end); int ext = 20; if(count == currentContext) { noStroke(); arc(centerX, centerY, contextSize + ext , contextSize + ext, begin, end); } if(context.contextType != previousContextType) { stroke(255); strokeWeight(3); } else { ext = 0; stroke(darkContextColor); strokeWeight(1); } //draw white line as divider float angle = (PI/2) + (count * eqd_angle); float x = centerX + (contextSize/2 + ext) * cos(angle); float y = centerY + (contextSize/2 + ext) * sin(angle); line(centerX,centerY,x,y); noStroke(); strokeWeight(1); begin = end; end = begin + eqd_angle; previousContextType = context.contextType; count++; } //draw first white divider from center down. stroke(255); strokeWeight(3); line(centerX,centerY,centerX,centerY + contextSize/2); strokeWeight(1); } //draw outer rim void drawBorder() { Context current = contexts[currentContext]; //set up colors for current context color contextColor = getColor(current.plMean, current.contextType); colorMode(HSB); color darkContextColor = darken(contextColor); colorMode(RGB); stroke(darkContextColor); fill(255); ellipse(centerX, centerY, contextSize + 10, contextSize + 10); } //draw title void drawTitle() { textFont(titleFont); fill(150); textAlign(CENTER); text("Do you want to entitle " + request.application + " ?\nPlease drag the request on to your preferred context." , centerX, 80); } /* interaction functions */ void mousePressed() { //test if request hit boolean hitRequest = (mouseX > requestX - requestSize/2 && mouseX < requestX + requestSize/2 && mouseY > requestY - requestSize/2 && mouseY < requestY + requestSize/2); //update global dragging = hitRequest; //test if context hit float deltaX = mouseX - centerX; float deltaY = mouseY - centerY; int index = hitContextIndex(deltaX,deltaY); if(index > -1) { currentContext = index; } } void mouseDragged() { float deltaX = mouseX - centerX; float deltaY = mouseY - centerY; float distance = sqrt(deltaX*deltaX + deltaY*deltaY); float fraction = distance / (contextSize / 2); float minDistanceFactor = (PROPORTION_CENTER + (1-PROPORTION_CENTER)/2); //outside the circle is allowed anywhere if(fraction > minDistanceFactor && dragging) { requestX = mouseX - xOffset; requestY = mouseY - yOffset; hitContext = hitContextIndex(deltaX,deltaY); if(hitContext > -1){currentContext = hitContext;} } else if(dragging) //else calculate closest point that is allowed { float mouseAngle = (PI/2) - atan2(deltaX, deltaY); float x = centerX + (contextSize/2 * minDistanceFactor) * cos(mouseAngle); //calculate xPos float y = centerY + (contextSize/2 * minDistanceFactor) * sin(mouseAngle); //calculate yPos requestX = x; requestY = y; hitContext = hitContextIndex(x - centerX, y - centerY); currentContext = hitContext; } } void mouseReleased() { dragging = false; //hitContext = -1; //PVector position = positionForSectorIndex(1); //requestX = position.x; //requestY = position.y; } //calculate which pie part was clicked, //by means of the angle between the mouse point an the center of the circle //returns -1 if no pie part was clicked //(because the distance is not between 0.8 and 1 times the distanceSize) int hitContextIndex(float deltaX, float deltaY) { float distance = sqrt(deltaX*deltaX + deltaY*deltaY); float fraction = distance / (contextSize / 2); if(fraction < PROPORTION_CENTER || fraction > 1) { return -1; } //overstaande, aanliggende => tan dus atan2 float mouseAngle = atan2(deltaX, deltaY); float degrees = mouseAngle * 180 / PI; if(degrees < 0){degrees = 360 + degrees;} //so everyting is on one continuum float part = eqd_angle * 180 /PI; int index = (contexts.length - ((int) (degrees / part))) - 1;//because we start at the bottom. return index; } /* data functions */ //create a string that lists the values of the intersection in d, with the values in c String getIntersection(Context c, Diff d) { String result = ""; for(String key : d.intersect) { String value = getPropertyValue(c, key); if(value != null) result += key.substring(key.indexOf(":")+1) + " (" + value + ")\n"; } return result; } //create a string that lists the missing keys in the intersection d String getMissing(Diff d) { String result = ""; for(String key : d.except) { result += key.substring(key.indexOf(":")+1) + "\n"; } return result; } //get the value of a property value in c designated by key String getPropertyValue(Context c, String key) { int MAX_LENGTH = 10; for(Property p : c.properties) { if(p.type == key) { String value = p.value; if(value.length > MAX_LENGTH) { value = value.substring(0, Math.min(value.length(), MAX_LENGTH)) + "..."; } return value; } } return null; } #+END_SRC #+name: context_switcher #+BEGIN_SRC processing :noweb yes <> <> <> <> #+END_SRC #+RESULTS: context_switcher #+BEGIN_EXPORT html #+END_EXPORT