QML append new PathCurve elements to List<PathElements> in ShapePath - qt

To be specific: I want to connect several Points on a Map with a spline curve. New points can be added with a Mouseclick and should also be connected to the existing Path. The points are stored within a model, so I can access them also in C++.
Unfortunately I can't figure out, how I can append new PathCurve elements to the existing List in the Shape::ShapePath Object.
I expected that something like this should work:
...
MapQuickItem {
coordinate: QtPositioning.coordinate(0.0000, 0.0000)
sourceItem: Shape {
id: myShape
anchors.fill: parent
vendorExtensionsEnabled: false
ShapePath {
id: myPath
strokeColor: "black"
strokeWidth: 2
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: 0; startY: 0
}
}
zoomLevel: 15
}
MouseArea {
anchors.fill: parent
onClicked: {
var coord = parent.toCoordinate(Qt.point(mouse.x,mouse.y))
myPath.pathElements.push( new PathCurve(mouse.x, mouse.y) ) //does not work
}
}
I also tried to fill the PathElements from C++, but the PathCurve class seems to be private and is only usable from within QML. Hardcoding PathCurve Elements works just fine like in every online example, but I want to dynamically modify the list of pathelements.
Any help would be appreciated very much!

You must dynamically components using the function createQmlObject, but for this you must bear in mind that it depends on the zoomLevel that you apply to the MapQuickItem the relation of sizes since that depends on the painting, the PathCurve does not use coordinates of the window but the coordinates of the Shape, and the Shape is painted according to how it is configured. So for in this case the zoomLevel of the MapQuickItem must be 0 or the same as the map. Considering the above, the solution is:
import QtQuick 2.9
import QtQuick.Window 2.2
import QtQuick.Shapes 1.12
import QtPositioning 5.9
import QtLocation 5.3
Window {
id: root
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Plugin {
id: mapPlugin
name: "osm" // "mapboxgl" "osm" "esri"
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
zoomLevel: 14
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
MapQuickItem {
id: map_item
coordinate: QtPositioning.coordinate(59.91, 10.75)
anchorPoint.x : 0
anchorPoint.y : 0
zoomLevel: map.zoomLevel
sourceItem: Shape {
id: myShape
anchors.fill: parent
vendorExtensionsEnabled: false
ShapePath {
id: myPath
strokeColor: "black"
strokeWidth: 2
capStyle: ShapePath.RoundCap
fillColor: "transparent"
startX: 0; startY: 0
}
}
}
}
MouseArea {
id: mousearea
anchors.fill: map
onClicked: {
var item_pos = map.fromCoordinate(map_item.coordinate, false)
var pathcurve = Qt.createQmlObject('import QtQuick 2.12; PathCurve {}',
myPath);
pathcurve.x = mouse.x - item_pos.x
pathcurve.y = mouse.y - item_pos.y
myPath.pathElements.push(pathcurve)
}
}
}

Related

QML osm Plugin draw both path & items

