Category: qt


QML Map’s addMapObject performance

After a crazy six-days holiday in Brazil and some never-ending flights I’m finally home and just to make it even better, Monday morning started with a head-on-brick-wall problem.

To put it shortly, adding objects to a QML maps appears to take linear time on the amount of mapObjects the map already holds. For some this might seem “not that bad” but it is. Basically, after adding 300 objects it’ll take approximately a second to add the 301th object. If you think in terms of “objects to be added”, the complete operation will be in O[n²].

(CHECK END OF ARTICLE FOR AN UPDATE & WORKAROUND)

Time for each insertion


I’ve already tried many different ways to add objects to the map (in addition to Map.addMapObject) without much improvement. I reckon the problem is that some checks/redrawing (prob O[n logn]) are performed at EVERY insertion and there’s no addMapObjectsList method that would first add all objects and then process/draw/check all items at the end on a single pass/go. . Am I missing something here or this feature/method is lacking only for simple laziness?

Related source seem to be at: https://qt.gitorious.org/qt-mobility/qt-mobility/blobs/master/src/location/maps/qgeomapdata.cpp#line454 which then goes to https://qt.gitorious.org/qt-mobility/qt-mobility/blobs/master/src/location/maps/qgeomapgroupobject.cpp#line158 .

Running out of ideas here except to lower the number of mapObjects …..

UPDATE: The real reason for such a bad performance is really only the (kind of) lack of a “addMapObjectsList”. Every time ONE item is added to the map, all items currently on the map are added. The solution (which becomes pretty obvious when you look at the source) is to add everything to a single MapGroup which wasn’t added to any map yet. Doing this the items are added but now drawn since there’s now linked map element. After all your items are added to this “root” MapGroup, adding that map group to the Map element will take linear time on the number of elements which is ok. Lots more info here .

While working on one of my current projects I came across the need for a map/geo browser.

From start, Qt Quick’s QML Map Element (using the nokia plugin) looked like the best option by a large margin and for all the good and sane reasons.

The problem is that the map element itself is quite simple/raw. At first I was expecting the Map Element to provide all the panning/pinch-zooming logic/functionality built in but it doesn’t.

Looking around for people with the same problem, I found this example from mlong (Huge thanks, that’s what got me started!). Seemed pretty good at first glance but I soon started missing some features. That example gives you the minimal shell around the Map QML element and is a great starting point.

The essential features I was still looking for were:

  1. Pinch-zooming centred at the pinch centre, not at the screen/map centre.
  2. While panning around, any minimal panning will trigger tile loading for the newly uncovered map regions (i.e. tiles adjacent to the on-screen map section are not pre-loaded as they could be)
  3. Zooming (on the original QML Map) is discrete (I confess this one was the one that bothered me more!). Your all human pinch gesture is translated to a dumb “Zoom In/Out” action just like old-style “+/-” buttons would..
  4. Flicking. The original QML Map’s  panning feels static and non-natural
  5. two-finger panning! Ok, this is a big one… As far as my world model goes, if while pinch-zooming(in/out) the user moves both fingers together, that means he’s panning/scrolling too! This is specially handy when you are looking for a place and you started the pinch gesture in a screen corner, it makes sense to bring that map section to the centre of the screen while zooming doesn’t it? It is a fact though that this is not a common feature in most mapping apps I tried (e.g. N8 and n950 Maps).
  6. Adapted to Portrait/Landscape nicely.

In short, the goal is that same pinch zooming behaviour that you get on the n950’s web browser.

As one can see it’s not just a “quick fix”. I kept thinking about this for a while and then it occurred to me that I could throw the Map element inside a Flickable and making the actual Map Element bigger than the screen. With this approach you’d solve the buffering/loading problems when panning and even get flickering for free. Then it also occurred me that I could use QML’s standard “scale” property to fill the gaps in between the different discrete zoom levels..

So, after quite a bit of hacking, I came up with this (.deb if you want to try it right away):

