EMMA Coverage Report (generated Mon Mar 04 13:03:13 CET 2013)
[all classes][net.pierrox.mcompass]

COVERAGE SUMMARY FOR SOURCE FILE [Turntable.java]

nameclass, %method, %block, %line, %
Turntable.java100% (1/1)100% (12/12)100% (1822/1825)100% (326/327)

COVERAGE BREAKDOWN BY CLASS AND METHOD

nameclass, %method, %block, %line, %
     
class Turntable100% (1/1)100% (12/12)100% (1822/1825)100% (326/327)
<static initializer> 100% (1/1)100% (65/65)100% (4/4)
Turntable (): void 100% (1/1)100% (15/15)100% (6/6)
buildCapObject (): void 100% (1/1)100% (229/229)100% (36/36)
buildDialObject (): void 100% (1/1)100% (288/288)100% (55/55)
buildDialTexture (GL10): void 100% (1/1)100% (404/404)100% (76/76)
buildObjects (): void 100% (1/1)100% (10/10)100% (5/5)
buildRingObject (): void 100% (1/1)100% (357/357)100% (56/56)
buildRingTexture (GL10): void 100% (1/1)100% (241/241)100% (41/41)
buildTextures (GL10): void 100% (1/1)100% (20/20)100% (6/6)
draw (GL10): void 100% (1/1)98%  (171/174)97%  (33/34)
setDetailsLevel (int): void 100% (1/1)100% (11/11)100% (4/4)
setReversedRing (boolean): void 100% (1/1)100% (11/11)100% (4/4)

