001/**
002 * Copyright (c) 2011, The University of Southampton and the individual contributors.
003 * All rights reserved.
004 *
005 * Redistribution and use in source and binary forms, with or without modification,
006 * are permitted provided that the following conditions are met:
007 *
008 *   *  Redistributions of source code must retain the above copyright notice,
009 *      this list of conditions and the following disclaimer.
010 *
011 *   *  Redistributions in binary form must reproduce the above copyright notice,
012 *      this list of conditions and the following disclaimer in the documentation
013 *      and/or other materials provided with the distribution.
014 *
015 *   *  Neither the name of the University of Southampton nor the names of its
016 *      contributors may be used to endorse or promote products derived from this
017 *      software without specific prior written permission.
018 *
019 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
020 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
021 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
022 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
023 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
024 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
025 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
026 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
027 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
028 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029 */
030package org.openimaj.demos.twitter;
031
032import java.util.ArrayList;
033import java.util.HashMap;
034import java.util.List;
035import java.util.Map;
036import java.util.Random;
037
038import org.openimaj.data.RandomData;
039import org.openimaj.image.MBFImage;
040import org.openimaj.image.colour.ColourMap;
041import org.openimaj.image.colour.ColourSpace;
042import org.openimaj.image.colour.RGBColour;
043import org.openimaj.image.renderer.MBFImageRenderer;
044import org.openimaj.image.renderer.RenderHints;
045import org.openimaj.image.typography.FontStyle.HorizontalAlignment;
046import org.openimaj.image.typography.FontStyle.VerticalAlignment;
047import org.openimaj.image.typography.general.GeneralFont;
048import org.openimaj.image.typography.general.GeneralFontStyle;
049import org.openimaj.math.geometry.point.Point2d;
050import org.openimaj.math.geometry.point.Point2dImpl;
051import org.openimaj.math.geometry.shape.Circle;
052import org.openimaj.math.geometry.shape.Rectangle;
053import org.openimaj.util.data.Context;
054import org.openimaj.util.function.Operation;
055import org.openimaj.util.pair.IndependentPair;
056import org.openimaj.video.Video;
057import org.openimaj.video.VideoDisplay;
058
059import twitter4j.Status;
060
061public class PrettyTagRenderer implements Operation<Context> {
062
063        private static final int HEIGHT = 1024;
064        private static final int WIDTH = 1024;
065        private static final int GRID_WH = 160;
066        private static final float GRID_CIRCLE = 0.9f;
067        private static final long BLIP_TIME = 1000;
068        private Map<String,Circle> hashCircles = new HashMap<String, Circle>();
069        private Map<String,Float[]> hashColours = new HashMap<String, Float[]>();
070        private Map<String,HashAggregation> hashAggregations = new HashMap<String, HashAggregation>();
071        
072        private Video<MBFImage> video;
073        private Random rand = new Random();
074        private List<IndependentPair<Point2dImpl,MBFImage>> textLayers = new ArrayList<IndependentPair<Point2dImpl, MBFImage>>();
075        static class HashAggregation{
076                int total;
077                long lastSeen;
078        }
079        public PrettyTagRenderer(String[] hashStrings) {
080                int index = 0;
081                int nx = WIDTH/GRID_WH; 
082                int[] randCols = RandomData.getUniqueRandomInts(hashStrings.length, 0, hashStrings.length);
083                GeneralFont f = new GeneralFont( "Helvetica", java.awt.Font.PLAIN);
084                
085                MBFImageRenderer tmpRenderer = new MBFImage(0,0,ColourSpace.RGBA).createRenderer();
086                for (String hash : hashStrings) {
087                        float y = (index / nx) * GRID_WH;
088                        float x = (index - (index / nx) * nx) * GRID_WH;
089                        float offset = 0;
090                        if(x == 0){
091                                offset = offset(hashStrings.length,index,nx);
092                        }
093                        y += GRID_WH/2;
094                        x += GRID_WH/2 + offset;
095                        float rad = GRID_WH*GRID_CIRCLE/2;
096                        this.hashCircles.put(hash, new Circle(x,y,rad));
097                        this.hashColours.put(hash, ColourMap.Autumn.apply(randCols[index] / (float)hashStrings.length));
098                        this.hashAggregations.put(hash,new HashAggregation());
099                        GeneralFontStyle<Float[]> fs = new GeneralFontStyle<Float[]>(f, tmpRenderer);
100                        Rectangle textSize = fs.getRenderer(tmpRenderer).getSize(hash, fs);
101                        MBFImage textLayer = new MBFImage((int)textSize.width,(int)textSize.height,ColourSpace.RGBA);
102                        MBFImageRenderer aaTextRend = textLayer.createRenderer(RenderHints.ANTI_ALIASED);
103                        fs = new GeneralFontStyle<Float[]>(f, aaTextRend);
104                        fs.setColour(new Float[]{1f,1f,1f,1f});
105                        fs.setHorizontalAlignment(HorizontalAlignment.HORIZONTAL_LEFT);
106                        fs.setVerticalAlignment(VerticalAlignment.VERTICAL_TOP);
107                        fs.setFontSize(16);
108                        aaTextRend.drawText(hash, new Point2dImpl(0, textSize.height*2/3), fs);
109                        this.textLayers.add(IndependentPair.pair(new Point2dImpl(x-textSize.width*1/3, y-textSize.height/3),textLayer));
110                        index++;
111                }
112                
113                final MBFImage output = new MBFImage(WIDTH,HEIGHT,ColourSpace.RGB);
114                
115                
116                
117                video = new Video<MBFImage>(){
118
119                        private MBFImage frame = output;
120
121                        @Override
122                        public MBFImage getNextFrame() {
123                                this.frame.fill(RGBColour.BLACK);
124                                redrawCircles(this.frame);
125                                return frame;
126                        }
127
128                        @Override
129                        public MBFImage getCurrentFrame() {
130                                return frame;
131                        }
132
133                        @Override
134                        public int getWidth() {
135                                return WIDTH;
136                        }
137
138                        @Override
139                        public int getHeight() {
140                                return HEIGHT;
141                        }
142
143                        @Override
144                        public long getTimeStamp() {
145                                return -1;
146                        }
147
148                        @Override
149                        public double getFPS() {
150                                return 30;
151                        }
152
153                        @Override
154                        public boolean hasNextFrame() {
155                                return true;
156                        }
157
158                        @Override
159                        public long countFrames() {
160                                return -1;
161                        }
162
163                        @Override
164                        public void reset() {
165                                // TODO Auto-generated method stub
166                                
167                        }
168                        
169                };
170                start();
171        }
172
173        private void start() {
174                VideoDisplay.createVideoDisplay(video);
175        }
176
177        private float offset(int N, int index, int nx) {
178                if(N - index >= nx) return 0;
179                int diff = N - index;
180                
181                return (diff * GRID_WH ) ;
182        }
183
184        private void redrawCircles(MBFImage output) {
185                long now = System.currentTimeMillis();
186                output.fill(RGBColour.WHITE);
187                MBFImageRenderer rend = output.createRenderer(RenderHints.ANTI_ALIASED);
188                for (String hash: this.hashCircles.keySet()) {
189                        Circle circle = this.hashCircles.get(hash);
190                        Float[] col = this.hashColours.get(hash);
191                        float level = 2;
192                        long lastSeen = this.hashAggregations.get(hash).lastSeen;
193                        if(lastSeen!=0){                                
194                                long diff = Math.abs(lastSeen - now);
195                                if(diff < BLIP_TIME){
196                                        level -= (1 - ( diff / (float)BLIP_TIME));
197                                }
198                        }
199                        Float[] offCircleColour = dark(col,level);
200                        
201                        drawHashCircle(rend , hash, circle, offCircleColour);
202                }
203                for (IndependentPair<Point2dImpl, MBFImage> pTextLayer : this.textLayers) {
204                        
205                        MBFImage textLayer = pTextLayer.getSecondObject();
206                        Point2d p = pTextLayer.firstObject();
207                        output.drawImage(textLayer , (int)p.getX(), (int)p.getY());
208                }
209        }
210
211        private void drawHashCircle(MBFImageRenderer rend, String hash, Circle circle, Float[] colour) {
212                
213                rend.drawShapeFilled(circle, colour);
214                rend.drawShape(circle, 3, RGBColour.BLACK);
215                        
216        }
217
218        private Float[] dark(Float[] col){
219                return dark(col,2.f);
220        }
221        private Float[] dark(Float[] col,float amount) {
222                if(amount == 0) return col;
223                Float[] hsiCol = ColourSpace.HSV.convertFromRGB(col);
224                hsiCol[2]/=amount;
225                return ColourSpace.HSV.convertToRGB(hsiCol);
226        }
227
228        private Float[] inverse(Float[] col) {
229                Float[] ret = new Float[]{
230                        1 - col[0],
231                        1 - col[1],
232                        1 - col[2],
233                };
234                return ret;
235        }
236
237        @Override
238        public void perform(Context object) {
239                String hashtag = object.getTyped(HashTagMatch.HASHTAG_KEY);
240                Status status = object.getTyped("status");
241                if(!this.hashCircles.containsKey(hashtag)) return;
242                activate(hashtag,status);
243        }
244        
245        
246        
247        private void activate(String hashtag, Status status) {
248//              synchronized (hashAggregations) {
249                        HashAggregation aggr = this.hashAggregations.get(hashtag);
250                        aggr.total ++;
251                        long time = System.currentTimeMillis();
252                        aggr.lastSeen = time;
253//              }
254        }
255        
256
257}