[sourcecode language=”javascript”]
/*
*
* This file is part of the Push Snowboarding Project, More info at:
* www.pushsnowboading.com
*
* Author: Clovis Scotti <scotti@ieee.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* See full license at http://www.gnu.org/licenses/gpl-3.0.html
*/
import QtQuick 1.1
import com.meego 1.0
import QtMobility.location 1.2
import Qt.labs.gestures 1.0
Flickable {
id: mapFlickable
property alias map: map
property double defaultLatitude: 51.5111
property double defaultLongitude: -0.0822898
property int defaultZoomLevel: 8
/*
zoomLevel gives powers of two.
*/
property double centeredContentX: map.size.width*0.75
property double centeredContentY: map.size.height*0.75
contentWidth: map.size.width*2
contentHeight: map.size.height*2
anchors.fill: parent
flickableDirection: Flickable.HorizontalAndVerticalFlick
pressDelay: 500//doesn’t really matter because I’m using the click
//normal panning happens at this level
//but if we’re close to the end of the map, we need
//to extend the panning to the map api.
states: [
State {
name: "inLandscape"
when: !rootWindow.inPortrait
StateChangeScript {
name: "updateToLandscape"
script: updateSizes("inLandscape")
}
},
State {
name: "inPortrait"
when: rootWindow.inPortrait
StateChangeScript {
name: "updateToPortrait"
script: updateSizes("inPortrait")
}
}
]
function updateSizes(newOrient)
{
// console.log("transformOrigin = " + map.transformOrigin)
// console.log("now in " + newOrient)
if(newOrient === "inPortrait") {
map.size.width = map.smallSize
map.size.height = map.bigSize-36*2
} else {
map.size.width = map.bigSize
map.size.height = map.smallSize-36*2
}
//// now in inPortrait
//// updateSizes, size = 970×1708
//// updateSizes, mapFlickable.size = 480×818 (status bar is 36px High)
//// now in inLandscape
//// updateSizes, size = 1708×970
//// updateSizes, mapFlickable.size = 854×444 (status bar is 36px high)
map.pos.x = map.size.width/2
map.pos.y = map.size.height/2
// map.center.latitude = defaultLatitude
// map.center.longitude = defaultLongitude
centeredContentX = map.size.width*0.75
centeredContentY = map.size.height*0.75
contentX = centeredContentX
contentY = centeredContentY
contentWidth = map.size.width*2
contentHeight = map.size.height*2
map.transformOrigin = Item.Center
map.scenter.x = map.width/2.0
map.scenter.y = map.height/2.0
console.log("tform at: " + map.scenter.x + " , " + map.scenter.y)
updateViewPort()
}
Component.onCompleted: {
if(inPortrait) {
updateSizes("inPortrait")
} else {
updateSizes("inLandscape")
}
contentX = centeredContentX
contentY = centeredContentY
map.pos.x = map.size.width/2
map.pos.y = map.size.height/2
flickDeceleration = 6000
maximumFlickVelocity = 4000
}
function updateViewPort() {
//one pass, pans map and updtes content(X|Y) accordingly
map.pan((contentX-centeredContentX)/map.getSkale,(contentY-centeredContentY)/map.getSkale)
//Division by scale rationale:
/*
content(X|Y)-centerContent(X|Y) distance is independent of scale.
scale === 1:
map’s top-left corner sits on top of content(X,Y)
scale === 1.5:
map is stretched, its top-left corner sits way outside
*/
contentX = centeredContentX
contentY = centeredContentY
}
onMovementEnded: {
updateViewPort()
}
Map {
smooth: true
id: map
property int smallSize: 2*480
property int bigSize: 2*854
//this values are constant and independent from scaling.
size.width: smallSize//mapFlickable.width*2//2*w;2*480//2*480//
size.height: bigSize //mapFlickable.height*2//2*h;2*854//2*854//
//this should stay constant at all times so that 2x zooming-in is possible
// pos.x: size.width/2
// pos.y: size.height/2
// scale: 0.1
/*
854 × 480
*/
// anchors.fill: parent
zoomLevel: defaultZoomLevel
plugin: Plugin { name: "nokia" }
mapType: Map.StreetMap
connectivityMode: Map.OfflineMode
center: Coordinate {
latitude: defaultLatitude
longitude:defaultLongitude
}
property alias scenter: tform.origin
property alias getSkale: tform.xScale
function setSkale(v) {
tform.xScale = v
tform.yScale = v
}
transform: Scale{
id: tform
}
}
PinchArea {
id: pincharea
anchors.fill: parent
property double initScale
property double p1toC_X
property double p1toC_Y
property double contentInitX
property double contentInitY
onPinchStarted: {
initScale = map.getSkale
p1toC_X = (pinch.center.x-map.size.width)
p1toC_Y = (pinch.center.y-map.size.height)
contentInitX = mapFlickable.contentX
contentInitY = mapFlickable.contentY
}
onPinchFinished: {
mapFlickable.updateViewPort()
}
onPinchUpdated: {
var contentDriftX = ((1-pinch.scale)*p1toC_X)
var contentDriftY = ((1-pinch.scale)*p1toC_Y)
//pinch.center.(x|y) drifts from to content, term in parenthesis offsets this back
//startCenter does not drift.
var tCenterDriftX = (pinch.center.x-(mapFlickable.contentX-contentInitX) – pinch.startCenter.x)
var tCenterDriftY = (pinch.center.y-(mapFlickable.contentY-contentInitY) – pinch.startCenter.y)
//test all two!
mapFlickable.contentX = contentInitX-contentDriftX-tCenterDriftX
mapFlickable.contentY = contentInitY-contentDriftY-tCenterDriftY
if(initScale*pinch.scale <= 0.75 && map.zoomLevel > 2) {
console.debug("zumUp")
map.zoomLevel -= 1
map.setSkale(1.5)
initScale = map.getSkale/pinch.scale
console.log("zoomUp to " + map.zoomLevel)
} else if(initScale*pinch.scale >= 1.5 && map.zoomLevel < 18) {
map.zoomLevel += 1
map.setSkale(0.75)
//map.scale 1.5 –> 0.75
//initScale_i*pinch = 1.5(map.scale_0)
//-> initScale_f*pinch = 0.75(map.scale_1)
// |{pinch = x} initScale = map.scale_1/pinch
initScale = map.getSkale/pinch.scale
console.log("zoomDown to " + map.zoomLevel)
} else {
if(! ((map.zoomLevel == 18 && (initScale*pinch.scale) > 2.0)
|| (map.zoomLevel == 2 && (initScale*pinch.scale) < 0.85))) {
map.setSkale(initScale*pinch.scale)
}
}
// console.log("S = " + map.scale + ", L = " + map.zoomLevel + ", I = " + initScale + ", C = " + mapFlickable.contentX + " , " + mapFlickable.contentY)
}
}
MouseArea {
anchors.fill: parent
onClicked: {
var coord = map.toCoordinate(Qt.point(mouseX-map.pos.x,mouseY-map.pos.y))
console.log("(long,lat) = (" + coord.latitude + " , " + coord.longitude + ")")
}
onPressed: {
console.log("(delay) pressed at " + mouseX + " , " + mouseY)
}
}
}