How can I draw both item icons & path between points from model using QML and osm plugin?
XmlListModel {
...
}
Plugin {
id: mapPlugin
objectName: "mapPlugin"
name: "osm"
...
}
Map {
id: map
objectName: "map"
anchors.fill: parent
plugin: mapPlugin
MapItemView {
id: mapItemView
model: mapModel
// draw item icons
delegate: MapQuickItem {
coordinate: QtPositioning.coordinate( model.latitude, model.longitude )
...
}
// could draw lines between points, but unable to use two delegates
/* delegate: MapPolyline {
path: pathRole
line.color: "red"
line.width: 5
} */
}
One trick you could do is create two MapItemViews. One for drawing the icons and the other for drawing line segments connecting the two, i.e.
MapItemView { // for drawing icons
model: mapModel
}
MapItemView { // for drawing line segments
model: mapModel.count - 1
}
Here's a sample of how you may do this:
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
Page {
Plugin {
id: mapPlugin
name: "osm"
}
Map {
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(43.0896, -79.0849)
zoomLevel: 12
MapItemView {
model: mapModel
delegate: MapQuickItem {
coordinate: QtPositioning.coordinate(lat, lon)
anchorPoint.x: 5
anchorPoint.y: 5
sourceItem: Rectangle {
width: 10
height: 10
color: "red"
border.color: "black"
}
}
}
MapItemView {
model: mapModel.count - 1
delegate: MapPolyline {
line.width: 3
line.color: "green"
path: [
{ latitude: mapModel.get(index).lat, longitude: mapModel.get(index).lon },
{ latitude: mapModel.get(index + 1).lat, longitude: mapModel.get(index + 1).lon }
]
}
}
}
ListModel {
id: mapModel
function appendCoordinate(lat, lon) {
append( {lat, lon} );
}
Component.onCompleted: {
appendCoordinate(43.0896, -79.0849);
appendCoordinate(43.0796, -79.0849);
appendCoordinate(43.0796, -79.0949);
}
}
}

Qt Select At Most 1 Marker on Map

In my code every marker that I clicked are selected(turn into green from red). I want just 1 can change. When I click another marker the marker I clicked before turns red again. Or When I click an empty area the marker I clicked before turns red again.
In qml my Item's code:
Component {
id: hazardous_img
MapQuickItem {
id: hazardousitem
anchorPoint.x: image.width/4
anchorPoint.y: image.height
coordinate: position
property bool isClicked: false
MouseArea {
anchors.fill: parent
onDoubleClicked: {
mainwindow.hazardousIconClicked(mapview.toCoordinate(Qt.point(mouse.x,mouse.y)))
}
onClicked: {
if (isClicked === false) {
image.source = "qrc:/grn-pushpin.png"
isClicked = true
} else {
image.source = "qrc:/red-pushpin.png"
isClicked = false
}
}
}
sourceItem: Image {
id: image
source: "qrc:/red-pushpin.png"
}
}
}
In QML this is usually done with using a ButtonGroup, but as you're not using AbstractButtons you need to write it yourself. Here is my solution for it.
I've used the ListModel to not only store the coordinates of each marker, but also a selected flag which is set to false by default. In the delegate I'm using the selected data role to show if a marker is selected or not.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
ApplicationWindow {
id: window
width: 640
height: 480
visible: true
title: qsTr("Map")
ListModel { id: markerModel }
Plugin {
id: mapPlugin
name: "osm"
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 14
MouseArea {
anchors.fill: parent
onDoubleClicked: {
var coordinate = map.toCoordinate(Qt.point(mouse.x, mouse.y))
var jsonObject = JSON.parse(JSON.stringify(coordinate))
jsonObject["selected"] = false
markerModel.append(jsonObject)
}
onClicked: map.deselectAll()
}
MapItemView {
model: markerModel
delegate: markerDelegate
}
function deselectAll() {
for (var i = 0; i < markerModel.count; ++i)
markerModel.setProperty(i, "selected", false)
}
Component {
id: markerDelegate
MapQuickItem {
id: markerItem
required property int index
required property real latitude
required property real longitude
required property bool selected
anchorPoint.x: waypointMarker.width / 2
anchorPoint.y: waypointMarker.height / 2
coordinate: QtPositioning.coordinate(latitude, longitude)
sourceItem: Rectangle {
id: waypointMarker
width: 20
height: 20
radius: 20
border.width: 1
border.color: mouseArea.containsMouse ? "red" : "black"
color: markerItem.selected ? "red" : "gray"
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onClicked: {
map.deselectAll()
markerModel.setProperty(markerItem.index, "selected", true)
}
}
}
}
}
}
I came up with yet another solution without looping over all items in the model. It just stores the index of the selected marker in a dedicated property. This has the drawback that if the model order changes the index can become invalid, also potential multi selection is hard to handle, but on the other hand it is faster because it doesn't need to iterate over all items.
I experimented a lot with DelegateModel, it seems to be a perfect match if one could use it in combination with MapItemView, because of the groups and the attached properties like inGroupName.
After that I've tried ItemSelectionModel, but it seems it is only intended to be used in combination with a view, e.g. TreeView. I couldn't find out how to generate a QModelIndex in QML without a TreeView.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
ApplicationWindow {
id: root
width: 640
height: 480
visible: true
title: qsTr("Map")
property int selectedMarker: -1
Map {
id: map
anchors.fill: parent
plugin: Plugin {
id: mapPlugin
name: "osm"
}
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 14
MouseArea {
anchors.fill: parent
onDoubleClicked: {
var coordinate = map.toCoordinate(Qt.point(mouse.x, mouse.y))
markerModel.append(JSON.parse(JSON.stringify(coordinate)))
}
onClicked: root.selectedMarker = -1
}
MapItemView {
model: ListModel { id: markerModel }
delegate: markerDelegate
}
Component {
id: markerDelegate
MapQuickItem {
id: markerItem
required property int index
required property real latitude
required property real longitude
anchorPoint.x: waypointMarker.width / 2
anchorPoint.y: waypointMarker.height / 2
coordinate: QtPositioning.coordinate(latitude, longitude)
sourceItem: Rectangle {
id: waypointMarker
width: 20
height: 20
radius: 20
border.width: 1
border.color: mouseArea.containsMouse ? "red" : "black"
color: markerItem.index === root.selectedMarker ? "red" : "gray"
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onClicked: root.selectedMarker = markerItem.index
}
}
}
}
}
I promise this is the last answer on that question.
This one is using an ItemSelectionModel and a few undocumented functions, e.g. ListModel.index(row, col).
itemSelectionModel.hasSelection is used in the color binding to trigger a reevaluation in order to call isRowSelected and set the color accordingly whenever the selection has changed.
If the user clicks on the background the clear() is called to clear the selection.
I think out of the three this is the best solution. It can be easily upgraded to allow multi selection as shown below. Also the ItemSelectionModel can be used by other views to show the data and selection.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtLocation 5.15
import QtPositioning 5.15
import QtQml.Models 2.15
ApplicationWindow {
id: root
width: 640
height: 480
visible: true
title: qsTr("Map")
Map {
id: map
anchors.fill: parent
plugin: Plugin {
id: mapPlugin
name: "osm"
}
center: QtPositioning.coordinate(59.91, 10.75) // Oslo
zoomLevel: 14
MouseArea {
anchors.fill: parent
onDoubleClicked: function(mouse) {
markerModel.append(map.toCoordinate(Qt.point(mouse.x, mouse.y)))
}
onClicked: itemSelectionModel.clear()
}
MapItemView {
model: ListModel { id: markerModel }
delegate: markerDelegate
}
ItemSelectionModel {
id: itemSelectionModel
model: markerModel
}
Component {
id: markerDelegate
MapQuickItem {
id: markerItem
required property int index
required property real latitude
required property real longitude
anchorPoint.x: waypointMarker.width / 2
anchorPoint.y: waypointMarker.height / 2
coordinate: QtPositioning.coordinate(latitude, longitude)
sourceItem: Rectangle {
id: waypointMarker
width: 20
height: 20
radius: 20
border.width: 1
border.color: mouseArea.containsMouse ? "red" : "black"
color: {
itemSelectionModel.hasSelection
return itemSelectionModel.isRowSelected(markerItem.index) ? "red" : "gray"
}
}
MouseArea {
id: mouseArea
hoverEnabled: true
anchors.fill: parent
onClicked: itemSelectionModel.select(markerModel.index(markerItem.index, 0),
ItemSelectionModel./*ClearAnd*/Select)
}
}
}
}
}

Displaying Marker in QT QML Map

I'm trying to add marker in QT QML Map. This is the code that I use to add a marker in Map, but the marker is not displaying. Please do help! I'm a beginner in QT Programming. Sorry for my grammar. Thank you!
Plugin {
id: mapPlugin
name: "osm"
}
function addMarker(latitude, longitude)
{
var Component = Qt.createComponent("qrc:///views/marker.qml")
var Item = Component.createObject(window, { coordinate:
QtPositioning.coordinate(latitude, longitude) })
Map.addMapItem(Item)
}
Map {
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(59.14, 14.15)
zoomLevel: 14
Component.onCompleted:
{
addMarker(59.14, 14.15)
}
}
Marker.qml
MapQuickItem
{
id: marker
anchorPoint.x: marker.width / 4
anchorPoint.y: marker.height
sourceItem: Image
{
Image
{
id: icon
source: "marker.png"
sourceSize.width: 40
sourceSize.height: 40
}
}
}
You have to use the ids to refer to the components, for example if you had Map and run Map.addMapItem(...) to what Map would the item be added ?. On the other hand you have a bad habit: you use names of existing elements such as Item that is already a type, in this case change it to item to avoid confusion, considering the above the solution is:
import QtQuick 2.9
import QtQuick.Window 2.2
import QtLocation 5.12
import QtPositioning 5.12
Window {
id: window
visible: true
width: 640
height: 480
title: qsTr("Hello World")
Plugin {
id: mapPlugin
name: "osm"
}
function addMarker(latitude, longitude)
{
var Component = Qt.createComponent("qrc:///views/marker.qml")
var item = Component.createObject(window, {
coordinate: QtPositioning.coordinate(latitude, longitude)
})
map.addMapItem(item)
}
Map {
id: map
anchors.fill: parent
plugin: mapPlugin
center: QtPositioning.coordinate(59.14, 14.15)
zoomLevel: 14
Component.onCompleted:addMarker(59.14, 14.15)
}
}
On the other hand in marker you point out that an Image has as a child another Image, do you think it is correct?, it is not necessary so the corrected Marker code is:
import QtQuick 2.0
import QtLocation 5.12
MapQuickItem{
id: marker
anchorPoint.x: marker.width / 4
anchorPoint.y: marker.height
sourceItem: Image{
id: icon
source: "marker.png"
sourceSize.width: 40
sourceSize.height: 40
}
}
The complete example is in the following link

QML Map plugin "itemsoverlay" does not clip to the base map for large zoom, using visibleRegion()

I have a somehow minimal example of a QML map (OSM plugin) and a respective map overlay with plugin itemsoverlay.
The following code will clip the overlay to the map, no matter what movement or zoom the map undergoes.
However, I can zoom the base map closer than the maximum OSM zoom level (resulting in some zoom level 21.07), e.g. by using mapBase.visibleRegion = rect (see below). But the overlay will not zoom closer than this, the zoom will stay at level 19.
You can test this by clicking on the red circle.
Any Idea how to have the overlay still have the same zoom level as the base map?
import QtQuick.Window 2.2
import QtQuick 2.7
import QtLocation 5.8
import QtPositioning 5.8
import QtQuick.Controls 2.2
Window {
width: 512
height: 512
visible: true
Map {
id: mapBase
anchors.fill: parent
gesture.enabled: true
plugin: Plugin { name: "osm" }
z: parent.z + 1
maximumZoomLevel: 30
center: QtPositioning.coordinate(51.51939, -0.11832)
Component.onCompleted: {
mapBase.zoomLevel = 19
}
}
Map {
id: map
anchors.fill: parent
plugin: Plugin { name: "itemsoverlay" }
gesture.enabled: false
center: mapBase.center
color: 'transparent'
minimumFieldOfView: mapBase.minimumFieldOfView
maximumFieldOfView: mapBase.maximumFieldOfView
minimumTilt: mapBase.minimumTilt
maximumTilt: mapBase.maximumTilt
minimumZoomLevel: mapBase.minimumZoomLevel
maximumZoomLevel: mapBase.maximumZoomLevel
zoomLevel: mapBase.zoomLevel
tilt: mapBase.tilt;
bearing: mapBase.bearing
fieldOfView: mapBase.fieldOfView
z: mapBase.z + 1
// visibleRegion: mapBase.visibleRegion
anchors.centerIn: parent
MapCircle {
center: QtPositioning.coordinate(51.51939, -0.11832)
radius: 10
color: "red"
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: {
var rect = QtPositioning.rectangle(
QtPositioning.coordinate(51.51949, -0.11842),
QtPositioning.coordinate(51.51929, -0.11822))
mapBase.visibleRegion = rect
parent.color = 'green'
console.log(map.zoomLevel, mapBase.zoomLevel)
}
}
}
}
}
I was playing with some parameters and found a fix by chance: replace the
Map {
id: map
// ...
maximumZoomLevel: mapBase.maximumZoomLevel
// ...
}
to
Map {
id: map
// ...
maximumZoomLevel: 30
// ...
}
However, I do not understand why the latter works in contrast to the former and if it is even just a bug?