1/*
2 * Copyright (C) 2009 Pierre Hďż˝bert <pierrox@pierrox.net>
3 * http://www.pierrox.net/mcompass/
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 *      http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17 
18package net.pierrox.mcompass;
19 
20import java.nio.ByteBuffer;
21import java.nio.ByteOrder;
22import java.nio.IntBuffer;
23 
24import javax.microedition.khronos.opengles.GL10;
25 
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.LinearGradient;
29import android.graphics.Paint;
30import android.graphics.Path;
31import android.graphics.RadialGradient;
32import android.graphics.Shader;
33 
34public class Turntable {
35        private static final int DETAIL_X[]={ 15, 25, 30 };
36        private static final int DETAIL_Y[]={ 3, 6, 6 };
37        private static final int RING_HEIGHT[]={ 2, 3, 3};
38        
39        private static final int TEXTURE_RING=0;
40        private static final int TEXTURE_DIAL=1;
41        
42        private static final String[] CARDINAL_POINTS={ "N", "W", "S", "E" };
43        
44        // preference values
45        private int mDetailsLevel;
46        private boolean mReversedRing;
47        
48        private int[] mTextures;
49 
50        private IntBuffer mRingVertexBuffer;
51        private IntBuffer mRingNormalBuffer;
52        private IntBuffer mRingTexCoordBuffer;
53        private ByteBuffer mRingIndexBuffer;
54        
55        private IntBuffer mDialVertexBuffer;
56        private IntBuffer mDialNormalBuffer;
57        private IntBuffer mDialTexCoordBuffer;
58        private ByteBuffer mDialIndexBuffer;
59        
60        private IntBuffer mCapVertexBuffer;
61        private ByteBuffer mCapIndexBuffer;
62 
63        private boolean mNeedObjectsUpdate;
64        private boolean mNeedTextureUpdate;
65        
66    
67        public Turntable() {
68                mDetailsLevel=0;
69                mReversedRing=false;
70                
71                // initially both objects and textures need to be built
72                mNeedObjectsUpdate=true;
73                mNeedTextureUpdate=true;
74        }
75        
76    private void buildObjects() {
77                buildRingObject();
78                buildCapObject();
79                buildDialObject();
80                
81                mNeedObjectsUpdate=false;
82        }
83    
84        void buildRingObject() {
85                // build vertices
86                int dx=DETAIL_X[mDetailsLevel];
87                int dy=DETAIL_Y[mDetailsLevel];
88                int rh=RING_HEIGHT[mDetailsLevel];
89                
90                int vertices[]=new int[((dx+1)*(rh+1))*3];
91                int normals[]=new int[((dx+1)*(rh+1))*3];
92                int n=0;
93        for(int i=0; i<=dx; i++) {
94                for(int j=0; j<=rh; j++) {
95                        double a = i*(Math.PI*2)/dx;
96                        double b = j*Math.PI/(dy*2);
97        
98                        double x = Math.sin(a)*Math.cos(b);
99                        double y = -Math.sin(b);
100                        double z = Math.cos(a)*Math.cos(b);
101                        
102                        vertices[n] = (int) (x*65536);
103                        vertices[n+1] = (int) (y*65536);
104                        vertices[n+2] = (int) (z*65536);
105                        normals[n] = vertices[n];
106                        normals[n+1] = vertices[n+1];
107                        normals[n+2] = vertices[n+2];
108                        n+=3;
109                }
110        }
111        
112        // build textures coordinates
113        int texCoords[]=new int[(dx+1)*(rh+1)*2];
114        n=0;
115        for(int i=0; i<=dx; i++) {
116                for(int j=0; j<=rh; j++) {
117                        texCoords[n++] = (i<<16)/dx;
118                        texCoords[n++] = (j<<16)/rh;
119                }
120        }
121        
122        // build indices
123        byte indices[]=new byte[dx*rh*3*2];
124        n=0;
125        for(int i=0; i<dx; i++) {
126                for(int j=0; j<rh; j++) {
127                        byte p0=(byte) ((rh+1)*i+j);
128                        indices[n++]=p0;
129                    indices[n++]=(byte) (p0+rh+1);
130                    indices[n++]=(byte) (p0+1);
131                    
132                        indices[n++]=(byte) (p0+rh+1);
133                        indices[n++]=(byte) (p0+rh+2);
134                        indices[n++]=(byte) (p0+1);
135                }
136        }
137 
138        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
139        vbb.order(ByteOrder.nativeOrder());
140        mRingVertexBuffer = vbb.asIntBuffer();
141        mRingVertexBuffer.put(vertices);
142        mRingVertexBuffer.position(0);
143        
144        ByteBuffer nbb = ByteBuffer.allocateDirect(normals.length*4);
145        nbb.order(ByteOrder.nativeOrder());
146        mRingNormalBuffer = nbb.asIntBuffer();
147        mRingNormalBuffer.put(normals);
148        mRingNormalBuffer.position(0);
149        
150        mRingIndexBuffer = ByteBuffer.allocateDirect(indices.length);
151        mRingIndexBuffer.put(indices);
152        mRingIndexBuffer.position(0);
153 
154        ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length*4);
155        tbb.order(ByteOrder.nativeOrder());
156        mRingTexCoordBuffer = tbb.asIntBuffer();
157        mRingTexCoordBuffer.put(texCoords);
158        mRingTexCoordBuffer.position(0);
159        }
160        
161        void buildCapObject() {
162                int dx=DETAIL_X[mDetailsLevel];
163                int dy=DETAIL_Y[mDetailsLevel];
164                int rh=RING_HEIGHT[mDetailsLevel];
165                
166        int h=dy-rh;
167        
168                // build vertices
169                int vertices[]=new int[((dx+1)*(h+1))*3];
170                int n=0;
171        for(int i=0; i<=dx; i++) {
172                for(int j=rh; j<=dy; j++) {
173                        double a = i*(Math.PI*2)/dx;
174                        double b = j*Math.PI/(dy*2);
175        
176                        double x = Math.sin(a)*Math.cos(b);
177                        double y = -Math.sin(b);
178                        double z = Math.cos(a)*Math.cos(b);
179                        
180                        vertices[n++] = (int) (x*65536);
181                        vertices[n++] = (int) (y*65536);
182                        vertices[n++] = (int) (z*65536);
183                }
184        }
185                
186        // build indices
187        byte indices[]=new byte[dx*h*3*2];
188        n=0;
189        for(int i=0; i<dx; i++) {
190                for(int j=0; j<h; j++) {
191                        byte p0=(byte) ((h+1)*i+j);
192                        indices[n++]=p0;
193                    indices[n++]=(byte) (p0+h+1);
194                    indices[n++]=(byte) (p0+1);
195                    
196                        indices[n++]=(byte) (p0+h+1);
197                        indices[n++]=(byte) (p0+h+2);
198                        indices[n++]=(byte) (p0+1);
199                }
200        }
201 
202        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
203        vbb.order(ByteOrder.nativeOrder());
204        mCapVertexBuffer = vbb.asIntBuffer();
205        mCapVertexBuffer.put(vertices);
206        mCapVertexBuffer.position(0);
207        
208        mCapIndexBuffer = ByteBuffer.allocateDirect(indices.length);
209        mCapIndexBuffer.put(indices);
210        mCapIndexBuffer.position(0);                
211        }
212        
213        void buildDialObject() {
214        // build vertices
215                int dx=DETAIL_X[mDetailsLevel];
216                
217                int vertices[]=new int[(dx+2)*3];
218                int normals[]=new int[(dx+2)*3];
219                int n=0;
220                // center of the dial
221        vertices[n] = 0;
222        vertices[n+1] = 0;
223        vertices[n+2] = 0;
224        normals[n] = 0;
225        normals[n+1] = 1<<16;
226        normals[n+2] = 0;
227        n+=3;
228                for(int i=0; i<=dx; i++) {
229                double a = i*(Math.PI*2)/dx;
230        
231                double x = Math.sin(a);
232                double z = Math.cos(a);
233                        
234                vertices[n] = (int) (x*65536);
235                vertices[n+1] = 0;
236                vertices[n+2] = (int) (z*65536);
237                normals[n] = 0;
238                normals[n+1] = 1<<16;
239                normals[n+2] = 0;
240                n+=3;
241        }
242        
243        // build textures coordinates
244        int texCoords[]=new int[(dx+2)*2];
245        n=0;
246        texCoords[n++] = (int)(0.5*65536);
247        texCoords[n++] = (int)(0.5*65536);
248        for(int i=0; i<=dx; i++) {
249                double a = i*(Math.PI*2)/dx;
250                        
251                double x = (Math.sin(a)+1)/2;
252                double z = (Math.cos(a)+1)/2;
253                    
254                texCoords[n++] = (int)(x*65536);
255                texCoords[n++] = (int)(z*65536);
256        }
257        
258        // build indices
259        byte indices[]=new byte[dx+2];
260        n=0;
261        for(int i=0; i<=(dx+1); i++) {
262                indices[n++]=(byte)i;
263        }        
264 
265        ByteBuffer vbb = ByteBuffer.allocateDirect(vertices.length*4);
266        vbb.order(ByteOrder.nativeOrder());
267        mDialVertexBuffer = vbb.asIntBuffer();
268        mDialVertexBuffer.put(vertices);
269        mDialVertexBuffer.position(0);
270        
271        ByteBuffer nbb = ByteBuffer.allocateDirect(normals.length*4);
272        nbb.order(ByteOrder.nativeOrder());
273        mDialNormalBuffer = nbb.asIntBuffer();
274        mDialNormalBuffer.put(normals);
275        mDialNormalBuffer.position(0);
276        
277        mDialIndexBuffer = ByteBuffer.allocateDirect(indices.length);
278        mDialIndexBuffer.put(indices);
279        mDialIndexBuffer.position(0);
280 
281        ByteBuffer tbb = ByteBuffer.allocateDirect(texCoords.length*4);
282        tbb.order(ByteOrder.nativeOrder());
283        mDialTexCoordBuffer = tbb.asIntBuffer();
284        mDialTexCoordBuffer.put(texCoords);
285        mDialTexCoordBuffer.position(0);
286        }
287        
288        public void draw(GL10 gl) {
289                // rebuild objects or textures if needed
290                if(mNeedObjectsUpdate) {
291                        buildObjects();
292                }
293                
294                if(mNeedTextureUpdate) {
295                        buildTextures(gl);
296                }
297                
298                int dx=DETAIL_X[mDetailsLevel];
299                int dy=DETAIL_Y[mDetailsLevel];
300                int rh=RING_HEIGHT[mDetailsLevel];
301                
302                gl.glFrontFace(GL10.GL_CW);
303                gl.glColor4x(1<<16, 0<<16, 0<<16, 1<<16);
304        
305                // common parameters for the ring and the dial
306                gl.glEnable(GL10.GL_TEXTURE_2D);
307        gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
308        
309        gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
310                gl.glColor4x(1<<16, 1<<16, 1<<16, 1<<16);
311                gl.glScalex(90000, 90000, 90000);
312                
313                // draw the ring
314                gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
315        gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextures[TEXTURE_RING]);
316        gl.glVertexPointer(3, GL10.GL_FIXED, 0, mRingVertexBuffer);
317        gl.glNormalPointer(GL10.GL_FIXED, 0, mRingNormalBuffer);
318                gl.glTexCoordPointer(2, GL10.GL_FIXED, 0, mRingTexCoordBuffer);
319                gl.glDrawElements(GL10.GL_TRIANGLES, dx*rh*6, GL10.GL_UNSIGNED_BYTE, mRingIndexBuffer);
320                
321                // draw the dial
322                gl.glFrontFace(GL10.GL_CCW);
323                gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextures[TEXTURE_DIAL]);
324                gl.glVertexPointer(3, GL10.GL_FIXED, 0, mDialVertexBuffer);
325                gl.glNormalPointer(GL10.GL_FIXED, 0, mDialNormalBuffer);
326                gl.glTexCoordPointer(2, GL10.GL_FIXED, 0, mDialTexCoordBuffer);
327                gl.glDrawElements(GL10.GL_TRIANGLE_FAN, dx+2, GL10.GL_UNSIGNED_BYTE, mDialIndexBuffer);
328                gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
329                
330                // draw the cap
331                gl.glFrontFace(GL10.GL_CW);
332                gl.glColor4x(0<<16, 0<<16, 0<<16, 1<<16);
333                gl.glDisable(GL10.GL_TEXTURE_2D);
334                gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
335                gl.glVertexPointer(3, GL10.GL_FIXED, 0, mCapVertexBuffer);
336                gl.glDrawElements(GL10.GL_TRIANGLES, dx*(dy-rh)*6, GL10.GL_UNSIGNED_BYTE, mCapIndexBuffer);
337    }
338 
339        void buildTextures(GL10 gl) {
340        mTextures=new int[2];
341        
342        gl.glGenTextures(2, mTextures, 0);
343        
344        buildRingTexture(gl);
345        buildDialTexture(gl);
346        
347        mNeedTextureUpdate=false;
348    }
349    
350    void buildRingTexture(GL10 gl) {
351        gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextures[TEXTURE_RING]);
352        gl.glPixelStorei(GL10.GL_UNPACK_ALIGNMENT, 1);
353        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
354        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
355        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
356        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
357        
358        final int length=512;
359        final int height=64;
360        Bitmap b=Bitmap.createBitmap(length, height, Bitmap.Config.ARGB_8888);
361        b.eraseColor(0xff000000);
362        Canvas canvas=new Canvas(b);
363        
364        Paint p=new Paint();
365        p.setAntiAlias(true);
366        
367        // draw minor graduations in grey
368        /*p.setColor(0xffa0a0a0);
369        for(int d=0; d<360; d++) {
370                canvas.drawLine(d*2, 0, d*2, 10, p);
371        }*/
372        
373        // draw medium graduations in white
374        p.setColor(0xffffffff);
375        for(int d=0; d<360; d+=10) {
376                int pos=d*length/360;
377                canvas.drawLine(pos, 0, pos, 20, p);
378        }
379        
380        // draw major graduations in red
381        p.setColor(0xffff0000);
382        for(int d=0; d<360; d+=90) {
383                int pos=d*length/360;
384                canvas.drawLine(pos, 0, pos, 30, p);
385        }
386        
387        // use center alignment for text
388        p.setTextAlign(Paint.Align.CENTER);
389 
390        // draw minor graduations text
391        p.setTextSize(9);
392        p.setColor(0xffffffff);
393        for(int d=0; d<360; d+=30) {
394                // do not draw 0/90/180/270
395                int pos=d*length/360;
396                int angle=mReversedRing ? (360+180-d)%360 : 360-d;
397                if(d%90!=0) canvas.drawText(Integer.toString(angle), pos, 30, p);
398        }
399        
400        // draw N/O/S/E
401        // hack : go till 360, so that "N" is printed at both end of the texture...
402        p.setTextSize(20);
403        p.setColor(0xffff0000);
404        for(int d=0; d<=360; d+=90) {
405                int pos=d*length/360;
406                if(mReversedRing) {
407                        canvas.drawText(CARDINAL_POINTS[((d+180)/90)%4], pos, 50, p);
408                } else {
409                        canvas.drawText(CARDINAL_POINTS[(d/90)%4], pos, 50, p);
410                }
411        }
412        
413        p.setShader(new LinearGradient(0, 5, 0, 0, 0xff000000, 0xffffffff, Shader.TileMode.CLAMP));
414        canvas.drawRect(0, 0, length, 5, p);
415        
416        /*BitmapDrawable bd=(BitmapDrawable)mContext.getResources().getDrawable(R.drawable.ruler);*/
417        //GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, b, 0);
418        ByteBuffer bb=ByteBuffer.allocate(length*height*4);
419        b.copyPixelsToBuffer(bb);
420        gl.glTexImage2D(GL10.GL_TEXTURE_2D, 0, GL10.GL_RGBA, length, height, 0, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, bb);
421   }
422    
423    void buildDialTexture(GL10 gl) {
424            int params[]=new int[1];
425            gl.glGetIntegerv(GL10.GL_MAX_TEXTURE_SIZE, params, 0);
426        gl.glBindTexture(GL10.GL_TEXTURE_2D, mTextures[TEXTURE_DIAL]);
427        gl.glPixelStorei(GL10.GL_UNPACK_ALIGNMENT, 1);
428        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, GL10.GL_CLAMP_TO_EDGE);
429        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, GL10.GL_CLAMP_TO_EDGE);
430        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR);
431        gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_LINEAR);
432        
433        final int radius=256;
434        Bitmap b=Bitmap.createBitmap(radius*2, radius*2, Bitmap.Config.ARGB_8888);
435        Canvas canvas=new Canvas(b);
436        
437        Paint p=new Paint();
438        p.setAntiAlias(true);
439 
440        // external shaded ring
441        int colors[]={0xff000000, 0xff000000, 0xffffffff, 0xff000000, 0x00000000};
442        float positions[]={0f, 0.94f, 0.95f, 0.98f, 1.0f};
443        p.setShader(new RadialGradient(radius, radius, radius, colors, positions, Shader.TileMode.CLAMP));
444        canvas.drawCircle(radius, radius, radius, p);
445        p.setShader(null);
446        
447        // build the inner decoration, using two symmetrical paths
448        Path pathl=new Path();
449        pathl.moveTo(radius, radius/2);
450        pathl.lineTo(radius+20, radius-20);
451        pathl.lineTo(radius, radius);
452        pathl.close();
453        Path pathr=new Path();
454        pathr.moveTo(radius, radius/2);
455        pathr.lineTo(radius-20, radius-20);
456        pathr.lineTo(radius, radius);
457        pathr.close();
458        canvas.save();
459        for(int i=0; i<4; i++) {
460                canvas.rotate((float) (i*90), radius, radius);
461                p.setColor(0xff808080);
462                canvas.drawPath(pathl, p);
463                p.setColor(0xffffffff);
464                canvas.drawPath(pathr, p);
465        }
466            canvas.restore();
467        
468        // draw medium graduations in white
469        p.setColor(0xffffffff);
470        p.setStrokeWidth(2);
471        for(int i=0; i<360; i+=10) {
472                canvas.save();
473                canvas.rotate(i, radius, radius);
474                canvas.drawLine(radius, radius*2, radius, 1.75f*radius, p);
475                canvas.restore();
476        }
477        
478 
479        // draw major graduations in red
480        p.setColor(0xffff0000);
481        p.setStrokeWidth(3);
482        for(int i=0; i<360; i+=90) {
483                canvas.save();
484                canvas.rotate(i, radius, radius);
485                canvas.drawLine(radius, radius*2, radius, 1.70f*radius, p);
486                canvas.restore();
487        }
488        
489        // medium graduation texts
490        p.setTextSize(24);
491        p.setTextAlign(Paint.Align.CENTER);
492        p.setColor(0xffffffff);
493        for(int i=0; i<360; i+=30) {
494                // do not draw 0/90/180/270
495                if((i%90)!=0) {
496                        double a = -i*(Math.PI*2)/360;
497                        float x = (float)(Math.sin(a)*0.7*radius+radius);
498                        float y = (float)(Math.cos(a)*0.7*radius+radius);
499                        
500                        canvas.save();
501                        canvas.rotate(i, x, y);
502                        canvas.drawText(Integer.toString(i), x, y, p);
503                        canvas.restore();
504                }
505        }
506 
507        // draw N/O/S/E
508        p.setTextSize(40);
509        p.setColor(0xffff0000);
510        for(int i=0; i<360; i+=90) {
511                double a = i*(Math.PI*2)/360;
512                float x = (float)(Math.sin(a)*0.65*radius+radius);
513                float y = (float)(Math.cos(a)*0.65*radius+radius);
514                
515                canvas.save();
516                canvas.rotate(-i, x, y);
517                canvas.drawText(CARDINAL_POINTS[i/90], x, y, p);
518                canvas.restore();
519        }
520 
521        //GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, b, 0);
522        ByteBuffer bb=ByteBuffer.allocate(radius*2*radius*2*4);
523        b.copyPixelsToBuffer(bb);
524        gl.glTexImage2D(GL10.GL_TEXTURE_2D, 0, GL10.GL_RGBA, radius*2, radius*2, 0, GL10.GL_RGBA, GL10.GL_UNSIGNED_BYTE, bb);
525    }
526 
527        public void setDetailsLevel(int detailsLevel) {
528                if(detailsLevel!=mDetailsLevel) {
529                        mDetailsLevel=detailsLevel;
530                        mNeedObjectsUpdate=true;
531                }
532        }
533 
534        public void setReversedRing(boolean reversedRing) {
535                if(reversedRing!=mReversedRing) {
536                        mReversedRing=reversedRing;
537                        mNeedTextureUpdate=true;
538                }
539        }
540}

[all classes][net.pierrox.mcompass]
EMMA 0.0.0 (unsupported private build) (C) Vladimir Roubtsov