[/sourcecode]

The “continuous” zoom works by actually scaling the Map element (using transform: Scale{}..) until the next level of discrete zoom (Map.zoomLevel) is reached. The map would then switch to that new zoomLevel (upper or lower) and fix the Map scale accordingly.

If you look at the source you’ll notice that I’m not using the usual “Map.scale” property. I had to use the transform: Scale{..} . The problem is that when the screen orientation changes the whole map+flickable+content are changed too to fit it but then the scale transformOrigin (the point that stays in place when you scale something) wouldn’t change accordingly no matter how hard I tried; it would stay fixed to the first Item.Centre point where the scale was first changed.

The two finger zooming uses the pinch.center point and makes sure that point stays still in relation to the screen; for all pinchUpdates, the map is panned around to offset two types of drifts. The first is that the pinch.center coordinate will normally drift away due to the scaling point not being at the pinch.center, the second is due to the user actually moving the “pinch” around and thus moving the centre (this is what mekes the two-finger scrolling/panning work).

To use it, all you need is to include that Item in your page, like this:

[sourcecode language=”javascript”]
Page {
id: mapPage

//considering you saved the previous item in a file named: "ContinuousMap.qml"
ContinuousMap {
id: mapFrame

//onMap landmarks can be added from cpp or directly here.
}

//You can layer your own buttons/UI on top of the map by adding them here.
}
//….
[/sourcecode]

Hope you find this helpful.

N9(|50) Full SDK + Scratchbox Installation + Setup

Hi there!

So I got a new computer, why am I even talking about it? Because now I’ll have to set up everything I have on the new box.

To start with, the box is a Lenovo ThinkPad T520 with a clean Ubuntu 11.04, amd64 install.

For the record, 10.10 just won’t work with the T520, apparently all that sand in the bridge makes the old ubuntu skid and fall to death. After installing 317Mb of updates, upgraded to 11.04, disabled Unity (after a horrid 30 minute trial; don’t wanna talk about it now..) and finally started doing something useful.

Ok, now let’s get to the useful part, this post will mostly be just a collection of links anyway.

QtSDK; follow the instructions on that page to run the installer. Run the custom installation and select, under Experimental, Harmattan  (I picked both the Remote Compiler  and the Qt Quick Components for Symbian ones too..). I`d recommend getting Qt`s sources here too. Installation takes a long time so don’t bother and take yourself some coffee time.

Harmattan Scratchbox: Even though most of the work/development for the N9(|50) can be done through the QtSDK, one’s quite locked in there. I highly recommend using scratchbox for many reasons. Among them:

  1. makes it really easy to compile/build a generic linux package to harmattan. Many times you can take a package’s source from ubuntu/devian, build it inside scratchbox (with the proper target – HARMATTAN_ARMEL – set) and you’ll have a working .deb for Harmattan even if it’s got nothing to do with Qt. Building a .deb within QtSDK for a package that doesn`t use qmake just doesn’t make sense and will probably be a PITA.
  2. Makes the task of packaging comprehensible and “clean” without too many black magic (if you are familiar with debian packages, you`ll feel at home).
  3. Using the HARMATTAN_X86 target you can test your application/package/sw in an equivalent (but compiled to your pc`s own native architecture) environment that really works and has got all the harmattan qt quick components. QEMU is just unusable for me.

Basically, if you consider using anything further than Qt/QML pure apps, you`ll probably need it. If you didn’t get anything I said above, forget this whole guide and go do something else.

A very good guide on how to set Scratchbox up is here . The guide is pretty good and the python script there is pure magic.

Finally this article explains really well how to use scratchbox.

This is basically all you need to start..

Back to work now! 😀