In QML, (how) can I make MapItemGroup as a MapItemView's delegate component?

Situation: I am able to use QML Map item with Model/View/delegate. I am able to work with individual items.
Problem: As a next step, I would like to be able to draw multiple items. I need to put multiple QML MapItems (like MapCircle, MapRectangle, etc..) in a single delegate component. In general, QML supports multiple items within a delegate. The problem is with MapItemView's delegate: it does not support multiple children items.
My Approach:
I thought using MapItemGroup would work. But it seems like I am missing something. The documentation is also not so extensive on how I can make it work as a delegate component. The attached snippet shows this implementation.
Qt's Documentation on MapItemGroup
In the code below:
delegateCircle, delegateRect work fine
delegateGroup is not displayed
A simple implementation:
import QtQuick 2.10
import QtPositioning 5.6
import QtLocation 5.9
import QtQuick.Controls 2.3 as QQc2
QQc2.ApplicationWindow {
visible: true
width: 640
height: 480
// Some list model
ListModel {
id: someModel
ListElement {lat: 0; lon: 0}
ListElement {lat: 5; lon: 0}
ListElement {lat: 5; lon: 5}
ListElement {lat: 0; lon: 5}
}
Map {
id: map
anchors.fill: parent
plugin: Plugin {name: "osm"}
center: QtPositioning.coordinate(2.5, 2.5)
zoomLevel: 6
// Some views to test the model
// delegateCircle, delegateRect work fine
// delegateGroup is not displayed
MapItemView {
model: someModel
delegate: MapCircle {
id: delegateCircle
border.color: "red"
border.width: 1
center: QtPositioning.coordinate(model.lat, model.lon)
radius: 50*1000
}
}
MapItemView {
model: someModel
delegate: MapRectangle {
id: delegateRect
border.color: "green"
border.width: 3
topLeft : QtPositioning.coordinate(model.lat+1, model.lon-1)
bottomRight : QtPositioning.coordinate(model.lat-1, model.lon+1)
}
}
MapItemView {
model: someModel
delegate: MapItemGroup {
id: delegateGroup
MapCircle {
id: innerCircle
border.color: "green"
border.width: 3
center: QtPositioning.coordinate(model.lat, model.lon)
radius: 75*1000
}
MapRectangle {
id: innerRect
border.color: "red"
border.width: 6
topLeft : QtPositioning.coordinate(model.lat+2, model.lon-2)
bottomRight : QtPositioning.coordinate(model.lat-2, model.lon+2)
}
}
}
}
}
The output of the above code is:
I also tried using the MapItemGroup as a sourceItem of MapQuickItem type. This did not work either.
What I want to achieve:
Well. I need to draw multiple map items using a MapItemView. Any other solution/method (including c++ backend program) is welcome.
EDIT
Thank you #GrecKo and #Yoann. Both of your solutions work. However, I chose to continue with Instantiator since it is more suited for my application.
I also found this interesting after seeing your solution: a developer's discussion on populating a model using Repeater and Instantiator.
Unfortunately, MapItemView only works with MapItem-derived items, and MapItemGroup is not one of them.
You could use a Repeater and it will work for delegates created from the start, but it won't work if you add rows in your model later on.
I explained in this other answer why Repeater is not well suited for a Map.
In my answer I advise to use MapItemView but it's not applicable here.
Hopefully, there is still one last available solution :
Instantiator with Map.addMapItemGroup() and Map.removeMapItemGroup().
import QtQuick 2.10
import QtPositioning 5.6
import QtLocation 5.9
import QtQuick.Controls 2.3 as QQc2
import QtQml 2.2
QQc2.ApplicationWindow {
visible: true
width: 640
height: 480
ListModel {
id: someModel
ListElement {lat: 0; lon: 0}
ListElement {lat: 5; lon: 0}
ListElement {lat: 5; lon: 5}
ListElement {lat: 0; lon: 5}
}
Timer {
interval: 1000
running: true
repeat: true
property bool toggle: true
onTriggered: {
if (toggle)
someModel.append({lat: 2.5, lon: 2.5});
else
someModel.remove(4);
toggle = !toggle;
}
}
Map {
id: map
anchors.fill: parent
plugin: Plugin {name: "osm"}
center: QtPositioning.coordinate(2.5, 2.5)
zoomLevel: 6
Instantiator {
model: someModel
delegate: MapItemGroup {
id: delegateGroup
MapCircle {
id: innerCircle
border.color: "green"
border.width: 3
center: QtPositioning.coordinate(model.lat, model.lon)
radius: 75*1000
}
MapRectangle {
id: innerRect
border.color: "red"
border.width: 6
topLeft : QtPositioning.coordinate(model.lat+2, model.lon-2)
bottomRight : QtPositioning.coordinate(model.lat-2, model.lon+2)
}
}
onObjectAdded: map.addMapItemGroup(object)
onObjectRemoved: map.removeMapItemGroup(object)
}
}
}
That was fixed in Qt 5.12, so You may use Your code as is now with
import QtLocation 5.12
import QtPositioning 5.12
You could use a simple Repeater instead of MapItemView
import QtQuick 2.10
import QtPositioning 5.6
import QtLocation 5.9
import QtQuick.Controls 2.3
ApplicationWindow {
visible: true
width: 640
height: 480
// Some list model
ListModel {
id: someModel
ListElement {lat: 0; lon: 0}
ListElement {lat: 5; lon: 0}
ListElement {lat: 5; lon: 5}
ListElement {lat: 0; lon: 5}
}
Map {
id: map
anchors.fill: parent
plugin: Plugin {name: "osm"}
center: QtPositioning.coordinate(2.5, 2.5)
zoomLevel: 6
Repeater
{
model: someModel
MapItemGroup {
id: delegateGroup
MapCircle {
id: innerCircle
border.color: "green"
border.width: 3
center: QtPositioning.coordinate(model.lat, model.lon)
radius: 75*1000
}
MapRectangle {
id: innerRect
border.color: "red"
border.width: 6
topLeft : QtPositioning.coordinate(model.lat+2, model.lon-2)
bottomRight : QtPositioning.coordinate(model.lat-2, model.lon+2)
}
Component.onCompleted: map.addMapItemGroup(this)
}
}
}
}
As GrecKo pointed out, in order for this to work with dynamic model, the itemGroup must be "manually" added to the map, hence the line Component.onCompleted: map.addMapItemGroup(this)

Resources